Merge branch 'stable-3.0' into stable-3.1

* stable-3.0:
  Allow async receive-commits to have a thread-local cache
  Fix RepoRefCache stale checks during NoteDb rebuild
  Lazy load change notes when submit by push

Change-Id: Ie82487167fc84ec543083b018c3fde1b0a041e1d
Release-Notes: skip
diff --git a/.bazelrc b/.bazelrc
index bf3aa6c..e560f2397 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,4 +1,4 @@
-build --workspace_status_command="python ./tools/workspace_status.py" --strategy=Closure=worker
+build --workspace_status_command="python3 ./tools/workspace_status.py" --strategy=Closure=worker
 build --repository_cache=~/.gerritcodereview/bazel-cache/repository
 build --action_env=PATH
 build --disk_cache=~/.gerritcodereview/bazel-cache/cas
diff --git a/.bazelversion b/.bazelversion
index 7c69a55d..fcdb2e1 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-3.7.0
+4.0.0
diff --git a/.gitmodules b/.gitmodules
index 6844f6a..9f67e77 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,7 @@
+[submodule "modules/jgit"]
+	path = modules/jgit
+	url = ../jgit
+
 [submodule "plugins/codemirror-editor"]
 	path = plugins/codemirror-editor
 	url = ../plugins/codemirror-editor
diff --git a/.gitreview b/.gitreview
index b3c37ad..9df7aae 100644
--- a/.gitreview
+++ b/.gitreview
@@ -2,4 +2,4 @@
 host=gerrit-review.googlesource.com
 scheme=https
 project=gerrit.git
-defaultbranch=stable-3.0
+defaultbranch=stable-3.1
diff --git a/.mailmap b/.mailmap
index 38c2a5f..721f3c0 100644
--- a/.mailmap
+++ b/.mailmap
@@ -4,6 +4,7 @@
 Alex Blewitt <alex.blewitt@gmail.com>                                                       <alex.blewitt@credit-suisse.com>
 Alex Ryazantsev <alex.ryazantsev@gmail.com>                                                 alex <alex.ryazantsev@gmail.com>
 Alex Ryazantsev <alex.ryazantsev@gmail.com>                                                 alex.ryazantsev <alex.ryazantsev@gmail.com>
+Alice Kober-Sotzek <aliceks@google.com>                                                     <aliceks@google.com>
 Alexandre Philbert <alexandre.philbert@ericsson.com>                                        <alexandre.philbert@hotmail.com>
 Andrew Bonventre <andybons@chromium.org>                                                    <andybons@google.com>
 Becky Siegel <beckysiegel@google.com>                                                       beckysiegel <beckysiegel@google.com>
@@ -14,6 +15,7 @@
 Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com>                           carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
 Chad Horohoe <chorohoe@wikimedia.org>                                                       <chadh@wikimedia.org>
 Changcheng Xiao <xchangcheng@google.com>                                                    xchangcheng
+Cheng Ke <chengke.info@gmail.com>                                                           <chengke.info@gmail.com>
 Dariusz Luksza <dluksza@collab.net>                                                         <dariusz@luksza.org>
 Darrien Glasser <darrien@arista.com>                                                        darrien <darrien@arista.com>
 Dave Borowitz <dborowitz@google.com>                                                        <dborowitz@google.com>
@@ -73,6 +75,7 @@
 Réda Housni Alaoui <reda.housnialaoui@gmail.com>                                            <alaoui.rda@gmail.com>
 Richard Möhn <richard.moehn@posteo.de>                                                      <richard.moehn@fu-berlin.de>
 Sam Saccone <samccone@google.com>                                                           <samccone@gmail.com>
+Sam Saccone <samccone@google.com>                                                           <samccone@google.com>
 Saša Živkov <sasa.zivkov@sap.com>                                                           Sasa Zivkov <sasa.zivkov@sap.com>
 Saša Živkov <sasa.zivkov@sap.com>                                                           Saša Živkov <zivkov@gmail.com>
 Saša Živkov <sasa.zivkov@sap.com>                                                           Sasa Zivkov <zivkov@gmail.com>
diff --git a/.zuul.yaml b/.zuul.yaml
index 463bc51..fe9dc80 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -6,7 +6,8 @@
 
       This adds required projects needed for all Gerrit-related builds
       (i.e., builds of Gerrit itself or plugins) on this branch.
-    # No additional required projects required for this branch.
+    required-projects:
+      - jgit
 
 - job:
     name: gerrit-build
diff --git a/BUILD b/BUILD
index 3989a75..c48b3b9 100644
--- a/BUILD
+++ b/BUILD
@@ -4,16 +4,16 @@
 package(default_visibility = ["//visibility:public"])
 
 config_setting(
-    name = "java9",
+    name = "java11",
     values = {
-        "java_toolchain": "@bazel_tools//tools/jdk:toolchain_java9",
+        "java_toolchain": "@bazel_tools//tools/jdk:toolchain_java11",
     },
 )
 
 config_setting(
     name = "java_next",
     values = {
-        "java_toolchain": "@bazel_tools//tools/jdk:toolchain_vanilla",
+        "java_toolchain": "//tools:toolchain_vanilla",
     },
 )
 
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 9f7c457..7961b7e 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -727,13 +727,29 @@
 A user must have this access granted in order to see a project, its
 changes, or any of its data.
 
-This category has a special behavior, where the per-project ACL is
-evaluated before the global all projects ACL.  If the per-project
-ACL has granted `Read` with 'DENY', and does not otherwise grant
-`Read` with 'ALLOW', then a `Read` in the all projects ACL
-is ignored.  This behavior is useful to hide a handful of projects
+[[read_special_behaviors]]
+==== Special behaviors
+
+This category has multiple special behaviors:
+
+The per-project ACL is evaluated before the global all projects ACL.
+If the per-project ACL has granted `Read` with 'DENY', and does not
+otherwise grant `Read` with 'ALLOW', then a `Read` in the all projects
+ACL is ignored.  This behavior is useful to hide a handful of projects
 on an otherwise public server.
 
+You cannot grant `Read` on the `refs/tags/` namespace.  Visibility to
+`refs/tags/` is derived from `Read` grants on refs namespaces other than
+`refs/tags/`, `refs/changes/`, and `refs/cache-automerge/` by finding
+tags reachable from those refs.  For example, if a tag `refs/tags/test`
+points to a commit on the branch `refs/heads/master`, then allowing
+`Read` access to `refs/heads/master` would also allow access to
+`refs/tags/test`.  If a tag is reachable from multiple refs, allowing
+access to any of those refs allows access to the tag.
+
+[[read_typical_usage]]
+==== Typical usage
+
 For an open source, public Gerrit installation it is common to grant
 `Read` to `Anonymous Users` in the `All-Projects` ACL, enabling
 casual browsing of any project's changes, as well as fetching any
@@ -919,7 +935,7 @@
 
 Suggested access rights to grant:
 
-* xref:category_read[`Read`] on 'refs/heads/\*' and 'refs/tags/*'
+* xref:category_read[`Read`] on 'refs/heads/\*'
 * xref:category_push[`Push`] to 'refs/for/refs/heads/*'
 * link:config-labels.html#label_Code-Review[`Code-Review`] with range '-1' to '+1' for 'refs/heads/*'
 
@@ -947,7 +963,7 @@
 
 Suggested access rights to grant:
 
-* xref:category_read[`Read`] on 'refs/heads/\*' and 'refs/tags/*'
+* xref:category_read[`Read`] on 'refs/heads/\*'
 * xref:category_push[`Push`] to 'refs/for/refs/heads/*'
 * xref:category_push_merge[`Push merge commit`] to 'refs/for/refs/heads/*'
 * xref:category_forge_author[`Forge Author Identity`] to 'refs/heads/*'
@@ -1002,7 +1018,7 @@
 
 Suggested access rights to grant, that won't block changes:
 
-* xref:category_read[`Read`] on 'refs/heads/\*' and 'refs/tags/*'
+* xref:category_read[`Read`] on 'refs/heads/\*'
 * link:config-labels.html#label_Code-Review[`Label: Code-Review`] with range '-1' to '0' for 'refs/heads/*'
 * link:config-labels.html#label_Verified[`Label: Verified`] with range '0' to '+1' for 'refs/heads/*'
 
diff --git a/Documentation/backup.txt b/Documentation/backup.txt
index ed044ba..7220c74 100644
--- a/Documentation/backup.txt
+++ b/Documentation/backup.txt
@@ -139,11 +139,12 @@
 server is read-only or down as short as possible.
 
 [#cons-backup-read-only]
-=== Turn master read-only for backup
+=== Turn primary server read-only for backup
 
-Make the server read-only before taking the backup. This means read-access
-is still available during backup, because only write operations have to be
-stopped to ensure consistency. This can be implemented using the
+Make the primary server handling write operations read-only before taking the
+backup. This means read-access is still available from replica servers during
+backup, because only write operations have to be stopped to ensure consistency.
+This can be implemented using the
 link:https://gerrit.googlesource.com/plugins/readonly/[_readonly_] plugin.
 
 [#cons-backup-replicate]
@@ -162,9 +163,9 @@
 Best you use a filesystem supporting snapshots to create a backup archive
 of such a replica.
 
-For 2.x Gerrit versions also set up a database slave for the data stored in the
+For 2.x Gerrit versions also set up a database replica for the data stored in the
 SQL database. If you are using 2.16 and migrated to _NoteDb_ you may consider to
-skip setting up a database slave, instead take a backup of the database which only
+skip setting up a database replica, instead take a backup of the database which only
 contains the current schema version in this case.
 In addition you need to ensure that no write operations are in flight before you
 take the replica offline. Otherwise the database backup might be inconsistent
@@ -176,15 +177,16 @@
 link:https://gerrit.googlesource.com/plugins/replication/+/refs/heads/master/src/main/resources/Documentation/config.md[server option]
 `remote.NAME.replicateProjectDeletions`.
 
-If you are using Gerrit slaves to offload read traffic you can use one of these
-slaves for creating backups.
+If you are using Gerrit replica to offload read traffic you can use one of these
+replica for creating backups.
 
 [#cons-backup-offline]
-=== Take master offline for backup
+=== Take primary server offline for backup
 
-Shutdown the server before taking a backup. This is simple but means downtime
-for the users. Also crons and currently running cron jobs (e.g. repacking
-repositories) which affect the repositories may need to be shut down.
+Shut down the primary server handling write operations before taking a backup.
+This is simple but means downtime for the users. Also crons and currently
+running cron jobs (e.g. repacking repositories) which affect the repositories
+may need to be shut down.
 
 [#backup-methods]
 == Backup methods
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index fb35dc2..1dd6720 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -122,16 +122,16 @@
 $ ssh -p 29418 review.example.com gerrit ls-projects
 platform/manifest
 tools/gerrit
-tools/gwtorm
+tools/gitiles
 
 $ curl http://review.example.com/projects/
 platform/manifest
 tools/gerrit
-tools/gwtorm
+tools/gitiles
 
 $ curl http://review.example.com/projects/tools/
 tools/gerrit
-tools/gwtorm
+tools/gitiles
 ----
 
 Clone any project visible to the user:
diff --git a/Documentation/cmd-plugin-remove.txt b/Documentation/cmd-plugin-remove.txt
index f5fe56b..012bf7b 100644
--- a/Documentation/cmd-plugin-remove.txt
+++ b/Documentation/cmd-plugin-remove.txt
@@ -20,6 +20,7 @@
 * Caller must be a member of the privileged 'Administrators' group.
 * link:config-gerrit.html#plugins.allowRemoteAdmin[plugins.allowRemoteAdmin]
 must be enabled in `$site_path/etc/gerrit.config`.
+* Mandatory plugin cannot be disabled
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/config-accounts.txt b/Documentation/config-accounts.txt
index addada11..90150b1 100644
--- a/Documentation/config-accounts.txt
+++ b/Documentation/config-accounts.txt
@@ -185,8 +185,6 @@
 link:user-review-ui.html#diff-preferences[diff] and edit preferences:
 
 ----
-[general]
-  showSiteHeader = false
 [diff]
   hideTopMenu = true
 [edit]
@@ -412,10 +410,10 @@
 allows to plug in alternate implementations for storing the reviewed
 flags. To replace the storage for reviewed flags a plugin needs to
 implement the link:dev-plugins.html#account-patch-review-store[
-AccountPatchReviewStore] interface. E.g. to support a multi-master
-setup where reviewed flags should be replicated between the master
-nodes one could implement a store for the reviewed flags that is
-based on MySQL with replication.
+AccountPatchReviewStore] interface. E.g. to support a cluster setup with
+multiple primary servers handling write operations where reviewed flags should
+be replicated between the primary nodes one could implement a store for the
+reviewed flags that is based on MySQL with replication.
 
 [[account-sequence]]
 == Account Sequence
@@ -443,7 +441,7 @@
 * `refs/meta/external-ids` (external IDs)
 * `refs/starred-changes/*` (star labels)
 * `refs/sequences/accounts` (account sequence numbers, not needed for Gerrit
-  slaves)
+  replicas)
 
 GERRIT
 ------
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 6d98877..c65325b 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -582,7 +582,14 @@
 By default this is set to `LDAP` when link:#auth.type[`auth.type`] is `LDAP`
 and `OAUTH` when link:#auth.type[`auth.type`] is `OAUTH`.
 Otherwise, the default value is `HTTP`.
-
++
+When gitBasicAuthPolicy is set to `LDAP` or `HTTP_LDAP` and the user
+is authenticating with the LDAP username/password, the Git client config
+needs to have `http.cookieFile` set to a local file, otherwise every
+single call would trigger a full LDAP authentication and groups resolution
+which could introduce a noticeable latency on the overall execution
+and produce unwanted load to the LDAP server.
++
 [[auth.gitOAuthProvider]]auth.gitOAuthProvider::
 +
 Selects the OAuth 2 provider to authenticate git over HTTP traffic with.
@@ -612,7 +619,9 @@
 existing accounts this username is already in lower case. It is not
 possible to convert the usernames of the existing accounts to lower
 case because this would break the access to existing per-user
-branches and Gerrit provides no tool to do such a conversion.
+branches and Gerrit provides no tool to do such a conversion. Accounts
+created using the REST API or the `create-account` SSH command will
+be created with all lowercase characters, when this option is set.
 +
 Setting this parameter to `true` will prevent all users from login that
 have a non-lower-case username.
@@ -833,7 +842,8 @@
 +
 Default value is 0 (disabled). It is disabled by default due to the fact
 that change updates are not communicated between Gerrit servers. Hence
-this cache should be disabled in an multi-master/multi-slave setup.
+this cache should be disabled in a cluster setup using multiple primary
+or multiple replica nodes.
 +
 The cache should be flushed whenever the database changes table is modified
 outside of Gerrit.
@@ -881,6 +891,12 @@
 away from the defaults. The cache may be persisted by setting
 `diskLimit`, which is only recommended if cold start performance is
 problematic.
++
+`external_ids_map` supports computing the new cache value based on a
+previously cached state. This applies modifications based on the Git
+diff and is almost always faster.
+`cache.external_ids_map.enablePartialReloads` turns this behavior on
+or off. The default is `true`.
 
 cache `"git_tags"`::
 +
@@ -1158,17 +1174,6 @@
 +
 Default is true.
 
-[[change.allowDrafts]]change.allowDrafts::
-+
-Legacy support for drafts workflow. If set to true, pushing a new change
-with draft option will create a private change. Pushing with draft option
-to an existing change will create change edit.
-+
-Enabling this option allows to push to the `refs/drafts/branch`. When
-disabled any push to `refs/drafts/branch` will be rejected.
-+
-Default is false.
-
 [[change.api.excludeMergeableInChangeInfo]]change.api.excludeMergeableInChangeInfo::
 +
 If true, the mergeability bit in
@@ -1210,6 +1215,25 @@
 +
 By default 500.
 
+[[change.maxUpdates]]change.maxUpdates::
++
+Maximum number of updates to a change. Counts only updates to the main NoteDb
+meta ref; draft comments, robot comments, stars, etc. do not count towards the
+total.
++
+Many NoteDb operations require walking the entire change meta ref and loading
+its contents into memory, so changes with arbitrarily many updates may cause
+high CPU usage, memory pressure, persistent cache bloat, and other problems.
++
+The following operations are allowed even when a change is at the limit:
+* Abandon
+* Submit
+* Submit by push with `%submit`
+* Auto-close by pushing directly to the branch
+* Fix with link:rest-api-changes.html#fix-input[`expect_merged_as`]
++
+By default 1000.
+
 [[change.move]]change.move::
 +
 Whether the link:rest-api-changes.html#move-change[Move Change] REST
@@ -1547,11 +1571,17 @@
 Execute `java -jar gerrit.war daemon --help` to see all possible
 options.
 
+[[container.replica]]container.replica::
++
+Used on Gerrit replica installations. If set to true the Gerrit JVM is
+called with the '--replica' switch, enabling replica mode. If no value is
+set (or any other value), Gerrit defaults to primary mode enabling write
+operations.
+
 [[container.slave]]container.slave::
 +
-Used on Gerrit slave installations. If set to true the Gerrit JVM is
-called with the '--slave' switch, enabling slave mode. If no value is
-set (or any other value), Gerrit defaults to master mode.
+Backward compatibility for link:#container.replica[`container.replica`]
+config setting.
 
 [[container.startupTimeout]]container.startupTimeout::
 +
@@ -1578,6 +1608,10 @@
 [[core]]
 === Section core
 
+[NOTE]
+The link:#jgitConfig[etc/jgit.config] file supports configuration of all JGit
+options.
+
 [[core.packedGitWindowSize]]core.packedGitWindowSize::
 +
 Number of bytes of a pack file to load into memory in a single
@@ -2122,6 +2156,36 @@
 +
 Defaults to the full hostname of the Gerrit server.
 
+[[gerrit.experimentalRollingUpgrade]]gerrit.experimentalRollingUpgrade::
++
+Enable Gerrit rolling upgrade to the next version.
+For example if Gerrit v3.1 is version N (All-Projects:refs/meta/version=181)
+then its next version N+1 is v3.2 (All-Projects:refs/meta/version=183).
+Allow Gerrit to start even if the underlying schema version has been bumped to
+the next Gerrit version.
++
+Set to true if Gerrit is installed in
+[high-availability configuration](https://gerrit.googlesource.com/plugins/high-availability/+/refs/heads/master/README.md)
+during the rolling upgrade to the next version.
++
+By default false.
++
+The rolling upgrade process, at high level, assumes that Gerrit is installed
+on two or more nodes sharing the repositories over NFS. The upgrade is composed
+of the following steps:
++
+1. Set gerrit.experimentalRollingUpgrade to true on all Gerrit masters
+2. Set the first master unhealthy
+3. Shutdown the first master and [upgrade](install.html#init) to the next version
+4. Startup the first master, wait for the online reindex to complete
+5. Verify the the first master upgrade is successful and online reindex is complete
+6. Set the first master healthy
+7. Repeat steps 2. to 6. for all the other Gerrit nodes
++
+[WARNING]
+Rolling upgrade may or may not be possible depending on the changes introduced
+by the target version of the upgrade. Refer to the release notes and check whether
+the rolling upgrade is possible or not and the associated constraints.
 
 [[gerrit.serverId]]gerrit.serverId::
 +
@@ -2826,28 +2890,28 @@
 ==== Subsection index.scheduledIndexer
 
 This section configures periodic indexing. Periodic indexing is
-intended to run only on slaves and only updates the group index.
-Replication to slaves happens on Git level so that Gerrit is not aware
-of incoming replication events. But slaves need an updated group index
+intended to run only on replicas and only updates the group index.
+Replication to replicas happens on Git level so that Gerrit is not aware
+of incoming replication events. But replicas need an updated group index
 to resolve memberships of users for ACL validation. To keep the group
-index in slaves up-to-date the Gerrit slave periodically scans the
+index in replicas up-to-date the Gerrit replica periodically scans the
 group refs in the All-Users repository to reindex groups if they are
 stale.
 
 The scheduled reindexer is not able to detect group deletions that
-happened while the slave was offline, but since group deletions are not
+happened while the replica was offline, but since group deletions are not
 supported this should never happen. If nevertheless groups refs were
-deleted while a slave was offline a full offline link:pgm-reindex.html[
+deleted while a replica was offline a full offline link:pgm-reindex.html[
 reindex] must be performed.
 
-This section is only used if Gerrit runs in slave mode, otherwise it is
+This section is only used if Gerrit runs in replica mode, otherwise it is
 ignored.
 
 [[index.scheduledIndexer.runOnStartup]]index.scheduledIndexer.runOnStartup::
 +
 Whether the scheduled indexer should run once immediately on startup.
-If set to `true` the slave startup is blocked until all stale groups
-were reindexed. Enabling this allows to prevent that slaves that were
+If set to `true` the replica startup is blocked until all stale groups
+were reindexed. Enabling this allows to prevent that replicas that were
 offline for a longer period of time run with outdated group information
 until the first scheduled indexing is done.
 +
@@ -2857,7 +2921,7 @@
 +
 Whether the scheduled indexer is enabled. If the scheduled indexer is
 disabled you must implement other means to keep the group index for the
-slave up-to-date (e.g. by using ElasticSearch for the indexes).
+replica up-to-date (e.g. by using ElasticSearch for the indexes).
 +
 Defaults to `true`.
 
@@ -2997,10 +3061,6 @@
 for production use. For compatibility information, please refer to the
 link:https://www.gerritcodereview.com/elasticsearch.html[project homepage].
 
-In Elasticsearch version 6.2 or later, the open and closed changes are merged
-into the default `_doc` type. The latter is also used for the accounts and groups
-indices starting with Elasticsearch 6.2.
-
 Note that when Gerrit is configured to use Elasticsearch, the Elasticsearch
 server(s) must be reachable during the site initialization.
 
@@ -3030,7 +3090,7 @@
 link:https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#_static_index_settings[
 Elasticsearch documentation] for details.
 +
-Defaults to 5 for Elasticsearch versions 5 and 6, and to 1 starting with Elasticsearch 7.
+Defaults to 1.
 
 [[elasticsearch.numberOfReplicas]]elasticsearch.numberOfReplicas::
 +
@@ -3070,6 +3130,31 @@
 +
 Not set by default.
 
+[[event]]
+=== Section event
+
+[[event.payload.listChangeOptions]]events.payload.listChangeOptions::
++
+List of options that Gerrit applies when rendering the payload of an
+internal event. This is the same set of options that are documented
+link:rest-api-changes.html#query-options[here].
++
+Depending on the setup, these events might get serialized using stream
+events.
++
+This can be set to the set of minimal options that consumers of Gerrit's
+events need. A minimal set would be (`SKIP_MERGEABLE`,`SKIP_DIFFSTAT`).
++
+Every option that gets added here will have a performance impact. The
+general recommendation is therefore to set this to a minimal set of
+required options.
++
+Defaults to all available options minus `CHANGE_ACTIONS`,
+`CURRENT_ACTIONS` and `CHECK`. This is a rich default to make sure the
+config is backwards compatible with what the default was before the config
+was added.
+
+
 [[ldap]]
 === Section ldap
 
@@ -3645,6 +3730,14 @@
 and SSH.  If set to true Administrators can install new plugins
 remotely, or disable existing plugins.  Defaults to false.
 
+[[plugins.mandatory]]plugins.mandatory::
++
+List of mandatory plugins. If a plugin from this list does not load,
+Gerrit will fail to start.
++
+Disabling and restarting of a mandatory plugin is rejected, but reloading
+of a mandatory plugin is still possible.
+
 [[plugins.jsLoadTimeout]]plugins.jsLoadTimeout::
 +
 Set the timeout value for loading JavaScript plugins in Gerrit UI.
@@ -3667,17 +3760,6 @@
 If no groups are added, any user will be allowed to execute
 'receive-pack' on the server.
 
-[[receive.allowPushToRefsChanges]]receive.allowPushToRefsChanges::
-+
-If true, it is possible to push directly to a change using `refs/changes/`.
-The possibility to push to `refs/changes/` is deprecated and it might be
-removed in future releases.
-See link:user-upload.html#manual_replacement_mapping[Manual Replacement Mapping].
-+
-False means pushing to `refs/changes/` is prohibited.
-+
-Defaults to false.
-
 [[receive.certNonceSeed]]receive.certNonceSeed::
 +
 If set to a non-empty value and server-side signed push validation is
@@ -3960,6 +4042,14 @@
 Defaults to link:#retry.timeout[`retry.timeout`]; unit suffixes are supported,
 and assumes milliseconds if not specified.
 
+[[retry.retryWithTraceOnFailure]]retry.retryWithTraceOnFailure::
++
+Whether Gerrit should automatically retry operations on failure with tracing
+enabled. The automatically generated traces can help with debugging. Please
+note that only some of the REST endpoints support automatic retry.
++
+By default this is set to false.
+
 [[rules]]
 === Section rules
 
@@ -4376,8 +4466,8 @@
 SSH-compression since git does not compress the ref announcement during
 handshake.
 +
-Compression can be especially useful when Gerrit slaves are being used
-for the larger clones and fetches and the master server mostly takes
+Compression can be especially useful when Gerrit replicas are being used
+for the larger clones and fetches and the primary server mostly takes
 small receive-packs.
 +
 By default, `false`.
@@ -4733,6 +4823,72 @@
 +
 By default 0.
 
+[[tracing]]
+=== Section tracing
+
+[[tracing.performanceLogging]]tracing.performanceLogging::
++
+Whether performance logging is enabled.
++
+When performance logging is enabled, performance events for some
+operations are collected in memory while a request is running. At the
+end of the request the performance events are handed over to the
+link:dev-plugins.html#performance-logger[PerformanceLogger] plugins.
+This means if performance logging is enabled, the memory footprint of
+requests is slightly increased.
++
+This setting has no effect if no
+link:dev-plugins.html#performance-logger[PerformanceLogger] plugins are
+installed, because then performance logging is always disabled.
++
+By default, true.
+
+[[tracing.traceid]]
+==== Subsection tracing.<trace-id>
+
+There can be multiple `tracing.<trace-id>` subsections to configure
+automatic tracing of requests. To be traced a request must match all
+conditions of one `tracing.<trace-id>` subsection. The subsection name
+is used as trace ID. Using this trace ID administrators can find
+matching log entries.
+
+[[tracing.traceid.requestType]]tracing.<trace-id>.requestType::
++
+Type of request for which request tracing should be always enabled (can
+be `GIT_RECEIVE`, `GIT_UPLOAD`, `REST` and `SSH`).
++
+May be specified multiple times.
++
+By default, unset (all request types are matched).
+
+[[tracing.traceid.requestUriPattern]]tracing.<trace-id>.requestUriPattern::
++
+Regular expression to match request URIs for which request tracing
+should be always enabled. Request URIs are only available for REST
+requests. Request URIs never include the '/a' prefix.
++
+May be specified multiple times.
++
+By default, unset (all request URIs are matched).
+
+[[tracing.traceid.account]]tracing.<trace-id>.account::
++
+Account ID of an account for which request tracing should be always
+enabled.
++
+May be specified multiple times.
++
+By default, unset (all accounts are matched).
+
+[[tracing.traceid.projectPattern]]tracing.<trace-id>.projectPattern::
++
+Regular expression to match project names for which request tracing
+should be always enabled.
++
+May be specified multiple times.
++
+By default, unset (all projects are matched).
+
 [[trackingid]]
 === Section trackingid
 
@@ -5086,6 +5242,89 @@
 +
 * link:config-themes.html[Themes]
 
+[[jgitConfig]]
+== File `etc/jgit.config`
+
+Gerrit uses the `$site_path/etc/jgit.config` file instead of the
+system-wide and user-global Git configuration for its runtime JGit
+configuration.
+
+Sample `etc/jgit.config` file:
+----
+[core]
+  trustFolderStat = false
+----
+
+[[jgit-gc]]
+=== Section gc
+
+Options in section gc are used when command link:cmd-gc.html[gerrit gc] is used
+or scheduled via options link:cmd-gc.html#gc.startTime[gc.startTime] and
+link:cmd-gc.html#gc.interval[gc.interval].
+
+[[gc.auto]]gc.auto::
++
+When there are approximately more than this many loose objects in the repository,
+auto gc will pack them. Some commands use this command to perform a light-weight
+garbage collection from time to time. The default value is 6700.
++
+Setting this to 0 disables not only automatic packing based on the number of
+loose objects, but any other heuristic auto gc will otherwise use to determine
+if there’s work to do, such as link:#gc.autoPackLimit[gc.autoPackLimit].
+
+[[gc.autodetach]]gc.autodetach::
++
+Makes auto gc run in a background thread. Default is `true`.
+
+[[gc.autopacklimit]]gc.autopacklimit::
++
+When there are more than this many packs that are not marked with `*.keep` file
+in the repository, auto gc consolidates them into one larger pack. The
+default value is 50. Setting this to 0 disables it. Setting `gc.auto` to 0 will
+also disable this.
+
+[[gc.packRefs]]gc.packRefs::
++
+This variable determines whether gc runs git pack-refs. The default is `true`.
+
+[[gc.reflogExpire]]gc.reflogExpire::
++
+Removes reflog entries older than this time; defaults to 90 days. The value "now"
+expires all entries immediately, and "never" suppresses expiration altogether.
+
+[[gc.reflogExpireUnreachable]]gc.reflogExpireUnreachable::
++
+Removes reflog entries older than this time and not reachable from the
+current tip; defaults to 30 days. The value "now" expires all entries immediately,
+and "never" suppresses expiration altogether.
+
+[[jgit-protocol]]
+=== Section protocol
+
+[[protocol.version]]protocol.version::
++
+If set, the server will accept requests from a client attempting to communicate
+using the specified protocol version. Otherwise communication falls back to version 0.
+If set in file `etc/jgit.config` this option will be used for all repositories of
+the site. It can be overridden for a given repository by configuring a different
+value in the repository's `config` file.
++
+Supported versions:
+0:: the original wire protocol.
+1:: the original wire protocol with the addition of a version string in the initial response from the server.
+2:: wire protocol version 2. Speeds up fetches from repositories with many refs by allowing the client
+    to specify which refs to list before the server lists them.
+
+[[jgit-receive]]
+=== Section receive
+
+[[receive.autogc]]receive.autogc::
++
+By default, `git-receive-pack` will run auto gc after receiving data from git-push and updating refs.
+You can stop it by setting this variable to `false`. This is recommended in gerrit to avoid the
+additional load this creates. Instead schedule gc using link:cmd-gc.html#gc.startTime[gc.startTime]
+and link:cmd-gc.html#gc.interval[gc.interval] or e.g. in a cron job that runs gc in a separate process.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-groups.txt b/Documentation/config-groups.txt
index 4db4cb3..afabbfc 100644
--- a/Documentation/config-groups.txt
+++ b/Documentation/config-groups.txt
@@ -103,7 +103,7 @@
 
 == Replication
 
-In a replicated setting (eg. backups and or master/slave
-configurations), all refs in the `All-Users` project must be copied
-onto all replicas, including `refs/groups/*`, `refs/meta/group-names`
-and `refs/sequences/groups`.
+In a replicated setting (eg. backups and or primary/replica configurations), all
+refs in the `All-Users` project on primary nodes must be copied onto all
+replicas, including `refs/groups/*`, `refs/meta/group-names` and
+`refs/sequences/groups`.
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index ff43520..193a96f 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -262,6 +262,12 @@
 
 Defaults to true.
 
+[[label_copyAnyScore]]
+=== `label.Label-Name.copyAnyScore`
+
+If true, any score for the label is copied forward when a new patch
+set is uploaded. Defaults to false.
+
 [[label_copyMinScore]]
 === `label.Label-Name.copyMinScore`
 
@@ -297,12 +303,13 @@
 [[label_copyAllScoresOnTrivialRebase]]
 === `label.Label-Name.copyAllScoresOnTrivialRebase`
 
-If true, all scores for the label are copied forward when a new patch
-set is uploaded that is a trivial rebase. A new patch set is considered
-as trivial rebase if the commit message is the same as in the previous
-patch set and if it has the same code delta as the previous patch set.
-This is the case if the change was rebased onto a different parent, or
-if the parent did not change at all.
+If true, all scores for the label are copied forward when a new patch set is
+uploaded that is a trivial rebase. A new patch set is considered to be trivial
+rebase if the commit message is the same as in the previous patch set and if it
+has the same diff (including context lines) as the previous patch set. This is
+the case if the change was rebased onto a different parent and that rebase did
+not require git to perform any conflict resolution, or if the parent did not
+change at all.
 
 This can be used to enable sticky approvals, reducing turn-around for
 trivial rebases prior to submitting a change.
@@ -313,13 +320,13 @@
 [[label_copyAllScoresIfNoCodeChange]]
 === `label.Label-Name.copyAllScoresIfNoCodeChange`
 
-If true, all scores for the label are copied forward when a new patch
-set is uploaded that has the same parent tree as the previous patch
-set and the same code delta as the previous patch set. This means only
-the commit message is different. This can be used to enable sticky
-approvals on labels that only depend on the code, reducing turn-around
-if only the commit message is changed prior to submitting a change.
-For the Verified label that is optionally installed by the
+If true, all scores for the label are copied forward when a new patch set is
+uploaded that has the same parent tree as the previous patch set and the same
+code diff (including context lines) as the previous patch set. This means only
+the commit message is different; the change hasn't even been rebased. This can
+be used to enable sticky approvals on labels that only depend on the code,
+reducing turn-around if only the commit message is changed prior to submitting a
+change. For the Verified label that is optionally installed by the
 link:pgm-init.html[init] site program this is enabled by default.
 
 Defaults to false.
diff --git a/Documentation/config-robot-comments.txt b/Documentation/config-robot-comments.txt
index 0077697..f5185a4 100644
--- a/Documentation/config-robot-comments.txt
+++ b/Documentation/config-robot-comments.txt
@@ -36,7 +36,6 @@
 
 == Limitations
 
-* Robot comments are not displayed in the web UI yet.
 * There is no support for draft robot comments, but robot comments are
   always published and visible to everyone who can see the change.
 
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt
index 6f3a32d..88de85e 100644
--- a/Documentation/config-sso.txt
+++ b/Documentation/config-sso.txt
@@ -43,9 +43,9 @@
 * `http://` -- trust all OpenID providers using the HTTP protocol
 * `https://` -- trust all OpenID providers using the HTTPS protocol
 
-To trust only Yahoo!:
+To trust only Launchpad:
 ----
-  git config --file $site_path/etc/gerrit.config auth.trustedOpenID https://me.yahoo.com
+  git config --file $site_path/etc/gerrit.config auth.trustedOpenID https://login.launchpad.net/+openid
 ----
 
 === Database Schema
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index 24932a8..cb953c1 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -122,6 +122,15 @@
 perform validation when an account is activated or deactivated via the Gerrit
 REST API or the Java extension API.
 
+[[review-comment-validation]]
+== Review comment validation
+
+
+The `CommentValidator` interface allows plugins to validate all review comments,
+i.e. inline comments, file comments and the review message. This works for the
+REST API, for `git push` when `--publish-comments` is used and for comments sent
+via email.
+
 
 GERRIT
 ------
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 3f9ff2e..10ee761 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -7,8 +7,9 @@
 
 * A Linux or macOS system (Windows is not supported at this time)
 * A JDK for Java 8|9|10|11|...
-* Python 2 or 3
-* Node.js
+* Python 3
+* link:https://github.com/nodesource/distributions/blob/master/README.md[Node.js (including npm)]
+* Bower (`sudo npm install -g bower`)
 * link:https://docs.bazel.build/versions/master/install.html[Bazel] -launched with
 link:https://github.com/bazelbuild/bazelisk[Bazelisk]
 * Maven
@@ -29,17 +30,31 @@
 [[java]]
 === Java
 
-[[java-10]]
-==== Java 10 support
+==== MacOS
 
-Java 10 (and newer) is supported through vanilla java toolchain
+On MacOS, ensure that "Java for MacOS X 10.5 Update 4" (or higher) is installed
+and that `JAVA_HOME` is set to the
+link:install.html#Requirements[required Java version].
+
+Java installations can typically be found in
+"/System/Library/Frameworks/JavaVM.framework/Versions".
+
+To check the installed version of Java, open a terminal window and run:
+
+`java -version`
+
+[[java-12]]
+==== Java 12 support
+
+Java 12 (and newer) is supported through vanilla java toolchain
 link:https://docs.bazel.build/versions/master/toolchains.html[Bazel option].
-To build Gerrit with Java 10 and newer, specify vanilla java toolchain and
+To build Gerrit with Java 12 and newer, specify vanilla java toolchain and
 provide the path to JDK home:
 
 ```
   $ bazel build \
-    --define=ABSOLUTE_JAVABASE=<path-to-java-10> \
+    --define=ABSOLUTE_JAVABASE=<path-to-java-12> \
+    --javabase=@bazel_tools//tools/jdk:absolute_javabase \
     --host_javabase=@bazel_tools//tools/jdk:absolute_javabase \
     --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
     --java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
@@ -51,7 +66,7 @@
 
 ```
   $ bazel test \
-    --define=ABSOLUTE_JAVABASE=<path-to-java-10> \
+    --define=ABSOLUTE_JAVABASE=<path-to-java-12> \
     --javabase=@bazel_tools//tools/jdk:absolute_javabase \
     --host_javabase=@bazel_tools//tools/jdk:absolute_javabase \
     --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
@@ -64,7 +79,7 @@
 
 ```
 $ cat << EOF > ~/.bazelrc
-build --define=ABSOLUTE_JAVABASE=<path-to-java-10>
+build --define=ABSOLUTE_JAVABASE=<path-to-java-12>
 build --javabase=@bazel_tools//tools/jdk:absolute_javabase
 build --host_javabase=@bazel_tools//tools/jdk:absolute_javabase
 build --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla
@@ -75,36 +90,24 @@
 Now, invoking Bazel with just `bazel build :release` would include
 all those options.
 
-Note that the follow option must be added to `container.javaOptions`
-in `$gerrit_site/etc/gerrit.config` to run Gerrit with Java 10|11|...:
+[[java-11]]
+==== Java 11 support
 
-```
-[container]
-  javaOptions = --add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED
-```
-
-[[java-9]]
-==== Java 9 support
-
-Java 9 is supported through alternative java toolchain
+Java 11 is supported through alternative java toolchain
 link:https://docs.bazel.build/versions/master/toolchains.html[Bazel option].
-The Java 9 support is backwards compatible. Java 8 is still the default.
-To build Gerrit with Java 9, specify JDK 9 java toolchain:
+To build Gerrit with Java 11, specify JDK 11 java toolchain:
 
 ```
   $ bazel build \
-      --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_java9 \
-      --java_toolchain=@bazel_tools//tools/jdk:toolchain_java9 \
+      --host_javabase=@bazel_tools//tools/jdk:remote_jdk11 \
+      --javabase=@bazel_tools//tools/jdk:remote_jdk11 \
+      --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_java11 \
+      --java_toolchain=@bazel_tools//tools/jdk:toolchain_java11 \
       :release
 ```
 
-Note that the follow option must be added to `container.javaOptions`
-in `$gerrit_site/etc/gerrit.config` to run Gerrit with Java 9:
-
-```
-[container]
-  javaOptions = --add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED
-```
+=== Node.js and npm packages
+See link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/README.md#installing-node_js-and-npm-packages[Installing Node.js and npm packages].
 
 [[build]]
 == Building on the Command Line
@@ -117,10 +120,6 @@
   bazel build gerrit
 ----
 
-[NOTE]
-PolyGerrit UI may require additional tools (such as npm). Please read
-the polygerrit-ui/README.md for more info.
-
 The output executable WAR will be placed in:
 
 ----
@@ -219,13 +218,6 @@
 Note that when building an individual plugin, the `core.zip` package
 is not regenerated.
 
-To build with all Error Prone warnings activated, run:
-
-----
-  bazel build --java_toolchain //tools:error_prone_warnings_toolchain //...
-----
-
-
 [[IDEs]]
 == Using an IDE.
 
@@ -254,7 +246,7 @@
 with BUILD Files` button of link:https://ij.bazel.build[Bazel plugin].
 
 [[documentation]]
-=== Documentation
+== Documentation
 
 To build only the documentation for testing or static hosting:
 
@@ -323,6 +315,12 @@
   bazel test --test_tag_filters=-docker //...
 ----
 
+To exclude tests that require very recent git client version:
+
+----
+  bazel test --test_tag_filters=-git-protocol-v2 //...
+----
+
 To ignore cached test results:
 
 ----
@@ -343,6 +341,7 @@
 * edit
 * elastic
 * git
+* git-protocol-v2
 * git-upload-archive
 * notedb
 * pgm
@@ -419,16 +418,16 @@
 
 == Building against unpublished Maven JARs
 
-To build against unpublished Maven JARs, like gwtorm or PrologCafe, the custom
-JARs must be installed in the local Maven repository (`mvn clean install`) and
+To build against unpublished Maven JARs, like PrologCafe, the custom JARs must
+be installed in the local Maven repository (`mvn clean install`) and
 `maven_jar()` must be updated to point to the `MAVEN_LOCAL` Maven repository for
 that artifact:
 
 [source,python]
 ----
  maven_jar(
-   name = 'gwtorm',
-   artifact = 'gwtorm:gwtorm:42',
+   name = 'prolog-runtime',
+   artifact = 'com.googlecode.prolog-cafe:prolog-runtime:42',
    repository = MAVEN_LOCAL,
  )
 ----
@@ -475,11 +474,18 @@
  )
 ----
 
-[[consume-jgit-from-development-tree]]
+== Building against SNAPSHOT Maven JARs
 
-To consume the JGit dependency from the development tree, edit
-`lib/jgit/jgit.bzl` setting LOCAL_JGIT_REPO to a directory holding a
-JGit repository.
+To build against SNAPSHOT Maven JARs, the complete SNAPSHOT version must be used:
+
+[source,python]
+----
+ maven_jar(
+   name = "pac4j-core",
+   artifact = "org.pac4j:pac4j-core:3.5.0-SNAPSHOT-20190112.120241-16",
+   sha1 = "da2b1cb68a8f87bfd40813179abd368de9f3a746",
+ )
+----
 
 [[bazel-local-caches]]
 
@@ -489,6 +495,10 @@
 * ~/.gerritcodereview/bazel-cache/repository
 * ~/.gerritcodereview/bazel-cache/cas
 
+The `downloaded-artifacts` cache can be relocated by setting the
+`GERRIT_CACHE_HOME` environment variable. The other two can be adjusted with
+`bazel build` options `--repository_cache` and `--disk_cache` respectively.
+
 Currently none of these caches have a maximum size limit. See
 link:https://github.com/bazelbuild/bazel/issues/5139[this bazel issue] for
 details. Users should watch the cache sizes and clean them manually if
diff --git a/Documentation/dev-build-plugins.txt b/Documentation/dev-build-plugins.txt
index 60c6b9d..219861f 100644
--- a/Documentation/dev-build-plugins.txt
+++ b/Documentation/dev-build-plugins.txt
@@ -75,6 +75,12 @@
 Some plugins describe their build process in `src/main/resources/Documentation/build.md`
 file. It may worth checking.
 
+=== Error Prone checks
+
+Error Prone checks are enabled by default for core Gerrit and all core plugins. To
+enable the checks for custom plugins, add it in the `error_prone_packages` group
+in `tools/BUILD`.
+
 === Plugins with external dependencies ===
 
 If the plugin has external dependencies, then they must be included from Gerrit's
diff --git a/Documentation/dev-cla.txt b/Documentation/dev-cla.txt
new file mode 100644
index 0000000..5bc34a7
--- /dev/null
+++ b/Documentation/dev-cla.txt
@@ -0,0 +1,26 @@
+= Gerrit Code Review - Contributor License Agreement
+
+In order to link:dev-community.html#how-to-contribute[contribute] to
+Gerrit a Contributor License Agreement must be completed before
+contributions are accepted. To view and accept the agreements do the
+following:
+
+. Click 'Sign In' at the top right corner of
+  https://gerrit-review.googlesource.com/
+. Sign In with your Google account
+. After signing in, go to the
+  link:https://gerrit-review.googlesource.com/#/settings/agreements[Agreements]
+  tab on the settings page
+. Click on 'New Contributor Agreement' and follow the instructions
+
+For reference, the actual agreements are linked below:
+
+* link:https://cla.developers.google.com/about/google-individual[Individual Agreement]
+* link:https://cla.developers.google.com/about/google-corporate[Corporate Agreement]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-community.txt b/Documentation/dev-community.txt
new file mode 100644
index 0000000..4fb025f
--- /dev/null
+++ b/Documentation/dev-community.txt
@@ -0,0 +1,70 @@
+= Gerrit Community
+
+Gerrit is developed as a
+link:https://gerrit-review.googlesource.com/[self-hosting open source project]
+and very much welcomes contributions from anyone with a
+link:dev-cla.html[contributor's agreement] on file with the project.
+
+[[project-information]]
+== Project Information
+
+* link:https://www.gerritcodereview.com/[Project Homepage]
+* link:https://www.gerritcodereview.com/codeofconduct.html[Code of Conduct]
+* link:https://www.gerritcodereview.com/releases-readme.html[Release Versions]
+* link:https://gerrit.googlesource.com/gerrit[Source]
+* link:https://bugs.chromium.org/p/gerrit/issues/list[Issue Tracking]
+* link:https://gerrit-review.googlesource.com/q/status:open+project:gerrit[Change Review]
+* link:dev-design.html[System Design]
+* Processes
+** link:dev-processes.html#project-governance[Project Governance / Engineering Steering Committee]
+** link:#how-to-contribute[Contribution Processes]
+** link:dev-design-docs.html#review[Design doc reviews]
+** link:dev-processes.html#dev-in-stable-branches[Development in stable branches]
+** link:dev-processes.html#backporting[Backporting to stable branches]
+** link:dev-processes.html#security-issues[Dealing with Security Issues]
+** link:dev-processes.html#upgrading-libraries[Upgrading Libraries]
+** link:dev-processes.html#deprecating-features[Deprecating features]
+* Roles
+** link:dev-roles.html#supporter[Supporter]
+** link:dev-roles.html#contributor[Contributor]
+** link:dev-roles.html#maintainer[Maintainer]
+** link:dev-roles.html#mentor[Mentor]
+** link:dev-roles.html#steering-committee-member[Engineering Steering Committee Member]
+** link:dev-roles.html#community-manager[Community Manager]
+** link:dev-roles.html#release-manager[Release Manager]
+
+[[how-to-contribute]]
+== How to Contribute
+* link:dev-cla.html[Contributor License Agreement]
+* link:dev-contributing.html#contribution-processes[Contribution Processes]
+** link:dev-contributing.html#lightweight-contribution-process[Lightweight Contribution Process]
+** link:dev-contributing.html#design-driven-contribution-process[Design-Driven Contribution Process]
+** link:dev-contributing.html#mentorship[Mentorship]
+* link:dev-design-docs.html[Design Docs]
+* link:dev-readme.html[Developer Setup]
+* link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui[Polymer Frontend Developer Setup]
+* link:dev-crafting-changes.html[Crafting Changes]
+* link:dev-starter-projects.html[Starter Projects]
+
+[[plugin-development]]
+== Plugin Development
+* link:dev-plugins-lifecycle.html[Plugin Lifecycle]
+* link:dev-plugins.html[Developing Plugins]
+* link:dev-build-plugins.html[Building Gerrit plugins]
+* link:js-api.html[JavaScript Plugin API]
+* link:config-validation.html[Validation Interfaces]
+* link:dev-stars.html[Starring Changes]
+* link:quota.html[Quota Enforcement]
+
+[[maintainer]]
+== Maintainer
+* link:dev-release.html[Making a Gerrit Release]
+* link:dev-release-subproject.html[Making a Release of a Gerrit Subproject]
+* link:https://www.gerritcodereview.com/publishing.html[Publish Gerrit Homepage]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 0a7c100..49f8fcf 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -1,410 +1,331 @@
 = Gerrit Code Review - Contributing
 
-== Introduction
-Gerrit is developed as a
-link:https://gerrit-review.googlesource.com/[self-hosting open source project]
-and very much welcomes contributions from anyone with a contributor's
-agreement on file with the project.
-
+[[cla]]
 == Contributor License Agreement
-A Contributor License Agreement must be completed before contributions
-are accepted.  To view and accept the agreements do the following:
 
-* Click 'Sign In' at the top right corner of https://gerrit-review.googlesource.com/
-* Sign In with your Google account
-* After signing in, go to the
-link:https://gerrit-review.googlesource.com/#/settings/agreements[Agreements]
-tab on the settings page
-* Click 'New Contributor Agreement' and follow the instructions
+In order to contribute to Gerrit a link:dev-cla.html[Contributor
+License Agreement] must be completed before contributions are accepted.
 
-For reference, the actual agreements are linked below:
+[[contribution-processes]]
+== Contribution Processes
 
-* link:https://cla.developers.google.com/about/google-individual[Individual Agreement]
-* link:https://cla.developers.google.com/about/google-corporate[Corporate Agreement]
+The Gerrit project offers two contribution processes:
 
-== Code Review
+* link:#lightweight-contribution-process[Lightweight Contribution
+  Process]
+* link:#design-driven-contribution-process[Design-Driven Contribution
+  Process]
+
+The lightweight contribution process has little overhead and is best
+suited for small contributions (documentation updates, bug fixes, small
+features). Contributions are pushed as changes and
+link:dev-roles.html#maintainer[maintainers] review them adhoc.
+
+For large/complex features, it is required to follow the
+link:#design-driven-contribution-process[design-driven contribution
+process] and specify the feature in a link:dev-design-docs.html[design
+doc] before starting with the implementation.
+
+If link:dev-roles.html#contributor[contributors] choose the lightweight
+contribution process and during the review it turns out that the feature
+is too large or complex, link:dev-roles.html#maintainer[maintainers] can
+require to follow the design-driven contribution process instead.
+
+If you are in doubt which process is right for you, consult the
+link:https://groups.google.com/d/forum/repo-discuss[repo-discuss]
+mailing list.
+
+These contribution processes apply to everyone who contributes code to
+the Gerrit project, including link:dev-roles.html#maintainer[maintainers].
+When reading this document, keep in mind that maintainers are also
+contributors when they contribute code.
+
+If a new feature is large or complex, it is often difficult to find a
+maintainer who can take the time that is needed for a thorough review,
+and who can help with getting the changes submitted. To avoid that this
+results in unpredictable long waiting times during code review,
+contributors can ask for link:#mentorship[mentor support]. A mentor
+helps with timely code reviews and technical guidance. Doing the
+implementation is still the responsibility of the contributor.
+
+[[comparison]]
+=== Quick Comparison
+
+[options="header"]
+|======================
+|        |Lightweight Contribution Process|Design-Driven Contribution Process
+|Overhead|low (write good commit message, address review comments)|
+high (write link:dev-design-docs.html[design doc] and get it approved)
+|Technical Guidance|by reviewer|during the design review and by
+reviewer/mentor
+|Review  |adhoc (when reviewer is available)|by a dedicated mentor (if
+a link:#mentorship[mentor] was assigned)
+|Caveats |features may get vetoed after the implementation was already
+done, maintainers may make the design-driven contribution process
+required if a change gets too complex/large|design doc must stay open
+for a minimum of 10 calendar days, a mentor may not be available
+immediately
+|Applicable to|documentation updates, bug fixes, small features|
+large/complex features
+|======================
+
+[[lightweight-contribution-process]]
+=== Lightweight Contribution Process
+
+The lightweight contribution process has little overhead and is best
+suited for small contributions (documentation updates, bug fixes, small
+features). For large/complex features the
+link:#design-driven-contribution-process[design-driven contribution
+process] is required.
+
 As Gerrit is a code review tool, naturally contributions will
 be reviewed before they will get submitted to the code base.  To
 start your contribution, please make a git commit and upload it
-for review to the main Gerrit review server.  To help speed up the
-review of your change, review these guidelines before submitting
-your change.  You can view the pending Gerrit contributions and
-their statuses
+for review to the link:https://gerrit-review.googlesource.com/[
+gerrit-review.googlesource.com] Gerrit server.  To help speed up the
+review of your change, review these link:dev-crafting-changes.html[
+guidelines] before submitting your change.  You can view the pending
+Gerrit contributions and their statuses
 link:https://gerrit-review.googlesource.com/#/q/status:open+project:gerrit[here].
 
 Depending on the size of that list it might take a while for
 your change to get reviewed.  Naturally there are fewer
-approvers than contributors; so anything that you can do to
-ensure that your contribution will undergo fewer revisions
-will speed up the contribution process.  This includes helping
-out reviewing other people's changes to relieve the load from
-the approvers.  Even if you are not familiar with Gerrit's
-internals, it would be of great help if you can download, try
-out, and comment on new features.  If it works as advertised,
-say so, and if you have the privileges to do so, go ahead
-and give it a +1 Verified.  If you would find the feature
-useful, say so and give it a +1 code review.
+link:dev-roles.html#maintainer[maintainers], that can approve changes,
+than link:dev-roles.html#contributor[contributors];
+so anything that you can do to ensure that your contribution will undergo fewer
+revisions will speed up the contribution process.  This includes
+helping out reviewing other people's changes to relieve the load from
+the maintainers.  Even if you are not familiar with Gerrit's internals,
+it would be of great help if you can download, try out, and comment on
+new features.  If it works as advertised, say so, and if you have the
+privileges to do so, go ahead and give it a `+1 Verified`.  If you
+would find the feature useful, say so and give it a `+1 Code Review`.
 
-And finally, the quicker you respond to the comments of your
-reviewers, the quicker your change might get merged!  Try to
-reply to every comment after submitting your new patch,
-particularly if you decided against making the suggested change.
-Reviewers don't want to seem like nags and pester you if you
-haven't replied or made a fix, so it helps them know if you
-missed it or decided against it.
+And finally, the quicker you respond to the comments of your reviewers,
+the quicker your change might get merged!  Try to reply to every
+comment after submitting your new patch, particularly if you decided
+against making the suggested change. Reviewers don't want to seem like
+nags and pester you if you haven't replied or made a fix, so it helps
+them know if you missed it or decided against it.
 
+[[design-driven-contribution-process]]
+=== Design-driven Contribution Process
 
-== Review Criteria
+The design-driven contribution process applies to large/complex
+features.
 
-Here are some hints as to what approvers may be looking for
-before approving or submitting changes to the Gerrit project.
-Let's start with the simple nit picky stuff.  You are likely
-excited that your code works; help us share your excitement
-by not distracting us with the simple stuff.  Thanks to Gerrit,
-problems are often highlighted and we find it hard to look
-beyond simple spacing issues.  Blame it on our short attention
-spans, we really do want your code.
+For large/complex features it is important to:
 
+* agree on the functionality and scope before spending too much time
+  on the implementation
+* ensure that they are in line with Gerrit's project scope and vision
+* ensure that they are well aligned with other features
+* think about possibilities how the feature could be evolved over time
 
-[[commit-message]]
-=== Commit Message
+This is why for large/complex features it is required to describe the
+feature in a link:dev-design-docs.html[design doc] and get it approved
+by the link:dev-processes.html#steering-committee[steering committee],
+before starting the implementation.
 
-It is essential to have a good commit message if you want your
-change to be reviewed.
+The design-driven contribution process has the following steps:
 
-  * Keep lines no longer than 72 chars
-  * Start with a short one line summary
-  * Followed by a blank line
-  * Followed by one or more explanatory paragraphs
-  * Use the present tense (fix instead of fixed)
-  * Use the past tense when describing the status before this commit
-  * Include a `Bug: Issue <#>` line if fixing a Gerrit issue, or a
-    `Feature: Issue <#>` line if implementing a feature request.
-  * Include a `Change-Id` line
+* A link:dev-roles.html#contributor[contributor]
+  link:dev-design-docs.html#propose[proposes] a new feature by uploading
+  a change with a link:dev-design-docs.html[design doc].
+* The design doc is link:dev-design-docs.html#review[reviewed]
+  by interested parties from the community. The design review is public
+  and everyone can comment and raise concerns.
+* Design docs should stay open for a minimum of 10 calendar days so
+  that everyone has a fair chance to join the review.
+* Within 14 calendar days the contributor should hear back from the
+  link:dev-processes.html#steering-committee[steering committee]
+  whether the proposed feature is in scope of the project and if it can
+  be accepted.
+* To be submitted, the design doc needs to be approved by the
+  link:dev-processes.html#steering-committee[steering committee].
+* After the design was approved, the implementation is done by pushing
+  changes for review, see link:#lightweight-contribution-process[
+  lightweight contribution process]. Changes that are associated with
+  a design should all share a common hashtag. The contributor is the
+  main driver of the implementation and responsible that it is done.
+  Others from the Gerrit community are usually much welcome to help
+  with the implementation.
 
-=== Setting up Vim for Git commit message
+In order to be accepted/submitted, it is not necessary that the design
+doc fully specifies all the details, but the idea of the feature and
+how it fits into Gerrit should be sufficiently clear (judged by the
+steering committee). Contributors are expected to keep the design doc
+updated and fill in gaps while they go forward with the implementation.
+We expect that implementing the feature and updating the design doc
+will be an iterative process.
 
-Git uses Vim as the default commit message editor. Put this into your
-`$HOME/.vimrc` file to configure Vim for Git commit message formatting
-and writing:
+While the design doc is still in review, contributors may already start
+with the implementation (e.g. do some prototyping to demonstrate parts
+of the proposed design), but those changes should not be submitted
+while the design wasn't approved yet.
 
-====
-  " Enable spell checking, which is not on by default for commit messages.
-  au FileType gitcommit setlocal spell
+By approving a design, the steering committee commits to:
 
-  " Reset textwidth if you've previously overridden it.
-  au FileType gitcommit setlocal textwidth=72
-====
+* Accepting the feature when it is implemented.
+* Supporting the feature by assigning a link:dev-roles.html#mentor[
+  mentor] (if requested, see link:#mentorship[mentorship]).
 
+If the implementation of a feature gets stuck and it's unclear whether
+the feature gets fully done, it should be discussed with the steering
+committee how to proceed. If the contributor cannot commit to finish
+the implementation and no other contributor can take over, changes that
+have already been submitted for the feature might get reverted so that
+there is no unused or half-finished code in the code base.
 
-[[git_commit_settings]]
-=== A sample good Gerrit commit message:
-====
-  Add sample commit message to guidelines doc
+For contributors, the design-driven contribution process has the
+following advantages:
 
-  The original patch set for the contributing guidelines doc did not
-  include a sample commit message, this new patchset does.  Hopefully this
-  makes things a bit clearer since examples can sometimes help when
-  explanations don't.
+* By writing a design doc, the feature gets more attention. During the
+  design review, feedback from various sides can be collected, which
+  likely leads to improvements of the feature.
+* Once a design was approved by the
+  link:dev-processes.html#steering-committee[steering committee],
+  the contributor can be almost certain that the feature will be accepted.
+  Hence, there is only a low risk to invest into implementing a feature
+  and see it being rejected later during the code review, as it can
+  happen with the lightweight contribution process.
+* The contributor can link:#mentorship[get a dedicated mentor assigned]
+  who provides timely reviews and serves as a contact person for
+  technical questions and discussing details of the design.
 
-  Note that the body of this commit message can be several paragraphs, and
-  that I word wrap it at 72 characters.  Also note that I keep the summary
-  line under 50 characters since it is often truncated by tools which
-  display just the git summary.
+[[mentorship]]
+== Mentorship
 
-  Bug: Issue 98765605
-  Change-Id: Ic4a7c07eeb98cdeaf44e9d231a65a51f3fceae52
-====
+For features for which a link:dev-design-docs.html[design]
+has been approved (see link:#design-driven-contribution-process[design-driven
+contribution process]), contributors can gain the support of a mentor
+if they are committed to implement the feature.
 
-The `Change-Id` line is, as usual, created by a local git hook.  To install it,
-simply copy it from the checkout and make it executable:
+A link:dev-roles.html#mentor[mentor] helps with:
 
-====
-  cp ./gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg .git/hooks/
-  chmod +x .git/hooks/commit-msg
-====
+* doing timely reviews
+* providing technical guidance during code reviews
+* discussing details of the design
+* ensuring that the quality standards are met (well documented,
+  sufficient test coverage, backwards compatible etc.)
 
-If you are working on core plugins, you will also need to install the
-same hook in the submodules:
+A feature can have more than one mentor. To be able to deliver the
+promised support, at least one of the mentors must be a
+link:dev-roles.html#maintainer[maintainer].
 
-====
-  export hook=$(pwd)/.git/hooks/commit-msg
-  git submodule foreach 'cp -p "$hook" "$(git rev-parse --git-dir)/hooks/"'
-====
+Mentors are assigned by the link:dev-processes.html#steering-committee[
+steering committee]. To gain a mentor, ask for a
+mentor in the link:dev-design-doc-template.html#implementation-plan[Implementation
+Plan] section of the design doc or ask the steering
+committee after the design has been approved.
 
+Mentors may not be available immediately. In this case, the steering
+committee should include the approved feature into the roadmap or
+prioritize it in the backlog. This way, it is transparent for the
+contributor when they can expect to be able to work on the feature with
+mentor support.
 
-To set up git's remote for easy pushing, run the following:
+Once the implementation phase starts, the contributor is expected to do
+the implementation in a timely manner.
 
-====
-  git remote add gerrit https://gerrit.googlesource.com/gerrit
-====
+For every mentorship, the end must be clearly defined. The design doc
+must specify:
 
-The HTTPS access requires proper username and password; this can be obtained
-by clicking the 'Obtain Password' link on the
-link:https://gerrit-review.googlesource.com/#/settings/http-password[HTTP
-Password tab of the user settings page].
+* a maximum time frame for the mentorship, after which the mentorship
+  automatically ends, even if the feature is not done yet
+* done criteria that define when the feature is done and the mentorship
+  ends
 
-Alternately, you may use the
-link:https://pypi.org/project/git-review/[git-review] tool to submit changes
-to Gerrit. If you do, it will set up the Change-Id hook and `gerrit` remote
-for you. You will still need to do the HTTP access step.
+If a feature is not finished in time, it should be discussed with the
+steering committee how to proceed. If the contributor cannot commit to
+finish the implementation in time and no other contributor can take
+over, changes that have already been submitted for the feature might
+get reverted so that there is no unused or half-finished code in the
+code base.
 
-[[style]]
-=== Style
+[[esc-dd-evaluation]]
+== How the ESC evaluates design documents
+This section describes how the ESC evaluates design documents. It’s
+meant as a guideline rather than being prescriptive for both ESC
+members and contributors.
 
-This project has a policy of Eclipse's warning free code. Eclipse
-configuration is added to git and we expect the changes to be
-warnings free.
+=== General Process
+As part of the design process, the ESC makes a final decision if a
+design gets to be implemented. If there are multiple alternative
+solutions, the ESC will decide which solution can be implemented.
 
-We do not ask you to use Eclipse for editing, obviously.  We do ask you
-to provide Eclipse's warning free patches only. If for some reasons, you
-are not able to set up Eclipse and verify, that your patch hasn't
-introduced any new Eclipse warnings, mention this in a comment to your
-change, so that reviewers will do it for you. Yes, the way to go is to
-extend gerrit CI to take care of this, but it's not yet implemented.
+The ESC should wait until all contributors had the chance to
+voice their opinion in review comments or by proposing alternative
+solutions. Due to the infrequent ESC meetings (every 2-4 weeks)
+the ESC might discuss documents in cases where the discussion is
+already advanced far enough, but not make a decision yet. In this
+case, contributors can still voice concerns or discuss alternatives.
+The decision can be at the next meeting or via email in between
+meetings.
 
-Gerrit follows the
-link:https://google.github.io/styleguide/javaguide.html[Google Java Style
-Guide].
+=== Evaluation
+Product/Vision fit
 
-To format Java source code, Gerrit uses the
-link:https://github.com/google/google-java-format[`google-java-format`]
-tool (version 1.7), and to format Bazel BUILD, WORKSPACE and .bzl files the
-link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`]
-tool (version 3.5.0). Unused dependencies are found and removed using the
-link:https://github.com/bazelbuild/buildtools/tree/master/unused_deps[`unused_deps`]
-build tool, a sibling of `buildifier`.
+Q: `Do we believe this feature belongs to Gerrit Code Review use-cases?`
 
-These tools automatically apply format according to the style guides; this
-streamlines code review by reducing the need for time-consuming, tedious,
-and contentious discussions about trivial issues like whitespace.
-
-You may download and run `google-java-format` on your own, or you may
-run `./tools/setup_gjf.sh` to download a local copy and set up a
-wrapper script. If you run your own copy, please use the same version,
-as there may be slight differences between versions.
-
-When to use `final` modifier and when not (in new code):
-
-Always:
-
-  * final fields: marking fields as final forces them to be
-  initialized in the constructor or at declaration
-  * final static fields: clearly communicates the intent
-  * to use final variables in inner anonymous classes
-
-Optional:
-
-  * final classes: use when appropriate, e.g. API restriction
-  * final methods: similar to final classes
-
-Never:
-
-  * local variables: it clutters the code, and makes the code less
-  readable. When copying old code to new location, finals should
-  be removed
-  * method parameters: similar to local variables
-
-=== Code Organization
-
-Do your best to organize classes and methods in a logical way.
-Here are some guidelines that Gerrit uses:
-
-  * Ensure a standard copyright header is included at the top
-    of any new files (copy it from another file, update the year).
-  * Always place loggers first in your class!
-  * Define any static interfaces next in your class.
-  * Define non static interfaces after static interfaces in your
-    class.
-  * Next you should define static types, static members, and
-    static methods, in decreasing order of visibility (public to private).
-  * Finally instance types, instance members, then constructors,
-    and then instance methods.
-  * Some common exceptions are private helper static methods, which
-    might appear near the instance methods which they help (but may
-    also appear at the top).
-  * Getters and setters for the same instance field should usually
-    be near each other barring a good reason not to.
-  * If you are using assisted injection, the factory for your class
-    should be before the instance members.
-  * Annotations should go before language keywords (`final`, `private`, etc) +
-    Example: `@Assisted @Nullable final type varName`
-  * Prefer to open multiple AutoCloseable resources in the same
-    try-with-resources block instead of nesting the try-with-resources
-    blocks and increasing the indentation level more than necessary.
-
-Wow that's a lot!  But don't worry, you'll get the habit and most
-of the code is organized this way already; so if you pay attention
-to the class you are editing you will likely pick up on it.
-Naturally new classes are a little harder; you may want to come
-back and consult this section when creating them.
-
-
-=== Design
-
-Here are some design level objectives that you should keep in mind
-when coding:
-
-  * Most client pages should perform only one RPC to load so as to
-    keep latencies down.  Exceptions would apply to RPCs which need
-    to load large data sets if splitting them out will help the
-    page load faster.  Generally page loads are expected to complete
-    in under 100ms.  This will be the case for most operations,
-    unless the data being fetched is not using Gerrit's caching
-    infrastructure.  In these slower cases, it is worth considering
-    mitigating this longer load by using a second RPC to fill in
-    this data after the page is displayed (or alternatively it might
-    be worth proposing caching this data).
-  * `@Inject` should be used on constructors, not on fields.  The
-    current exceptions are the ssh commands, these were implemented
-    earlier in Gerrit's development.  To stay consistent, new ssh
-    commands should follow this older pattern; but eventually these
-    should get converted to eliminate this exception.
-  * Don't leave repository objects (git or schema) open.  A .close()
-    after every open should be placed in a finally{} block.
-  * Don't leave UI components, which can cause new actions to occur,
-    enabled during RPCs which update Git repositories, including NoteDb.
-    This is to prevent people from submitting actions more than once
-    when operating on slow links.  If the action buttons are disabled,
-    they cannot be resubmitted and the user can see that Gerrit is still
-    busy.
-  * ...and so is Guava (previously known as Google Collections).
-
-
-=== Tests
-
-  * Tests for new code will greatly help your change get approved.
-
-
-=== Change Size/Number of Files Touched
-
-And finally, I probably cannot say enough about change sizes.
-Generally, smaller is better, hopefully within reason.  Do try to
-keep things which will be confusing on their own together,
-especially if changing one without the other will break something!
-
-  * If a new feature is implemented and it is a larger one, try to
-    identify if it can be split into smaller logical features; when
-    in doubt, err on the smaller side.
-  * Separate bug fixes from feature improvements.  The bug fix may
-    be an easy candidate for approval and should not need to wait
-    for new features to be approved.  Also, combining the two makes
-    reviewing harder since then there is no clear line between the
-    fix and the feature.
-  * Separate supporting refactoring from feature changes.  If your
-    new feature requires some refactoring, it helps to make the
-    refactoring a separate change which your feature change
-    depends on.  This way, reviewers can easily review the refactor
-    change as a something that should not alter the current
-    functionality, and feel more confident they can more easily
-    spot errors this way.  Of course, it also makes it easier to
-    test and locate later on if an unfortunate error does slip in.
-    Lastly, by not having to see refactoring changes at the same
-    time, it helps reviewers understand how your feature changes
-    the current functionality.
-  * Separate logical features into separate changes.  This
-    is often the hardest part.  Here is an example:  when adding a
-    new ability, make separate changes for the UI and the ssh
-    commands if possible.
-  * Do only what the commit message describes.  In other words, things which
-    are not strictly related to the commit message shouldn't be part of
-    a change, even trivial things like externalizing a string somewhere
-    or fixing a typo.  This helps keep `git blame` more useful in the future
-    and it also makes `git revert` more useful.
-  * Use topics to link your separate changes together.
-
-[[process]]
-== Process
-
-[[dev-in-stable-branches]]
-=== Development in stable branches
-
-As their name suggests stable branches are intended to be stable. This means that generally
-only bug-fixes should be done on stable branches, however this is not strictly enforced and
-exceptions may apply:
+* Yes: Customizable dashboards
+* No: UI for managing an LDAP server
 
-  * When a stable branch is initially created to prepare a new release the Gerrit community
-    discusses on the mailing list if there are pending features which should still make it into the
-    release. Those features are blocking the release and should be implemented on the stable
-    branch before the first release candidate is created.
-  * To stabilize the code before doing a major release several release candidates are created. Once
-    the first release candidate was done no more features should be accepted on the stable branch.
-    If more features are found to be required they should be discussed with the Gerrit maintainers
-    and should only be allowed if the risk of breaking things is considered to be low.
-  * Once a major release is done only bug-fixes and documentation updates should be done on the
-    stable branch. These updates will be included in the next minor release.
-  * For minor releases new features are only acceptable if they are important to the Gerrit
-    community, if they are backwards compatible and the risk of breaking things is low and if there
-    are no objections from the Gerrit community.
-  * In cases of doubt it's the responsibility of the release maintainer to evaluate the risk of new
-    features and make a decision based on these rules and opinions from the Gerrit community.
-  * The older a stable branch is the more stable it should be. This means old stable branches
-    should only receive bug-fixes that are either important or low risk. Security fixes, including
-    security updates for third party dependencies, are always considered as important and hence can
-    always be done on stable branches.
+Q: `Is the proposed solution aligned with Gerrit’s vision?`
 
-=== Backporting to stable branches
+* Yes: Showing comments of older patch sets in newer patch sets to
+  keep track (core code review)
+* No: Implement a bug tracker in Gerrit (not core code review).
 
-From time to time bug fix releases are made for existing stable branches.
+=== Impact
+Q: `Will the new feature have a measurable, positive impact?`
 
-Developers concerned with stable branches are encouraged to backport or push fixes to these
-branches, even if no new release is planned. Backporting features is only possible in compliance
-with the rules link:#dev-in-stable-branches[above].
+* Yes: Increased productivity, faster/smoother workflow, etc.
+* Yes: Better latency, more reliable system.
+* No: Unclear impact or lacking metrics to measure.
 
-Fixes that are known to be needed for a particular release should be pushed for review on that
-release's stable branch. They will then be included into the master branch when the stable branch
-is merged back.
+=== Complexity
+Q: `Can we support/maintain this feature once it is in Gerrit?`
 
-=== Finding starter projects to work on
+* Yes: Code will fit into codebase, be well tested, easy to
+  understand.
+* No: Will add code or a workflow that is hard to understand
+  and easy to misinterpret.
 
-We have created a
-link:https://bugs.chromium.org/p/gerrit/issues/list?can=2&q=label%3AStarterProject[StarterProject]
-category in the issue tracker and try to assign easy hack projects to it. If in
-doubt, do not hesitate to ask on the developer
-link:https://groups.google.com/forum/#!forum/repo-discuss[mailing list].
+Q: `Is the proposed feature or rework adding unnecessary complexity?`
 
-=== Upgrading Libraries
+* Yes: Adding a dependency on a well-supported library.
+* No: Adding a dependency on a library that is not widely used
+  or not actively maintained.
 
-Gerrit's library dependencies should only be upgraded if the new version contains
-something we need in Gerrit. This includes new features, API changes as well as bug
-or security fixes.
-An exception to this rule is that right after a new Gerrit release was branched
-off, all libraries should be upgraded to the latest version to prevent Gerrit
-from falling behind. Doing those upgrades should conclude at the latest two
-months after the branch was cut. This should happen on the master branch to ensure
-that they are vetted long enough before they go into a release and we can be sure
-that the update doesn't introduce a regression.
+=== Core vs. Plugin decision
+Q: `Would this fit better in a plugin?`
 
-[[deprecating-features]]
-=== Deprecating features
+* Yes:The proposed feature or rework is an implementation (e.g. Lucene
+  is an index implementation) of a generic concept that others
+  might want to implement differently.
+* Yes: The proposed feature or rework is very specific to a custom setup.
+* No: The proposed feature or rework is applicable to a wider user
+  base.
+* No: The proposed feature or rework is a `core code review feature`.
 
-Gerrit should be as stable as possible and we aim to add only features that last.
-However, sometimes we are required to deprecate and remove features to be able
-to move forward with the project and keep the code-base clean. The following process
-should serve as a guideline on how to deprecate functionality in Gerrit. Its purpose
-is that we have a structured process for deprecation that users, administrators and
-developers can agree and rely on.
+=== Commitment
+Q: `Is someone willing to implement it?` (this question is
+especially important when reviewers propose alternative designs
+to the author’s own solution).
 
-General process:
+* Yes: The author or someone else commits to implementing the
+  proposed solution.
+* Yes: If a mentorship is required, a mentor is willing to help.
+* No: Unclear ownership, mentorship or implementation plan.
 
-  * Make sure that the feature (e.g. a field on the API) is not needed anymore or blocks
-    further development or improvement. If in doubt, consult the mailing list.
-  * If you can provide a schema migration that moves users to a comparable feature, do
-    so and stop here.
-  * Mark the feature as deprecated in the documentation and release notes.
-  * If possible, mark the feature deprecated in any user-visible interface. For example,
-    if you are deprecating a Git push option, add a message to the Git response if
-    the user provided the option informing them about deprecation.
-  * Annotate the code with `@Deprecated` and `@RemoveAfter(x.xx)` if applicable.
-    Alternatively, use `// DEPRECATED, remove after x.xx` (where x.xx is the version
-    number that has to be branched off before removing the feature)
-  * Gate the feature behind a config that is off by default (forcing admins to turn
-    the deprecated feature on explicitly).
-  * After the next release was branched off, remove any code that backed the feature.
+=== Community
+Q: `If in doubt, is there a substantial benefit to a long-standing
+community member with many users?`
 
-You can optionally consult the mailing list to ask if there are users of the feature you
-wish to deprecate. If there are no major users, you can remove the feature without
-following this process and without the grace period of one release.
+* The community shapes the future of Gerrit as a product. In
+  cases of doubt, the ESC can be more generous with long-standing
+  community members compared to `drive-by` contributions.
 
 GERRIT
 ------
diff --git a/Documentation/dev-crafting-changes.txt b/Documentation/dev-crafting-changes.txt
new file mode 100644
index 0000000..2bbbc45
--- /dev/null
+++ b/Documentation/dev-crafting-changes.txt
@@ -0,0 +1,306 @@
+= Gerrit Code Review - Crafting Changes
+
+Here are some hints as to what approvers may be looking for
+before approving or submitting changes to the Gerrit project.
+Let's start with the simple nit picky stuff.  You are likely
+excited that your code works; help us share your excitement
+by not distracting us with the simple stuff.  Thanks to Gerrit,
+problems are often highlighted and we find it hard to look
+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
+
+It is essential to have a good commit message if you want your
+change to be reviewed.
+
+  * Keep lines no longer than 72 chars
+  * Start with a short one line summary
+  * Followed by a blank line
+  * Followed by one or more explanatory paragraphs
+  * Use the present tense (fix instead of fixed)
+  * Use the past tense when describing the status before this commit
+  * Include a `Bug: Issue <#>` line if fixing a Gerrit issue, or a
+    `Feature: Issue <#>` line if implementing a feature request.
+  * Include a `Change-Id` line
+
+[[vim-setup]]
+=== Setting up Vim for Git commit message
+
+Git uses Vim as the default commit message editor. Put this into your
+`$HOME/.vimrc` file to configure Vim for Git commit message formatting
+and writing:
+
+====
+  " Enable spell checking, which is not on by default for commit messages.
+  au FileType gitcommit setlocal spell
+
+  " Reset textwidth if you've previously overridden it.
+  au FileType gitcommit setlocal textwidth=72
+====
+
+
+[[git-commit-settings]]
+=== A sample good Gerrit commit message:
+====
+  Add sample commit message to guidelines doc
+
+  The original patch set for the contributing guidelines doc did not
+  include a sample commit message, this new patchset does.  Hopefully this
+  makes things a bit clearer since examples can sometimes help when
+  explanations don't.
+
+  Note that the body of this commit message can be several paragraphs, and
+  that I word wrap it at 72 characters.  Also note that I keep the summary
+  line under 50 characters since it is often truncated by tools which
+  display just the git summary.
+
+  Bug: Issue 98765605
+  Change-Id: Ic4a7c07eeb98cdeaf44e9d231a65a51f3fceae52
+====
+
+The `Change-Id` line is, as usual, created by a local git hook.  To install it,
+simply copy it from the checkout and make it executable:
+
+====
+  cp ./gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg .git/hooks/
+  chmod +x .git/hooks/commit-msg
+====
+
+If you are working on core plugins, you will also need to install the
+same hook in the submodules:
+
+====
+  export hook=$(pwd)/.git/hooks/commit-msg
+  git submodule foreach 'cp -p "$hook" "$(git rev-parse --git-dir)/hooks/"'
+====
+
+
+To set up git's remote for easy pushing, run the following:
+
+====
+  git remote add gerrit https://gerrit.googlesource.com/gerrit
+====
+
+The HTTPS access requires proper username and password; this can be obtained
+by clicking the 'Obtain Password' link on the
+link:https://gerrit-review.googlesource.com/#/settings/http-password[HTTP
+Password tab of the user settings page].
+
+Alternately, you may use the
+link:https://pypi.org/project/git-review/[git-review] tool to submit changes
+to Gerrit. If you do, it will set up the Change-Id hook and `gerrit` remote
+for you. You will still need to do the HTTP access step.
+
+[[style]]
+== Style
+
+This project has a policy of Eclipse's warning free code. Eclipse
+configuration is added to git and we expect the changes to be
+warnings free.
+
+We do not ask you to use Eclipse for editing, obviously.  We do ask you
+to provide Eclipse's warning free patches only. If for some reasons, you
+are not able to set up Eclipse and verify, that your patch hasn't
+introduced any new Eclipse warnings, mention this in a comment to your
+change, so that reviewers will do it for you. Yes, the way to go is to
+extend gerrit CI to take care of this, but it's not yet implemented.
+
+Gerrit follows the
+link:https://google.github.io/styleguide/javaguide.html[Google Java Style
+Guide].
+
+To format Java source code, Gerrit uses the
+link:https://github.com/google/google-java-format[`google-java-format`]
+tool (version 1.7), and to format Bazel BUILD, WORKSPACE and .bzl files the
+link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`]
+tool (version 4.0.0). Unused dependencies are found and removed using the
+link:https://github.com/bazelbuild/buildtools/tree/master/unused_deps[`unused_deps`]
+build tool, a sibling of `buildifier`.
+
+These tools automatically apply format according to the style guides; this
+streamlines code review by reducing the need for time-consuming, tedious,
+and contentious discussions about trivial issues like whitespace.
+
+You may download and run `google-java-format` on your own, or you may
+run `./tools/setup_gjf.sh` to download a local copy and set up a
+wrapper script. If you run your own copy, please use the same version,
+as there may be slight differences between versions.
+
+[[code-rules]]
+== Code Rules
+=== Final
+When to use `final` modifier and when not (in new code):
+
+Always:
+
+  * final fields: marking fields as final forces them to be
+  initialized in the constructor or at declaration
+  * final static fields: clearly communicates the intent
+  * to use final variables in inner anonymous classes
+
+Optional:
+
+  * final classes: use when appropriate, e.g. API restriction
+  * final methods: similar to final classes
+
+Never:
+
+  * local variables: it clutters the code, and makes the code less
+  readable. When copying old code to new location, finals should
+  be removed
+  * method parameters: similar to local variables
+
+=== Optional / Nullable
+Recommended:
+
+  * Optionals in arguments are discouraged (use @Nullable instead)
+  * Return types should be objects or Optionals of objects, but not null/nullable
+
+[[code-organization]]
+== Code Organization
+
+Do your best to organize classes and methods in a logical way.
+Here are some guidelines that Gerrit uses:
+
+  * Ensure a standard copyright header is included at the top
+    of any new files (copy it from another file, update the year).
+  * Always place loggers first in your class!
+  * Define any static interfaces next in your class.
+  * Define non static interfaces after static interfaces in your
+    class.
+  * Next you should define static types, static members, and
+    static methods, in decreasing order of visibility (public to private).
+  * Finally instance types, instance members, then constructors,
+    and then instance methods.
+  * Some common exceptions are private helper static methods, which
+    might appear near the instance methods which they help (but may
+    also appear at the top).
+  * Getters and setters for the same instance field should usually
+    be near each other barring a good reason not to.
+  * If you are using assisted injection, the factory for your class
+    should be before the instance members.
+  * Annotations should go before language keywords (`final`, `private`, etc) +
+    Example: `@Assisted @Nullable final type varName`
+  * Prefer to open multiple AutoCloseable resources in the same
+    try-with-resources block instead of nesting the try-with-resources
+    blocks and increasing the indentation level more than necessary.
+
+Wow that's a lot!  But don't worry, you'll get the habit and most
+of the code is organized this way already; so if you pay attention
+to the class you are editing you will likely pick up on it.
+Naturally new classes are a little harder; you may want to come
+back and consult this section when creating them.
+
+[[design]]
+== Design
+
+Here are some design level objectives that you should keep in mind
+when coding:
+
+  * Most client pages should perform only one RPC to load so as to
+    keep latencies down.  Exceptions would apply to RPCs which need
+    to load large data sets if splitting them out will help the
+    page load faster.  Generally page loads are expected to complete
+    in under 100ms.  This will be the case for most operations,
+    unless the data being fetched is not using Gerrit's caching
+    infrastructure.  In these slower cases, it is worth considering
+    mitigating this longer load by using a second RPC to fill in
+    this data after the page is displayed (or alternatively it might
+    be worth proposing caching this data).
+  * `@Inject` should be used on constructors, not on fields.  The
+    current exceptions are the ssh commands, these were implemented
+    earlier in Gerrit's development.  To stay consistent, new ssh
+    commands should follow this older pattern; but eventually these
+    should get converted to eliminate this exception.
+  * Don't leave repository objects (git or schema) open.  Use a
+    try-with-resources statement to ensure that repository objects get
+    closed after use.
+  * Don't leave UI components, which can cause new actions to occur,
+    enabled during RPCs which update Git repositories, including NoteDb.
+    This is to prevent people from submitting actions more than once
+    when operating on slow links.  If the action buttons are disabled,
+    they cannot be resubmitted and the user can see that Gerrit is still
+    busy.
+
+[[tests]]
+== Tests
+
+  * Tests for new code will greatly help your change get approved.
+
+[[change-size]]
+== Change Size/Number of Files Touched
+
+And finally, I probably cannot say enough about change sizes.
+Generally, smaller is better, hopefully within reason.  Do try to
+keep things which will be confusing on their own together,
+especially if changing one without the other will break something!
+
+  * If a new feature is implemented and it is a larger one, try to
+    identify if it can be split into smaller logical features; when
+    in doubt, err on the smaller side.
+  * Separate bug fixes from feature improvements.  The bug fix may
+    be an easy candidate for approval and should not need to wait
+    for new features to be approved.  Also, combining the two makes
+    reviewing harder since then there is no clear line between the
+    fix and the feature.
+  * Separate supporting refactoring from feature changes.  If your
+    new feature requires some refactoring, it helps to make the
+    refactoring a separate change which your feature change
+    depends on.  This way, reviewers can easily review the refactor
+    change as a something that should not alter the current
+    functionality, and feel more confident they can more easily
+    spot errors this way.  Of course, it also makes it easier to
+    test and locate later on if an unfortunate error does slip in.
+    Lastly, by not having to see refactoring changes at the same
+    time, it helps reviewers understand how your feature changes
+    the current functionality.
+  * Separate logical features into separate changes.  This
+    is often the hardest part.  Here is an example:  when adding a
+    new ability, make separate changes for the UI and the ssh
+    commands if possible.
+  * Do only what the commit message describes.  In other words, things which
+    are not strictly related to the commit message shouldn't be part of
+    a change, even trivial things like externalizing a string somewhere
+    or fixing a typo.  This helps keep `git blame` more useful in the future
+    and it also makes `git revert` more useful.
+  * Use topics to link your separate changes together.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-design-doc-conclusion-template.md b/Documentation/dev-design-doc-conclusion-template.md
new file mode 100644
index 0000000..0625f2b
--- /dev/null
+++ b/Documentation/dev-design-doc-conclusion-template.md
@@ -0,0 +1,18 @@
+---
+title: "Design Doc - ${title} - Conclusion"
+sidebar: gerritdoc_sidebar
+permalink: design-doc-${folder-name}-conclusion.html
+hide_sidebar: true
+hide_navtoggle: true
+toc: false
+folder: design-docs/${folder-name}
+---
+
+# Conclusion
+
+Describe which decision was made and what were the reasons for it.
+
+## <a id="implementation-plan"> Implementation Plan
+
+If known, say who is driving the implementation, for when the
+implementation is planned and which priority it has.
diff --git a/Documentation/dev-design-doc-index-template.md b/Documentation/dev-design-doc-index-template.md
new file mode 100644
index 0000000..10b4a81
--- /dev/null
+++ b/Documentation/dev-design-doc-index-template.md
@@ -0,0 +1,18 @@
+---
+title: "Design Doc - ${title}"
+sidebar: gerritdoc_sidebar
+permalink: design-doc-${folder-name}.html
+hide_sidebar: true
+hide_navtoggle: true
+toc: false
+folder: design-docs/${folder-name}
+---
+
+# Design Doc - ${title}
+
+* [Use Cases](use-cases.html)
+* [Solution - ${solution-name-1}](solution-1.html)
+* [Solution - ${solution-name-2}](solution-2.html)
+* ...
+* [Conclusion](conclusion.html)
+
diff --git a/Documentation/dev-design-doc-solution-template.md b/Documentation/dev-design-doc-solution-template.md
new file mode 100644
index 0000000..8b2a8c0
--- /dev/null
+++ b/Documentation/dev-design-doc-solution-template.md
@@ -0,0 +1,72 @@
+---
+title: "Design Doc - ${title} - Solution - ${solution-name}"
+sidebar: gerritdoc_sidebar
+permalink: design-doc-${folder-name}-solution-${solution-name}.html
+hide_sidebar: true
+hide_navtoggle: true
+toc: false
+folder: design-docs/${folder-name}
+---
+
+# Solution - ${solution-name}
+
+## <a id="overview"> Overview
+
+High-level overview; put details in the next section and background in
+the 'Background' section (see dev-design-doc-use-cases-template.txt).
+
+Should be understandable by engineers that are not working on Gerrit.
+
+If a solution is a variant of another solution, that other solution
+should be linked here.
+
+## <a id="detailed-design"> Detailed Design
+
+How does the overall design work? Details about the algorithms,
+storage format, APIs, etc., should be included here.
+
+For the initial review, it is ok for this to lack implementation
+details of minor importance.
+
+### <a id="scalability"> Scalability
+
+How does the solution scale?
+
+If applicable, consider:
+
+* data size increase
+* traffic increase
+* effects on replication across sites (master-replica and master-master)
+
+## <a id="alternatives-considered"> Alternatives Considered
+
+Within the scope of this solution you may need to describe what you did
+not do or why simpler approaches don't work. Mention other things to
+watch out for (if any).
+
+Do not describe alternative solutions in this section, as each solution
+should be described in a separate file.
+
+## <a id="pros-and-cons"> Pros and Cons
+
+Objectively list all points that speak in favor/against this solution.
+
+## <a id="implementation-plan"> Implementation Plan
+
+If known, say who would be willing to drive the implementation.
+
+It is possible to contribute solutions without having resources to do
+the implementation. In this case, say so here.
+
+If mentor support is desired, say so here. Also briefly describe any
+circumstances that can help with finding a suitable mentor.
+
+## <a id="time-estimation"> Time Estimation
+
+A rough itemized estimation of how much time it takes to implement this
+feature. Break down the feature into work items and estimate each item
+separately.
+
+If a mentor is assigned, this section must define a maximum time frame
+after which the mentorship automatically ends even if the feature isn't
+fully done yet.
diff --git a/Documentation/dev-design-doc-use-cases-template.md b/Documentation/dev-design-doc-use-cases-template.md
new file mode 100644
index 0000000..02c2fb5
--- /dev/null
+++ b/Documentation/dev-design-doc-use-cases-template.md
@@ -0,0 +1,48 @@
+---
+title: "Design Doc - ${title} - Use Cases"
+sidebar: gerritdoc_sidebar
+permalink: design-doc-${folder-name}-use-cases.html
+hide_sidebar: true
+hide_navtoggle: true
+toc: false
+folder: design-docs/${folder-name}
+---
+
+# Use Cases
+
+In a few sentences, describe the use-cases as interactions between a
+user and a system to attain particular goals.
+
+Should be understandable by anyone who is familiar with using Gerrit.
+
+Optionally, differentiate between primary and secondary use-cases.
+Secondary use-cases are related to the primary use-cases, but
+addressing them within the scope of this design is not mandatory. This
+means they may not be covered by all proposed solutions. Secondary
+use-cases that are not addressed by the concluded solution, may be
+discussed in separate design docs. In this case links to these design
+docs should be added here.
+
+Optionally, define non-goals.
+
+It is possible that use-cases are specific to custom setups (e.g. the
+multi-master setup at Google). In this case, say so here.
+
+## <a id="acceptance-criteria"> Acceptance Criteria
+
+Describe conditions that must be satisfied to consider the feature as
+done.
+
+If a mentor is assigned, the mentorship ends when this state is reached.
+Please note that a mentorship can also end earlier if the maximum time
+frame for the mentorship has exceeded (see section 'Time Estimation'
+in dev-design-doc-conclusion-template.txt).
+
+## <a id="background"> Background
+
+Stuff one needs to know to understand the use-cases (e.g. motivating
+examples, previous versions and problems, links to related
+changes/design docs, etc.).
+
+Note: this is background; do not write about your design or ideas to
+solve problems here.
diff --git a/Documentation/dev-design-docs.txt b/Documentation/dev-design-docs.txt
new file mode 100644
index 0000000..a339da3
--- /dev/null
+++ b/Documentation/dev-design-docs.txt
@@ -0,0 +1,153 @@
+= Gerrit Code Review - Design Docs
+
+For the link:dev-contributing.html#design-driven-contribution-process[
+design-driven contribution process] it is required to specify features
+upfront in a design doc.
+
+[[structure]]
+== Design Doc Structure
+
+A design doc should discuss the following aspects:
+
+* Use-Cases:
+  The interactions between a user and a system to attain particular
+  goals.
+* Acceptance Criteria
+  Conditions that must be satisfied to consider the feature as done.
+* Background:
+  Stuff one needs to know to understand the use-cases (e.g. motivating
+  examples, previous versions and problems, links to related
+  changes/design docs, etc.)
+* Possible Solutions:
+  Possible solutions with the pros and cons, and explanation of
+  implementation details.
+* Conclusion:
+  Which decision was made and what were the reasons for it.
+
+[[collaboration]]
+As community we want to collaborate on design docs as much as possible
+and write them together, in an iterative manner. To make this work well
+design docs are split into multiple files that can be written and
+refined by several persons in parallel:
+
+* `index.md`:
+  Entry file that links to the files below (also see
+  'dev-design-doc-index-template.md').
+* `use-cases.md`:
+  Describes the use-cases, acceptance criteria and background (also see
+  'dev-design-doc-use-cases-template.md').
+* `solution-<n>.md`:
+  Each possible solution (with the pros and cons, and implementation
+  details) is described in a separate file (also see
+  'dev-design-doc-solution-template.md').
+* `conclusion.md`:
+  Describes the conclusion of the design discussion (also see
+  'dev-design-doc-conclusion-template.md').
+
+[[expectation]]
+It is expected that:
+
+* An agreement on the use-cases is achieved before solutions are being
+  discussed in detail.
+* Anyone who has ideas for an alternative solution uploads a change
+  with a `solution-<n>.md` that describes their solution. In case of
+  doubt whether an idea is a refinement of an existing solution or an
+  alternative solution, it's up to the owner of the discussed solution
+  to decide if the solution should be updated, or if the proposer
+  should start a new alternative solution.
+* All possible solutions are fairly discussed with their pros and cons,
+  and treated equally until a conclusion is made.
+* Unrelated issues (judged by the design doc owner) that are identified
+  during discussions may be extracted into new design docs (initially
+  consisting only of an `index.md` and a `use-cases.md` file). Doing so
+  is optional yet can be done by either the design owner or reviewers.
+* Changes making iterative improvements can be submitted frequently
+  (e.g. additional uses-cases can be added later, solutions can be
+  submitted without describing implementation details, etc.).
+* After a conclusion has been approved contributors are expected to
+  keep the design doc updated and fill in gaps while they go forward
+  with the implementation.
+
+[[propose]]
+== How to propose a new design?
+
+To propose a new design, upload a change to the
+link:https://gerrit-review.googlesource.com/admin/repos/homepage[
+homepage] repository that adds a new folder under `pages/design-docs/`
+which contains at least an `index.md` and a `uses-cases.md` file (see
+link:#structure[design doc structure] above).
+
+Pushing a design doc for review requires to be a
+link:dev-roles.html#contributor[contributor].
+
+When contributing design docs, contributors should make clear whether
+they are committed to do the implementation. It is possible to
+contribute designs without having resources to do the implementation,
+but in this case the implementation is only done if someone volunteers
+to do it (which is not guaranteed to happen).
+
+Only very few maintainers actively watch out for uploaded design docs.
+To raise awareness you may want to send a notification to the
+link:https://groups.google.com/d/forum/repo-discuss[repo-discuss]
+mailing list about your uploaded design doc. But the discussion should
+not take place on the mailing list, comments should be made by reviewing
+the change in Gerrit.
+
+[[review]]
+== Design doc review
+
+Everyone in the link:dev-roles.html[Gerrit community] is welcome to
+take part in the design review and comment on the design. As such, every
+design reviewer is expected to respect the community
+link:https://www.gerritcodereview.com/codeofconduct.html[Code of Conduct].
+
+Ideas for alternative solutions should be uploaded as a change that
+describes the solution (see link:#collaboration[above]). This should be
+done as early as possible during the review process, so that related
+comment threads stop there and do not clutter the current review. It is up
+to the alternative reviews to then host their related comments.
+
+Verification should be based on the generated `jekyll` site using the
+local `docker`, rather than via the rendering in `gitiles` (via
+`gerrit-review`).
+
+Changes which make a conclusion on a design (changes that add/change
+the `conclusion.md` file, see link:#structure[Design Doc Structure])
+should stay open for a minimum of 10 calendar days so that everyone has
+a fair chance to see them. It is important that concerns regarding a
+feature are raised during this time frame since once a conclusion is
+approved and submitted the implementation may start immediately.
+
+Other design doc changes can and should be submitted quickly so that
+collaboration and iterative refinements work smoothly (see
+link:#collaboration[above]).
+
+For proposed features the contributor should hear back from the
+link:dev-processes.html#steering-committee[engineering steering
+committee] within 14 calendar days whether the proposed feature is in
+scope of the project and if it can be accepted.
+
+[[meetings]]
+=== Meeting discussions
+
+If the Gerrit review doesn't start efficiently enough, stalls, gets off-track
+too much or becomes overly complex, one can use a meeting to refocus it. From
+that review thread, the organizer can volunteer oneself, or be proposed (even
+requested) by a reviewer. link:https://www.gerritcodereview.com/members.html#community-managers[
+Community managers] may help facilitate that if ultimately necessary.
+
+[[watch-designs]]
+== How to get notified for new design docs?
+
+. Go to the
+  link:https://gerrit-review.googlesource.com/settings/#Notifications[
+  notification settings]
+. Add a project watch for the `homepage` repository with the following
+  query: `dir:pages/design-docs`
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index 69af18d..0243a58 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -178,17 +178,6 @@
 repositories for each project.
 
 
-== Project Information
-
-Gerrit is developed as a self-hosting open source project:
-
-* link:https://www.gerritcodereview.com/[Project Homepage]
-* link:https://www.gerritcodereview.com/download/index.html[Release Versions]
-* link:https://gerrit.googlesource.com/gerrit[Source]
-* link:https://bugs.chromium.org/p/gerrit/issues/list[Issue Tracking]
-* link:https://review.source.android.com/[Change Review]
-
-
 == Internationalization and Localization
 
 As a source code review system for open source projects, where the
@@ -204,8 +193,6 @@
 RTL into consideration, while others probably need to be modified
 before translating the UI to an RTL language.
 
-* link:i18n-readme.html[Gerrit's i18n Support]
-
 
 == Accessibility Considerations
 
@@ -268,11 +255,7 @@
 
 Gerrit integrates with any OpenID provider for user authentication,
 making it easier for users to join a Gerrit site and manage their
-authentication credentials to it.  To make use of Google Accounts
-as an OpenID provider easier, Gerrit has a shorthand "Sign in with
-a Google Account" link on its sign-in screen.  Gerrit also supports
-a shorthand sign in link for Yahoo!.  Other providers may also be
-supported more directly in the future.
+authentication credentials to it.
 
 Site administrators may limit the range of OpenID providers to
 a subset of "reliable providers".  Users may continue to use
@@ -533,7 +516,7 @@
 
 Because of the distributed nature of Git, end-users don't need to
 contact the central Gerrit Code Review server very often. For `git
-fetch` traffic, link:pgm-daemon.html[slave mode] is known to be an
+fetch` traffic, link:pgm-daemon.html[replica mode] is known to be an
 effective way to offload traffic from the main server, permitting it
 to scale to a large user base without needing an excessive number of
 cores in a single system.
@@ -640,29 +623,6 @@
 scope of Gerrit.
 
 
-== Testing Plan
-
-Gerrit is currently manually tested through its web UI.
-
-JGit has a fairly extensive automated unit test suite.  Most new
-changes to JGit are rejected unless corresponding automated unit
-tests are included.
-
-
-== Caveats
-
-Rietveld can't be used as it does not provide the "submit over the
-web" feature that Gerrit provides for Git.
-
-Gitosis can't be used as it does not provide any code review
-features, but it does provide basic access controls.
-
-Email based code review does not scale to a project as large and
-complex as Android.  Most contributors at least need some sort of
-dashboard to keep track of any pending reviews, and some way to
-correlate updated revisions back to the comments written on prior
-revisions of the same logical change.
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index 20484e6..bf35c32 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -228,6 +228,17 @@
 Scenario development is often done using locally running Gerrit systems under test, which are
 sometimes dockerized.
 
+==== Number of users
+
+The `number_of_users` property can be used to scale scenario steps to run with the specified number
+of concurrent users. The value of this property remains `1` by default. For example, this sets the
+number of concurrent users to 10:
+
+* `-Dcom.google.gerrit.scenarios.number_of_users=10`
+
+This will make scenarios that support the `number_of_users` property to inject that many users
+concurrently for load testing.
+
 == How to run tests
 
 Run all tests:
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 420151b..bdd6360 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -86,11 +86,6 @@
 * Add JRE, e.g.: directory: /usr/lib64/jvm/java-9-openjdk, name: java-9-openjdk-9
 * Change execution environment for gerrit project to: JavaSE-9 (java-9-openjdk-9)
 * Check that compiler compliance level in gerrit project is set to: 9
-* Add this parameter to VM argument for gerrit_daemin launcher:
-----
-  --add-modules java.activation \
-  --add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED
-----
 
 [[Formatting]]
 == Code Formatter Settings
@@ -98,7 +93,7 @@
 To format source code, Gerrit uses the
 link:https://github.com/google/google-java-format[`google-java-format`]
 tool (version 1.7), which automatically formats code to follow the
-style guide. See link:dev-contributing.html#style[Code Style] for the
+style guide. See link:dev-crafting-changes.html#style[Code Style] for the
 instruction how to set up command line tool that uses this formatter.
 The Eclipse plugin is provided that allows to format with the same
 formatter from within the Eclipse IDE. See
diff --git a/Documentation/dev-inspector.txt b/Documentation/dev-inspector.txt
index b1559ca..39736d7 100644
--- a/Documentation/dev-inspector.txt
+++ b/Documentation/dev-inspector.txt
@@ -11,7 +11,7 @@
   [--enable-httpd | --disable-httpd]
   [--enable-sshd | --disable-sshd]
   [--console-log]
-  [--slave]
+  [--replica]
   -s
 --
 
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index 5077079..81790db 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -104,7 +104,7 @@
 *Code -> Reformat Code*, keyboard shortcuts, or the commit dialog will use the
 custom style defined by the `google-java-format` plugin.
 
-Please refer to the documentation on the <<dev-contributing#style,code style>>
+Please refer to the documentation on the <<dev-crafting-changes#style,code style>>
 for which version of `google-java-format` is used with Gerrit.
 
 ==== Code style settings
@@ -159,7 +159,7 @@
 plugin in IntelliJ IDEA.
 
 To simplify the creation of commit messages which are compliant with the
-<<dev-contributing#commit-message,Commit Message>> format, do the following:
+<<dev-crafting-changes#commit-message,Commit Message>> format, do the following:
 
 . Go to *File -> Settings -> Version Control -> Commit Dialog*.
 . In the *Commit message inspections*, activate the three inspections:
@@ -171,7 +171,7 @@
 right margin*.
 
 In addition, you should follow the instructions of
-<<dev-contributing#git_commit_settings,this section>> (if you haven't
+<<dev-crafting-changes#git-commit-settings,this section>> (if you haven't
 done so already):
 
 * Install the Git commit message hook for the `Change-Id` line.
diff --git a/Documentation/dev-plugins-lifecycle.txt b/Documentation/dev-plugins-lifecycle.txt
new file mode 100644
index 0000000..b552472
--- /dev/null
+++ b/Documentation/dev-plugins-lifecycle.txt
@@ -0,0 +1,254 @@
+= Plugin Lifecycle
+
+Most of the plugins are hosted on the same instance as the
+link:https://gerrit-review.googlesource.com[Gerrit project itself] to make them
+more discoverable and have more chances to be reviewed by the whole community.
+
+[[hosting_lifecycle]]
+== Hosting Lifecycle
+
+The process of writing a new plugin goes through different phases:
+
+- Ideation and Discussion:
++
+The idea of creating a new plugin is posted and discussed on the
+link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] mailing list.
++
+Also see section link#ideation_discussion[Ideation and discussion] below.
+
+- Prototyping (optional):
++
+The author of the plugin creates a working prototype on a public repository
+accessible to the community.
++
+Also see section link#plugin_prototyping[Plugin Prototyping] below.
+
+- Proposal and Hosting:
++
+The author proposes to release the plugin under the
+link:https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 OpenSource
+license] and requests the plugin to be hosted on
+link:https://gerrit-review.googlesource.com[the Gerrit project site]. The
+proposal must be   accepted by at least one Gerrit maintainer. In case of
+disagreement between maintainers, the issue can be escalated to the
+link:dev-processes.html#steering-committee[Engineering Steering Committee]. If
+the plugin is accepted, the Gerrit maintainer creates the project under the
+plugins path on link:https://gerrit-review.googlesource.com[the Gerrit project
+site].
++
+Also see section link#plugin_proposal[Plugin Proposal] below.
+
+- Build:
++
+To make the consumption of the plugin easy and to notice plugin breakages early
+the plugin author should setup build jobs on
+link:https://gerrit-ci.gerritforge.com[the GerritForge CI] that build the
+plugin for each Gerrit version that it supports.
++
+Also see section link#build[Build] below.
+
+- Development and Contribution:
++
+The author develops a production-ready code base of the plugin, with
+contributions, reviews, and help from the Gerrit community.
++
+Also see section link#development_contribution[Development and contribution]
+below.
+
+- Release:
++
+The author releases the plugin by creating a Git tag and announcing the plugin
+on the link:https://groups.google.com/d/forum/repo-discuss[repo-discuss]
+mailing list.
++
+Also see section link#plugin_release[Plugin release] below.
+
+- Maintenance:
++
+The author maintains their plugins as new Gerrit versions are released, updates
+them when necessary, develops further existing or new features and reviews
+incoming contributions.
+
+- Deprecation:
++
+The author declares that the plugin is not maintained anymore or is deprecated
+and should not be used anymore.
++
+Also see section link#plugin_deprecation[Plugin deprecation] below.
+
+[[ideation_discussion]]
+== Ideation and Discussion
+
+Starting a new plugin project is a community effort: it starts with the
+identification of a gap in the Gerrit Code Review product but evolves with the
+contribution of ideas and suggestions by the whole community.
+
+The ideator of the plugin starts with an RFC (Request For Comments) post on the
+link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] mailing list
+with a description of the main reasons for starting a new plugin.
+
+Example of a post:
+
+----
+  [RFC] Code-Formatter plugin
+
+  Hello, community,
+  I am proposing to create a new plugin for Gerrit called 'Code-Formatter', see
+  the details below.
+
+  *The gap*
+  Often, when I post a new change to Gerrit, I forget to run the common code
+  formatting tool (e.g. Google-Java-Format for the Gerrit project). I would
+  like Gerrit to be in charge of highlighting these issues to me and save many
+  people's time.
+
+  *The proposal*
+  The Code-Formatter plugin reads the formatting rules in the project config
+  and applies them automatically to every patch-set. Any issue is reported as a
+  regular review comment to the patchset, highlighting the part of the code to
+  be changed.
+
+  What do you think? Did anyone have the same idea or need?
+----
+
+The idea is discussed on the mailing list and can evolve based on the needs and
+inputs from the entire community.
+
+After the discussion, the ideator of the plugin can decide to start prototyping
+on it or park the proposal, if the feedback provided an alternative solution to
+the problem. The prototype phase can be optionally skipped if the idea is clear
+enough and receives a general agreement from the Gerrit maintainers. The author
+can be given a "leap of faith" and can go directly to the format plugin
+proposal (see below) and the creation of the plugin repository.
+
+[[plugin_prototyping]]
+== Plugin Prototyping
+
+The initial idea is translated to code by the plugin author. The development
+can happen on any public or private source code repository and can involve one
+or more contributors. The purpose of prototyping is to verify that the idea can
+be implemented and provides the expected benefits.
+
+Once a working prototype is ready, it can be announced as a follow-up to the
+initial RFC proposal so that other members of the community can see the code
+and try the plugin themselves.
+
+[[plugin_proposal]]
+== Plugin Proposal
+
+The author decides that the plugin prototype makes sense as a general purpose
+plugin and decides to release the code with the same
+link:https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license]
+as the Gerrit Code Review project and have it hosted on
+link:https://gerrit-review.googlesource.com[the Gerrit project site].
+
+The plugin author formalizes the proposal with a follow-up of the initial RFC
+post and asks for public opinion on it.
+
+Example:
+
+----
+  Re - [RFC] Code-Formatter plugin
+
+  Hello, community,
+  thanks for your feedback on the prototype. I have now decided to donate the
+  project to the Gerrit Code Review project and make it a plugin:
+
+  Plugin name:
+  /plugins/code-formatter
+
+  Plugin description:
+    Plugin to allow automatic posting review based on code-formatting rules
+----
+
+The community discusses the proposal and the value of the plugin for the whole
+project; the result of the discussion can end up in one of the following cases:
+
+- The plugin's project request is widely appreciated and formally accepted by
+  at least one Gerrit maintainer who creates the repository as child project of
+  'Public-Projects' on link:https://gerrit-review.googlesource.com[the Gerrit
+  project site], creates an associated plugin owners group with "Owner"
+  permissions for the plugin and adds the plugin's author as member of it.
+- The plugin's project is widely appreciated; however, another existing plugin
+  already partially covers the same use-case and thus it would make more sense
+  to have the features integrated into the existing plugin. The new plugin's
+  author contributes his prototype commits refactored to be included as change
+  into the existing plugin.
+- The plugin's project is found useful; however, it is too specific to the
+  author's use-case and would not make sense outside of it. The plugin remains
+  in a public repository, widely accessible and OpenSource, but not hosted on
+  link:https://gerrit-review.googlesource.com[the Gerrit project site].
+
+[[build]]
+== Build
+
+The plugin's maintainer creates a job on the
+link:https://gerrit-ci.gerritforge.com[GerritForge CI] by creating a new YAML
+definition in the link:https://gerrit.googlesource.com/gerrit-ci-scripts[Gerrit
+CI Scripts] repository.
+
+Example of a YAML CI job for plugins:
+
+----
+  - project:
+    name: code-formatter
+    jobs:
+      - 'plugin-{name}-bazel-{branch}':
+          branch:
+            - master
+----
+
+[[development_contribution]]
+== Development and Contribution
+
+The plugin follows the same lifecycle as Gerrit Code Review and needs to be
+kept up-to-date with the current active branches, according to the
+link:https://www.gerritcodereview.com/#support[current support policy].
+During the development, the plugin's maintainer can reward contributors
+requesting to be more involved and making them maintainers of his plugin,
+adding them to the list of the project owners.
+
+[[plugin_release]]
+== Plugin Release
+
+The plugin's maintainer is the only person responsible for making and
+announcing the official releases, typically, but not limited to, in conjunction
+with the major releases of Gerrit Code Review. The plugin's maintainer may tag
+his plugin and follow the notation and semantics of the Gerrit Code Review
+project; however it is not mandatory and many of the plugins do not have any
+tags or releases.
+
+Example of a YAML CI job for a plugin compatible with multiple Gerrit versions:
+
+----
+  - project:
+    name: code-formatter
+    jobs:
+      - 'plugin-{name}-bazel-{branch}-{gerrit-branch}':
+          branch:
+            - master
+          gerrit-branch:
+            - master
+            - stable-3.0
+            - stable-2.16
+----
+
+[[plugin_deprecation]]
+== Plugin Deprecation
+
+The plugin's maintainer and the community have agreed that the plugin is not
+useful anymore or there isn't anyone willing to contribute to bringing it
+forward and keeping it up-to-date with the recent versions of Gerrit Code
+Review.
+
+The plugin's maintainer puts a deprecation notice in the README.md of the
+plugin and pushes it for review. If nobody is willing to bring the code
+forward, the change gets merged, and the master branch is removed from the list
+of branches to be built on the GerritFoge CI.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 2190326..f5af914 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1,7 +1,8 @@
 = Gerrit Code Review - Plugin Development
 
 The Gerrit server functionality can be extended by installing plugins.
-This page describes how plugins for Gerrit can be developed.
+This page describes how plugins for Gerrit can be developed and hosted
+on gerrit-review.googlesource.com.
 
 For PolyGerrit-specific plugin development, consult with
 link:pg-plugin-dev.html[PolyGerrit Plugin Development] guide.
@@ -389,6 +390,10 @@
 same link:cmd-stream-events.html#events[events] that are also streamed
 by the link:cmd-stream-events.html[gerrit stream-events] command.
 
+* `com.google.gerrit.extensions.events.AccountActivationListener`:
++
+User account got activated or deactivated
+
 * `com.google.gerrit.extensions.events.LifecycleListener`:
 +
 Plugin start and stop
@@ -1020,6 +1025,11 @@
 }
 ----
 
+Implementors of the `ChangeAttributeFactory` interface should check whether
+they need to contribute to the link:#change-etag-computation[change ETag
+computation] to prevent callers using ETags from potentially seeing outdated
+plugin attributes.
+
 [[simple-configuration]]
 == Simple Configuration in `gerrit.config`
 
@@ -2105,6 +2115,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
 
@@ -2357,7 +2375,8 @@
 volume efficiently.
 
 Gerrit implements this extension point, but plugins may bind another
-implementation, e.g. one that supports multi-master.
+implementation, e.g. one that supports cluster setup with multiple
+primary Gerrit nodes handling write operations.
 
 ----
 DynamicItem.bind(binder(), AccountPatchReviewStore.class)
@@ -2524,10 +2543,10 @@
 [source, java]
 ----
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 
 import java.util.Set;
 
@@ -2651,10 +2670,8 @@
 modifications, `NOT_READY`. Other statuses are available for particular cases.
 A change can be submitted if all the plugins accept the change.
 
-Plugins may also decide not to vote on a given change by returning an empty
-Collection (ie: the plugin is not enabled for this repository), or to vote
-several times (ie: one SubmitRecord per project in the hierarchy).
-The results are handled as if multiple plugins voted for the change.
+Plugins may also decide not to vote on a given change by returning an
+`Optional.empty()` (ie: the plugin is not enabled for this repository).
 
 If a plugin decides not to vote, it's name will not be displayed in the UI and
 it will not be recoded in the database.
@@ -2689,20 +2706,20 @@
 
 [source, java]
 ----
-import java.util.Collection;
+import java.util.Optional;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRecord.Status;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.SubmitRule;
 
 public class MyPluginRules implements SubmitRule {
-  public Collection<SubmitRecord> evaluate(ChangeData changeData) {
+  public Optional<SubmitRecord> evaluate(ChangeData changeData) {
     // Implement your submitability logic here
 
     // Assuming we want to prevent this change from being submitted:
-    SubmitRecord record;
+    SubmitRecord record = new SubmitRecord();
     record.status = Status.NOT_READY;
-    return record;
+    return Optional.of(record);
   }
 }
 ----
@@ -2735,6 +2752,82 @@
 are met, but marked as `OK`. If the requirements were not displayed, reviewers
 would need to use their precious time to manually check that they were met.
 
+Implementors of the `SubmitRule` interface should check whether they need to
+contribute to the link:#change-etag-computation[change ETag computation] to
+prevent callers using ETags from potentially seeing outdated submittability
+information.
+
+[[change-etag-computation]]
+== Change ETag Computation
+
+By implementing the `com.google.gerrit.server.change.ChangeETagComputation`
+interface plugins can contribute a value to the change ETag computation.
+
+Plugins can affect the result of the get change / get change details REST
+endpoints by:
+
+* providing link:#query_attributes[plugin defined attributes] in
+  link:rest-api-changes.html#change-info[ChangeInfo]
+* implementing a link:#pre-submit-evaluator[pre-submit evaluator] which affects
+  the computation of `submittable` field in
+  link:rest-api-changes.html#change-info[ChangeInfo]
+
+If the plugin defined part of link:rest-api-changes.html#change-info[
+ChangeInfo] depends on plugin specific data, callers that use change ETags to
+avoid unneeded recomputations of ChangeInfos may see outdated plugin attributes
+and/or outdated submittable information, because a ChangeInfo is only reloaded
+if the change ETag changes.
+
+By implementating the `com.google.gerrit.server.change.ChangeETagComputation`
+interface plugins can contribute to the ETag computation and thus ensure that
+the change ETag changes when the plugin data was changed. This way it can be
+ensured that callers do not see outdated ChangeInfos.
+
+IMPORTANT: Change ETags are computed very frequently and the computation must
+be cheap. Take good care to not perform any expensive computations when
+implementing this.
+
+[source, java]
+----
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.hash.Hasher;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.change.ChangeETagComputation;
+
+public class MyPluginChangeETagComputation implements ChangeETagComputation {
+  public String getETag(Project.NameKey projectName, Change.Id changeId) {
+    Hasher hasher = Hashing.murmur3_128().newHasher();
+
+    // Add hashes for all plugin-specific data that affects change infos.
+    hasher.putString(sha1OfPluginSpecificChangeRef, UTF_8);
+
+    return hasher.hash().toString();
+  }
+}
+----
+
+[[exception-hook]]
+== ExceptionHook
+
+An `ExceptionHook` allows implementors to control how certain
+exceptions should be handled.
+
+This interface is intended to be implemented for multi-master setups to
+control the behavior for handling exceptions that are thrown by a lower
+layer that handles the consensus and synchronization between different
+server nodes. E.g. if an operation fails because consensus for a Git
+update could not be achieved (e.g. due to slow responding server nodes)
+this interface can be used to retry the request instead of failing it
+immediately.
+
+[[mail-soy-template-provider]]
+== MailSoyTemplateProvider
+
+This extension point allows to provide soy templates for registration
+so that they can be used for sending emails from a plugin.
+
 [[quota-enforcer]]
 == Quota Enforcer
 
@@ -2823,6 +2916,20 @@
 }
 ----
 
+[[performance-logger]]
+== Performance Logger
+
+`com.google.gerrit.server.logging.PerformanceLogger` is an extension point that
+is invoked for all operations for which the execution time is measured. The
+invocation of the extension point does not happen immediately, but only at the
+end of a request (REST call, SSH call, git push). Implementors can write the
+execution times into a performance log for further analysis.
+
+[[request-listener]]
+== Request Listener
+
+`com.google.gerrit.server.RequestListener` is an extension point that is
+invoked each time the server executes a request from a user.
 
 == SEE ALSO
 
diff --git a/Documentation/dev-processes.txt b/Documentation/dev-processes.txt
new file mode 100644
index 0000000..09dcbee
--- /dev/null
+++ b/Documentation/dev-processes.txt
@@ -0,0 +1,355 @@
+= Gerrit Code Review - Development Processes
+
+[[project-governance]]
+[[steering-committee]]
+== Project Governance / Engineering Steering Committee
+
+The Gerrit project has an engineering steering committee (ESC) that is
+in charge of:
+
+* Gerrit core (the `gerrit` project) and the core plugins
+* defining the project vision and the project scope
+* maintaining a roadmap, a release plan and a prioritized backlog
+* ensuring timely design reviews
+* ensuring that new features are compatible with the project vision and
+  are well aligned with other features (give feedback on new
+  link:dev-design-docs.html[design docs] within 14 calendar days)
+* approving/rejecting link:dev-design-docs.html[designs], vetoing new
+  features
+* assigning link:dev-roles.html#mentor[mentors] for approved features
+* accepting new plugins as core plugins
+* making changes to the project governance process and the
+  link:dev-contributing.html#contribution-processes[contribution
+  processes]
+
+The steering committee has 5 members:
+
+* 3 Googlers that are appointed by Google
+* 2 non-Google maintainers, elected by non-Google maintainers for the
+  period of 1 year (see link:#steering-committee-election[below])
+
+Refer to the project homepage for the link:https://www.gerritcodereview.com/members.html#engineering-steering-committee[
+list of current committee members].
+
+The steering committee should act in the interest of the Gerrit project
+and the whole Gerrit community.
+
+For decisions, consensus between steering committee members and all
+other maintainers is desired. If consensus cannot be reached, decisions
+can also be made by simple majority in the steering committee (should
+be applied only in exceptional situations).
+
+The steering committee is empowered to overrule positive/negative votes
+from individual maintainers, but should do so only in exceptional
+situations after attempts to reach consensus have failed.
+
+As an integral part of the Gerrit community, the steering committee is
+committed to transparency and to answering incoming requests in a
+timely manner.
+
+[[steering-committee-election]]
+=== Election of non-Google steering committee members
+
+The election of the non-Google steering committee members happens once
+a year in May. Non-Google link:dev-roles.html#maintainer[maintainers]
+can nominate themselves by posting an informal application on the
+non-public maintainers mailing list by end of April (deadline for 2019
+is Mon 13th of May). By applying to be steering committee member, the
+candidate confirms to be able to dedicate the time that is needed to
+fulfill this role (also see
+link:dev-roles.html#steering-committee-member[steering committee
+member]).
+
+Each non-Google maintainer can vote for 2 candidates. The voting
+happens by posting on the maintainer mailing list. The voting period is
+14 calendar days from the nomination deadline (except for 2019, where
+the initial steering committee should be confirmed during the Munich
+hackathon, the voting period goes from 14th May to 16th May).
+
+Google maintainers do not take part in this vote, because Google
+already has dedicated seats in the steering committee (see section
+link:#steering-committee[steering committee]).
+
+[[contribution-process]]
+== Contribution Process
+
+See link:dev-contributing.html[here].
+
+[[design-doc-review]]
+== Design Doc Review
+
+See link:dev-design-docs.html#review[here].
+
+[[versioning]]
+== Semantic versioning
+
+Gerrit follows a light link:https://semver.org/[semantic versioning scheme] MAJOR.MINOR[.PATCH[.HOTFIX]]
+format:
+
+  * MAJOR is incremented when there are substantial incompatible changes and/or
+    new features in Gerrit.
+  * MINOR is incremented when there are changes that are typically backward compatible
+    with the earlier minor version. Features can be removed following the
+    link:#deprecating-features[feature deprecation process]. Dependencies can be upgraded
+    according to the link:dev-processes.html#upgrading-libraries[libraries upgrade policy].
+  * PATCH is incremented when there are backward-compatible bug fixes in Gerrit or its
+    dependencies. When PATCH is zero, it can be omitted.
+  * HOTFIX is present only when immediately after a patch release, some urgent
+    fixes in the code or the packaging format are required but do not justify a
+    new patch release.
+
+For every MAJOR.MINOR release there is an associated stable branch that follows well defined
+link:#dev-in-stable-branches[rules of development].
+
+Within a stable branch, there are multiple MAJOR.MINOR.PATCH tags created associated to the
+bug-fix releases of that stable release.
+
+Examples:
+
+* Gerrit v3.0.0 contains breaking incompatible changes in the functionality because
+  the ReviewDb storage has been totally removed.
+* Gerrit v2.15 contains brand-new features like NoteDb, however, still supports the existing
+  ReviewDb storage for changes and thus is considered a minor release.
+* Gerrit v2.14.20 is the 20th patch-release of the stable Gerrit v2.14.* and thus does not contain
+  new features but only bug-fixes.
+
+[[dev-in-stable-branches]]
+== Development in stable branches
+
+As their name suggests stable branches are intended to be stable. This means that generally
+only bug-fixes should be done on stable branches, however this is not strictly enforced and
+exceptions may apply:
+
+  * When a stable branch is initially created to prepare a new release the Gerrit community
+    discusses on the mailing list if there are pending features which should still make it into the
+    release. Those features are blocking the release and should be implemented on the stable
+    branch before the first release candidate is created.
+  * To stabilize the code before doing a major release several release candidates are created. Once
+    the first release candidate was done no more features should be accepted on the stable branch.
+    If more features are found to be required they should be discussed with the steering committee
+    and should only be allowed if the risk of breaking things is considered to be low.
+  * Once a major release is done only bug-fixes and documentation updates should be done on the
+    stable branch. These updates will be included in the next minor release.
+  * For minor releases new features could be acceptable if the following conditions are met:
+    ** they are result of a new feature introduced through a merge of an earlier stable branch
+    ** they are justified for completing, extending or fixing an existing feature
+    ** does not involve API, user-interface changes or data migrations
+    ** is backward compatible with all existing features
+    ** the parts of the code in common with existing features are properly covered by end-to-end tests
+    ** is important to the Gerrit community and no Gerrit maintainers have raised objections.
+  * In cases of doubt or conflicting opinions on new features, it's the responsibility of the
+    steering committee to evaluate the risk of new features and make a decision based on these
+    rules and opinions from the Gerrit community.
+  * The older a stable branch is the more stable it should be. This means old stable branches
+    should only receive bug-fixes that are either important or low risk. Security fixes, including
+    security updates for third party dependencies, are always considered as important and hence can
+    always be done on stable branches.
+
+Examples:
+
+* Gerrit v3.0.0-rc1 and v3.0.0-rc2 may contain new features and API changes without notice,
+  even if they are both cut on the same stable-3.0 branch.
+* Gerrit v2.14.8 introduced the support for ElasticSearch as a new feature. This was an exception
+  agreed amongst the Gerrit maintainers, did not touch the Lucene indexing code-base, was supported
+  by container-based E2E tests and represents a completion of an high-level feature.
+
+[[backporting]]
+== Backporting to stable branches
+
+From time to time bug fix releases are made for existing stable branches.
+
+Developers concerned with stable branches are encouraged to backport or push fixes to these
+branches, even if no new release is planned. Backporting features is only possible in compliance
+with the rules link:#dev-in-stable-branches[above].
+
+Fixes that are known to be needed for a particular release should be pushed for review on that
+release's stable branch. They will then be included into the master branch when the stable branch
+is merged back.
+
+[[security-issues]]
+== Dealing with Security Issues
+
+If a security vulnerability in Gerrit is discovered, we place an link:#embargo[
+embargo] on it until a fixed release or mitigation is available. Fixing the
+issue is usually pursued with high priority (depends on the severity of the
+security vulnerability). The embargo is lifted and the vulnerability is
+disclosed to the community as soon as a fix release or another mitigation is
+available.
+
+[[report-security-issue]]
+=== How to report a security vulnerability?
+
+To report a security vulnerability file a
+link:https://bugs.chromium.org/p/gerrit/issues/entry?template=Security+Issue[
+security issue] in the Gerrit issue tracker. The visibility of issues that are
+created with the `Security Issue` template is automatically restricted to
+Gerrit maintainers and a few long-term contributors. This means as a reporter
+you may not be able to see the issue once it is created. Security issues are
+created on the `ESC` component so that they will be discussed at the next
+meeting of the link:#steering-committee[Engineering Steering Committee] which
+takes place biweekly.
+
+If an existing issue is found to be a security vulnerability it should be
+turned into a security issue by:
+
+. Setting the component to `ESC`
+. Adding the labels `Security` and `NonPublic`
+
+In case of doubt, or if an issue cannot wait until the next ESC meeting,
+contact the link:#steering-committee[Engineering Steering Committee] directly
+by sending them an mailto:gerritcodereview-esc@googlegroups.com[email].
+
+If needed, the ESC will contact the reporter for additional details.
+
+[[embargo]]
+=== The Embargo
+
+Once an issue has been identified as security vulnerability, we keep it under
+embargo until a fixed release or a mitigation is available. This means that the
+issue is not discussed publicly, but only on issues with restricted visibility
+(see link:#report-security-issue[above]) and at the mailing lists of the ESC,
+community managers and Gerrit maintainers. Since the `repo-discuss` mailing
+list is public, security issues must not be discussed on this mailing list
+while the embargo is in place.
+
+The reason for keeping an embargo is to prevent attackers from taking advantage
+of a vulnerability while no fixed releases are available yet, and Gerrit
+administrators cannot make their systems secure.
+
+Once a fix release or mitigation is available, the embargo is lifted and the
+community is informed about the security vulnerability with the advise to
+address the security vulnerability immediately (either by upgrading to a fixed
+release or applying the mitigation). The information about the security
+vulnerability is disclosed via the
+link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] mailing list.
+
+[[handle-security-issue]]
+=== Handling of the Security Vulnerability
+
+. Engineering Steering Committee evaluates the security vulnerability:
++
+The ESC discusses the security vulnerability and which actions should be taken
+to address it. One person, usually one of the Gerrit maintainers, should be
+appointed to drive and coordinate the investigation and the fix of the security
+vulnerability. This coordinator doesn't need to do all the work alone, but is
+responsible that the security vulnerability is getting fixed in a timely
+manner.
++
+If the security vulnerability affects multiple or older releases the ESC should
+decide which of the releases should be fixed. For critical security issue we
+also consider fixing old releases that are otherwise not receiving any
+bug-fixes anymore.
++
+It's also possible that the ESC decides that an issue is not a security issue
+and the embargo is lifted immediately.
+
+. Implementation of the security fix:
++
+To keep the embargo intact, security fixes cannot be developed and reviewed in
+the public `gerrit` repository. In particular it's not secure to use private
+changes for implementing and reviewing security fixes (see general notes about
+link:intro-user.html[security-fixes]).
++
+Instead security fixes should be implemented and reviewed in the non-public
+link:https://gerrit-review.googlesource.com/admin/repos/gerrit-security-fixes[
+gerrit-security-fixes] repository which is only accessible by Gerrit
+maintainers and Gerrit community members that work on security fixes.
++
+The change that fixes the security vulnerability should contain an integration
+test that verifies that the security vulnerability is no longer present.
++
+Review and approval of the security fixes must be done by the Gerrit
+maintainers. Verifications must be done manually since the Gerrit CI doesn't
+build and test changes of the `gerrit-security-fixes` repository (and it
+shouldn't because everything on the CI server is public which would break
+the embargo).
++
+Once a security fix is ready and submitted, it should be cherry-picked to all
+branches that should be fixed.
+
+. Creation of fixed releases and announcement of the security vulnerability:
++
+A release manager should create new bug fix releases for all fixed branches.
++
+The new releases should be tested against the security vulnerability to
+double-check that the release was built from the correct source that contains
+the fix for the security vulnerability.
++
+Before publishing the fixed releases, an announcement to the Gerrit community
+should be prepared. The announcement should clearly describe the security
+vulnerability, which releases are affected and which releases contain the fix.
+The announcement should recommend to upgrade to fixed releases immediately.
++
+Once all releases are ready and tested and the announcement is prepared, the
+releases should be all published at the same time. Immediately after that, the
+announcement should be sent out to the
+link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] mailing list.
++
+This ends the embargo and any issue that discusses the security vulnerability
+should be made public.
+
+. Follow-Up
++
+The ESC should discuss if there are any learnings from the security
+vulnerability and define action items to follow up in the
+link:https://bugs.chromium.org/p/gerrit[issue tracker].
+
+[[upgrading-libraries]]
+== Upgrading Libraries
+
+Changes that add new libraries or upgrade existing libraries require an approval on the
+`Library-Compliance` label. For an approval the following things are checked:
+
+* The library has a license that is suitable for use within Gerrit.
+* If the library is used within Google, the version of the library must be compatible with the
+  version that is used at Google.
+
+Only maintainers from Google can vote on the `Library-Compliance` label.
+
+Gerrit's library dependencies should only be upgraded if the new version contains
+something we need in Gerrit. This includes new features, API changes as well as bug
+or security fixes.
+An exception to this rule is that right after a new Gerrit release was branched
+off, all libraries should be upgraded to the latest version to prevent Gerrit
+from falling behind. Doing those upgrades should conclude at the latest two
+months after the branch was cut. This should happen on the master branch to ensure
+that they are vetted long enough before they go into a release and we can be sure
+that the update doesn't introduce a regression.
+
+[[deprecating-features]]
+== Deprecating features
+
+Gerrit should be as stable as possible and we aim to add only features that last.
+However, sometimes we are required to deprecate and remove features to be able
+to move forward with the project and keep the code-base clean. The following process
+should serve as a guideline on how to deprecate functionality in Gerrit. Its purpose
+is that we have a structured process for deprecation that users, administrators and
+developers can agree and rely on.
+
+General process:
+
+  * Make sure that the feature (e.g. a field on the API) is not needed anymore or blocks
+    further development or improvement. If in doubt, consult the mailing list.
+  * If you can provide a schema migration that moves users to a comparable feature, do
+    so and stop here.
+  * Mark the feature as deprecated in the documentation and release notes.
+  * If possible, mark the feature deprecated in any user-visible interface. For example,
+    if you are deprecating a Git push option, add a message to the Git response if
+    the user provided the option informing them about deprecation.
+  * Annotate the code with `@Deprecated` and `@RemoveAfter(x.xx)` if applicable.
+    Alternatively, use `// DEPRECATED, remove after x.xx` (where x.xx is the version
+    number that has to be branched off before removing the feature)
+  * Gate the feature behind a config that is off by default (forcing admins to turn
+    the deprecated feature on explicitly).
+  * After the next release was branched off, remove any code that backed the feature.
+
+You can optionally consult the mailing list to ask if there are users of the feature you
+wish to deprecate. If there are no major users, you can remove the feature without
+following this process and without the grace period of one release.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index c014687..ad25147 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -3,7 +3,9 @@
 To build a developer instance, you'll need link:https://bazel.build/[Bazel] to
 compile the code, preferably launched with link:https://github.com/bazelbuild/bazelisk[Bazelisk].
 
-== Getting the Source
+== Git Setup
+
+=== Getting the Source
 
 Create a new client workspace:
 
@@ -12,102 +14,36 @@
   cd gerrit
 ----
 
-The `--recursive` option is needed on `git clone` to ensure that
-the core plugins, which are included as git submodules, are also
-cloned.
+The `--recurse-submodules` option is needed on `git clone` to ensure that the
+core plugins, which are included as git submodules, are also cloned.
+
+=== Switching between branches
+
+When using `git checkout` without `--recurse-submodules` to switch between
+branches, submodule revisions are not altered, which can result in:
+
+*  Incorrect or unneeded plugin revisions.
+*  Missing plugins.
+
+After you switch branches, ensure that you have the correct versions of
+the submodules.
+
+CAUTION: If you store Eclipse or IntelliJ project files in the Gerrit source
+directories, do *_not_* run `git clean -fdx`. Doing so may remove untracked files and damage your project. For more information, see
+link:https://git-scm.com/docs/git-clean[git-clean].
+
+Run the following:
+
+----
+  git submodule update
+  git clean -ffd
+----
 
 [[compile_project]]
 == Compiling
 
 For details, see <<dev-bazel#,Building with Bazel>>.
 
-== Configuring Eclipse
-
-To use the Eclipse IDE for development, see
-link:dev-eclipse.html[Eclipse Setup].
-
-To configure the Eclipse workspace with Bazel, see
-link:dev-bazel.html#eclipse[Eclipse integration with Bazel].
-
-== Configuring IntelliJ IDEA
-
-See <<dev-intellij#,IntelliJ Setup>> for details.
-
-== MacOS
-
-On MacOS, ensure that "Java for MacOS X 10.5 Update 4" (or higher) is installed
-and that `JAVA_HOME` is set to the
-link:install.html#Requirements[required Java version].
-
-Java installations can typically be found in
-"/System/Library/Frameworks/JavaVM.framework/Versions".
-
-To check the installed version of Java, open a terminal window and run:
-
-`java -version`
-
-[[init]]
-== Site Initialization
-
-After you compile the project <<compile_project,(above)>>, run the Gerrit
-`init`
-command to create a test site:
-
-----
-  $(bazel info output_base)/external/local_jdk/bin/java \
-     -jar bazel-bin/gerrit.war init -d ../gerrit_testsite
-----
-
-[[special_bazel_java_version]]
-NOTE: You must use the same Java version that Bazel used for the build, which
-is available at `$(bazel info output_base)/external/local_jdk/bin/java`.
-
-During initialization, change two settings from the defaults:
-
-*  To ensure the development instance is not externally accessible, change the
-listen addresses from '*' to 'localhost'.
-*  To allow yourself to create and act as arbitrary test accounts on your
-development instance, change the auth type from 'OPENID' to 'DEVELOPMENT_BECOME_ANY_ACCOUNT'.
-
-After initializing the test site, Gerrit starts serving in the background. A
-web browser displays the Start page.
-
-On the Start page, you can:
-
-.  Log in as the account you created during the initialization process.
-.  Register additional accounts.
-.  Create projects.
-
-To shut down the daemon, run:
-
-----
-  ../gerrit_testsite/bin/gerrit.sh stop
-----
-
-
-[[localdev]]
-== Working with the Local Server
-
-To create more accounts on your development instance:
-
-.  Click 'become' in the upper right corner.
-.  Select 'Switch User'.
-.  Register a new account.
-.  link:user-upload.html#ssh[Configure your SSH key].
-
-Use the `ssh` protocol to clone from and push to the local server. For
-example, to clone a repository that you've created through the admin
-interface, run:
-
-----
-git clone ssh://username@localhost:29418/projectname
-----
-
-To create changes as users of Gerrit would, run:
-
-----
-git push origin HEAD:refs/for/master
-----
 
 == Testing
 
@@ -130,6 +66,92 @@
 <<dev-e2e-tests#,This document>> describes how `e2e` (load or functional) test
 scenarios are implemented using link:https://gatling.io/[`Gatling`].
 
+
+== Local server
+
+[[init]]
+=== Site Initialization
+
+After you compile the project <<compile_project,(above)>>, run the Gerrit
+`init`
+command to create a test site:
+
+----
+  export GERRIT_SITE=~/gerrit_testsite
+  $(bazel info output_base)/external/local_jdk/bin/java \
+      -jar bazel-bin/gerrit.war init --batch --dev -d $GERRIT_SITE
+----
+
+[[special_bazel_java_version]]
+NOTE: You must use the same Java version that Bazel used for the build, which
+is available at `$(bazel info output_base)/external/local_jdk/bin/java`.
+
+This command takes two parameters:
+
+* `--batch` assigns default values to several Gerrit configuration
+    options. To learn more about these options, see
+    link:config-gerrit.html[Configuration].
+* `--dev` configures the Gerrit server to use the authentication
+  option, `DEVELOPMENT_BECOME_ANY_ACCOUNT`, which enables you to
+  switch between different users to explore how Gerrit works. To learn more
+  about setting up Gerrit for development, see
+  link:dev-readme.html[Gerrit Code Review: Developer Setup].
+
+After initializing the test site, Gerrit starts serving in the background. A
+web browser displays the Start page.
+
+On the Start page, you can:
+
+.  Log in as the account you created during the initialization process.
+.  Register additional accounts.
+.  Create projects.
+
+To shut down the daemon, run:
+
+----
+  $GERRIT_SITE/bin/gerrit.sh stop
+----
+
+
+[[localdev]]
+=== Working with the Local Server
+
+To create more accounts on your development instance:
+
+.  Click 'become' in the upper right corner.
+.  Select 'Switch User'.
+.  Register a new account.
+.  link:user-upload.html#ssh[Configure your SSH key].
+
+Use the `ssh` protocol to clone from and push to the local server. For
+example, to clone a repository that you've created through the admin
+interface, run:
+
+----
+git clone ssh://username@localhost:29418/projectname
+----
+
+To use the `HTTP` protocol, run:
+
+----
+git clone http://username@localhost:8080/projectname
+----
+
+The default password for user `admin` is `secret`. You can regenerate a
+password in the UI under User Settings -- HTTP credentials. The password can be
+stored locally to avoid retyping it:
+
+----
+git config --global credential.helper store
+git pull
+----
+
+To create changes as users of Gerrit would, run:
+
+----
+git push origin HEAD:refs/for/master
+----
+
 [[run_daemon]]
 === Running the Daemon
 
@@ -138,7 +160,7 @@
 
 ----
   $(bazel info output_base)/external/local_jdk/bin/java \
-     -jar bazel-bin/gerrit.war daemon -d ../gerrit_testsite \
+     -jar bazel-bin/gerrit.war daemon -d $GERRIT_SITE \
      --console-log
 ----
 
@@ -170,7 +192,7 @@
 
 ----
   $(bazel info output_base)/external/local_jdk/bin/java \
-     -jar bazel-bin/gerrit.war daemon -d ../gerrit_testsite -s
+     -jar bazel-bin/gerrit.war daemon -d $GERRIT_SITE -s
 ----
 
 NOTE: To learn why using `java -jar` isn't sufficient, see
@@ -194,27 +216,24 @@
 CAUTION: When using the Inspector, be careful not to modify the internal state
 of the system.
 
-== Switching between branches
 
-When using `git checkout` without `--recurse-submodules` to switch between
-branches, submodule revisions are not altered, which can result in:
+== Setup for backend developers
 
-*  Incorrect or unneeded plugin revisions.
-*  Missing plugins.
+=== Configuring Eclipse
 
-After you switch branches, ensure that you have the correct versions of
-the submodules.
+To use the Eclipse IDE for development, see
+link:dev-eclipse.html[Eclipse Setup].
 
-CAUTION: If you store Eclipse or IntelliJ project files in the Gerrit source
-directories, do *_not_* run `git clean -fdx`. Doing so may remove untracked files and damage your project. For more information, see
-link:https://git-scm.com/docs/git-clean[git-clean].
+To configure the Eclipse workspace with Bazel, see
+link:dev-bazel.html#eclipse[Eclipse integration with Bazel].
 
-Run the following:
+=== Configuring IntelliJ IDEA
 
-----
-  git submodule update
-  git clean -ffd
-----
+See <<dev-intellij#,IntelliJ Setup>> for details.
+
+== Setup for frontend developers
+See link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/README.md[Frontend Developer Setup].
+
 
 GERRIT
 ------
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index 5411927..98a3df5 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -91,7 +91,7 @@
 
 * `gerrit-maven`:
 +
-Bucket to store Gerrit Subproject Artifacts (e.g. `gwtorm` etc.).
+Bucket to store Gerrit Subproject Artifacts (e.g. Prolog Cafe).
 
 To upload artifacts to a bucket the user must authenticate with a
 username and password. The username and password need to be retrieved
diff --git a/Documentation/dev-release-jgit.txt b/Documentation/dev-release-jgit.txt
deleted file mode 100644
index 1a8b501..0000000
--- a/Documentation/dev-release-jgit.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-= Making a Snapshot Release of JGit
-
-This step is only necessary if we need to create an unofficial JGit
-snapshot release and publish it to the
-link:https://developers.google.com/storage/[Google Cloud Storage].
-
-[[prepare-environment]]
-== Prepare the Maven Environment
-
-First, make sure you have done the necessary
-link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
-configuration in Maven `settings.xml`].
-
-To apply the necessary settings in JGit's `pom.xml`, follow the instructions
-in link:dev-release-deploy-config.html#deploy-configuration-subprojects[
-Configuration for Subprojects in `pom.xml`], or apply the provided diff by
-executing the following command in the JGit workspace:
-
-----
-  git apply /path/to/gerrit/tools/jgit-snapshot-deploy-pom.diff
-----
-
-[[prepare-release]]
-== Prepare the Release
-
-Since JGit has its own release process we do not push any release tags. Instead
-we will use the output of `git describe` as the version of the current JGit
-snapshot.
-
-In the JGit workspace, execute the following command:
-
-----
-  ./tools/version.sh --release $(git describe)
-----
-
-[[publish-release]]
-== Publish the Release
-
-To deploy the new snapshot, execute the following command in the JGit
-workspace:
-
-----
-  mvn deploy
-----
-
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index c10457d..2131d00 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -150,7 +150,7 @@
 Tag the plugins:
 
 ----
-  git submodule foreach git tag -s -m "v$version" "v$version"
+  git submodule foreach '[ "$path" == "modules/jgit" ] || git tag -s -m "v$version" "v$version"'
 ----
 
 [[build-gerrit]]
@@ -409,7 +409,7 @@
 feature-deprecations that we were holding off on to have a stable release where
 the feature is still contained, but marked as deprecated.
 
-See link:dev-contributing.html#deprecating-features[Deprecating features] for
+See link:dev-processes.html#deprecating-features[Deprecating features] for
 details.
 
 GERRIT
diff --git a/Documentation/dev-roles.txt b/Documentation/dev-roles.txt
new file mode 100644
index 0000000..9dbc450
--- /dev/null
+++ b/Documentation/dev-roles.txt
@@ -0,0 +1,378 @@
+= Gerrit Code Review - Supporting Roles
+
+As an open source project Gerrit has a large community of people
+driving the project forward. There are many ways to engage with
+the project and get involved.
+
+[[supporter]]
+== Supporter
+
+Supporters are individuals who help the Gerrit project and the Gerrit
+community in any way. This includes users that provide feedback to the
+Gerrit community or get in touch by other means.
+
+There are many possibilities to support the project, e.g.:
+
+* get involved in discussions on the
+  link:https://groups.google.com/d/forum/repo-discuss[repo-discuss]
+  mailing list (post your questions, provide feedback, share your
+  experiences, help other users)
+* attend community events like user summits (see
+  link:https://calendar.google.com/calendar?cid=Z29vZ2xlLmNvbV91YmIxcGxhNmlqNzg1b3FianI2MWg0dmRpc0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t[
+  community calendar])
+* report link:https://bugs.chromium.org/p/gerrit/issues/list[issues]
+  and help to clarify existing issues
+* provide feedback on
+  link:https://www.gerritcodereview.com/releases-readme.html[new
+  releases and release candidates]
+* review
+  link:https://gerrit-review.googlesource.com/q/status:open[changes]
+  and help to verify that they work as advertised, comment if you like
+  or dislike a feature
+* serve as contact person for a proprietary Gerrit installation and
+  channel feedback from users back to the Gerrit community
+
+Supporters can:
+
+* post on the
+  link:https://groups.google.com/d/forum/repo-discuss[repo-discuss]
+  mailing list (Please note that the `repo-discuss` mailing list is
+  managed to prevent spam posts. This means posts from new participants
+  must be approved manually before they appear on the mailing list.
+  Approvals normally happen within 1 work day. Posts of people who
+  participate in mailing list discussions frequently are approved
+  automatically)
+* comment on
+  link:https://gerrit-review.googlesource.com/q/status:open[changes]
+  and vote from `-1` to `+1` on the `Code-Review` label (these votes
+  are important to understand the interest in a change and to address
+  concerns early, however link:#maintainer[maintainers] can
+  overrule/ignore these votes)
+* download changes to try them out, feedback can be provided as
+  comments and by voting (preferably on the `Verified` label,
+  permissions to vote on the `Verified` label are granted by request,
+  see below)
+* file issues in the link:https://bugs.chromium.org/p/gerrit/issues/list[
+  issue tracker] and comment on existing issues
+* support the
+  link:dev-processes.html#design-driven-contribution-process[
+  design-driven contribution process] by reviewing incoming
+  link:dev-design-docs.html[design docs] and raising concerns during
+  the design review
+
+Supporters who want to engage further can get additional privileges
+on request (ask for it on the
+link:https://groups.google.com/d/forum/repo-discuss[repo-discuss]
+mailing list):
+
+* become member of the `gerrit-verifiers` group, which allows to:
+** vote on the `Verified` and `Code-Style` labels
+** edit hashtags on all changes
+** edit topics on all open changes
+** abandon changes
+* approve posts to the
+  link:https://groups.google.com/d/forum/repo-discuss[repo-discuss]
+  mailing list
+* administrate issues in the
+  link:https://bugs.chromium.org/p/gerrit/issues/list[issue tracker]
+
+Supporters can become link:#contributor[contributors] by signing a
+contributor license agreement and contributing code to the Gerrit
+project.
+
+[[contributor]]
+== Contributor
+
+Everyone who has a valid link:dev-cla.html[contributor license
+agreement] and who has link:dev-contributing.html[contributed] at least
+one change to any project on
+link:https://gerrit-review.googlesource.com/[
+gerrit-review.googlesource.com] is a contributor.
+
+Contributions can be:
+
+* new features
+* bug fixes
+* code cleanups
+* documentation updates
+* release notes updates
+* propose link:#dev-design-docs[design docs] as part of the
+  link:dev-contributing.html#design-driven-contribution-process[
+  design-driven contribution process]
+* scripts which are of interest to the community
+
+Contributors have all the permissions that link:#supporter[supporters]
+have. In addition they have signed a link:dev-cla.html[contributor
+license agreement] which enables them to push changes.
+
+Regular contributors can ask to be added to the `gerrit-verifiers`
+group, which allows to:
+
+* add patch sets to changes of other users
+* propose project config changes (push changes for the
+  `refs/meta/config` branch
+
+Being member of the `gerrit-verifiers` group includes further
+permissions (see link:#supporter[supporter] section above).
+
+It's highly appreciated if contributors engage in code reviews,
+link:dev-design-docs.html#review[design reviews] and mailing list
+discussions. If wanted, contributors can also serve as link:#mentor[
+mentors] to support other contributors with getting their features
+done.
+
+Contributors may also be invited to join the Gerrit hackathons which
+happen regularly (e.g. twice a year). Hackathons are announced on the
+link:https://groups.google.com/d/forum/repo-discuss[repo-discuss]
+mailing list (also see
+link:https://calendar.google.com/calendar?cid=Z29vZ2xlLmNvbV91YmIxcGxhNmlqNzg1b3FianI2MWg0dmRpc0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t[
+community calendar]).
+
+Outstanding contributors that are actively engaged in the community, in
+activities outlined above, may be nominated as link:#maintainer[
+maintainers].
+
+[[maintainer]]
+== Maintainer
+
+Maintainers are the gatekeepers of the project and are in charge of
+approving and submitting changes. Refer to the project homepage for
+the link:https://www.gerritcodereview.com/members.html#maintainers[
+list of current maintainers].
+
+Maintainers should only approve changes that:
+
+* they fully understand
+* are in line with the project vision and project scope that are
+  defined by the link:dev-processes.html#steering-committee[engineering steering
+  committee], and should consult them, when in doubt
+* meet the quality expectations of the project (well-tested, properly
+  documented, scalable, backwards-compatible)
+* implement usable features or bug fixes (no incomplete/unusable
+  things)
+* are not authored by themselves (exceptions are changes which are
+  trivial according to the judgment of the maintainer and changes that
+  are required by the release process and branch management)
+
+Maintainers are trusted to assess changes, but are also expected to
+align with the other maintainers, especially if large new features are
+being added.
+
+Maintainers are highly encouraged to dedicate some of their time to the
+following tasks (but are not required to do so):
+
+* reviewing changes
+* mailing list discussions and support
+* bug fixing and bug triaging
+* supporting the
+  link:dev-processes.html#design-driven-contribution-process[
+  design-driven contribution process] by reviewing incoming
+  link:dev-design-docs.html[design docs] and raising concerns during
+  the design review
+* serving as link:#mentor[mentor]
+* doing releases (see link#release-manager[release manager])
+
+Maintainers can:
+
+* approve changes (vote `+2` on the `Code-Review` label); when
+  approving changes, `-1` votes on the `Code-Review` label can be
+  ignored if there is a good reason, in this case the reason should be
+  clearly communicated on the change
+* submit changes
+* block submission of changes if they disagree with how a feature is
+  being implemented (vote `-2` on the `Code-Review` label), but their
+  vote can be overruled by the steering committee, see
+  link:dev-processes.html#project-governance[Project Governance]
+* nominate new maintainers and vote on nominations (see below)
+* administrate the link:https://groups.google.com/d/forum/repo-discuss[
+  mailing list], the
+  link:https://bugs.chromium.org/p/gerrit/issues/list[issue tracker]
+  and the link:https://www.gerritcodereview.com/[homepage]
+* gain permissions to do Gerrit releases and publish release artifacts
+* create new projects and groups on
+  link:https://gerrit-review.googlesource.com/[
+  gerrit-review.googlesource.com]
+* administrate the Gerrit projects on
+  link:https://gerrit-review.googlesource.com/[
+  gerrit-review.googlesource.com] (e.g. edit ACLs, update project
+  configuration)
+* create events in the
+  link:https://calendar.google.com/calendar?cid=Z29vZ2xlLmNvbV91YmIxcGxhNmlqNzg1b3FianI2MWg0dmRpc0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t[
+  community calendar]
+* discuss with other maintainers on the private maintainers mailing
+  list and Slack channel
+
+In addition, maintainers from Google can:
+
+* approve/reject changes that update project dependencies (vote `-1` to
+  `+1` on the `Library-Compliance` label), see
+  link:dev-processes.html#upgrading-libraries[Upgrading Libraries]
+* edit permissions on the Gerrit core projects
+
+[[maintainer-election]]
+Maintainers can nominate new maintainers by posting a nomination on the
+non-public maintainers mailing list. Nominations should stay open for
+at least 14 calendar days so that all maintainers have a chance to
+vote. To be approved as maintainer a minimum of 5 positive votes and no
+negative votes is required. This means if 5 positive votes without
+negative votes have been reached and 14 calendar days have passed, any
+maintainer can close the vote and welcome the new maintainer. Extending
+the voting period during holiday season or if there are not enough
+votes is possible, but the voting period should not exceed 1 month. If
+there are negative votes that are considered unjustified, the
+link:dev-processes.html#steering-committee[engineering steering
+committee] may get involved to decide whether the new maintainer can be
+accepted anyway.
+
+To become a maintainer, a link:#contributor[contributor] should have a
+history of deep technical contributions across different parts of the
+core Gerrit codebase. However, it is not required to be an expert on
+everything. Things that we want to see from potential maintainers
+include:
+
+* high quality code contributions
+* high quality code reviews
+* activity on the mailing list
+
+[[steering-committee-member]]
+== Engineering Steering Committee Member
+
+The Gerrit project has an Engineering Steering Committee (ESC) that
+governs the project, see link:dev-processes.html#project-governance[Project Governance].
+
+Members of the steering committee are expected to act in the interest
+of the Gerrit project and the whole Gerrit community. Refer to the project
+homepage for the link:https://www.gerritcodereview.com/members.html#engineering-steering-committee[
+list of current committee members].
+
+For those that are familiar with scrum, the steering committee member
+role is similar to the role of an agile product owner.
+
+Steering committee members must be able to dedicate sufficient time to
+their role so that the steering committee can satisfy its
+responsibilities and live up to the promise of answering incoming
+requests in a timely manner.
+
+Community members may submit new items under the
+link:https://bugs.chromium.org/p/gerrit/issues/list?q=component:ESC[ESC component]
+in the issue tracker, or add that component to existing items, to raise them to
+the attention of ESC members.
+
+Community members may contact the ESC members directly using
+mailto:gerritcodereview-esc@googlegroups.com[this mailing list].
+This is a group that remains private between the individual community
+member and ESC members.
+
+link:#maintainer[Maintainers] can become steering committee member by
+election, or by being appointed by Google (only for the seats that
+belong to Google).
+
+[[mentor]]
+== Mentor
+
+A mentor is a link:#maintainer[maintainer] or link:#contributor[
+contributor] who is assigned to support the development of a feature
+that was specified in a link:dev-design-docs.html[design doc] and was
+approved by the link:dev-processes.html#steering-committee[steering
+committee].
+
+The goal of the mentor is to make the feature successful by:
+
+* doing timely reviews
+* providing technical guidance during code reviews
+* discussing details of the design
+* ensuring that the quality standards are met (well documented,
+  sufficient test coverage, backwards compatible etc.)
+
+The implementation is fully done by the contributor, but optionally
+mentors can help out with contributing some changes.
+
+link:#maintainer[Maintainers] and link:#contributor[contributors] can
+volunteer to generally serve as mentors, or to mentor specific features
+(e.g. if they see an upcoming feature on the roadmap that they are
+interested in). To volunteer as mentor, contact the
+link:dev-processes.html#steering-committee[steering committee] or
+comment on a change that adds a link:dev-design-docs.html#propose[
+design doc].
+
+[[community-manager]]
+== Community Manager
+
+Community managers should act as stakeholders for the Gerrit community
+and focus on the health of the community. Refer to the project homepage
+for the link:https://www.gerritcodereview.com/members.html#community-managers[
+list of current community managers].
+
+Tasks:
+
+* act as stakeholder for the Gerrit community towards the
+  link:dev-processes.html#steering-committee[steering committee]
+* ensure that the link:dev-contributing.html#mentorship[mentorship
+  process] works
+* deescalate conflicts in the Gerrit community
+* constantly improve community processes (e.g. contribution process)
+* watch out for community issues and address them proactively
+* serve as contact person for community issues
+
+Community members may submit new items under the
+link:https://bugs.chromium.org/p/gerrit/issues/list?q=component:Community[Community component]
+backlog, for community managers to refine. Only public topics should be
+issued through that backlog.
+
+Sensitive topics are to be privately discussed using
+mailto:gerritcodereview-community-managers@googlegroups.com[this mailing list].
+This is a group that remains private between the individual community
+member and community managers.
+
+The community managers should be a pair or trio that shares the work:
+
+* One Googler that is appointed by Google.
+* One or two non-Googlers, elected by the community if there are more
+  than two candidates. If there is no candidate, we only have the one
+  community manager from Google.
+
+Community managers must not be link:#steering-committee-member[
+steering committee members] at the same time so that they can represent
+the community without conflict of interest.
+
+Anybody from the Gerrit community can candidate as community manager.
+This means, in contrast to candidating for the ESC, candidating as
+community manager is not limited to Gerrit maintainers. Otherwise the
+nomination process, election process and election period for the
+non-Google community manager are the same as for
+link:dev-processes.html#steering-committee-election[steering committee
+members].
+
+[[release-manager]]
+== Release Manager
+
+Each major Gerrit release is driven by a Gerrit link:#maintainer[
+maintainer], the so called release manager.
+
+The release manager is responsible for:
+
+* identifying release blockers and informing about them
+* creating stable branches and updating version numbers
+* creating release candidates, the final major release and minor
+  releases
+* announcing releases on the mailing list and collecting feedback
+* ensuring that releases meet minimal quality expectations (Gerrit
+  starts, upgrade from previous version works)
+* publishing release artifacts
+* ensuring quality and completeness of the release notes
+* cherry-picking bug fixes, see link:dev-processes.html#backporting[
+  Backporting to stable branches]
+* estimating the risk of new features that are added on stable
+  branches, see link:dev-processes.html#dev-in-stable-branches[
+  Development in stable branches]
+
+Before each release, the release manager is appointed by consensus among
+the maintainers. Volunteers are welcome, but it's also a goal to fairly
+share this work between maintainers and contributing companies.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-starter-projects.txt b/Documentation/dev-starter-projects.txt
new file mode 100644
index 0000000..ae40ea6
--- /dev/null
+++ b/Documentation/dev-starter-projects.txt
@@ -0,0 +1,14 @@
+= Gerrit Code Review - Starter Projects
+
+We have created a
+link:https://bugs.chromium.org/p/gerrit/issues/list?can=2&q=label%3AStarterProject[StarterProject]
+category in the issue tracker and try to assign easy hack projects to it. If in
+doubt, do not hesitate to ask on the developer
+link:https://groups.google.com/forum/#!forum/repo-discuss[mailing list].
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/error-change-does-not-belong-to-project.txt b/Documentation/error-change-does-not-belong-to-project.txt
deleted file mode 100644
index 21596b1..0000000
--- a/Documentation/error-change-does-not-belong-to-project.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-= change ... does not belong to project ...
-
-With this error message Gerrit rejects to push a commit to a change
-that belongs to another project.
-
-This error message means that the user explicitly pushed a commit to
-a change that belongs to another project by specifying it as target
-ref. This way of adding a new patch set to a change is deprecated as
-explained link:user-upload.html#manual_replacement_mapping[here]. It is recommended to only rely on Change-Ids for
-link:user-upload.html#push_replace[replacing changes].
-
-
-GERRIT
-------
-Part of link:error-messages.html[Gerrit Error Messages]
-
-SEARCHBOX
----------
diff --git a/Documentation/error-change-not-found.txt b/Documentation/error-change-not-found.txt
deleted file mode 100644
index df99388..0000000
--- a/Documentation/error-change-not-found.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-= change ... not found
-
-With this error message Gerrit rejects to push a commit to a change
-that cannot be found.
-
-This error message means that the user explicitly pushed a commit to
-a non-existing change by specifying it as target ref. This way of
-adding a new patch set to a change is deprecated as explained link:user-upload.html#manual_replacement_mapping[here].
-It is recommended to only rely on Change-Ids for link:user-upload.html#push_replace[replacing changes].
-
-
-GERRIT
-------
-Part of link:error-messages.html[Gerrit Error Messages]
-
-SEARCHBOX
----------
diff --git a/Documentation/error-commit-already-exists.txt b/Documentation/error-commit-already-exists.txt
index d2b7c9d..2832c78 100644
--- a/Documentation/error-commit-already-exists.txt
+++ b/Documentation/error-commit-already-exists.txt
@@ -1,6 +1,6 @@
 = commit already exists
 
-With "commit already exists (as current patchset)" or
+With "commit(s) already exists (as current patchset)" or
 "commit already exists (in the change)" error message
 Gerrit rejects to push a commit to an existing change via
 `refs/changes/n` if the commit was already successfully
diff --git a/Documentation/error-messages.txt b/Documentation/error-messages.txt
index b523663..eedae39 100644
--- a/Documentation/error-messages.txt
+++ b/Documentation/error-messages.txt
@@ -9,8 +9,6 @@
 
 * link:error-branch-not-found.html[branch ... not found]
 * link:error-change-closed.html[change ... closed]
-* link:error-change-does-not-belong-to-project.html[change ... does not belong to project ...]
-* link:error-change-not-found.html[change ... not found]
 * link:error-commit-already-exists.html[commit already exists]
 * link:error-contains-banned-commit.html[contains banned commit ...]
 * link:error-has-duplicates.html[... has duplicates]
@@ -35,7 +33,6 @@
 * link:error-same-change-id-in-multiple-changes.html[same Change-Id in multiple changes]
 * link:error-too-many-commits.html[too many commits]
 * link:error-upload-denied.html[Upload denied for project \'...']
-* link:error-push-refschanges-not-allowed.html[upload to refs/changes not allowed]
 * link:error-not-allowed-to-upload-merges.html[you are not allowed to upload merges]
 
 
diff --git a/Documentation/error-push-refschanges-not-allowed.txt b/Documentation/error-push-refschanges-not-allowed.txt
deleted file mode 100644
index 2bbdc3e..0000000
--- a/Documentation/error-push-refschanges-not-allowed.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-= upload to refs/changes not allowed
-
-Pushing to `refs/changes/` is deprecated and is not allowed on this Gerrit server.
-See the documentation for link:user-upload.html#push_create[creating changes] for
-alternate ways to push to existing changes.
-
-
-GERRIT
-------
-Part of link:error-messages.html[Gerrit Error Messages]
-
-SEARCHBOX
----------
diff --git a/Documentation/i18n-readme.txt b/Documentation/i18n-readme.txt
deleted file mode 100644
index 180fc53..0000000
--- a/Documentation/i18n-readme.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-= Gerrit Code Review - i18n
-
-Aside from actually writing translations, there are some issues with
-the way the code produces output.  Most of the UI should support
-right-to-left (RTL) languages.
-
-== Labels
-
-Labels and their values are defined in project.config by the Gerrit
-administrator or project owners.  Only a single translation of these
-strings is supported.
-
-== /Gerrit Gerrit.html
-
-* The title of the host page is not translated.
-
-* The <noscript> tag is not translated.
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/Documentation/index.txt b/Documentation/index.txt
index f9e39b5..4de55a7 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -10,7 +10,11 @@
 . link:intro-how-gerrit-works.html[How Gerrit Works]
 . link:intro-gerrit-walkthrough.html[Basic Gerrit Walkthrough]
 
-== Guides
+== Contributor Guides
+. link:dev-community.html[Gerrit Community]
+. link:dev-community.html#how-to-contribute[How to Contribute]
+
+== User Guides
 . link:intro-user.html[User Guide]
 . link:intro-project-owner.html[Project Owner Guide]
 . link:https://source.android.com/source/developing[Default Android Workflow] (external)
@@ -73,28 +77,6 @@
 . link:config-accounts.html[Accounts on NoteDb]
 . link:config-groups.html[Groups on NoteDb]
 
-== Developer
-. Getting Started
-.. link:dev-readme.html[Developer Setup]
-.. link:dev-bazel.html[Building with Bazel]
-.. link:dev-eclipse.html[Eclipse Setup]
-.. link:dev-intellij.html[IntelliJ Setup]
-.. link:dev-contributing.html[Contributing to Gerrit]
-. Plugin Development
-.. link:dev-plugins.html[Developing Plugins]
-.. link:dev-build-plugins.html[Building Gerrit plugins]
-.. link:js-api.html[JavaScript Plugin API]
-.. link:config-validation.html[Validation Interfaces]
-.. link:dev-stars.html[Starring Changes]
-.. link:quota.html[Quota Enforcement]
-. link:dev-design.html[System Design]
-. link:i18n-readme.html[i18n Support]
-
-== Maintainer
-. link:dev-release.html[Making a Gerrit Release]
-. link:dev-release-subproject.html[Making a Release of a Gerrit Subproject]
-. link:dev-release-jgit.html[Making a Release of JGit]
-
 == Concepts
 . link:config-labels.html[Review Labels]
 . link:access-control.html[Access Controls]
@@ -105,7 +87,7 @@
 == Resources
 * link:licenses.html[Licenses and Notices]
 * link:https://www.gerritcodereview.com/[Homepage]
-* link:https://www.gerritcodereview.com/download/index.html[Downloads]
+* link:https://gerrit-releases.storage.googleapis.com/index.html[Downloads]
 * link:https://bugs.chromium.org/p/gerrit/issues/list[Issue Tracking]
 * link:https://gerrit.googlesource.com/gerrit[Source Code]
 * link:https://www.gerritcodereview.com/about.md[A History of Gerrit Code Review]
diff --git a/Documentation/install.txt b/Documentation/install.txt
index aaefc86f..09ebbba 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -46,7 +46,7 @@
 == Download Gerrit
 
 Current and past binary releases of Gerrit can be obtained from
-the link:https://www.gerritcodereview.com/download/index.html[
+the link:https://gerrit-releases.storage.googleapis.com/index.html[
 Gerrit Releases site].
 
 Download any current `*.war` package. The war will be referred to as
diff --git a/Documentation/intro-gerrit-walkthrough.txt b/Documentation/intro-gerrit-walkthrough.txt
index 1fba1dc..b4f799c2 100644
--- a/Documentation/intro-gerrit-walkthrough.txt
+++ b/Documentation/intro-gerrit-walkthrough.txt
@@ -28,7 +28,7 @@
 modify. To get this code, he runs the following `git clone` command:
 
 ----
-clone ssh://gerrithost:29418/RecipeBook.git RecipeBook
+git clone ssh://gerrithost:29418/RecipeBook.git RecipeBook
 ----
 
 After he clones the repository, he runs a couple of commands to add a
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 04778f1..a54774b 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -726,36 +726,6 @@
 
 The following preferences can be configured:
 
-- [[review-category]]`Display In Review Category`:
-+
-This setting controls how the values of the review labels in change
-lists and dashboards are visualized.
-+
-** `None`:
-+
-For each review label only the voting value is shown. Approvals are
-rendered as a green check mark icon, vetoes as a red X icon.
-+
-** `Show Name`:
-+
-For each review label the voting value is shown together with the full
-name of the voting user.
-+
-** `Show Email`:
-+
-For each review label the voting value is shown together with the email
-address of the voting user.
-+
-** `Show Username`:
-+
-For each review label the voting value is shown together with the
-username of the voting user.
-+
-** `Show Abbreviated Name`:
-+
-For each review label the voting value is shown together with the
-initials of the full name of the voting user.
-
 - [[page-size]]`Maximum Page Size`:
 +
 The maximum number of entries that are shown on one page, e.g. used
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
index 4ef2a6c..030541d 100644
--- a/Documentation/js-api.txt
+++ b/Documentation/js-api.txt
@@ -24,123 +24,17 @@
 The plugin instance is passed to the plugin's initialization function
 and provides a number of utility services to plugin authors.
 
-[[self_delete]]
-=== self.delete() / self.del()
-Issues a DELETE REST API request to the Gerrit server.
-
-.Signature
-[source,javascript]
-----
-Gerrit.delete(url, callback)
-Gerrit.del(url, callback)
-----
-
-* url: URL relative to the plugin's URL space. The JavaScript
-  library prefixes the supplied URL with `/plugins/{getPluginName}/`.
-
-* callback: JavaScript function to be invoked with the parsed
-  JSON result of the API call. DELETE methods often return
-  `204 No Content`, which is passed as null.
-
-[[self_get]]
-=== self.get()
-Issues a GET REST API request to the Gerrit server.
-
-.Signature
-[source,javascript]
-----
-self.get(url, callback)
-----
-
-* url: URL relative to the plugin's URL space. The JavaScript
-  library prefixes the supplied URL with `/plugins/{getPluginName}/`.
-
-* callback: JavaScript function to be invoked with the parsed JSON
-  result of the API call. If the API returns a string the result is
-  a string, otherwise the result is a JavaScript object or array,
-  as described in the relevant REST API documentation.
-
 [[self_getServerInfo]]
 === self.getServerInfo()
 Returns the server's link:rest-api-config.html#server-info[ServerInfo]
 data.
 
-[[self_getCurrentUser]]
-=== self.getCurrentUser()
-Returns the currently signed in user's AccountInfo data; empty account
-data if no user is currently signed in.
-
-[[Gerrit_getUserPreferences]]
-=== Gerrit.getUserPreferences()
-Returns the preferences of the currently signed in user; the default
-preferences if no user is currently signed in.
-
-[[Gerrit_refreshUserPreferences]]
-=== Gerrit.refreshUserPreferences()
-Refreshes the preferences of the current user.
-
 [[self_getPluginName]]
 === self.getPluginName()
 Returns the name this plugin was installed as by the server
 administrator. The plugin name is required to access REST API
 views installed by the plugin, or to access resources.
 
-[[self_post]]
-=== self.post()
-Issues a POST REST API request to the Gerrit server.
-
-.Signature
-[source,javascript]
-----
-self.post(url, input, callback)
-----
-
-* url: URL relative to the plugin's URL space. The JavaScript
-  library prefixes the supplied URL with `/plugins/{getPluginName}/`.
-
-* input: JavaScript object to serialize as the request payload.
-
-* callback: JavaScript function to be invoked with the parsed JSON
-  result of the API call. If the API returns a string the result is
-  a string, otherwise the result is a JavaScript object or array,
-  as described in the relevant REST API documentation.
-
-[source,javascript]
-----
-self.post(
-  '/my-servlet',
-  {start_build: true, platform_type: 'Linux'},
-  function (r) {});
-----
-
-[[self_put]]
-=== self.put()
-Issues a PUT REST API request to the Gerrit server.
-
-.Signature
-[source,javascript]
-----
-self.put(url, input, callback)
-----
-
-* url: URL relative to the plugin's URL space. The JavaScript
-  library prefixes the supplied URL with `/plugins/{getPluginName}/`.
-
-* input: JavaScript object to serialize as the request payload.
-
-* callback: JavaScript function to be invoked with the parsed JSON
-  result of the API call. If the API returns a string the result is
-  a string, otherwise the result is a JavaScript object or array,
-  as described in the relevant REST API documentation.
-
-[source,javascript]
-----
-self.put(
-  '/builds',
-  {start_build: true, platform_type: 'Linux'},
-  function (r) {});
-----
-
 [[self_on]]
 === self.on()
 Register a JavaScript callback to be invoked when events occur within
@@ -149,7 +43,7 @@
 .Signature
 [source,javascript]
 ----
-Gerrit.on(event, callback);
+self.on(event, callback);
 ----
 
 * event: A supported event type. See below for description.
@@ -194,39 +88,26 @@
   This event can be used to register a new language highlighter with
   the highlight.js library before syntax highlighting begins.
 
-[[self_onAction]]
-=== self.onAction()
-Register a JavaScript callback to be invoked when the user clicks
-on a button associated with a server side `UiAction`.
+[[self_changeActions]]
+=== self.changeActions()
+Returns an instance of ChangeActions API.
 
 .Signature
 [source,javascript]
 ----
-self.onAction(type, view_name, callback);
+self.changeActions();
 ----
 
-* type: `'change'`, `'edit'`, `'revision'`, `'project'`, or `'branch'`
-  indicating which type of resource the `UiAction` was bound to
-  in the server.
-
-* view_name: string appearing in URLs to name the view. This is the
-  second argument of the `get()`, `post()`, `put()`, and `delete()`
-  binding methods in a `RestApiModule`.
-
-* callback: JavaScript function to invoke when the user clicks. The
-  function will be passed a link:#ActionContext[action context].
-
 [[self_screen]]
 === self.screen()
-Register a JavaScript callback to be invoked when the user navigates
+Register a module to be attached when the user navigates
 to an extension screen provided by the plugin. Extension screens are
 usually linked from the link:dev-plugins.html#top-menu-extensions[top menu].
-The callback can populate the DOM with the screen's contents.
 
 .Signature
 [source,javascript]
 ----
-self.screen(pattern, callback);
+self.screen(pattern, opt_moduleName);
 ----
 
 * pattern: URL token pattern to identify the screen. Argument can be
@@ -234,52 +115,34 @@
   If a RegExp is used the matching groups will be available inside of
   the context as `token_match`.
 
-* callback: JavaScript function to invoke when the user navigates to
+* opt_moduleName: The module to load when the user navigates to
   the screen. The function will be passed a link:#ScreenContext[screen context].
 
-[[self_settingsScreen]]
-=== self.settingsScreen()
-Register a JavaScript callback to be invoked when the user navigates
-to an extension settings screen provided by the plugin. Extension settings
-screens are automatically linked from the settings menu under the given
-menu entry.
-The callback can populate the DOM with the screen's contents.
+[[self_settings]]
+=== self.settings()
+Returns the Settings API.
 
 .Signature
 [source,javascript]
 ----
-self.settingsScreen(path, menu, callback);
+self.settings();
 ----
 
-* path: URL path to identify the settings screen.
-
-* menu: The name of the menu entry in the settings menu that should
-  link to the settings screen.
-
-* callback: JavaScript function to invoke when the user navigates to
-  the settings screen. The function will be passed a
-  link:#SettingsScreenContext[settings screen context].
-
-[[self_panel]]
-=== self.panel()
-Register a JavaScript callback to be invoked when a screen with the
-given extension point is loaded.
-The callback can populate the DOM with the panel's contents.
+[[self_registerCustomComponent]]
+=== self.registerCustomComponent()
+Register a custom component to a specific endpoint.
 
 .Signature
 [source,javascript]
 ----
-self.panel(extensionpoint, callback);
+self.registerCustomComponent(endpointName, opt_moduleName, opt_options);
 ----
 
-* extensionpoint: The name of the extension point that marks the
-  position where the panel is added to an existing screen. The
-  available extension points are described in the
-  link:dev-plugins.html#panels[plugin development documentation].
+* endpointName: The endpoint this plugin should be reigistered to.
 
-* callback: JavaScript function to invoke when a screen with the
-  extension point is loaded. The function will be passed a
-  link:#PanelContext[panel context].
+* opt_moduleName: The module name the custom component will use.
+
+* opt_options: Options to register this custom component.
 
 [[self_url]]
 === self.url()
@@ -293,398 +156,260 @@
 self.url('/static/icon.png');  // "https://gerrit-review.googlesource.com/plugins/demo/static/icon.png"
 ----
 
-
-[[ActionContext]]
-== Action Context
-A new action context is passed to the `onAction` callback function
-each time the associated action button is clicked by the user. A
-context is initialized with sufficient state to issue the associated
-REST API RPC.
-
-[[context_action]]
-=== context.action
-An link:rest-api-changes.html#action-info[ActionInfo] object instance
-supplied by the server describing the UI button the user used to
-invoke the action.
-
-[[context_call]]
-=== context.call()
-Issues the REST API call associated with the action. The HTTP method
-used comes from `context.action.method`, hiding the JavaScript from
-needing to care.
+[[self_restApi]]
+=== self.restApi()
+Returns an instance of the Plugin REST API.
 
 .Signature
 [source,javascript]
 ----
-context.call(input, callback)
+self.restApi(prefix_url)
 ----
 
-* input: JavaScript object to serialize as the request payload. This
-  parameter is ignored for GET and DELETE methods.
+* prefix_url: Base url for subsequent .get(), .post() etc requests.
 
-* callback: JavaScript function to be invoked with the parsed JSON
-  result of the API call. If the API returns a string the result is
-  a string, otherwise the result is a JavaScript object or array,
-  as described in the relevant REST API documentation.
+[[PluginRestAPI]]
+== Plugin Rest API
 
-[source,javascript]
-----
-context.call(
-  {message: "..."},
-  function (result) {
-    // ... use result here ...
-  });
-----
-
-[[context_change]]
-=== context.change
-When the action is invoked on a change a
-link:rest-api-changes.html#change-info[ChangeInfo] object instance
-describing the change. Available fields of the ChangeInfo may vary
-based on the options used by the UI when it loaded the change.
-
-[[context_delete]]
-=== context.delete()
-Issues a DELETE REST API call to the URL associated with the action.
+[[plugin_rest_delete]]
+=== restApi.delete()
+Issues a DELETE REST API request to the Gerrit server.
+Returns a promise with the response of the request.
 
 .Signature
 [source,javascript]
 ----
-context.delete(callback)
+restApi.delete(url)
 ----
 
-* callback: JavaScript function to be invoked with the parsed
-  JSON result of the API call. DELETE methods often return
-  `204 No Content`, which is passed as null.
+* url: URL relative to the base url.
 
-[source,javascript]
-----
-context.delete(function () {});
-----
-
-[[context_get]]
-=== context.get()
-Issues a GET REST API call to the URL associated with the action.
+[[plugin_rest_get]]
+=== restApi.get()
+Issues a GET REST API request to the Gerrit server.
+Returns a promise with the response of the request.
 
 .Signature
 [source,javascript]
 ----
-context.get(callback)
+restApi.get(url)
 ----
 
-* callback: JavaScript function to be invoked with the parsed JSON
-  result of the API call. If the API returns a string the result is
-  a string, otherwise the result is a JavaScript object or array,
-  as described in the relevant REST API documentation.
+* url: URL relative to the base url.
 
-[source,javascript]
-----
-context.get(function (result) {
-  // ... use result here ...
-});
-----
-
-[[context_go]]
-=== context.go()
-Go to a screen. Shorthand for link:#Gerrit_go[`Gerrit.go()`].
-
-[[context_hide]]
-=== context.hide()
-Hide the currently visible popup displayed by
-link:#context_popup[`context.popup()`].
-
-[[context_post]]
-=== context.post()
-Issues a POST REST API call to the URL associated with the action.
+[[plugin_rest_post]]
+=== restApi.post()
+Issues a POST REST API request to the Gerrit server.
+Returns a promise with the response of the request.
 
 .Signature
 [source,javascript]
 ----
-context.post(input, callback)
+restApi.post(url, opt_payload, opt_errFn, opt_contentType)
 ----
 
-* input: JavaScript object to serialize as the request payload.
+* url: URL relative to the base url.
 
-* callback: JavaScript function to be invoked with the parsed JSON
-  result of the API call. If the API returns a string the result is
-  a string, otherwise the result is a JavaScript object or array,
-  as described in the relevant REST API documentation.
+* opt_payload: JavaScript object to serialize as the request payload.
+
+* opt_errFn: JavaScript function to be invoked when error occured.
+
+* opt_contentType: Content-Type to be sent along with the request.
 
 [source,javascript]
 ----
-context.post(
-  {message: "..."},
-  function (result) {
-    // ... use result here ...
-  });
+restApi.post(
+  '/my-servlet',
+  {start_build: true, platform_type: 'Linux'});
 ----
 
-[[context_popup]]
-=== context.popup()
-
-Displays a small popup near the activation button to gather
-additional input from the user before executing the REST API RPC.
-
-The caller is always responsible for closing the popup with
-link#context_hide[`context.hide()`]. Gerrit will handle closing a
-popup if the user presses `Escape` while keyboard focus is within
-the popup.
+[[plugin_rest_put]]
+=== restApi.put()
+Issues a PUT REST API request to the Gerrit server.
+Returns a promise with the response of the request.
 
 .Signature
 [source,javascript]
 ----
-context.popup(element)
+restApi.put(url, opt_payload, opt_errFn, opt_contentType)
 ----
 
-* element: an HTML DOM element to display as the body of the
-  popup. This is typically a `div` element but can be any valid HTML
-  element. CSS can be used to style the element beyond the defaults.
+* url: URL relative to the base url.
 
-A common usage is to gather more input:
+* opt_payload: JavaScript object to serialize as the request payload.
+
+* opt_errFn: JavaScript function to be invoked when error occured.
+
+* opt_contentType: Content-Type to be sent along with the request.
 
 [source,javascript]
 ----
-self.onAction('revision', 'start-build', function (c) {
-  var l = c.checkbox();
-  var m = c.checkbox();
-  c.popup(c.div(
-    c.div(c.label(l, 'Linux')),
-    c.div(c.label(m, 'Mac OS X')),
-    c.button('Build', {onclick: function() {
-      c.call(
-        {
-          commit: c.revision.name,
-          linux: l.checked,
-          mac: m.checked,
-        },
-        function() { c.hide() });
-    });
-});
+restApi.put(
+  '/builds',
+  {start_build: true, platform_type: 'Linux'});
 ----
 
-[[context_put]]
-=== context.put()
-Issues a PUT REST API call to the URL associated with the action.
+[[ChangeActions]]
+== Change Actions API
+A new Change Actions API instance will be created when `changeActions()`
+is invoked.
+
+[[change_actions_add]]
+=== changeActions.add()
+Adds a new action to the change actions section.
+Returns the key of the newly added action.
 
 .Signature
 [source,javascript]
 ----
-context.put(input, callback)
+changeActions.add(type, label)
 ----
 
-* input: JavaScript object to serialize as the request payload.
+* type: The type of the action, either `change` or `revision`.
 
-* callback: JavaScript function to be invoked with the parsed JSON
-  result of the API call. If the API returns a string the result is
-  a string, otherwise the result is a JavaScript object or array,
-  as described in the relevant REST API documentation.
+* label: The label to be used in UI for this action.
 
 [source,javascript]
 ----
-context.put(
-  {message: "..."},
-  function (result) {
-    // ... use result here ...
-  });
+changeActions.add("change", "test")
 ----
 
-[[context_refresh]]
-=== context.refresh()
-Refresh the current display. Shorthand for
-link:#Gerrit_refresh[`Gerrit.refresh()`].
-
-[[context_revision]]
-=== context.revision
-When the action is invoked on a specific revision of a change,
-a link:rest-api-changes.html#revision-info[RevisionInfo]
-object instance describing the revision. Available fields of the
-RevisionInfo may vary based on the options used by the UI when it
-loaded the change.
-
-[[context_project]]
-=== context.project
-When the action is invoked on a specific project,
-the name of the project.
-
-=== HTML Helpers
-The link:#ActionContext[action context] includes some HTML helper
-functions to make working with DOM based widgets less painful.
-
-* `br()`: new `<br>` element.
-
-* `button(label, options)`: new `<button>` with the string `label`
-  wrapped inside of a `div`. The optional `options` object may
-  define `onclick` as a function to be invoked upon clicking. This
-  calling pattern avoids circular references between the element
-  and the onclick handler.
-
-* `checkbox()`: new `<input type='checkbox'>` element.
-* `div(...)`: a new `<div>` wrapping the (optional) arguments.
-* `hr()`: new `<hr>` element.
-
-* `label(c, label)`: a new `<label>` element wrapping element `c`
-  and the string `label`. Used to wrap a checkbox with its label,
-  `label(checkbox(), 'Click Me')`.
-
-* `prependLabel(label, c)`: a new `<label>` element wrapping element `c`
-  and the string `label`. Used to wrap an input field with its label,
-  `prependLabel('Greeting message', textfield())`.
-
-* `textarea(options)`: new `<textarea>` element. The options
-  object may optionally include `rows` and `cols`. The textarea
-  comes with an onkeypress handler installed to play nicely with
-  Gerrit's keyboard binding system.
-
-* `textfield()`: new `<input type='text'>` element.  The text field
-  comes with an onkeypress handler installed to play nicely with
-  Gerrit's keyboard binding system.
-
-* `select(a,i)`: a new `<select>` element containing one `<option>`
-  element for each entry in the provided array `a`.  The option with
-  the index `i` will be pre-selected in the drop-down-list.
-
-* `selected(s)`: returns the text of the `<option>` element that is
-  currently selected in the provided `<select>` element `s`.
-
-* `span(...)`: a new `<span>` wrapping the (optional) arguments.
-
-* `msg(label)`: a new label.
-
-
-[[ScreenContext]]
-== Screen Context
-A new screen context is passed to the `screen` callback function
-each time the user navigates to a matching URL.
-
-[[screen_body]]
-=== screen.body
-Empty HTML `<div>` node the plugin should add its content to.  The
-node is already attached to the document, but is invisible.  Plugins
-must call `screen.show()` to display the DOM node.  Deferred display
-allows an implementor to partially populate the DOM, make remote HTTP
-requests, finish populating when the callbacks arrive, and only then
-make the view visible to the user.
-
-[[screen_token]]
-=== screen.token
-URL token fragment that activated this screen.  The value is identical
-to `screen.token_match[0]`.  If the URL is `/#/x/hello/list` the token
-will be `"list"`.
-
-[[screen_token_match]]
-=== screen.token_match
-Array of matching subgroups from the pattern specified to `screen()`.
-This is identical to the result of RegExp.exec. Index 0 contains the
-entire matching expression; index 1 the first matching group, etc.
-
-[[screen_onUnload]]
-=== screen.onUnload()
-Configures an optional callback to be invoked just before the screen
-is deleted from the browser DOM.  Plugins can use this callback to
-remove event listeners from DOM nodes, preventing memory leaks.
+[[change_actions_remove]]
+=== changeActions.remove()
+Removes an action from the change actions section.
 
 .Signature
 [source,javascript]
 ----
-screen.onUnload(callback)
+changeActions.remove(key)
 ----
 
-* callback: JavaScript function to be invoked just before the
-  `screen.body` DOM element is removed from the browser DOM.
-  This event happens when the user navigates to another screen.
+* key: The key of the action.
 
-[[screen.setTitle]]
-=== screen.setTitle()
-Sets the heading text to be displayed when the screen is visible.
-This is presented in a large bold font below the menus, but above the
-content in `screen.body`.  Setting the title also sets the window
-title to the same string, if it has not already been set.
+[[change_actions_addTapListener]]
+=== changeActions.addTapListener()
+Adds a tap listener to an action that will be invoked when the action
+is tapped.
 
 .Signature
 [source,javascript]
 ----
-screen.setPageTitle(titleText)
+changeActions.addTapListener(key, callback)
 ----
 
-[[screen.setWindowTitle]]
-=== screen.setWindowTitle()
-Sets the text to be displayed in the browser's title bar when the
-screen is visible.  Plugins should always prefer this method over
-trying to set `window.title` directly.  The window title defaults to
-the title given to `setTitle`.
+* key: The key of the action.
+
+* callback: JavaScript function to be invoked when action tapped.
+
+[source,javascript]
+----
+changeActions.addTapListener("__key_for_my_action__", () => {
+  // do something when my action gets clicked
+})
+----
+
+[[change_actions_removeTapListener]]
+=== changeActions.removeTapListener()
+Removes an existing tap listener on an action.
 
 .Signature
 [source,javascript]
 ----
-screen.setWindowTitle(titleText)
+changeActions.removeTapListener(key, callback)
 ----
 
-[[screen_show]]
-=== screen.show()
-Destroy the currently visible screen and display the plugin's screen.
-This method must be called after adding content to `screen.body`.
+* key: The key of the action.
 
-[[SettingsScreenContext]]
-== Settings Screen Context
-A new settings screen context is passed to the `settingsScreen` callback
-function each time the user navigates to a matching URL.
+* callback: JavaScript function to be removed.
 
-[[settingsScreen_body]]
-=== settingsScreen.body
-Empty HTML `<div>` node the plugin should add its content to.  The
-node is already attached to the document, but is invisible.  Plugins
-must call `settingsScreen.show()` to display the DOM node.  Deferred
-display allows an implementor to partially populate the DOM, make
-remote HTTP requests, finish populating when the callbacks arrive, and
-only then make the view visible to the user.
-
-[[settingsScreen_onUnload]]
-=== settingsScreen.onUnload()
-Configures an optional callback to be invoked just before the screen
-is deleted from the browser DOM.  Plugins can use this callback to
-remove event listeners from DOM nodes, preventing memory leaks.
+[[change_actions_setLabel]]
+=== changeActions.setLabel()
+Sets the label for an action.
 
 .Signature
 [source,javascript]
 ----
-settingsScreen.onUnload(callback)
+changeActions.setLabel(key, label)
 ----
 
-* callback: JavaScript function to be invoked just before the
-  `settingsScreen.body` DOM element is removed from the browser DOM.
-  This event happens when the user navigates to another screen.
+* key: The key of the action.
 
-[[settingsScreen.setTitle]]
-=== settingsScreen.setTitle()
-Sets the heading text to be displayed when the screen is visible.
-This is presented in a large bold font below the menus, but above the
-content in `settingsScreen.body`. Setting the title also sets the
-window title to the same string, if it has not already been set.
+* label: The label of the action.
+
+[[change_actions_setTitle]]
+=== changeActions.setTitle()
+Sets the title for an action.
 
 .Signature
 [source,javascript]
 ----
-settingsScreen.setPageTitle(titleText)
+changeActions.setTitle(key, title)
 ----
 
-[[settingsScreen.setWindowTitle]]
-=== settingsScreen.setWindowTitle()
-Sets the text to be displayed in the browser's title bar when the
-screen is visible.  Plugins should always prefer this method over
-trying to set `window.title` directly.  The window title defaults to
-the title given to `setTitle`.
+* key: The key of the action.
+
+* title: The title of the action.
+
+[[change_actions_setIcon]]
+=== changeActions.setIcon()
+Sets an icon for an action.
 
 .Signature
 [source,javascript]
 ----
-settingsScreen.setWindowTitle(titleText)
+changeActions.setIcon(key, icon)
 ----
 
-[[settingsScreen_show]]
-=== settingsScreen.show()
-Destroy the currently visible screen and display the plugin's screen.
-This method must be called after adding content to
-`settingsScreen.body`.
+* key: The key of the action.
+
+* icon: The name of the icon.
+
+[[change_actions_setEnabled]]
+=== changeActions.setEnabled()
+Sets an action to enabled or disabled.
+
+.Signature
+[source,javascript]
+----
+changeActions.setEnabled(key, enabled)
+----
+
+* key: The key of the action.
+
+* enabled: The status of the action, true to enable.
+
+[[change_actions_setActionHidden]]
+=== changeActions.setActionHidden()
+Sets an action to be hidden.
+
+.Signature
+[source,javascript]
+----
+changeActions.setActionHidden(type, key, hidden)
+----
+
+* type: The type of the action.
+
+* key: The key of the action.
+
+* hidden: True to hide the action, false to show the action.
+
+[[change_actions_setActionOverflow]]
+=== changeActions.setActionOverflow()
+Sets an action to show in overflow menu.
+
+.Signature
+[source,javascript]
+----
+changeActions.setActionOverflow(type, key, overflow)
+----
+
+* type: The type of the action.
+
+* key: The key of the action.
+
+* overflow: True to move the action to overflow menu, false to move
+  the action out of the overflow menu.
 
 [[PanelContext]]
 == Panel Context
@@ -713,10 +438,12 @@
 
 [[Gerrit_css]]
 === Gerrit.css()
+[WARNING]
+This method is deprecated. It doesn't work with Shadow DOM and
+will be removed in the future. Please, use link:pg-plugin-dev.html#plugin-styles[plugin.styles] instead.
+
 Creates a new unique CSS class and injects it into the document.
 The name of the class is returned and can be used by the plugin.
-See link:#Gerrit_html[`Gerrit.html()`] for an easy way to use
-generated class names.
 
 Classes created with this function should be created once at install
 time and reused throughout the plugin.  Repeatedly creating the same
@@ -732,194 +459,6 @@
 });
 ----
 
-[[Gerrit_delete]]
-=== Gerrit.delete()
-Issues a DELETE REST API request to the Gerrit server. For plugin
-private REST API URLs see link:#self_delete[self.delete()].
-
-.Signature
-[source,javascript]
-----
-Gerrit.delete(url, callback)
-----
-
-* url: URL relative to the Gerrit server. For example to access the
-  link:rest-api-changes.html[changes REST API] use `'/changes/'`.
-
-* callback: JavaScript function to be invoked with the parsed
-  JSON result of the API call. DELETE methods often return
-  `204 No Content`, which is passed as null.
-
-[source,javascript]
-----
-Gerrit.delete(
-  '/changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic',
-  function () {});
-----
-
-[[Gerrit_get]]
-=== Gerrit.get()
-Issues a GET REST API request to the Gerrit server. For plugin
-private REST API URLs see link:#self_get[self.get()].
-
-.Signature
-[source,javascript]
-----
-Gerrit.get(url, callback)
-----
-
-* url: URL relative to the Gerrit server. For example to access the
-  link:rest-api-changes.html[changes REST API] use `'/changes/'`.
-
-* callback: JavaScript function to be invoked with the parsed JSON
-  result of the API call. If the API returns a string the result is
-  a string, otherwise the result is a JavaScript object or array,
-  as described in the relevant REST API documentation.
-
-[source,javascript]
-----
-Gerrit.get('/changes/?q=status:open', function (open) {
-  for (var i = 0; i < open.length; i++) {
-    console.log(open[i].change_id);
-  }
-});
-----
-
-[[Gerrit_getCurrentUser]]
-=== Gerrit.getCurrentUser()
-Returns the currently signed in user's AccountInfo data; empty account
-data if no user is currently signed in.
-
-[[Gerrit_getPluginName]]
-=== Gerrit.getPluginName()
-Returns the name this plugin was installed as by the server
-administrator. The plugin name is required to access REST API
-views installed by the plugin, or to access resources.
-
-Unlike link:#self_getPluginName[`self.getPluginName()`] this method
-must guess the name from the JavaScript call stack. Plugins are
-encouraged to use `self.getPluginName()` whenever possible.
-
-[[Gerrit_go]]
-=== Gerrit.go()
-Updates the web UI to display the screen identified by the supplied
-URL token. The URL token is the text after `#` in the browser URL.
-
-[source,javascript]
-----
-Gerrit.go('/admin/projects/');
-----
-
-If the URL passed matches `http://...`, `https://...`, or `//...`
-the current browser window will navigate to the non-Gerrit URL.
-The user can return to Gerrit with the back button.
-
-[[Gerrit_html]]
-=== Gerrit.html()
-Parses an HTML fragment after performing template replacements.  If
-the HTML has a single root element or node that node is returned,
-otherwise it is wrapped inside a `<div>` and the div is returned.
-
-.Signature
-[source,javascript]
-----
-Gerrit.html(htmlText, options, wantElements);
-----
-
-* htmlText: string of HTML to be parsed.  A new unattached `<div>` is
-  created in the browser's document and the innerHTML property is
-  assigned to the passed string, after performing replacements.  If
-  the div has exactly one child, that child will be returned instead
-  of the div.
-
-* options: optional object reference supplying replacements for any
-  `{name}` references in htmlText.  Navigation through objects is
-  supported permitting `{style.bar}` to be replaced with `"foo"` if
-  options was `{style: {bar: "foo"}}`.  Value replacements are HTML
-  escaped before being inserted into the document fragment.
-
-* wantElements: if options is given and wantElements is also true
-  an object consisting of `{root: parsedElement, elements: {...}}` is
-  returned instead of the parsed element. The elements object contains
-  a property for each element using `id={name}` in htmlText.
-
-.Example
-[source,javascript]
-----
-var style = {bar: Gerrit.css('background: yellow')};
-Gerrit.html(
-  '<span class="{style.bar}">Hello {name}!</span>',
-  {style: style, name: "World"});
-----
-
-Event handlers can be automatically attached to elements referenced
-through an attribute id.  Object navigation is not supported for ids,
-and the parser strips the id attribute before returning the result.
-Handler functions must begin with `on` and be a function to be
-installed on the element.  This approach is useful for onclick and
-other handlers that do not want to create circular references that
-will eventually leak browser memory.
-
-.Example
-[source,javascript]
-----
-var options = {
-  link: {
-    onclick: function(e) { window.close() },
-  },
-};
-Gerrit.html('<a href="javascript:;" id="{link}">Close</a>', options);
-----
-
-When using options to install handlers care must be taken to not
-accidentally include the returned element into the event handler's
-closure.  This is why options is built before calling `Gerrit.html()`
-and not inline as a shown above with "Hello World".
-
-DOM nodes can optionally be returned, allowing handlers to access the
-elements identified by `id={name}` at a later point in time.
-
-.Example
-[source,javascript]
-----
-var w = Gerrit.html(
-    '<div>Name: <input type="text" id="{name}"></div>'
-  + '<div>Age: <input type="text" id="{age}"></div>'
-  + '<button id="{submit}"><div>Save</div></button>',
-  {
-    submit: {
-      onclick: function(s) {
-        var e = w.elements;
-        window.alert(e.name.value + " is " + e.age.value);
-      },
-    },
-  }, true);
-----
-
-To prevent memory leaks `w.root` and `w.elements` should be set to
-null when the elements are no longer necessary.  Screens can use
-link:#screen_onUnload[screen.onUnload()] to define a callback function
-to perform this cleanup:
-
-[source,javascript]
-----
-var w = Gerrit.html(...);
-screen.body.appendElement(w.root);
-screen.onUnload(function() { w.clear() });
-----
-
-[[Gerrit_injectCss]]
-=== Gerrit.injectCss()
-Injects CSS rules into the document by appending onto the end of the
-existing rule list.  CSS rules are global to the entire application
-and must be manually scoped by each plugin.  For an automatic scoping
-alternative see link:#Gerrit_css[`css()`].
-
-[source,javascript]
-----
-Gerrit.injectCss('.myplugin_bg {background: #000}');
-----
-
 [[Gerrit_install]]
 === Gerrit.install()
 Registers a new plugin by invoking the supplied initialization
@@ -932,136 +471,6 @@
 });
 ----
 
-[[Gerrit_post]]
-=== Gerrit.post()
-Issues a POST REST API request to the Gerrit server. For plugin
-private REST API URLs see link:#self_post[self.post()].
-
-.Signature
-[source,javascript]
-----
-Gerrit.post(url, input, callback)
-----
-
-* url: URL relative to the Gerrit server. For example to access the
-  link:rest-api-changes.html[changes REST API] use `'/changes/'`.
-
-* input: JavaScript object to serialize as the request payload.
-
-* callback: JavaScript function to be invoked with the parsed JSON
-  result of the API call. If the API returns a string the result is
-  a string, otherwise the result is a JavaScript object or array,
-  as described in the relevant REST API documentation.
-
-[source,javascript]
-----
-Gerrit.post(
-  '/changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic',
-  {topic: 'tests', message: 'Classify work as for testing.'},
-  function (r) {});
-----
-
-[[Gerrit_put]]
-=== Gerrit.put()
-Issues a PUT REST API request to the Gerrit server. For plugin
-private REST API URLs see link:#self_put[self.put()].
-
-.Signature
-[source,javascript]
-----
-Gerrit.put(url, input, callback)
-----
-
-* url: URL relative to the Gerrit server. For example to access the
-  link:rest-api-changes.html[changes REST API] use `'/changes/'`.
-
-* input: JavaScript object to serialize as the request payload.
-
-* callback: JavaScript function to be invoked with the parsed JSON
-  result of the API call. If the API returns a string the result is
-  a string, otherwise the result is a JavaScript object or array,
-  as described in the relevant REST API documentation.
-
-[source,javascript]
-----
-Gerrit.put(
-  '/changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic',
-  {topic: 'tests', message: 'Classify work as for testing.'},
-  function (r) {});
-----
-
-[[Gerrit_onAction]]
-=== Gerrit.onAction()
-Register a JavaScript callback to be invoked when the user clicks
-on a button associated with a server side `UiAction`.
-
-.Signature
-[source,javascript]
-----
-Gerrit.onAction(type, view_name, callback);
-----
-
-* type: `'change'`, `'edit'`, `'revision'`, `'project'` or `'branch'`
-  indicating what sort of resource the `UiAction` was bound to in the server.
-
-* view_name: string appearing in URLs to name the view. This is the
-  second argument of the `get()`, `post()`, `put()`, and `delete()`
-  binding methods in a `RestApiModule`.
-
-* callback: JavaScript function to invoke when the user clicks. The
-  function will be passed a link:#ActionContext[ActionContext].
-
-[[Gerrit_screen]]
-=== Gerrit.screen()
-Register a JavaScript callback to be invoked when the user navigates
-to an extension screen provided by the plugin. Extension screens are
-usually linked from the link:dev-plugins.html#top-menu-extensions[top menu].
-The callback can populate the DOM with the screen's contents.
-
-.Signature
-[source,javascript]
-----
-Gerrit.screen(pattern, callback);
-----
-
-* pattern: URL token pattern to identify the screen. Argument can be
-  either a string (`'index'`) or a RegExp object (`/list\/(.*)/`).
-  If a RegExp is used the matching groups will be available inside of
-  the context as `token_match`.
-
-* callback: JavaScript function to invoke when the user navigates to
-  the screen. The function will be passed link:#ScreenContext[screen context].
-
-[[Gerrit_refresh]]
-=== Gerrit.refresh()
-Redisplays the current web UI view, refreshing all information.
-
-[[Gerrit_refreshMenuBar]]
-=== Gerrit.refreshMenuBar()
-Refreshes Gerrit's menu bar.
-
-[[Gerrit_isSignedIn]]
-=== Gerrit.isSignedIn()
-Checks if user is signed in.
-
-[[Gerrit_url]]
-=== Gerrit.url()
-Returns the URL of the Gerrit Code Review server. If invoked with
-no parameter the URL of the site is returned. If passed a string
-the argument is appended to the site URL.
-
-[source,javascript]
-----
-Gerrit.url();        // "https://gerrit-review.googlesource.com/"
-Gerrit.url('/123');  // "https://gerrit-review.googlesource.com/123"
-----
-
-For a plugin specific version see link:#self_url()[`self.url()`].
-
-[[Gerrit_showError]]
-=== Gerrit.showError(message)
-Displays the given message in the Gerrit ErrorDialog.
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index bb1399d..c2bdfbb3 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -3,7 +3,6 @@
 Apache2.0
 
 * fonts:robotofonts
-* js:web-animations-js
 * polymer_externs:polymer_closure
 
 [[Apache2_0_license]]
@@ -477,33 +476,33 @@
 ----
 
 
-[[promise-polyfill]]
-promise-polyfill
+[[shadycss]]
+shadycss
 
-* js:promise-polyfill
+* js:shadycss
 
-[[promise-polyfill_license]]
+[[shadycss_license]]
 ----
-Copyright (c) 2014 Taylor Hakes
-Copyright (c) 2014 Forbes Lindesay
+# License
 
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Everything in this repo is BSD style license unless otherwise specified.
 
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+Copyright (c) 2015 The Polymer Authors. All rights reserved.
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+* Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
 
 ----
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index e94f297..9f7bd99 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -52,6 +52,7 @@
 * commons:pool
 * commons:validator
 * dropwizard:dropwizard-core
+* errorprone:annotations
 * flogger:api
 * fonts:robotofonts
 * guice:guice
@@ -72,8 +73,7 @@
 * jetty:server
 * jetty:servlet
 * jetty:util
-* jgit/org.eclipse.jgit:javaewah
-* js:web-animations-js
+* jetty:util-ajax
 * log:json-smart
 * log:jsonevent-layout
 * log:log4j
@@ -98,6 +98,7 @@
 * guava-retrying
 * html-types
 * j2objc
+* javaewah
 * jsr305
 * mime-util
 * servlet-api
@@ -2428,9 +2429,9 @@
 [[jgit]]
 jgit
 
-* jgit/org.eclipse.jgit.archive:jgit-archive
-* jgit/org.eclipse.jgit.http.server:jgit-servlet
-* jgit/org.eclipse.jgit:jgit
+* jgit
+* jgit-archive
+* jgit-servlet
 
 [[jgit_license]]
 ----
@@ -3329,37 +3330,6 @@
 ----
 
 
-[[promise-polyfill]]
-promise-polyfill
-
-* js:promise-polyfill
-
-[[promise-polyfill_license]]
-----
-Copyright (c) 2014 Taylor Hakes
-Copyright (c) 2014 Forbes Lindesay
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-----
-
-
 [[protobuf]]
 protobuf
 
@@ -3404,6 +3374,37 @@
 ----
 
 
+[[shadycss]]
+shadycss
+
+* js:shadycss
+
+[[shadycss_license]]
+----
+# License
+
+Everything in this repo is BSD style license unless otherwise specified.
+
+Copyright (c) 2015 The Polymer Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+* Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+----
+
+
 [[slf4j]]
 slf4j
 
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt
index bfebc6a..643bde0 100644
--- a/Documentation/linux-quickstart.txt
+++ b/Documentation/linux-quickstart.txt
@@ -29,10 +29,10 @@
 . Download the desired Gerrit archive.
 
 To view previous archives, see
-link:https://gerrit-releases.storage.googleapis.com/index.html[Gerrit Code Review: Releases]. The steps below install Gerrit 2.15.1:
+link:https://gerrit-releases.storage.googleapis.com/index.html[Gerrit Code Review: Releases]. The steps below install Gerrit 3.1.3:
 
 ....
-wget https://www.gerritcodereview.com/download/gerrit-2.15.1.war
+wget https://gerrit-releases.storage.googleapis.com/gerrit-3.1.3.war
 ....
 
 NOTE: To build and install Gerrit from the source files, see
@@ -43,7 +43,8 @@
 From the command line, enter:
 
 ....
-java -jar gerrit*.war init --batch --dev -d ~/gerrit_testsite
+export GERRIT_SITE=~/gerrit_testsite
+java -jar gerrit*.war init --batch --dev -d $GERRIT_SITE
 ....
 
 This command takes two parameters:
@@ -78,7 +79,7 @@
 `localhost`. For example:
 
 ....
-git config --file ~/gerrit_testsite/etc/gerrit.config httpd.listenUrl 'http://localhost:8080'
+git config --file $GERRIT_SITE/etc/gerrit.config httpd.listenUrl 'http://localhost:8080'
 ....
 
 == Restart the Gerrit service
@@ -87,7 +88,7 @@
 changes to take effect:
 
 ....
-~/gerrit_testsite/bin/gerrit.sh restart
+$GERRIT_SITE/bin/gerrit.sh restart
 ....
 
 == Viewing Gerrit
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index bd3a25e..e64d1de 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -15,10 +15,12 @@
 
 === Actions
 
-* `action/retry_attempt_counts`: Distribution of number of attempts made
-by RetryHelper to execute an action (1 == single attempt, no retry)
+* `action/retry_attempt_count`: Number of retry attempts made
+by RetryHelper to execute an action (0 == single attempt, no retry)
 * `action/retry_timeout_count`: Number of action executions of RetryHelper
 that ultimately timed out
+* `action/auto_retry_count`: Number of automatic retries with tracing
+* `action/failures_on_auto_retry_count`: Number of failures on auto retry
 
 === Pushes
 
@@ -68,6 +70,11 @@
 link:config.gerrit.html#cache.enableDiskStatMetrics[`cache.enableDiskStatMetrics`]
 setting.
 
+=== Change
+
+* `change/submit_rule_evaluation`: Latency for evaluating submit rules on a change.
+* `change/submit_type_evaluation`: Latency for evaluating the submit type on a change.
+
 === HTTP
 
 ==== Jetty
@@ -89,6 +96,13 @@
 * `http/server/jetty/threadpool/pool_size`: Current thread pool size
 * `http/server/jetty/threadpool/queue_size`: Queued requests waiting for a thread
 
+==== LDAP
+
+* `ldap/login_latency`: Latency of logins.
+* `ldap/user_search_latency`: Latency for searching the user account.
+* `ldap/group_search_latency`: Latency for querying the group memberships of an account.
+* `ldap/group_expansion_latency`: Latency for expanding nested groups.
+
 ==== REST API
 
 * `http/server/error_count`: Rate of REST API error responses.
@@ -184,6 +198,10 @@
 * `notedb/stage_update_latency`: Latency for staging updates to NoteDb by table.
 * `notedb/read_latency`: NoteDb read latency by table.
 * `notedb/parse_latency`: NoteDb parse latency by table.
+* `notedb/external_id_cache_load_count`: Total number of times the external ID
+  cache loader was called.
+* `notedb/external_id_partial_read_latency`: Latency for generating a new external ID
+  cache state from a prior state.
 * `notedb/external_id_update_count`: Total number of external ID updates.
 * `notedb/read_all_external_ids_latency`: Latency for reading all
 external ID's from NoteDb.
diff --git a/Documentation/pg-plugin-admin-api.txt b/Documentation/pg-plugin-admin-api.txt
index 084fa2c..1a41778 100644
--- a/Documentation/pg-plugin-admin-api.txt
+++ b/Documentation/pg-plugin-admin-api.txt
@@ -4,14 +4,18 @@
 and provides customization of the admin menu.
 
 == addMenuLink
-`adminApi.addMenuLink(text, url, opt_external, opt_capabilities)`
+`adminApi.addMenuLink(text, url, opt_capability)`
 
 Add a new link to the end of the admin navigation menu.
 
 .Params
 - *text* String text to appear in the link.
 - *url* String of the destination URL for the link.
+- *opt_capability* String of capability required to show this link.
 
 When adding an external link, the URL provided should be a full URL. Otherwise,
 a non-external link should be relative beginning with a slash. For example, to
-create a link to open changes, use the value `/q/status:open`.
\ No newline at end of file
+create a link to open changes, use the value `/q/status:open`.
+
+See more about capability from
+link:rest-api-accounts.html#list-account-capabilities[List Account Capabilities].
\ No newline at end of file
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index 8fb5655..d901851 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -360,6 +360,16 @@
 
 Deprecated. Use link:#plugin-settings[`plugin.settings()`] instead.
 
+[[plugin-styles]]
+=== styles
+`plugin.styles()`
+
+.Params:
+- none
+
+.Returns:
+- Instance of link:pg-plugin-styles-api.html[GrStylesApi]
+
 === changeMetadata
 `plugin.changeMetadata()`
 
@@ -372,6 +382,7 @@
 === theme
 `plugin.theme()`
 
+
 Note: TODO
 
 === url
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index 3d66dd4..a8b3330 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -134,6 +134,11 @@
 === header-title
 This endpoint wraps the title-text in the application header.
 
+=== confirm-revert-change
+This endpoint is inside the confirm revert dialog. By default it displays a
+generic confirmation message regarding reverting the change. Plugins may add
+content to this message or replace it entirely.
+
 === confirm-submit-change
 This endpoint is inside the confirm submit dialog. By default it displays a
 generic confirmation message regarding submission of the change. Plugins may add
diff --git a/Documentation/pg-plugin-style-object.txt b/Documentation/pg-plugin-style-object.txt
new file mode 100644
index 0000000..cdcfb55
--- /dev/null
+++ b/Documentation/pg-plugin-style-object.txt
@@ -0,0 +1,33 @@
+= Gerrit Code Review - GrStyleObject
+
+Store information about css style properties. You can't create this object
+directly. Instead you should use the link:pg-plugin-styles-api.html#css[css] method.
+This object allows to apply style correctly to elements within different shadow
+subtree.
+
+[[get-class-name]]
+== getClassName
+`styleObject.getClassName(element)`
+
+.Params
+- `element` - an HTMLElement.
+
+.Returns
+- `string` - class name. The class name is valid only within the shadow root of `element`.
+
+Creates a new unique CSS class and injects it into the appropriate place
+in DOM (it can be document or shadow root for element). This class can be later
+added to the element or to any other element in the same shadow root. It is guarantee,
+that method adds CSS class only once for each shadow root.
+
+== apply
+`styleObject.apply(element)`
+
+.Params
+- `element` - element to apply style.
+
+Create a new unique CSS class (see link:#get-class-name[getClassName]) and
+adds class to the element.
+
+
+
diff --git a/Documentation/pg-plugin-styles-api.txt b/Documentation/pg-plugin-styles-api.txt
new file mode 100644
index 0000000..a829325
--- /dev/null
+++ b/Documentation/pg-plugin-styles-api.txt
@@ -0,0 +1,29 @@
+= Gerrit Code Review - Plugin styles API
+
+This API is provided by link:pg-plugin-dev.html#plugin-styles[plugin.styles()]
+and provides a way to apply dynamically created styles to elements in a
+document.
+
+[[css]]
+== css
+`styles.css(rulesStr)`
+
+.Params
+- `*string* rulesStr` string with CSS styling declarations.
+
+Example:
+----
+const styleObject = plugin.styles().css('background: black; color: white;');
+...
+const className = styleObject.getClassName(element)
+...
+element.classList.add(className);
+...
+styleObject.apply(someOtherElement);
+----
+
+.Returns
+- Instance of link:pg-plugin-style-object.html[GrStyleObject].
+
+
+
diff --git a/Documentation/pg-plugin-styling.txt b/Documentation/pg-plugin-styling.txt
index 301da51..2453bad 100644
--- a/Documentation/pg-plugin-styling.txt
+++ b/Documentation/pg-plugin-styling.txt
@@ -23,13 +23,15 @@
 
 ``` html
   <dom-module id="some-style">
-    <style>
-      :root {
-        --css-mixin-name: {
-          property: value;
+    <template>
+      <style>
+        html {
+          --css-mixin-name: {
+            property: value;
+          }
         }
-      }
-    </style>
+      </style>
+    </template>
   </dom-module>
 ```
 
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index 25ca4dd..cf6560b 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -11,7 +11,7 @@
   [--enable-httpd | --disable-httpd]
   [--enable-sshd | --disable-sshd]
   [--console-log]
-  [--slave]
+  [--replica]
   [--headless]
   [--init]
   [-s]
@@ -32,15 +32,15 @@
 --enable-httpd::
 --disable-httpd::
 	Enable (or disable) the internal HTTP daemon, answering
-	web requests. Enabled by default when --slave is not used.
+	web requests. Enabled by default when --replica is not used.
 
 --enable-sshd::
 --disable-sshd::
 	Enable (or disable) the internal SSH daemon, answering SSH
 	clients and remotely executed commands.  Enabled by default.
 
---slave::
-	Run in slave mode, permitting only read operations
+--replica::
+	Run in replica mode, permitting only read operations
     by clients.  Commands which modify state such as
     link:cmd-receive-pack.html[receive-pack] (creates new changes
     or updates existing ones) or link:cmd-review.html[review]
@@ -81,9 +81,9 @@
 external log cleaning service to clean up the prior logs.
 
 == KNOWN ISSUES
-Slave daemon caches can quickly become out of date when modifications
-are made on the master.  The following configuration is suggested in
-a slave to reduce the maxAge for each cache entry, so that changes
+Replica daemon caches can quickly become out of date when modifications
+are made on the primary node.  The following configuration is suggested in
+a replica to reduce the maxAge for each cache entry, so that changes
 are recognized in a reasonable period of time:
 
 ----
@@ -107,7 +107,7 @@
   maxAge = 5 min
 ----
 
-Automatic cache coherency between master and slave systems is
+Automatic cache coherency between primary and replica systems is
 planned to be implemented in a future version.
 
 GERRIT
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 9a23a27..f291920 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -74,6 +74,9 @@
 link:pgm-prolog-shell.html[prolog-shell] program which opens an interactive
 Prolog interpreter shell.
 
+For batch or unit tests, see the examples in Gerrit source directory
+link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/prologtests/examples/[prologtests/examples].
+
 [NOTE]
 The interactive shell is just a prolog shell, it does not load
 a gerrit server environment and thus is not intended for
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index aaa9223..f270231 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # coding=utf-8
 # Copyright (C) 2013 The Android Open Source Project
 #
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 1d4f6fb..6f0d828 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -58,8 +58,8 @@
 
 [[details]]
 --
-* `DETAILS`: Includes full name, preferred email, username and avatars
-for each account.
+* `DETAILS`: Includes full name, preferred email, username, avatars,
+status and state for each account.
 --
 
 [[all-emails]]
@@ -1254,13 +1254,10 @@
   )]}'
   {
     "changes_per_page": 25,
-    "show_site_header": true,
-    "use_flash_clipboard": true,
     "date_format": "STD",
     "time_format": "HHMM_12",
     "diff_view": "SIDE_BY_SIDE",
     "size_bar_in_change_table": true,
-    "review_category_strategy": "ABBREV",
     "mute_common_path_prefixes": true,
     "publish_comments_on_push": true,
     "work_in_progress_by_default": true,
@@ -1309,13 +1306,10 @@
 
   {
     "changes_per_page": 50,
-    "show_site_header": true,
-    "use_flash_clipboard": true,
     "expand_inline_diffs": true,
     "date_format": "STD",
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
-    "review_category_strategy": "NAME",
     "diff_view": "SIDE_BY_SIDE",
     "mute_common_path_prefixes": true,
     "my": [
@@ -1359,13 +1353,10 @@
   )]}'
   {
     "changes_per_page": 50,
-    "show_site_header": true,
-    "use_flash_clipboard": true,
     "expand_inline_diffs": true,
     "date_format": "STD",
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
-    "review_category_strategy": "NAME",
     "diff_view": "SIDE_BY_SIDE",
     "publish_comments_on_push": true,
     "work_in_progress_by_default": true,
@@ -2201,7 +2192,7 @@
 account.
 
 `AccountDetailInfo` has the same fields as link:#account-info[
-AccountInfo]. In addition `AccountDetailInfo` has the following fields:
+AccountInfo]. In addition `AccountDetailInfo` has the following field:
 
 [options="header",cols="1,^1,5"]
 |=================================
@@ -2209,8 +2200,6 @@
 |`registered_on`     ||
 The link:rest-api.html#timestamp[timestamp] of when the account was
 registered.
-|`inactive`          |not set if `false`|
-Whether the account is inactive.
 |=================================
 
 [[account-external-id-info]]
@@ -2261,9 +2250,14 @@
 See option link:rest-api-changes.html#detailed-accounts[
 DETAILED_ACCOUNTS] for change queries +
 and option link:#details[DETAILS] for account queries.
+|`avatars`         |optional|List of link:#avatar-info[AvatarInfo] +
+entities that provide information about avatar images of the account.
 |`_more_accounts`  |optional, not set if `false`|
 Whether the query would deliver more results if not limited. +
 Only set on the last account that is returned.
+|`status`          |optional|Status message of the account.
+|`inactive`        |not set if `false`|
+Whether the account is inactive.
 |===============================
 
 [[account-input]]
@@ -2309,6 +2303,19 @@
 If not set or if set to an empty string, the account status is deleted.
 |=============================
 
+[[avatar-info]]
+=== AvatarInfo
+The `AccountInfo` entity contains information about an avatar image of
+an account.
+
+[options="header",cols="1,6"]
+|======================
+|Field Name|Description
+|`url`     |The URL to the avatar image.
+|`height`  |The height of the avatar image in pixels.
+|`width`   |The width of the avatar image in pixels.
+|======================
+
 [[capability-info]]
 === CapabilityInfo
 The `CapabilityInfo` entity contains information about the global
@@ -2705,10 +2712,6 @@
 |`changes_per_page`             ||
 The number of changes to show on each page.
 Allowed values are `10`, `25`, `50`, `100`.
-|`show_site_header`             |not set if `false`|
-Whether the site header should be shown.
-|`use_flash_clipboard`          |not set if `false`|
-Whether to use the flash clipboard widget.
 |`expand_inline_diffs`          |not set if `false`|
 Whether to expand diffs inline instead of opening as separate page
 (PolyGerrit only).
@@ -2731,9 +2734,6 @@
 Whether to show the change sizes as colored bars in the change table.
 |`legacycid_in_change_table`    |not set if `false`|
 Whether to show change number in the change table.
-|`review_category_strategy`     ||
-The strategy used to displayed info in the review category column.
-Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
 |`mute_common_path_prefixes`    |not set if `false`|
 Whether to mute common path prefixes in file names in the file table.
 |`signed_off_by`                |not set if `false`|
@@ -2774,10 +2774,6 @@
 |`changes_per_page`             |optional|
 The number of changes to show on each page.
 Allowed values are `10`, `25`, `50`, `100`.
-|`show_site_header`             |optional|
-Whether the site header should be shown.
-|`use_flash_clipboard`          |optional|
-Whether to use the flash clipboard widget.
 |`expand_inline_diffs`          |not set if `false`|
 Whether to expand diffs inline instead of opening as separate page
 (PolyGerrit only).
@@ -2798,9 +2794,6 @@
 Whether to show the change sizes as colored bars in the change table.
 |`legacycid_in_change_table`    |optional|
 Whether to show change number in the change table.
-|`review_category_strategy`     |optional|
-The strategy used to displayed info in the review category column.
-Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
 |`mute_common_path_prefixes`    |optional|
 Whether to mute common path prefixes in file names in the file table.
 |`signed_off_by`                |optional|
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 199c13d..e39355c 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -138,6 +138,12 @@
 The `S` or `start` query parameter can be supplied to skip a number
 of changes from the list.
 
+Administrators can use the `skip-visibility` query parameter to skip visibility filtering.
+This can be used to ensure that no changes are missed e.g. when querying for changes which
+need to be reindexed. Without this parameter query results the user has no permission to read
+are filtered out. REST requests with the skip-visibility option are rejected when the current
+user doesn't have the ADMINISTRATE_SERVER capability.
+
 Clients are allowed to specify more than one query by setting the `q`
 parameter multiple times. In this case the result is an array of
 arrays, one per query in the same order the queries were given in.
@@ -318,6 +324,11 @@
 A change's mergeability can be requested separately by calling the
 link:#get-mergeable[get-mergeable] endpoint.
 --
+[[skip_diffstat]]
+--
+* `SKIP_DIFFSTAT`: skip the 'insertions' and 'deletions' field in link:#change-info[ChangeInfo].
+ For large trees, their computation may be expensive.
+--
 
 [[submittable]]
 --
@@ -844,8 +855,10 @@
 Creates a new patch set with a new commit message.
 
 The new commit message must be provided in the request body inside a
-link:#commit-message-input[CommitMessageInput] entity and contain the change ID footer if
-link:project-configuration.html#require-change-id[Require Change-Id] was specified.
+link:#commit-message-input[CommitMessageInput] entity. If a Change-Id
+footer is specified, it must match the current Change-Id footer. If
+the Change-Id footer is absent, the current Change-Id is added to the
+message.
 
 .Request
 ----
@@ -2628,6 +2641,14 @@
   HTTP/1.1 204 No Content
 ----
 
+When the change edit is a no-op, for example when providing the same file
+content that the file already has, '409 no changes were made' is returned.
+
+.Response
+----
+  HTTP/1.1 409 no changes were made
+----
+
 [[post-edit]]
 === Restore file content or rename files in Change Edit
 --
@@ -2974,7 +2995,20 @@
 Suggest the reviewers for a given query `q` and result limit `n`. If result
 limit is not passed, then the default 10 is used.
 
-Groups can be excluded from the results by specifying 'e=f'.
+This REST endpoint only suggests accounts that
+
+* are active
+* can see the change
+* are visible to the calling user
+* are not already reviewer on the change
+* don't own the change
+
+Groups can be excluded from the results by specifying the 'exclude-groups'
+request parameter:
+
+--
+'GET /changes/link:#change-id[\{change-id\}]/suggest_reviewers?q=J&n=5&exclude-groups'
+--
 
 As result a list of link:#suggested-reviewer-info[SuggestedReviewerInfo] entries is returned.
 
@@ -3009,6 +3043,14 @@
   ]
 ----
 
+To suggest CCs `reviewer-state=CC` can be specified as additional URL
+parameter. This includes existing reviewers in the result, but excludes
+existing CCs.
+
+--
+'GET /changes/link:#change-id[\{change-id\}]/suggest_reviewers?q=J&reviewer-state=CC'
+--
+
 [[get-reviewer]]
 === Get Reviewer
 --
@@ -3054,6 +3096,12 @@
 The reviewer to be added to the change must be provided in the request
 body as a link:#reviewer-input[ReviewerInput] entity.
 
+Users can be moved from reviewer to CC and vice versa. This means if a
+user is added as CC that is already a reviewer on the change, the
+reviewer state of that user is updated to CC. If a user that is already
+a CC on the change is added as reviewer, the reviewer state of that
+user is updated to reviewer.
+
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
@@ -3097,6 +3145,12 @@
 If a group with many members is added as reviewer a confirmation may be
 required.
 
+If a group is added as CC and members of this group are already
+reviewers on the change, these members stay reviewers on the change
+(they are not downgraded to CC). However if a group is added as
+reviewer, all group members become reviewer of the change, even if they
+have been added as CC before.
+
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
@@ -4331,8 +4385,10 @@
     R = label('Any-Label-Name', reject(_)).
 ----
 
-The response is a list of link:#submit-record[SubmitRecord] entries
-describing the permutations that satisfy the tested submit rule.
+The response is a link:#submit-record[SubmitRecord] describing the
+permutations that satisfy the tested submit rule.
+
+If the submit rule was a no-op, the response is "`204 No Content`".
 
 .Response
 ----
@@ -4341,14 +4397,12 @@
   Content-Type: application/json; charset=UTF-8
 
   )]}'
-  [
-    {
-      "status": "NOT_READY",
-      "reject": {
-        "Any-Label-Name": {}
-      }
+  {
+    "status": "NOT_READY",
+    "reject": {
+      "Any-Label-Name": {}
     }
-  ]
+  }
 ----
 
 When testing with the `curl` command line client the
@@ -4905,7 +4959,8 @@
 For merge commits only, the integer-valued request parameter `parent`
 changes the response to return a map of the files which are different
 in this commit compared to the given parent commit. The value is the
-1-based index of the parent's position in the commit object. If not
+1-based index of the parent's position in the commit object,
+with the first parent always belonging to the target branch. If not
 specified, the response contains a map of the files different in the
 auto merge result.
 
@@ -5966,7 +6021,9 @@
 [options="header",cols="1,^1,5"]
 |===========================
 |Field Name         ||Description
-|`message`          ||Commit message for the cherry-pick change
+|`message`          |optional|
+Commit message for the cherry-pick change. If not set, the commit message of
+the cherry-picked commit is used.
 |`destination`      ||Destination branch
 |`base`             |optional|
 40-hex digit SHA-1 of the commit which will be the parent commit of the newly created change.
@@ -6246,10 +6303,12 @@
 |`a`            |optional|Content only in the file on side A (deleted in B).
 |`b`            |optional|Content only in the file on side B (added in B).
 |`ab`           |optional|Content in the file on both sides (unchanged).
-|`edit_a`       |only present during a replace, i.e. both `a` and `b` are present|
+|`edit_a`       |only present when the `intraline` parameter is set and the
+DiffContent is a replace, i.e. both `a` and `b` are present|
 Text sections deleted from side A as a
 link:#diff-intraline-info[DiffIntralineInfo] entity.
-|`edit_b`       |only present during a replace, i.e. both `a` and `b` are present|
+|`edit_b`       |only present when the `intraline` parameter is set and the
+DiffContent is a replace, i.e. both `a` and `b` are present|
 Text sections inserted in side B as a
 link:#diff-intraline-info[DiffIntralineInfo] entity.
 |`due_to_rebase`|not set if `false`|Indicates whether this entry was introduced by a
@@ -6312,11 +6371,12 @@
 The `DiffIntralineInfo` entity contains information about intraline edits in a
 file.
 
-The information consists of a list of `<skip length, mark length>` pairs, where
+The information consists of a list of `<skip length, edit length>` pairs, where
 the skip length is the number of characters between the end of the previous edit
-and the start of this edit, and the mark length is the number of edited characters
+and the start of this edit, and the edit length is the number of edited characters
 following the skip. The start of the edits is from the beginning of the related
-diff content lines.
+diff content lines. If the list is empty, the entire DiffContent should be considered
+as unedited.
 
 Note that the implied newline character at the end of each line is included in
 the length calculation, and thus it is possible for the edits to span newlines.
@@ -6505,7 +6565,7 @@
 |=======================
 |Field Name||Description
 |`add`     |optional|The list of hashtags to be added to the change.
-|`remove   |optional|The list of hashtags to be removed from the change.
+|`remove`  |optional|The list of hashtags to be removed from the change.
 |=======================
 
 [[included-in-info]]
@@ -6863,6 +6923,9 @@
 |`notify_details`|optional|
 Additional information about whom to notify about the revert as a map
 of recipient type to link:#notify-info[NotifyInfo] entity.
+|`topic`         |optional|
+Name of the topic for the revert change. If not set, the default is the topic
+of the change being reverted.
 |=============================
 
 [[review-info]]
@@ -7071,7 +7134,7 @@
 |`reviewed`     |optional|
 Indicates whether the caller is authenticated and has commented on the
 current revision. Only set if link:#reviewed[REVIEWED] option is requested.
-|`messageWithFooter` |optional|
+|`commit_with_footers` |optional|
 If the link:#commit-footers[COMMIT_FOOTERS] option is requested and
 this is the current patch set, contains the full commit message with
 Gerrit-specific commit footers, as if this revision were submitted
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 9359112..063e54d 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1054,14 +1054,11 @@
   )]}'
   {
     "changes_per_page": 25,
-    "show_site_header": true,
-    "use_flash_clipboard": true,
     "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "diff_view": "SIDE_BY_SIDE",
     "size_bar_in_change_table": true,
-    "review_category_strategy": "NONE",
     "mute_common_path_prefixes": true,
     "publish_comments_on_push": true,
     "my": [
@@ -1133,14 +1130,11 @@
   )]}'
   {
     "changes_per_page": 50,
-    "show_site_header": true,
-    "use_flash_clipboard": true,
     "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "diff_view": "SIDE_BY_SIDE",
     "size_bar_in_change_table": true,
-    "review_category_strategy": "NONE",
     "mute_common_path_prefixes": true,
     "publish_comments_on_push": true,
     "my": [
@@ -1567,11 +1561,15 @@
 |`update_delay`       ||
 link:config-gerrit.html#change.updateDelay[How often in seconds the web
 interface should poll for updates to the currently open change].
-|`submit_whole_topic` ||
+|`submit_whole_topic` |not set if `false`|
 link:config-gerrit.html#change.submitWholeTopic[A configuration if
 the whole topic is submitted].
 |`disable_private_changes` |not set if `false`|
 Returns true if private changes are disabled.
+|`exclude_mergeable_in_change_info` |not set if `false`|
+Value of the link:config-gerrit.html#change.api.excludeMergeableInChangeInfo[
+configuration parameter] that controls whether the mergeability bit in
+link:rest-api-changes.html#change-info[ChangeInfo] will never be set.
 |=============================
 
 [[check-account-external-ids-input]]
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 85f7329..c4ba973 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -508,8 +508,9 @@
   }
 ----
 
-If the group creation fails because the name is already in use the
-response is "`409 Conflict`".
+If the group creation fails because the name is already in use, or the
+UUID was specified and the UUID is already in use, the response is
+"`409 Conflict`".
 
 [[get-group-detail]]
 === Get Group Detail
@@ -1596,6 +1597,7 @@
 |Field Name      ||Description
 |`name`          |optional|The name of the group (not encoded). +
 If set, must match the group name in the URL.
+|`uuid`          |optional|The UUID of the group.
 |`description`   |optional|The description of the group.
 |`visible_to_all`|optional|
 Whether the group is visible to all registered users. +
diff --git a/Documentation/rest-api-plugins.txt b/Documentation/rest-api-plugins.txt
index 92b692f..ce26280 100644
--- a/Documentation/rest-api-plugins.txt
+++ b/Documentation/rest-api-plugins.txt
@@ -391,6 +391,24 @@
   }
 ----
 
+Disabling of a link:config-gerrit.html#plugins.mandatory[mandatory plugin]
+is rejected:
+
+.Request
+----
+  DELETE /plugins/replication HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 405 Method Not Allowed
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  Plugin replication is mandatory
+----
+
 [[reload-plugin]]
 === Reload Plugin
 --
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 76151b4..63c3279 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2504,6 +2504,9 @@
 Retrieves the branches and tags in which a change is included. As result
 an link:rest-api-changes.html#included-in-info[IncludedInInfo] entity is returned.
 
+Branches that are not visible to the calling user according to the project's
+read permissions are filtered out from the result.
+
 .Request
 ----
   GET /projects/work%2Fmy-project/commits/a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96/in HTTP/1.0
@@ -2787,17 +2790,18 @@
   }
 ----
 
-[[set-dashboard]]
-=== Set Dashboard
+[[create-dashboard]]
+=== Create Dashboard
 --
 'PUT /projects/link:#project-name[\{project-name\}]/dashboards/link:#dashboard-id[\{dashboard-id\}]'
 --
 
-Updates/Creates a project dashboard.
+Creates a project dashboard, if a project dashboard with the given
+dashboard ID doesn't exist yet.
 
 Currently only supported for the `default` dashboard.
 
-The creation/update information for the dashboard must be provided in
+The creation information for the dashboard must be provided in
 the request body as a link:#dashboard-input[DashboardInput] entity.
 
 .Request
@@ -2811,7 +2815,63 @@
   }
 ----
 
-As response the new/updated dashboard is returned as a
+As response the new dashboard is returned as a link:#dashboard-info[
+DashboardInfo] entity.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "main:closed",
+    "ref": "main",
+    "path": "closed",
+    "description": "Merged and abandoned changes in last 7 weeks",
+    "url": "/dashboard/?title\u003dClosed+changes\u0026Merged\u003dstatus:merged+age:7w\u0026Abandoned\u003dstatus:abandoned+age:7w",
+    "is_default": true,
+    "title": "Closed changes",
+    "sections": [
+      {
+        "name": "Merged",
+        "query": "status:merged age:7w"
+      },
+      {
+        "name": "Abandoned",
+        "query": "status:abandoned age:7w"
+      }
+    ]
+  }
+----
+
+[[update-dashboard]]
+=== Update Dashboard
+--
+'PUT /projects/link:#project-name[\{project-name\}]/dashboards/link:#dashboard-id[\{dashboard-id\}]'
+--
+
+Updates a project dashboard, if a project dashboard with the given
+dashboard ID already exists.
+
+Currently only supported for the `default` dashboard.
+
+The update information for the dashboard must be provided in
+the request body as a link:#dashboard-input[DashboardInput] entity.
+
+.Request
+----
+  PUT /projects/work%2Fmy-project/dashboards/default HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "id": "main:closed",
+    "commit_message": "Update the default dashboard"
+  }
+----
+
+As response the updated dashboard is returned as a
 link:#dashboard-info[DashboardInfo] entity.
 
 .Response
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
index 28f01e9..05765ee 100644
--- a/Documentation/user-inline-edit.txt
+++ b/Documentation/user-inline-edit.txt
@@ -72,7 +72,7 @@
 
 To add a file to the change:
 
-. In the top left corner of the change, click Edit.
+. In the top right corner of the change, click Edit.
 . Next to Files, click Open:
 
 +
diff --git a/Documentation/user-request-tracing.txt b/Documentation/user-request-tracing.txt
index 8430e97..b26f4c1 100644
--- a/Documentation/user-request-tracing.txt
+++ b/Documentation/user-request-tracing.txt
@@ -1,5 +1,8 @@
 = Request Tracing
 
+[[on-demand]]
+== On-demand Request Tracing
+
 Gerrit supports on-demand tracing of single requests that results in
 additional logs with debug information that are written to the
 `error_log`. The logs that correspond to a traced request are
@@ -19,17 +22,24 @@
   `--trace` option. More information about this can be found in
   the link:cmd-index.html#trace[Trace] section of the
   link:cmd-index.html[SSH command documentation].
-* Git: For Git pushes tracing can be enabled by setting the
-  `trace` push option, the trace ID is returned in the command output.
-  More information about this can be found in
-  the link:user-upload.html#trace[Trace] section of the
-  link:user-upload.html[upload documentation]. Tracing for Git requests
-  other than Git push is not supported.
+* Git Push (requires usage of git protocol v2): For Git pushes tracing
+  can be enabled by setting the `trace` push option, the trace ID is
+  returned in the command output. More information about this can be
+  found in the link:user-upload.html#trace[Trace] section of the
+  link:user-upload.html[upload documentation].
+* Git Clone/Fetch/Ls-Remote (requires usage of git protocol v2): For
+  Git clone/fetch/ls-remote tracing can be enabled by setting the
+  `trace` server option. Use '-o trace=<TRACE-ID>' for `git fetch` and
+  `git ls-remote`, and '--server-option trace=<TRACE-ID>' for
+  `git clone`. If the `trace` server option is set without a value
+  (without trace ID) a trace ID is generated but the generated trace ID
+  is not returned to the client (hence a trace ID should always be
+  set).
 
 When request tracing is enabled it is possible to provide an ID that
 should be used as trace ID. If a trace ID is not provided a trace ID is
 automatically generated. The trace ID must be provided to the support
-team so that they can find the trace.
+team (administrator of the server) so that they can find the trace.
 
 When doing traces it is recommended to specify the ID of the issue
 that is being investigated as trace ID so that the traces of the issue
@@ -41,6 +51,71 @@
 debugging. In particular bots should never enable tracing for all their
 requests by default.
 
+[[auto-retry]]
+== Automatic Request Tracing
+
+Gerrit can be link:config-gerrit.html#retry.retryWithTraceOnFailure[
+configured] to automatically retry requests on non-recoverable failures
+with tracing enabled. This allows to automatically captures traces of
+these failures for further analysis by the Gerrit administrators.
+
+The auto-retry on failure behaves the same way as if the calling user
+would retry the failed operation with tracing enabled.
+
+It is expected that the auto-retry fails with the same exception that
+triggered the auto-retry, however this is not guaranteed:
+
+* Not all Gerrit operations are fully atomic and it can happen that
+  some parts of the operation have been successfully performed before
+  the failure happened. In this case the auto-retry may fail with a
+  different exception.
+* Some exceptions may mistakenly be considered as non-recoverable and
+  the auto-retry actually succeeds.
+
+[[auto-retry-succeeded]]
+If an auto-retry succeeds you may consider filing this as
+link:https://bugs.chromium.org/p/gerrit/issues/entry?template=GoogleSource+Issue[
+Gerrit issue] so that the Gerrit developers can fix this and treat this
+exception as recoverable.
+
+The trace IDs for auto-retries are generated and start with
+`retry-on-failure-`. For REST requests they are returned to the client
+as `X-Gerrit-Trace` header.
+
+The best way to search for auto-retries in logs is to do a grep by
+`AutoRetry`. For each auto-retry that happened this should match 1 or 2
+log entries:
+
+* one `ERROR` log entry with the exception that triggered the
+  auto-retry
+* one `FINE` log entry with the exception that happened on auto-retry
+  (if this log entry is not present the operation succeeded on
+  auto-retry)
+
+To inspect single auto-retry occurrences in detail you can do a
+link:#find-trace[grep by the trace ID]. The trace ID is part of the log
+entries which have been found by the previous grep (watch out for
+something like: `retry-on-failure-1534166888910-3985dfba`).
+
+[TIP]
+Auto-retrying on failures is only supported by some of the REST
+endpoints (change REST endpoints that perform updates).
+
+[[auto-retry-metrics]]
+=== Metrics
+
+If auto-retry is link:config-gerrit.html#retry.retryWithTraceOnFailure[
+enabled] the following metrics are reported:
+
+* `action/auto_retry_count`: Number of automatic retries with tracing
+* `action/failures_on_auto_retry_count`: Number of failures on auto retry
+
+By comparing the values of these counters one can see how often the
+auto-retry succeeds. As explained link:#auto-retry-succeeded[above] if
+auto-retries succeed that's an issue with Gerrit that you may want to
+report.
+
+[[find-trace]]
 == Find log entries for a trace ID
 
 If tracing is enabled all log messages that correspond to the traced
@@ -55,6 +130,9 @@
 By doing a grep with the trace ID over the error log the log entries
 that correspond to the request can be found.
 
+[TIP]
+Usually only server administrators have access to the logs.
+
 == Which information is captured in a trace?
 
 * request details
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 6c4b78b..0845956 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -53,7 +53,7 @@
 +
 Amount of time that has expired since the change was last updated
 with a review comment or new patch set.  The age must be specified
-to include a unit suffix, for example `age:2d`:
+to include a unit suffix, for example `-age:2d`:
 +
 * s, sec, second, seconds
 * m, min, minute, minutes
@@ -63,6 +63,10 @@
 * mon, month, months (`1 month` is treated as `30 days`)
 * y, year, years (`1 year` is treated as `365 days`)
 
+`age` can be used both forward and backward looking: `age:2d`
+means 'everything older than 2 days' while `-age:2d` means
+'everything with an age of at most 2 days'.
+
 [[assignee]]
 assignee:'USER'::
 +
@@ -330,7 +334,7 @@
 regular expression. The link:http://www.brics.dk/automaton/[dk.brics.automaton
 library] is used for evaluation of such patterns.
 
-[[footer]]
+[[footer-operator]]
 footer:'FOOTER'::
 +
 Matches any change that has 'FOOTER' as footer in the commit message of the
@@ -412,7 +416,7 @@
 True on any change where the current user is in CC.
 Same as `cc:self`.
 
-is:open, is:pending::
+is:open, is:pending, is:new::
 +
 True if the change is open.
 
@@ -464,7 +468,7 @@
 True if the change is Work In Progress.
 
 [[status]]
-status:open, status:pending::
+status:open, status:pending, status:new::
 +
 True if the change state is 'review in progress'.
 
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index c702c76..d55cb48 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -28,6 +28,13 @@
 * Both, the HTTP and the LDAP passwords (in this order) if `gitBasicAuthPolicy`
   is `HTTP_LDAP`.
 
+When gitBasicAuthPolicy is set to `LDAP` or `HTTP_LDAP` and the user
+is authenticating with the LDAP username/password, the Git client config
+needs to have `http.cookieFile` set to a local file, otherwise every
+single call would trigger a full LDAP authentication and groups resolution
+which could introduce a noticeable latency on the overall execution
+and produce unwanted load to the LDAP server.
+
 When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can
 be regenerated by going to `Settings`, and then accessing the `HTTP
 Password` tab. Revocation can effectively be done by regenerating the
@@ -465,74 +472,8 @@
 amending the message and copying the line from the change's page
 on the web, and then using `git push` as described above.
 
-If Change-Id lines are not available, then the user must use the
-manual mapping technique described below.
-
 For more about Change-Ids, see link:user-changeid.html[Change-Id Lines].
 
-[[manual_replacement_mapping]]
-==== Manual Replacement Mapping
-
-[NOTE]
---
-The remainder of this section describes a manual method of replacing
-changes by matching each commit name to an existing change number.
-End-users should instead prefer to use Change-Id lines in their
-commit messages, as the process is then fully automated by Gerrit
-during normal uploads.
-
-See above for the preferred technique of replacing changes.
-
-Pushing directly to `refs/changes/` is deprecated. If you see the error
-message 'upload to refs/changes not allowed', it means that pushing directly
-to `refs/changes` is disabled on the Gerrit server and the below section does
-not apply to you.
---
-
-To add an additional patch set to a change, replacing it with an
-updated version of the same logical modification, send the new
-commit to the change's ref.  For example, to add the commit whose
-SHA-1 starts with `c0ffee` as a new patch set for change number
-`1979`, use the push refspec `c0ffee:refs/changes/1979` as below:
-
-----
-  git push ssh://sshusername@hostname:29418/projectname c0ffee:refs/changes/1979
-----
-
-This form can be combined together with `refs/for/'branchname'`
-(above) to simultaneously create new changes and replace changes
-during one network transaction.
-
-For example, consider the following sequence of events:
-
-----
-  $ git commit -m A                    ; # create 3 commits
-  $ git commit -m B
-  $ git commit -m C
-
-  $ git push ... HEAD:refs/for/master  ; # upload for review
-  ... A is 1500 ...
-  ... B is 1501 ...
-  ... C is 1502 ...
-
-  $ git rebase -i HEAD~3               ; # edit "A", insert D before B
-                                       ; # now series is A'-D-B'-C'
-  $ git push ...
-      HEAD:refs/for/master
-      HEAD~3:refs/changes/1500
-      HEAD~1:refs/changes/1501
-      HEAD~0:refs/changes/1502         ; # upload replacements
-----
-
-At the final step during the push Gerrit will attach A' as a new
-patch set on change 1500; B' as a new patch set on change 1501; C'
-as a new patch set on 1502; and D will be created as a new change.
-
-Ensuring D is created as a new change requires passing the refspec
-`HEAD:refs/for/branchname`, otherwise Gerrit will ignore D and
-won't do anything with it.  For this reason it is a good idea to
-always include the create change refspec when uploading replacements.
-
 
 [[bypass_review]]
 === Bypass Review
@@ -716,10 +657,6 @@
 amending the message and copying the line from the change's page
 on the web.
 
-If Change-Id lines are not available, then the user must use the much
-more <<manual_replacement_mapping,manual mapping technique>> offered
-by using `git push` to a specific `refs/changes/change#` reference.
-
 For more about Change-Ids, see link:user-changeid.html[Change-Id Lines].
 
 
diff --git a/WORKSPACE b/WORKSPACE
index 454423a..54c0f8f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -8,11 +8,11 @@
 
 http_archive(
     name = "bazel_toolchains",
-    sha256 = "726b5423e1c7a3866a3a6d68e7123b4a955e9fcbe912a51e0f737e6dab1d0af2",
-    strip_prefix = "bazel-toolchains-3.1.0",
+    sha256 = "1adf7a8e9901287c644dcf9ca08dd8d67a69df94bedbd57a841490a84dc1e9ed",
+    strip_prefix = "bazel-toolchains-5.0.0",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/releases/download/3.1.0/bazel-toolchains-3.1.0.tar.gz",
-        "https://github.com/bazelbuild/bazel-toolchains/releases/download/3.1.0/bazel-toolchains-3.1.0.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/archive/v5.0.0.tar.gz",
+        "https://github.com/bazelbuild/bazel-toolchains/archive/v5.0.0.tar.gz",
     ],
 )
 
@@ -92,6 +92,12 @@
     importpath = "github.com/howeyc/fsnotify",
 )
 
+# JGit external repository consumed from git submodule
+local_repository(
+    name = "jgit",
+    path = "modules/jgit",
+)
+
 ANTLR_VERS = "3.5.2"
 
 maven_jar(
@@ -161,9 +167,18 @@
     sha1 = "021a212688ec94fe77aff74ab34cc74f6f940e60",
 )
 
-load("//lib/jgit:jgit.bzl", "jgit_repos")
+# JGit's transitive dependencies
+maven_jar(
+    name = "hamcrest-library",
+    artifact = "org.hamcrest:hamcrest-library:1.3",
+    sha1 = "4785a3c21320980282f9f33d0d1264a69040538f",
+)
 
-jgit_repos()
+maven_jar(
+    name = "jzlib",
+    artifact = "com.jcraft:jzlib:1.1.1",
+    sha1 = "a1551373315ffc2f96130a0e5704f74e151777ba",
+)
 
 maven_jar(
     name = "javaewah",
@@ -173,6 +188,12 @@
 )
 
 maven_jar(
+    name = "error-prone-annotations",
+    artifact = "com.google.errorprone:error_prone_annotations:2.3.3",
+    sha1 = "42aa5155a54a87d70af32d4b0d06bf43779de0e2",
+)
+
+maven_jar(
     name = "gson",
     artifact = "com.google.code.gson:gson:2.8.5",
     sha1 = "f645ed69d595b24d4cf8b3fbb64cc505bede8829",
@@ -291,8 +312,8 @@
 # When upgrading commons-compress, also upgrade tukaani-xz
 maven_jar(
     name = "commons-compress",
-    artifact = "org.apache.commons:commons-compress:1.15",
-    sha1 = "b686cd04abaef1ea7bc5e143c080563668eec17e",
+    artifact = "org.apache.commons:commons-compress:1.18",
+    sha1 = "1191f9f2bc0c47a8cce69193feb1ff0a8bcb37d5",
 )
 
 maven_jar(
@@ -599,18 +620,18 @@
     sha1 = "a3ae34e57fa8a4040e28247291d0cc3d6b8c7bcf",
 )
 
-AUTO_VALUE_VERSION = "1.6.5"
+AUTO_VALUE_VERSION = "1.7"
 
 maven_jar(
     name = "auto-value",
     artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
-    sha1 = "816872c85048f36a67a276ef7a49cc2e4595711c",
+    sha1 = "fe8387764ed19460eda4f106849c664f51c07121",
 )
 
 maven_jar(
     name = "auto-value-annotations",
     artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
-    sha1 = "c3dad10377f0e2242c9a4b88e9704eaf79103679",
+    sha1 = "5be124948ebdc7807df68207f35a0f23ce427f29",
 )
 
 declare_nongoogle_deps()
@@ -702,7 +723,7 @@
     sha1 = "f7be08ec23c21485b9b5a1cf1654c2ec8c58168d",
 )
 
-GITILES_VERS = "0.2-12"
+GITILES_VERS = "0.3-7"
 
 GITILES_REPO = GERRIT
 
@@ -711,14 +732,14 @@
     artifact = "com.google.gitiles:blame-cache:" + GITILES_VERS,
     attach_source = False,
     repository = GITILES_REPO,
-    sha1 = "e175e4366d83f20378905ca58a505ba8adac291d",
+    sha1 = "af6212a62363906c63d367f8276ae1645f83bf93",
 )
 
 maven_jar(
     name = "gitiles-servlet",
     artifact = "com.google.gitiles:gitiles-servlet:" + GITILES_VERS,
     repository = GITILES_REPO,
-    sha1 = "53f654f79ec65b9af7fbe645c99bf7888cd1ac48",
+    sha1 = "6a53f722f8572a2f1bcb7d86e5692168844bab76",
 )
 
 # prettify must match the version used in Gitiles
@@ -731,8 +752,8 @@
 # Keep this version of Soy synchronized with the version used in Gitiles.
 maven_jar(
     name = "soy",
-    artifact = "com.google.template:soy:2019-03-11",
-    sha1 = "119ac4b3eb0e2c638526ca99374013965c727097",
+    artifact = "com.google.template:soy:2019-10-08",
+    sha1 = "4518bf8bac2dbbed684849bc209c39c4cb546237",
 )
 
 maven_jar(
@@ -748,24 +769,24 @@
 )
 
 # When updating Bouncy Castle, also update it in bazlets.
-BC_VERS = "1.60"
+BC_VERS = "1.61"
 
 maven_jar(
     name = "bcprov",
     artifact = "org.bouncycastle:bcprov-jdk15on:" + BC_VERS,
-    sha1 = "bd47ad3bd14b8e82595c7adaa143501e60842a84",
+    sha1 = "00df4b474e71be02c1349c3292d98886f888d1f7",
 )
 
 maven_jar(
     name = "bcpg",
     artifact = "org.bouncycastle:bcpg-jdk15on:" + BC_VERS,
-    sha1 = "13c7a199c484127daad298996e95818478431a2c",
+    sha1 = "422656435514ab8a28752b117d5d2646660a0ace",
 )
 
 maven_jar(
     name = "bcpkix",
     artifact = "org.bouncycastle:bcpkix-jdk15on:" + BC_VERS,
-    sha1 = "d0c46320fbc07be3a24eb13a56cee4e3d38e0c75",
+    sha1 = "89bb3aa5b98b48e584eee2a7401b7682a46779b4",
 )
 
 maven_jar(
@@ -774,27 +795,30 @@
     sha1 = "fd369423346b2f1525c413e33f8cf95b09c92cbd",
 )
 
-# Note that all of the following org.apache.httpcomponents have newer versions,
-# but 4.4.1 is the only version that is available for all of them.
-# TODO: Check what combination of new versions are compatible.
-HTTPCOMP_VERS = "4.4.1"
+# Base the following org.apache.httpcomponents versions on what
+# elasticsearch-rest-client explicitly depends on, except for
+# commons-codec (non-http) which is not necessary yet. Note that
+# below httpcore version(s) differs from the HTTPCOMP_VERS range,
+# upstream: that specific dependency has no HTTPCOMP_VERS version
+# equivalent currently.
+HTTPCOMP_VERS = "4.5.2"
 
 maven_jar(
     name = "fluent-hc",
     artifact = "org.apache.httpcomponents:fluent-hc:" + HTTPCOMP_VERS,
-    sha1 = "96fb842b68a44cc640c661186828b60590c71261",
+    sha1 = "7bfdfa49de6d720ad3c8cedb6a5238eec564dfed",
 )
 
 maven_jar(
     name = "httpclient",
     artifact = "org.apache.httpcomponents:httpclient:" + HTTPCOMP_VERS,
-    sha1 = "016d0bc512222f1253ee6b64d389c84e22f697f0",
+    sha1 = "733db77aa8d9b2d68015189df76ab06304406e50",
 )
 
 maven_jar(
     name = "httpcore",
-    artifact = "org.apache.httpcomponents:httpcore:" + HTTPCOMP_VERS,
-    sha1 = "f5aa318bda4c6c8d688c9d00b90681dcd82ce636",
+    artifact = "org.apache.httpcomponents:httpcore:4.4.4",
+    sha1 = "b31526a230871fbe285fbcbe2813f9c0839ae9b0",
 )
 
 # Test-only dependencies below.
@@ -817,30 +841,30 @@
     sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
 )
 
-TRUTH_VERS = "0.44"
+TRUTH_VERS = "1.0"
 
 maven_jar(
     name = "truth",
     artifact = "com.google.truth:truth:" + TRUTH_VERS,
-    sha1 = "11eff954c0c14da7d43276d7b3bcf71463105368",
+    sha1 = "998e5fb3fa31df716574b4c9e8d374855e800451",
 )
 
 maven_jar(
     name = "truth-java8-extension",
     artifact = "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERS,
-    sha1 = "2081a0721d3101e1cf559f013e59c6129b4b10b0",
+    sha1 = "d85fbc1daf0510821f552f2aa71d9605e97aa438",
 )
 
 maven_jar(
     name = "truth-liteproto-extension",
     artifact = "com.google.truth.extensions:truth-liteproto-extension:" + TRUTH_VERS,
-    sha1 = "64f47e4e3f79b0a582573098b9c3c6b73599f7c6",
+    sha1 = "7a279c50a0f93da15533cef4993b45606cf67d72",
 )
 
 maven_jar(
     name = "truth-proto-extension",
     artifact = "com.google.truth.extensions:truth-proto-extension:" + TRUTH_VERS,
-    sha1 = "c03fbc16087d8cb3bf0f3265a04566d4beb88a6d",
+    sha1 = "8c0c2ea61750f02d0d5ce9c653106b6a5dc82d12",
 )
 
 maven_jar(
@@ -849,61 +873,61 @@
     sha1 = "7e060dd5b19431e6d198e91ff670644372f60fbd",
 )
 
-# When bumping the easymock version number, make sure to also move powermock to a compatible version
-maven_jar(
-    name = "easymock",
-    artifact = "org.easymock:easymock:3.1",
-    sha1 = "3e127311a86fc2e8f550ef8ee4abe094bbcf7e7e",
-)
-
-JETTY_VERS = "9.4.33.v20201020"
+JETTY_VERS = "9.4.35.v20201120"
 
 maven_jar(
     name = "jetty-servlet",
     artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS,
-    sha1 = "101609e8e5365c4406e4448099459eb605ac551f",
+    sha1 = "3e61bcb471e1bfc545ce866cbbe33c3aedeec9b1",
 )
 
 maven_jar(
     name = "jetty-security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS,
-    sha1 = "c150bf2aca6cb1636e7195f844a2bb156546e50e",
+    sha1 = "80dc2f422789c78315de76d289b7a5b36c3232d5",
 )
 
 maven_jar(
     name = "jetty-server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
-    sha1 = "f586ff2ee048ad2575866c1833d854288f402307",
+    sha1 = "513502352fd689d4730b2935421b990ada8cc818",
 )
 
 maven_jar(
     name = "jetty-jmx",
     artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS,
-    sha1 = "56b723070eeafc51b943cd9bf1a064a037e806a7",
+    sha1 = "38812031940a466d626ab5d9bbbd9d5d39e9f735",
 )
 
 maven_jar(
     name = "jetty-continuation",
     artifact = "org.eclipse.jetty:jetty-continuation:" + JETTY_VERS,
-    sha1 = "f672e58d528fc83060558ab4fc6a797c8137dfcb",
+    sha1 = "09f021e5895471f622ec8f95e28f5815ea7ee192",
 )
 
 maven_jar(
     name = "jetty-http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS,
-    sha1 = "ad28940f89ffde6ec1bd1656fe3f8493b01ba3c2",
+    sha1 = "45d35131a35a1e76991682174421e8cdf765fb9f",
 )
 
 maven_jar(
     name = "jetty-io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS,
-    sha1 = "9e4b0048285b71f4769908780f957a470eca11da",
+    sha1 = "eb9460700b99b71ecd82a53697f5ff99f69b9e1c",
 )
 
 maven_jar(
     name = "jetty-util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS,
-    sha1 = "c88807f210ab216aa831b48569ef50bd797384bc",
+    sha1 = "ef61b83f9715c3b5355b633d9f01d2834f908ece",
+)
+
+maven_jar(
+    name = "jetty-util-ajax",
+    artifact = "org.eclipse.jetty:jetty-util-ajax:" + JETTY_VERS,
+    sha1 = "ebbb43912c6423bedb3458e44aee28eeb4d66f27",
+    src_sha1 = "b3acea974a17493afb125a9dfbe783870ce1d2f9",
 )
 
 maven_jar(
@@ -925,6 +949,12 @@
 )
 
 maven_jar(
+    name = "javax-annotation",
+    artifact = "javax.annotation:javax.annotation-api:1.3.2",
+    sha1 = "934c04d3cfef185a8008e7bf34331b79730a9d43",
+)
+
+maven_jar(
     name = "mockito",
     artifact = "org.mockito:mockito-core:2.24.0",
     sha1 = "969a7bcb6f16e076904336ebc7ca171d412cc1f9",
@@ -933,13 +963,13 @@
 BYTE_BUDDY_VERSION = "1.9.7"
 
 maven_jar(
-    name = "byte-buddy",
+    name = "bytebuddy",
     artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
     sha1 = "8fea78fea6449e1738b675cb155ce8422661e237",
 )
 
 maven_jar(
-    name = "byte-buddy-agent",
+    name = "bytebuddy-agent",
     artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
     sha1 = "8e7d1b599f4943851ffea125fd9780e572727fc0",
 )
@@ -974,8 +1004,8 @@
 bower_archive(
     name = "iron-autogrow-textarea",
     package = "polymerelements/iron-autogrow-textarea",
-    sha1 = "68f0ece9b1e56ac26f8ce31d9938c504f6951bca",
-    version = "2.1.0",
+    sha1 = "2f04c7e2a72d462de36093ab2b4889db20f699f6",
+    version = "2.2.0",
 )
 
 bower_archive(
@@ -995,64 +1025,64 @@
 bower_archive(
     name = "iron-dropdown",
     package = "polymerelements/iron-dropdown",
-    sha1 = "ac96fe31cdf203a63426fa75131b43c98c0597d3",
-    version = "1.5.5",
+    sha1 = "3902ba164552b1bfc59e6fa692efa4a1fd8dd4ea",
+    version = "2.2.1",
 )
 
 bower_archive(
     name = "iron-input",
     package = "polymerelements/iron-input",
-    sha1 = "9bc0c8e81de2527125383cbcf74dd9f27e7fa9ac",
-    version = "1.0.10",
+    sha1 = "f79952ff4f6f103c0a2cbd3dacf25935257ff392",
+    version = "2.1.3",
 )
 
 bower_archive(
     name = "iron-overlay-behavior",
     package = "polymerelements/iron-overlay-behavior",
-    sha1 = "74cda9d7bf98e7a5e5004bc7ebdb6d208d49e11e",
-    version = "2.0.0",
+    sha1 = "c2d2eac1b162420d9475ade2f16d5db8959b93fc",
+    version = "2.3.4",
 )
 
 bower_archive(
     name = "iron-selector",
     package = "polymerelements/iron-selector",
-    sha1 = "e0ee46c28523bf17730318c3b481a8ed4331c3b2",
-    version = "2.0.0",
+    sha1 = "3f3fcb55f6bd606ea493f99eab9daae21f7a6139",
+    version = "2.1.0",
 )
 
 bower_archive(
     name = "paper-button",
     package = "polymerelements/paper-button",
-    sha1 = "3b01774f58a8085d3c903fc5a32944b26ab7be72",
-    version = "2.0.0",
+    sha1 = "bcb783d74e1177c1d0836340e7c0280699d1438c",
+    version = "2.1.3",
 )
 
 bower_archive(
     name = "paper-input",
     package = "polymerelements/paper-input",
-    sha1 = "6c934805e80ab201e143406edc73ea0ef35abf80",
-    version = "1.1.18",
+    sha1 = "c1a81a4173d22e72e8ab609eb3715a75273396b3",
+    version = "2.2.3",
 )
 
 bower_archive(
     name = "paper-tabs",
     package = "polymerelements/paper-tabs",
-    sha1 = "b6dd2fbd7ee887534334057a29eb545b940fc5cf",
-    version = "2.0.0",
+    sha1 = "589b8e6efa0f171c93233137c8ea013dcea0ffc7",
+    version = "2.1.1",
 )
 
 bower_archive(
     name = "iron-icon",
     package = "polymerelements/iron-icon",
-    sha1 = "7da49a0d33cd56017740e0dbcf41d2b71532023f",
-    version = "2.0.0",
+    sha1 = "d21e7d4f1bdc6de881390f888e28d53155eeb551",
+    version = "2.1.0",
 )
 
 bower_archive(
     name = "iron-iconset-svg",
     package = "polymerelements/iron-iconset-svg",
-    sha1 = "4d0c406239cad2ff2975c6dd95fa189de0fe6b50",
-    version = "2.1.0",
+    sha1 = "07c0ce02ce6479856758893416a3709009db7f22",
+    version = "2.2.1",
 )
 
 bower_archive(
@@ -1065,36 +1095,36 @@
 bower_archive(
     name = "page",
     package = "visionmedia/page.js",
-    sha1 = "51a05428dd4f68fae1df5f12d0e2b61ba67f7757",
-    version = "1.7.1",
+    sha1 = "4a31889cd75cc5e7f68a4c7f256eecaf27102eee",
+    version = "1.11.4",
 )
 
 bower_archive(
     name = "paper-item",
     package = "polymerelements/paper-item",
-    sha1 = "803273ceb9ffebec8ecc9373ea638af4cd34af58",
-    version = "1.1.4",
+    sha1 = "c3bad022cf182d2bf1c8a44374c7fcb1409afbfa",
+    version = "2.1.1",
 )
 
 bower_archive(
     name = "paper-listbox",
     package = "polymerelements/paper-listbox",
-    sha1 = "ccc1a90ab0a96878c7bf7c9c4cfe47c85b09c8e3",
-    version = "2.0.0",
+    sha1 = "78247cc32bb776f204efef17cff3095878036a40",
+    version = "2.1.1",
 )
 
 bower_archive(
     name = "paper-toggle-button",
     package = "polymerelements/paper-toggle-button",
-    sha1 = "4a2edbdb52c4531d39fe091f12de650bccda270f",
-    version = "1.2.0",
+    sha1 = "9927960afb0062726ec1b585ef3e32764c3bbac9",
+    version = "2.1.1",
 )
 
 bower_archive(
     name = "polymer",
     package = "polymer/polymer",
-    sha1 = "158443ab05ade5e2cdc24ebc01f1deef9aebac1b",
-    version = "1.11.3",
+    sha1 = "d06e17a1d8dc6187ee5aa8c5b3501da10901c82f",
+    version = "2.7.2",
 )
 
 bower_archive(
@@ -1105,13 +1135,6 @@
 )
 
 bower_archive(
-    name = "promise-polyfill",
-    package = "polymerlabs/promise-polyfill",
-    sha1 = "a3b598c06cbd7f441402e666ff748326030905d6",
-    version = "1.0.0",
-)
-
-bower_archive(
     name = "resemblejs",
     package = "rsmbl/Resemble.js",
     sha1 = "49d5f022417c389b630d6f7ee667aa9540075c42",
@@ -1130,15 +1153,15 @@
 bower_archive(
     name = "iron-test-helpers",
     package = "polymerelements/iron-test-helpers",
-    sha1 = "433b03b106f5ff32049b84150cd70938e18b67ac",
-    version = "1.2.5",
+    sha1 = "882be2d4c8714b39299b5f7bf25253c4e8a40761",
+    version = "2.0.1",
 )
 
 bower_archive(
     name = "test-fixture",
     package = "polymerelements/test-fixture",
-    sha1 = "e373bd21c069163c3a754e234d52c07c77b20d3c",
-    version = "1.1.1",
+    sha1 = "7d72ddfebf555a2dd1fc60a85427d9026b509723",
+    version = "3.0.0",
 )
 
 bower_archive(
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
deleted file mode 100755
index 2e01131..0000000
--- a/contrib/abandon_stale.py
+++ /dev/null
@@ -1,225 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# The MIT License
-#
-# Copyright 2014 Sony Mobile Communications. All rights reserved.
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-""" Script to abandon stale changes from the review server.
-
-Fetches a list of open changes that have not been updated since a given age in
-days, months or years (default 6 months), and then abandons them.
-
-Requires the user's credentials for the Gerrit server to be declared in the
-.netrc file. Supports either basic or digest authentication.
-
-Example to abandon changes that have not been updated for 3 months:
-
-  ./abandon_stale --gerrit-url http://review.example.com/ --age 3months
-
-Supports dry-run mode to only list the stale changes, but not actually
-abandon them.
-
-See the --help output for more information about options.
-
-Requires pygerrit2 (https://github.com/dpursehouse/pygerrit2) to be installed
-and available for import.
-
-"""
-
-import logging
-import optparse
-import re
-import sys
-
-from pygerrit2.rest import GerritRestAPI
-from pygerrit2.rest.auth import HTTPBasicAuthFromNetrc, HTTPDigestAuthFromNetrc
-
-
-def _main():
-    parser = optparse.OptionParser()
-    parser.add_option('-g', '--gerrit-url', dest='gerrit_url',
-                      metavar='URL',
-                      default=None,
-                      help='gerrit server URL')
-    parser.add_option('-b', '--basic-auth', dest='basic_auth',
-                      action='store_true',
-                      help='(deprecated) use HTTP basic authentication instead'
-                      ' of digest')
-    parser.add_option('-d', '--digest-auth', dest='digest_auth',
-                      action='store_true',
-                      help='use HTTP digest authentication instead of basic')
-    parser.add_option('-n', '--dry-run', dest='dry_run',
-                      action='store_true',
-                      help='enable dry-run mode: show stale changes but do '
-                           'not abandon them')
-    parser.add_option('-t', '--test', dest='testmode', action='store_true',
-                      help='test mode: query changes with the `test-abandon` '
-                           'topic and ignore age option')
-    parser.add_option('-a', '--age', dest='age',
-                      metavar='AGE',
-                      default="6months",
-                      help='age of change since last update in days, months'
-                           ' or years (default: %default)')
-    parser.add_option('-m', '--message', dest='message',
-                      metavar='STRING', default=None,
-                      help='custom message to append to abandon message')
-    parser.add_option('--branch', dest='branches', metavar='BRANCH_NAME',
-                      default=[], action='append',
-                      help='abandon changes only on the given branch')
-    parser.add_option('--exclude-branch', dest='exclude_branches',
-                      metavar='BRANCH_NAME',
-                      default=[],
-                      action='append',
-                      help='do not abandon changes on given branch')
-    parser.add_option('--project', dest='projects', metavar='PROJECT_NAME',
-                      default=[], action='append',
-                      help='abandon changes only on the given project')
-    parser.add_option('--exclude-project', dest='exclude_projects',
-                      metavar='PROJECT_NAME',
-                      default=[],
-                      action='append',
-                      help='do not abandon changes on given project')
-    parser.add_option('--owner', dest='owner',
-                      metavar='USERNAME',
-                      default=None,
-                      action='store',
-                      help='only abandon changes owned by the given user')
-    parser.add_option('--exclude-wip', dest='exclude_wip',
-                      action='store_true',
-                      help='Exclude changes that are Work-in-Progress')
-    parser.add_option('-v', '--verbose', dest='verbose',
-                      action='store_true',
-                      help='enable verbose (debug) logging')
-
-    (options, _args) = parser.parse_args()
-
-    level = logging.DEBUG if options.verbose else logging.INFO
-    logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
-                        level=level)
-
-    if not options.gerrit_url:
-        logging.error("Gerrit URL is required")
-        return 1
-
-    if options.testmode:
-        message = "Abandoning in test mode"
-    else:
-        pattern = re.compile(r"^([\d]+)(month[s]?|year[s]?|week[s]?)")
-        match = pattern.match(options.age)
-        if not match:
-            logging.error("Invalid age: %s", options.age)
-            return 1
-        message = "Abandoning after %s %s or more of inactivity." % \
-            (match.group(1), match.group(2))
-
-    if options.digest_auth:
-        auth_type = HTTPDigestAuthFromNetrc
-    else:
-        auth_type = HTTPBasicAuthFromNetrc
-
-    try:
-        auth = auth_type(url=options.gerrit_url)
-        gerrit = GerritRestAPI(url=options.gerrit_url, auth=auth)
-    except Exception as e:
-        logging.error(e)
-        return 1
-
-    logging.info(message)
-    try:
-        stale_changes = []
-        offset = 0
-        step = 500
-        if options.testmode:
-            query_terms = ["status:new", "owner:self", "topic:test-abandon"]
-        else:
-            query_terms = ["status:new", "age:%s" % options.age]
-        if options.exclude_wip:
-            query_terms += ["-is:wip"]
-        if options.branches:
-            query_terms += ["branch:%s" % b for b in options.branches]
-        elif options.exclude_branches:
-            query_terms += ["-branch:%s" % b for b in options.exclude_branches]
-        if options.projects:
-            query_terms += ["project:%s" % p for p in options.projects]
-        elif options.exclude_projects:
-            query_terms = ["-project:%s" % p for p in options.exclude_projects]
-        if options.owner and not options.testmode:
-            query_terms += ["owner:%s" % options.owner]
-        query = "%20".join(query_terms)
-        while True:
-            q = query + "&o=DETAILED_ACCOUNTS&n=%d&S=%d" % (step, offset)
-            logging.debug("Query: %s", q)
-            url = "/changes/?q=" + q
-            result = gerrit.get(url)
-            logging.debug("%d changes", len(result))
-            if not result:
-                break
-            stale_changes += result
-            last = result[-1]
-            if "_more_changes" in last:
-                logging.debug("More...")
-                offset += step
-            else:
-                break
-    except Exception as e:
-        logging.error(e)
-        return 1
-
-    abandoned = 0
-    errors = 0
-    abandon_message = message
-    if options.message:
-        abandon_message += "\n\n" + options.message
-    for change in stale_changes:
-        number = change["_number"]
-        project = ""
-        if len(options.projects) != 1:
-            project = "%s: " % change["project"]
-        owner = ""
-        if options.verbose:
-            try:
-                o = change["owner"]["name"]
-            except KeyError:
-                o = "Unknown"
-            owner = " (%s)" % o
-        subject = change["subject"]
-        if len(subject) > 70:
-            subject = subject[:65] + " [...]"
-        change_id = change["id"]
-        logging.info("%s%s: %s%s", number, owner, project, subject)
-        if options.dry_run:
-            continue
-
-        try:
-            gerrit.post("/changes/" + change_id + "/abandon",
-                        json={"message": "%s" % abandon_message})
-            abandoned += 1
-        except Exception as e:
-            errors += 1
-            logging.error(e)
-    logging.info("Total %d stale open changes", len(stale_changes))
-    if not options.dry_run:
-        logging.info("Abandoned %d changes. %d errors.", abandoned, errors)
-
-
-if __name__ == "__main__":
-    sys.exit(_main())
diff --git a/contrib/benchmark-createchange.go b/contrib/benchmark-createchange.go
new file mode 100644
index 0000000..dc320d6
--- /dev/null
+++ b/contrib/benchmark-createchange.go
@@ -0,0 +1,103 @@
+// Copyright (C) 2019 Google LLC
+//
+// 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.
+
+// Program to benchmark Gerrit.  Creates pending changes in a loop,
+// which tests performance of BatchRefUpdate and Lucene indexing
+package main
+
+import (
+	"bytes"
+	"encoding/base64"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"sort"
+	"time"
+)
+
+func main() {
+	user := flag.String("user", "admin", "username for basic auth")
+	pw := flag.String("password", "secret", "HTTP password for basic auth")
+	project := flag.String("project", "", "project to create changes in")
+	gerritURL := flag.String("url", "http://localhost:8080/", "URL to gerrit instance")
+	numChanges := flag.Int("n", 100, "number of changes to create")
+	flag.Parse()
+	if *gerritURL == "" {
+		log.Fatal("provide --url")
+	}
+	if *project == "" {
+		log.Fatal("provide --project")
+	}
+
+	u, err := url.Parse(*gerritURL)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	basicAuth := fmt.Sprintf("%s:%s", *user, *pw)
+	authHeader := base64.StdEncoding.EncodeToString([]byte(basicAuth))
+
+	client := &http.Client{}
+
+	var dts []time.Duration
+	startAll := time.Now()
+	var lastSec int
+	for i := 0; i < *numChanges; i++ {
+		body := fmt.Sprintf(`{
+    "project" : "%s",
+    "subject" : "change %d",
+    "branch" : "master",
+    "status" : "NEW"
+  }`, *project, i)
+		start := time.Now()
+
+		thisSec := int(start.Sub(startAll) / time.Second)
+		if thisSec != lastSec {
+			log.Printf("change %d", i)
+		}
+		lastSec = thisSec
+
+		u.Path = "/a/changes/"
+		req, err := http.NewRequest("POST", u.String(), bytes.NewBufferString(body))
+		if err != nil {
+			log.Fatal(err)
+		}
+		req.Header.Add("Authorization", "Basic "+authHeader)
+		req.Header.Add("Content-Type", "application/json; charset=UTF-8")
+		resp, err := client.Do(req)
+		if err != nil {
+			log.Fatal(err)
+		}
+		dt := time.Now().Sub(start)
+		dts = append(dts, dt)
+
+		if resp.StatusCode/100 == 2 {
+			continue
+		}
+		log.Println("code", resp.StatusCode)
+		io.Copy(os.Stdout, resp.Body)
+	}
+
+	sort.Slice(dts, func(i, j int) bool { return dts[i] < dts[j] })
+
+	var total time.Duration
+	for _, dt := range dts {
+		total += dt
+	}
+	log.Printf("min %v max %v median %v avg %v", dts[0], dts[len(dts)-1], dts[len(dts)/2], total/time.Duration(len(dts)))
+}
diff --git a/contrib/check-valid-commit.py b/contrib/check-valid-commit.py
index 763ae3e..bb018f9 100755
--- a/contrib/check-valid-commit.py
+++ b/contrib/check-valid-commit.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 from __future__ import print_function
 
diff --git a/contrib/find-duplicate-usernames.sh b/contrib/find-duplicate-usernames.sh
new file mode 100755
index 0000000..b59e5be
--- /dev/null
+++ b/contrib/find-duplicate-usernames.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+usage() {
+  f="$(basename -- $0)"
+  cat <<EOF
+Usage:
+    cd /path/to/All-Users.git
+    "$f [username|gerrit|external]"
+
+This script finds duplicate usernames only differing in case in the given
+account schema ("username", "gerrit" or "external") and their respective accountIds.
+EOF
+  exit 1
+}
+
+if [[ "$#" -ne "1" ]] || ! [[ "$1" =~ ^(gerrit|username|external)$ ]]; then
+  usage
+fi
+
+# 1. find lines with user name and subsequent line in external-ids notes branch
+#    example output of git grep -A1 "\[externalId \"username:" refs/meta/external-ids:
+#    refs/meta/external-ids:00/1d/abd037e437f71d42134e6ad532a06948a2ba:[externalId "username:johndoe"]
+#    refs/meta/external-ids:00/1d/abd037e437f71d42134e6ad532a06948a2ba-      accountId = 1000815
+#    --
+#    refs/meta/external-ids:00/1f/0270fc2a6fc3a2439c454c8ab0c75323fdb0:[externalId "username:JohnDoe"]
+#    refs/meta/external-ids:00/1f/0270fc2a6fc3a2439c454c8ab0c75323fdb0-      accountId = 1000816
+#    --
+# 2. remove group separators
+# 3. remove line break between user name and accountId lines
+# 4. unify separators to ":"
+# 5. cut on ":", select username and accountId fields
+# 6. sort case-insensitive
+# 7. flip columns
+# 8. uniq case-insensitive, only show duplicates, avoid comparing first field
+# 9. flip columns back
+git grep -A1 "\[externalId \"$1:" refs/meta/external-ids \
+  | sed -E "/$1/,/accountId/!d" \
+  | paste -d ' ' - - \
+  | tr \"= : \
+  | cut -d: --output-delimiter="" -f 5,8 \
+  | sort -f \
+  | sed -E "s/(.*) (.*)/\2 \1/" \
+  | uniq -Di -f1 \
+  | sed -E "s/(.*) (.*)/\2 \1/"
diff --git a/contrib/git-push-review b/contrib/git-push-review
index b995fc2..5a7f664 100755
--- a/contrib/git-push-review
+++ b/contrib/git-push-review
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2014 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/contrib/hooks/post-receive-move-tmp-refs b/contrib/hooks/post-receive-move-tmp-refs
new file mode 100755
index 0000000..fa0684f
--- /dev/null
+++ b/contrib/hooks/post-receive-move-tmp-refs
@@ -0,0 +1,78 @@
+#!/bin/sh
+#
+# 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.
+# --------------------------------------------------------
+# Install this hook script as post-receive hook in replicated repositories
+# hosted by a gerrit replica which are updated by push replication from the
+# corresponding gerrit primary node.
+#
+# In the gerrit primary node configure the replication plugin to push changes from
+# refs/changes/ to refs/tmp/changes/
+#   remote.NAME.push = +refs/changes/*:refs/tmp/changes/*
+#   remote.NAME.push = +refs/heads/*:refs/heads/*
+#   remote.NAME.push = +refs/tags/*:refs/tags/*
+# And if it's a Gerrit mirror:
+#   remote.NAME.push = +refs/meta/*:refs/meta/*
+#
+# In the replicated repository in the gerrit replica configure
+#    receive.hideRefs = refs/changes/
+# in order to not advertise the big number of refs in this namespace when
+# the gerrit primary's replication plugin is pushing a change
+#
+# Whenever a ref under refs/tmp/changes/ is arriving this hook will move it
+# to refs/changes/. This helps to avoid the large overhead of advertising all
+# refs/changes/ refs to the gerrit primary when it replicates changes to the
+# replica.
+#
+# Make this script executable then link to it in the repository you would like
+# to use it in.
+#   cd /path/to/your/repository.git
+#   ln -sf <shared hooks directory>/post-receive-move-tmp-refs hooks/post-receive
+#
+# If you want to use this by default for repositories on the Gerrit replica you
+# can set up a git template directory $TEMPLATE_DIR/hooks/post-receive and
+# configure init.templateDir in the ~/.gitconfig of the user that receives the
+# replication on the mirror host. That way when a new repository is created on
+# the primary and hence on the mirror (if configured that way) it will
+# automatically have the "tmp-refs" commit hook installed.
+# See https://git-scm.com/docs/git-init#_template_directory for details.
+
+# move new changes arriving under refs/tmp/changes/ to refs/changes/
+mv_tmp_refs()
+{
+	oldrev=$1
+	newrev=$2
+	refname=$3
+	case "$refname" in refs/tmp/changes/*)
+			short_refname=${refname##refs/tmp/changes/}
+			$(git update-ref refs/changes/$short_refname $newrev 2>/dev/null)
+			$(git update-ref -d $refname $newrev 2>/dev/null)
+			echo "moved \"$refname\" to \"refs/changes/$short_refname\""
+			;;
+	esac
+	return 0
+}
+
+GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
+if [ -z "$GIT_DIR" ]; then
+	echo >&2 "fatal: post-receive: GIT_DIR not set"
+	exit 1
+fi
+
+# read ref updates passed to post-receive hook
+while read oldrev newrev refname
+do
+	mv_tmp_refs $oldrev $newrev $refname || continue
+done
diff --git a/contrib/populate-fixture-data.py b/contrib/populate-fixture-data.py
index 4c6769c..e51e29d 100755
--- a/contrib/populate-fixture-data.py
+++ b/contrib/populate-fixture-data.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2016 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/contrib/refresh_plugin_in_testsite.sh b/contrib/refresh_plugin_in_testsite.sh
new file mode 100755
index 0000000..bb42ce8
--- /dev/null
+++ b/contrib/refresh_plugin_in_testsite.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+#
+# 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.
+
+# This script compiles a Gerrit plugin whose name is passed as first parameter
+# and copies it over to the plugin folder of the testsite. The path to the
+# testsite needs to be provided by the variable GERRIT_TESTSITE or as second
+# parameter.
+
+SCRIPT_DIR=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
+GERRIT_CODE_DIR="$SCRIPT_DIR/.."
+cd "$GERRIT_CODE_DIR"
+
+if [ "$#" -lt 1 ]
+then
+  echo "No plugin name provided as first argument. Stopping."
+  exit 1
+else
+  PLUGIN_NAME="$1"
+fi
+
+
+if [ "$#" -lt 2 ]
+then
+  if [ -z ${GERRIT_TESTSITE+x} ]
+  then
+    echo "Path to local testsite is neiter set as GERRIT_TESTSITE nor passed as second argument. Stopping."
+    exit 1
+  fi
+else
+  GERRIT_TESTSITE="$2"
+fi
+
+if [ ! -d "$GERRIT_TESTSITE" ]
+then
+  echo "Testsite directory $GERRIT_TESTSITE does not exist. Stopping."
+  exit 1
+fi
+
+bazel build //plugins/"$PLUGIN_NAME"/...
+if [ $? -ne 0 ]
+then
+  echo "Building the $PLUGIN_NAME plugin failed"
+  exit 1
+fi
+
+yes | cp -f "$GERRIT_CODE_DIR/bazel-genfiles/plugins/$PLUGIN_NAME/$PLUGIN_NAME.jar" "$GERRIT_TESTSITE/plugins/"
+if [ $? -eq 0 ]
+then
+  echo "Plugin $PLUGIN_NAME copied successfully to testsite."
+fi
diff --git a/contrib/reindex/.flake8 b/contrib/reindex/.flake8
new file mode 100644
index 0000000..151557f
--- /dev/null
+++ b/contrib/reindex/.flake8
@@ -0,0 +1,9 @@
+[flake8]
+max-line-length=100
+ignore=
+    # E203 whitespace before ':'
+    E203,
+    # W503: Line break before binary operator
+    W503,
+    # W504: Line break after binary operator
+    W504
diff --git a/contrib/reindex/.gitignore b/contrib/reindex/.gitignore
new file mode 100644
index 0000000..fd8c78f
--- /dev/null
+++ b/contrib/reindex/.gitignore
@@ -0,0 +1 @@
+changes-to-reindex.list
diff --git a/contrib/reindex/Pipfile b/contrib/reindex/Pipfile
new file mode 100644
index 0000000..21ffd90
--- /dev/null
+++ b/contrib/reindex/Pipfile
@@ -0,0 +1,19 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+pygerrit2 = "*"
+requests = "*"
+tqdm = "*"
+
+[dev-packages]
+flake8 = "*"
+black = "*"
+
+[requires]
+python_version = "3.9"
+
+[pipenv]
+allow_prereleases = true
diff --git a/contrib/reindex/Pipfile.lock b/contrib/reindex/Pipfile.lock
new file mode 100644
index 0000000..bb7cc2d
--- /dev/null
+++ b/contrib/reindex/Pipfile.lock
@@ -0,0 +1,248 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "37be5a74a22d0e084ebfe168bfdcd7bcaa87ad7b42be66b1d9fbff5e936ebe72"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.9"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "certifi": {
+            "hashes": [
+                "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
+                "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
+            ],
+            "version": "==2020.12.5"
+        },
+        "chardet": {
+            "hashes": [
+                "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
+                "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+            "version": "==4.0.0"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
+                "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.10"
+        },
+        "pbr": {
+            "hashes": [
+                "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
+                "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
+            ],
+            "markers": "python_version >= '2.6'",
+            "version": "==5.5.1"
+        },
+        "pygerrit2": {
+            "hashes": [
+                "sha256:d12cff5cc514dd61281d997ea86771e7f818030c3d2ef230b25bb14dae7d3f86"
+            ],
+            "index": "pypi",
+            "version": "==2.0.14"
+        },
+        "requests": {
+            "hashes": [
+                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
+                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
+            ],
+            "index": "pypi",
+            "version": "==2.25.1"
+        },
+        "tqdm": {
+            "hashes": [
+                "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5",
+                "sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1"
+            ],
+            "index": "pypi",
+            "version": "==4.54.1"
+        },
+        "urllib3": {
+            "hashes": [
+                "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
+                "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
+            "version": "==1.26.2"
+        }
+    },
+    "develop": {
+        "appdirs": {
+            "hashes": [
+                "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
+                "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
+            ],
+            "version": "==1.4.4"
+        },
+        "black": {
+            "hashes": [
+                "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
+            ],
+            "index": "pypi",
+            "version": "==20.8b1"
+        },
+        "click": {
+            "hashes": [
+                "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
+                "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+            "version": "==7.1.2"
+        },
+        "flake8": {
+            "hashes": [
+                "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
+                "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
+            ],
+            "index": "pypi",
+            "version": "==3.8.4"
+        },
+        "mccabe": {
+            "hashes": [
+                "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
+                "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
+            ],
+            "version": "==0.6.1"
+        },
+        "mypy-extensions": {
+            "hashes": [
+                "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
+                "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
+            ],
+            "version": "==0.4.3"
+        },
+        "pathspec": {
+            "hashes": [
+                "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd",
+                "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"
+            ],
+            "version": "==0.8.1"
+        },
+        "pycodestyle": {
+            "hashes": [
+                "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
+                "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.6.0"
+        },
+        "pyflakes": {
+            "hashes": [
+                "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
+                "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.2.0"
+        },
+        "regex": {
+            "hashes": [
+                "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538",
+                "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4",
+                "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc",
+                "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa",
+                "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444",
+                "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1",
+                "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af",
+                "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8",
+                "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9",
+                "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88",
+                "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba",
+                "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364",
+                "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e",
+                "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7",
+                "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0",
+                "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31",
+                "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683",
+                "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee",
+                "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b",
+                "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884",
+                "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c",
+                "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e",
+                "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562",
+                "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85",
+                "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c",
+                "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6",
+                "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d",
+                "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b",
+                "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70",
+                "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b",
+                "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b",
+                "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f",
+                "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0",
+                "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5",
+                "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5",
+                "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f",
+                "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e",
+                "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512",
+                "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d",
+                "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917",
+                "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"
+            ],
+            "version": "==2020.11.13"
+        },
+        "toml": {
+            "hashes": [
+                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+            ],
+            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==0.10.2"
+        },
+        "typed-ast": {
+            "hashes": [
+                "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
+                "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
+                "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d",
+                "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
+                "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
+                "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
+                "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c",
+                "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
+                "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
+                "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
+                "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
+                "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
+                "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
+                "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d",
+                "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
+                "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
+                "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c",
+                "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
+                "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395",
+                "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
+                "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
+                "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
+                "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
+                "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
+                "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072",
+                "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298",
+                "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91",
+                "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
+                "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f",
+                "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
+            ],
+            "version": "==1.4.1"
+        },
+        "typing-extensions": {
+            "hashes": [
+                "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
+                "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
+                "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
+            ],
+            "version": "==3.7.4.3"
+        }
+    }
+}
diff --git a/contrib/reindex/README.md b/contrib/reindex/README.md
new file mode 100644
index 0000000..acb9588
--- /dev/null
+++ b/contrib/reindex/README.md
@@ -0,0 +1,63 @@
+# Incremental reindexing during upgrade of large gerrit site
+
+In order to shorten the downtime needed to reindex changes during a
+Gerrit upgrade the following strategy can be used:
+
+- index preparation
+  - create a full consistent backup
+  - note down the timestamp when the backup was created (backup-time)
+  - create a complete copy of the production system from the backup
+  - upgrade this copy to the new Gerrit version
+  - online reindex this copy
+- upgrade of the production system
+  - make system unavailable so that users can't reach it anymore
+    e.g. by changing port numbers (downtime starts)
+  - take a full backup
+  - run
+
+    ``` bash
+    ./reindex.py -u gerrit-url -s backup-time
+    ```
+
+    to write the list of changes which have been created or modified
+    since the backup for the index preparation was created to a file
+    "changes-to-reindex.list"
+  - upgrade the production system to the new gerrit version skipping
+    reindexing
+  - copy the bulk of the new index from the copy system to the
+    production system
+  - run
+
+    ``` bash
+    ./reindex.py -u gerrit-url
+    ```
+
+    this reindexes all changes which have been created or modified after
+    the backup was taken reading these changes from the file
+    "changes-to-reindex.list"
+  - smoketest the system
+  - make the production system available to the users again
+    (downtime ends)
+
+## Online help
+
+For help on all available options run
+
+``` bash
+./reindex -h
+```
+
+## Python environment
+
+Prerequisites:
+
+- python 3.9
+- pipenv
+
+Install virtual python environment and run the script
+
+``` bash
+pipenv sync
+pipenv shell
+./reindex <options>
+```
diff --git a/contrib/reindex/reindex.py b/contrib/reindex/reindex.py
new file mode 100755
index 0000000..266f5ec
--- /dev/null
+++ b/contrib/reindex/reindex.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+from argparse import ArgumentParser, RawTextHelpFormatter
+from itertools import islice
+import getpass
+import logging
+import os
+
+from pygerrit2 import GerritRestAPI, HTTPBasicAuth, HTTPBasicAuthFromNetrc
+from tqdm import tqdm
+
+EPILOG = """\
+To query the list of changes which have been created or modified since the
+given timestamp and write them to a file "changes-to-reindex.list" run
+$ ./reindex.py -u gerrit-url -s timestamp
+
+To reindex the list of changes in file "changes-to-reindex.list" run
+$ ./reindex.py -u gerrit-url
+"""
+
+
+def _parse_options():
+    parser = ArgumentParser(
+        formatter_class=RawTextHelpFormatter,
+        epilog=EPILOG,
+    )
+    parser.add_argument(
+        "-u",
+        "--url",
+        dest="url",
+        help="gerrit url",
+    )
+    parser.add_argument(
+        "-s",
+        "--since",
+        dest="time",
+        help=(
+            "changes modified after the given 'TIME', inclusive. Must be in the\n"
+            "format '2006-01-02[ 15:04:05[.890][ -0700]]', omitting the time defaults\n"
+            "to 00:00:00 and omitting the timezone defaults to UTC."
+        ),
+    )
+    parser.add_argument(
+        "-f",
+        "--file",
+        default="changes-to-reindex.list",
+        dest="file",
+        help=(
+            "file path to store list of changes if --since is given,\n"
+            "otherwise file path to read list of changes from"
+        ),
+    )
+    parser.add_argument(
+        "-c",
+        "--chunk",
+        default=100,
+        dest="chunksize",
+        help="chunk size defining how many changes are reindexed per request",
+        type=int,
+    )
+    parser.add_argument(
+        "--cert",
+        dest="cert",
+        type=str,
+        help="path to file containing custom ca certificates to trust",
+    )
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        dest="verbose",
+        action="store_true",
+        help="verbose debugging output",
+    )
+    parser.add_argument(
+        "-n",
+        "--netrc",
+        default=True,
+        dest="netrc",
+        action="store_true",
+        help=(
+            "read credentials from .netrc, default to environment variables\n"
+            "USERNAME and PASSWORD, otherwise prompt for credentials interactively"
+        ),
+    )
+    return parser.parse_args()
+
+
+def _chunker(iterable, chunksize):
+    it = map(lambda s: s.strip(), iterable)
+    while True:
+        chunk = list(islice(it, chunksize))
+        if not chunk:
+            return
+        yield chunk
+
+
+class Reindexer:
+    """Class for reindexing Gerrit changes"""
+
+    def __init__(self):
+        self.options = _parse_options()
+        self._init_logger()
+        credentials = self._authenticate()
+        if self.options.cert:
+            certs = os.path.expanduser(self.options.cert)
+            self.api = GerritRestAPI(
+                url=self.options.url, auth=credentials, verify=certs
+            )
+        else:
+            self.api = GerritRestAPI(url=self.options.url, auth=credentials)
+
+    def _init_logger(self):
+        self.logger = logging.getLogger("Reindexer")
+        self.logger.setLevel(logging.DEBUG)
+        h = logging.StreamHandler()
+        if self.options.verbose:
+            h.setLevel(logging.DEBUG)
+        else:
+            h.setLevel(logging.INFO)
+        formatter = logging.Formatter("%(message)s")
+        h.setFormatter(formatter)
+        self.logger.addHandler(h)
+
+    def _authenticate(self):
+        username = password = None
+        if self.options.netrc:
+            auth = HTTPBasicAuthFromNetrc(url=self.options.url)
+            username = auth.username
+            password = auth.password
+        if not username:
+            username = os.environ.get("USERNAME")
+        if not password:
+            password = os.environ.get("PASSWORD")
+        while not username:
+            username = input("user: ")
+        while not password:
+            password = getpass.getpass("password: ")
+        auth = HTTPBasicAuth(username, password)
+        return auth
+
+    def _query(self):
+        start = 0
+        more_changes = True
+        while more_changes:
+            query = f"since:{self.options.time}&start={start}&skip-visibility"
+            for change in self.api.get(f"changes/?q={query}"):
+                more_changes = change.get("_more_changes") is not None
+                start += 1
+                yield change.get("_number")
+            break
+
+    def _query_to_file(self):
+        self.logger.debug(
+            f"writing changes since {self.options.time} to file {self.options.file}:"
+        )
+        with open(self.options.file, "w") as output:
+            for id in self._query():
+                self.logger.debug(id)
+                output.write(f"{id}\n")
+
+    def _reindex_chunk(self, chunk):
+        self.logger.debug(f"indexing {chunk}")
+        response = self.api.post(
+            "/config/server/index.changes",
+            chunk,
+        )
+        self.logger.debug(f"response: {response}")
+
+    def _reindex(self):
+        self.logger.debug(f"indexing changes from file {self.options.file}")
+        with open(self.options.file, "r") as f:
+            with tqdm(unit="changes", desc="Indexed") as pbar:
+                for chunk in _chunker(f, self.options.chunksize):
+                    self._reindex_chunk(chunk)
+                    pbar.update(len(chunk))
+
+    def execute(self):
+        if self.options.time:
+            self._query_to_file()
+        else:
+            self._reindex()
+
+
+def main():
+    reindexer = Reindexer()
+    reindexer.execute()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/contrib/show_new_gerrit_doc_in_chrome.sh b/contrib/show_new_gerrit_doc_in_chrome.sh
new file mode 100755
index 0000000..d57bc8a
--- /dev/null
+++ b/contrib/show_new_gerrit_doc_in_chrome.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+#
+# 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.
+
+# This script builds Gerrit's documentation and shows the current state in
+# Chrome. Specific pages (e.g. rest-api-changes.txt) including anchors can be
+# passed as parameter to jump directly to them.
+
+SCRIPT_DIR=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
+GERRIT_CODE_DIR="$SCRIPT_DIR/.."
+cd "$GERRIT_CODE_DIR"
+
+bazel build Documentation:searchfree
+if [ $? -ne 0 ]
+then
+  echo "Building the documentation failed. Stopping."
+  exit 1
+fi
+
+TMP_DOCS_DIR=/tmp/gerrit_docs
+rm -rf "$TMP_DOCS_DIR"
+unzip bazel-bin/Documentation/searchfree.zip -d "$TMP_DOCS_DIR" </dev/null >/dev/null 2>&1 & disown
+if [ $? -ne 0 ]
+then
+  echo "Unzipping the documentation to $TMP_DOCS_DIR failed. Stopping."
+  exit 1
+fi
+
+if [ "$#" -lt 1 ]
+then
+  FILE_NAME="index.html"
+else
+  FILE_NAME="$1"
+fi
+DOC_FILE_NAME="${FILE_NAME/.txt/.html}"
+google-chrome "file:///$TMP_DOCS_DIR/Documentation/$DOC_FILE_NAME" </dev/null >/dev/null 2>&1 & disown
diff --git a/contrib/start_testsite.sh b/contrib/start_testsite.sh
new file mode 100755
index 0000000..014eba9
--- /dev/null
+++ b/contrib/start_testsite.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+#
+# 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.
+
+# This script starts the local testsite in debug mode. If the flag "-u" is
+# passed, Gerrit is built from the current state of the repository and the
+# testsite is refreshed. The path to the testsite needs to be provided by
+# the variable GERRIT_TESTSITE or as parameter (after any used flags).
+# The testsite can be stopped by interrupting this script.
+
+SCRIPT_DIR=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
+GERRIT_CODE_DIR="$SCRIPT_DIR/.."
+cd "$GERRIT_CODE_DIR"
+
+UPDATE=false
+while getopts ':u' flag; do
+  case "${flag}" in
+    u) UPDATE=true ;;
+  esac
+done
+shift $(($OPTIND-1))
+
+if [ "$#" -lt 1 ]
+then
+  if [ -z ${GERRIT_TESTSITE+x} ]
+  then
+    echo "Path to local testsite is neither set as GERRIT_TESTSITE nor passed as first argument. Stopping."
+    exit 1
+  fi
+else
+  GERRIT_TESTSITE="$1"
+fi
+
+if [ "$UPDATE" = true ]
+then
+  echo "Refreshing testsite"
+  bazel build gerrit
+  if [ $? -ne 0 ]
+  then
+    echo "Build failed. Stopping."
+    exit 1
+  fi
+  $(bazel info output_base)/external/local_jdk/bin/java -jar bazel-bin/gerrit.war init --batch -d "$GERRIT_TESTSITE"
+  if [ $? -ne 0 ]
+  then
+    echo "Patching the testsite failed. Stopping."
+    exit 1
+  fi
+fi
+
+$(bazel info output_base)/external/local_jdk/bin/java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 bazel-bin/gerrit.war daemon -d "$GERRIT_TESTSITE" --console-log
diff --git a/e2e-tests/README b/e2e-tests/README
new file mode 100755
index 0000000..59e0fba
--- /dev/null
+++ b/e2e-tests/README
@@ -0,0 +1,53 @@
+#!/bin/bash
+#
+# Example usage only-
+# 1. Optional: replace test@mail.com below with your own, reachable locally.
+# 2. Use the '>>' operator below instead to not overwrite your known_hosts; keep '>' otherwise.
+# 3. Note that appending as proposed above may potentially repeat the same line multiple times.
+# 4. Init your local Gerrit test site then start it; you may refer to [1] below.
+# 5. Set GIT_HTTP_PASSWORD below to yours, from [2].
+# 6. Change to this directory to execute ./README (this executable file) in its own terminal.
+# 7. Install sbt if missing, based on your operating system; re-run to compile.
+# 8. Optional: add the below generated (displayed) key to your local admin user [3].
+# 9. Otherwise keep the lines below that use your existing user ssh keys for admin testing.
+# 10. This script assumes the google-sourced version of the example json file [4].
+# 11. If running that scenario locally as below reports authentication failures, [4] may be a fork.
+# 12. Uncomment any one of the below sbt commands at will; you may add some locally.
+# 13. See [5] for how to start using JAVA_OPTS below; you may leave it empty for these sbt commands.
+# 14. You can initialize an IDE sbt (Scala) project from/in this root folder; see [6].
+#
+# [1] https://gerrit-review.googlesource.com/Documentation/dev-readme.html#init
+# [2] http://localhost:8080/settings/#HTTPCredentials
+# [3] http://localhost:8080/settings/#SSHKeys
+# [4] ./src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
+# [5] https://gerrit-review.googlesource.com/Documentation/dev-e2e-tests.html#_environment_properties
+# [6] https://gerrit-review.googlesource.com/Documentation/dev-e2e-tests.html#_ide_intellij
+
+# DO NOT change this (assumed) directory; force-removed *recursively* below!
+gatlingGitKeys=/tmp/ssh-keys
+
+userSshDir=$HOME/.ssh
+
+# Comment this group of lines out if willing to generate other keys as below.
+rm -f $gatlingGitKeys
+ln -s "$userSshDir" $gatlingGitKeys
+
+# Comment this group of lines out if keys already generated, as either below or above.
+#rm -fr $gatlingGitKeys
+#mkdir $gatlingGitKeys
+#ssh-keygen -m PEM -t rsa -C "test@mail.com" -f $gatlingGitKeys/id_rsa
+
+ssh-keyscan -t rsa -p 29418 localhost > "$userSshDir"/known_hosts
+cat $gatlingGitKeys/id_rsa.pub
+
+export GIT_HTTP_USERNAME="admin"
+export GIT_HTTP_PASSWORD="TODO"
+export JAVA_OPTS="\
+"
+#-Dx=y \
+
+#sbt clean
+#sbt update
+sbt compile
+#sbt "gatling:testOnly com.google.gerrit.scenarios.CloneUsingBothProtocols"
+#sbt "gatling:lastReport"
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/AbandonChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/AbandonChange.json
new file mode 100644
index 0000000..665cc4d
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/AbandonChange.json
@@ -0,0 +1,6 @@
+[
+  {
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/",
+    "number": "NUMBER"
+  }
+]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateBranch-body.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateBranch-body.json
new file mode 100644
index 0000000..f69e575
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateBranch-body.json
@@ -0,0 +1,3 @@
+{
+  "revision": "master"
+}
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateBranch.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateBranch.json
new file mode 100644
index 0000000..5459f11
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateBranch.json
@@ -0,0 +1,6 @@
+[
+  {
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/projects/PROJECT/branches/",
+    "project": "PROJECT"
+  }
+]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange-body.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange-body.json
index 23bf26c..8babac8 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange-body.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange-body.json
@@ -1,5 +1,5 @@
 {
   "project": "${project}",
-  "branch": "master",
+  "branch": "${branch}",
   "subject": "Change"
 }
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject-body.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject-body.json
new file mode 100644
index 0000000..488de6d
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject-body.json
@@ -0,0 +1,3 @@
+{
+  "force": "${force_project_deletion}"
+}
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/RestoreChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/RestoreChange.json
new file mode 100644
index 0000000..665cc4d
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/RestoreChange.json
@@ -0,0 +1,6 @@
+[
+  {
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/",
+    "number": "NUMBER"
+  }
+]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChangeInBranch.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChangeInBranch.json
new file mode 100644
index 0000000..301c65b
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChangeInBranch.json
@@ -0,0 +1,5 @@
+[
+  {
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/"
+  }
+]
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/AbandonChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/AbandonChange.scala
new file mode 100644
index 0000000..d387a3e
--- /dev/null
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/AbandonChange.scala
@@ -0,0 +1,71 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.scenarios
+
+import io.gatling.core.Predef.{atOnceUsers, _}
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import io.gatling.http.Predef.http
+
+import scala.collection.mutable
+import scala.concurrent.duration.DurationInt
+
+class AbandonChange extends GerritSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
+  private val projectName = className
+  private var numbersCopy: mutable.Queue[Int] = mutable.Queue[Int]()
+  private var createChange: Option[CreateChange] = Some(new CreateChange(projectName))
+
+  override def relativeRuntimeWeight = 10
+
+  def this(createChange: CreateChange) {
+    this()
+    this.createChange = Some(createChange)
+  }
+
+  val test: ScenarioBuilder = scenario(uniqueName)
+      .feed(data)
+      .exec(session => {
+        if (createChange.nonEmpty) {
+          if (numbersCopy.isEmpty) {
+            numbersCopy = createChange.get.numbers.clone()
+          }
+        }
+        session.set(numberKey, numbersCopy.dequeue())
+      })
+      .exec(http(uniqueName).post("${url}${" + numberKey + "}/abandon"))
+
+  private val createProject = new CreateProject(projectName)
+  private val deleteProject = new DeleteProject(projectName)
+
+  setUp(
+    createProject.test.inject(
+      nothingFor(stepWaitTime(createProject) seconds),
+      atOnceUsers(single)
+    ),
+    createChange.get.test.inject(
+      nothingFor(stepWaitTime(createChange.get) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    test.inject(
+      nothingFor(stepWaitTime(this) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    deleteProject.test.inject(
+      nothingFor(stepWaitTime(deleteProject) seconds),
+      atOnceUsers(single)
+    ),
+  ).protocols(httpProtocol)
+}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ApproveChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ApproveChange.scala
index 8ae69d7..9a91153 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ApproveChange.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ApproveChange.scala
@@ -19,10 +19,15 @@
 import io.gatling.core.structure.ScenarioBuilder
 import io.gatling.http.Predef.http
 
+import scala.collection.mutable
+
 class ApproveChange extends GerritSimulation {
-  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
+  private var numbersCopy: mutable.Queue[Int] = mutable.Queue[Int]()
   private var createChange: Option[CreateChange] = None
 
+  override def relativeRuntimeWeight = 10
+
   def this(createChange: CreateChange) {
     this()
     this.createChange = Some(createChange)
@@ -32,13 +37,16 @@
       .feed(data)
       .exec(session => {
         if (createChange.nonEmpty) {
-          session.set("number", createChange.get.number)
+          if (numbersCopy.isEmpty) {
+            numbersCopy = createChange.get.numbers.clone()
+          }
+          session.set(numberKey, numbersCopy.dequeue())
         } else {
           session
         }
       })
       .exec(http(uniqueName)
-          .post("${url}${number}/revisions/current/review")
+          .post("${url}${" + numberKey + "}/revisions/current/review")
           .body(ElFileBody(body)).asJson)
 
   setUp(
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.scala
index 96943ce..900702a 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.scala
@@ -37,7 +37,7 @@
         }
       })
       .exec(http(uniqueName).get("${url}")
-          .check(regex("\"" + memKey + "\": (\\d+)")
+          .check(regex("\"" + memKey + "\":(\\d+)")
               .is(session => session(entriesKey).as[String])))
 
   setUp(
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
index 08966a8..c283861 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
@@ -21,9 +21,9 @@
 import scala.concurrent.duration._
 
 class CloneUsingBothProtocols extends GitSimulation {
-  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
   private val projectName = className
-  private val duration = 2
+  private val duration = 2 * numberOfUsers
 
   override def replaceOverride(in: String): String = {
     replaceKeyWith("_project", projectName, in)
@@ -43,7 +43,7 @@
     ),
     test.inject(
       nothingFor(stepWaitTime(this) seconds),
-      constantUsersPerSec(single) during (duration seconds)
+      constantUsersPerSec(numberOfUsers) during (duration seconds)
     ).protocols(gitProtocol),
     deleteProject.test.inject(
       nothingFor(stepWaitTime(deleteProject) + duration seconds),
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateBranch.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateBranch.scala
new file mode 100644
index 0000000..3630a7a
--- /dev/null
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateBranch.scala
@@ -0,0 +1,65 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.scenarios
+
+import io.gatling.core.Predef.{atOnceUsers, _}
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import io.gatling.http.Predef._
+
+import scala.collection.mutable
+import scala.concurrent.duration._
+
+class CreateBranch extends ProjectSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
+  private val branchIdKey = "branchId"
+  private var counter = 0
+  var branches: mutable.Queue[String] = mutable.Queue[String]()
+
+  def this(projectName: String) {
+    this()
+    this.projectName = projectName
+  }
+
+  val test: ScenarioBuilder = scenario(uniqueName)
+      .feed(data)
+      .exec(session => {
+        counter += 1
+        val branchId = "branch-" + counter
+        branches += branchId
+        session.set(branchIdKey, branchId)
+      })
+      .exec(http(uniqueName)
+          .post("${url}${" + branchIdKey + "}")
+          .body(ElFileBody(body)).asJson)
+
+  private val createProject = new CreateProject(projectName)
+  private val deleteProject = new DeleteProject(projectName)
+
+  setUp(
+    createProject.test.inject(
+      nothingFor(stepWaitTime(createProject) seconds),
+      atOnceUsers(single)
+    ),
+    test.inject(
+      nothingFor(stepWaitTime(this) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    deleteProject.test.inject(
+      nothingFor(stepWaitTime(deleteProject) seconds),
+      atOnceUsers(single)
+    ),
+  ).protocols(httpProtocol)
+}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateChange.scala
index f3692a9..fb41075 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateChange.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateChange.scala
@@ -19,27 +19,48 @@
 import io.gatling.core.structure.ScenarioBuilder
 import io.gatling.http.Predef._
 
+import scala.collection.mutable
 import scala.concurrent.duration._
 
 class CreateChange extends ProjectSimulation {
-  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
-  private val numberKey = "_number"
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
+  private val weightPerUser = 0.1
+  private var createBranch: Option[CreateBranch] = None
+  private var branchesCopy: mutable.Queue[String] = mutable.Queue[String]()
   var number = 0
+  var numbers: mutable.Queue[Int] = mutable.Queue[Int]()
 
-  override def relativeRuntimeWeight = 2
+  override def relativeRuntimeWeight: Int = 2 + (numberOfUsers * weightPerUser).toInt
 
   def this(projectName: String) {
     this()
     this.projectName = projectName
   }
 
+  def this(projectName: String, createBranch: CreateBranch) {
+    this()
+    this.projectName = projectName
+    this.createBranch = Some(createBranch)
+  }
+
   val test: ScenarioBuilder = scenario(uniqueName)
       .feed(data)
+      .exec(session => {
+        var branchId = "master"
+        if (createBranch.nonEmpty) {
+          if (branchesCopy.isEmpty) {
+            branchesCopy = createBranch.get.branches.clone()
+          }
+          branchId = branchesCopy.dequeue()
+        }
+        session.set("branch", branchId)
+      })
       .exec(httpRequest
           .body(ElFileBody(body)).asJson
-          .check(regex("\"" + numberKey + "\":(\\d+),").saveAs(numberKey)))
+          .check(regex("\"_" + numberKey + "\":(\\d+),").saveAs(numberKey)))
       .exec(session => {
         number = session(numberKey).as[Int]
+        numbers += number
         session
       })
 
@@ -54,11 +75,11 @@
     ),
     test.inject(
       nothingFor(stepWaitTime(this) seconds),
-      atOnceUsers(single)
+      atOnceUsers(numberOfUsers)
     ),
     deleteChange.test.inject(
       nothingFor(stepWaitTime(deleteChange) seconds),
-      atOnceUsers(single)
+      atOnceUsers(numberOfUsers)
     ),
     deleteProject.test.inject(
       nothingFor(stepWaitTime(deleteProject) seconds),
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteChange.scala
index d832bde..743219f 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteChange.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteChange.scala
@@ -20,7 +20,7 @@
 import io.gatling.http.Predef.http
 
 class DeleteChange extends GerritSimulation {
-  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
   private var createChange: Option[CreateChange] = None
 
   override def relativeRuntimeWeight = 2
@@ -34,12 +34,12 @@
       .feed(data)
       .exec(session => {
         if (createChange.nonEmpty) {
-          session.set("number", createChange.get.number)
+          session.set(numberKey, createChange.get.numbers.dequeue())
         } else {
           session
         }
       })
-      .exec(http(uniqueName).delete("${url}${number}"))
+      .exec(http(uniqueName).delete("${url}${" + numberKey + "}"))
 
   setUp(
     test.inject(
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
index 1752634..eb4df30 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
@@ -20,6 +20,7 @@
 
 class DeleteProject extends ProjectSimulation {
   private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+  private val forceKey = "force_project_deletion"
 
   def this(projectName: String) {
     this()
@@ -28,7 +29,10 @@
 
   val test: ScenarioBuilder = scenario(uniqueName)
       .feed(data)
-      .exec(httpRequest)
+      .exec(session => {
+        session.set(forceKey, getProperty(forceKey, "false"))
+      })
+      .exec(httpRequest.body(ElFileBody(body)).asJson)
 
   setUp(
     test.inject(
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 7b31b3d..c199dd9 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
@@ -23,6 +23,8 @@
 class GerritSimulation extends Simulation {
   implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
 
+  protected val numberKey: String = "number"
+
   private val packageName = getClass.getPackage.getName
   private val path = packageName.replaceAllLiterally(".", "/")
 
@@ -34,6 +36,7 @@
   protected val uniqueName: String = className + "-" + hashCode()
   protected val single = 1
 
+  val numberOfUsers: Int = replaceProperty("number_of_users", single).toInt
   val replicationDelay: Int = replaceProperty("replication_delay", 15).toInt
   private val powerFactor = replaceProperty("power_factor", 1.0).toDouble
   protected val SecondsPerWeightUnit = 2
@@ -63,9 +66,9 @@
   protected val keys: PartialFunction[(String, Any), Any] = {
     case ("entries", entries) =>
       replaceProperty("projects_entries", "1", entries.toString)
-    case ("number", number) =>
-      val precedes = replaceKeyWith("_number", 0, number.toString)
-      replaceProperty("number", 1, precedes)
+    case (`numberKey`, number) =>
+      val precedes = replaceKeyWith("_" + numberKey, 0, number.toString)
+      replaceProperty(numberKey, 1, precedes)
     case ("parent", parent) =>
       replaceProperty("parent", "All-Projects", parent.toString)
     case ("project", project) =>
@@ -89,6 +92,11 @@
   }
 
   protected def replaceProperty(term: String, default: Any, in: String): String = {
+    val value = getProperty(term, default)
+    replaceKeyWith(term, value, in)
+  }
+
+  protected def getProperty(term: String, default: Any): String = {
     val property = packageName + "." + term
     var value = default
     default match {
@@ -100,7 +108,7 @@
       case _: Integer =>
         value = Integer.getInteger(property, default.asInstanceOf[Integer])
     }
-    replaceKeyWith(term, value, in)
+    value.toString
   }
 
   protected def replaceKeyWith(term: String, value: Any, in: String): String = {
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GetMasterBranchRevision.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GetMasterBranchRevision.scala
index 1137ad5..f2236d1 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GetMasterBranchRevision.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GetMasterBranchRevision.scala
@@ -23,7 +23,7 @@
   private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
   var revision: Option[String] = None
   val revisionKey = "revision"
-  val revisionPattern: String = "\"" + revisionKey + "\": \"(.+)\""
+  val revisionPattern: String = "\"" + revisionKey + "\":\"(.+)\""
 
   val test: ScenarioBuilder = scenario(uniqueName)
       .feed(data)
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GetProjectsCacheEntries.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GetProjectsCacheEntries.scala
index 266c0b9..0bb3afb 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GetProjectsCacheEntries.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GetProjectsCacheEntries.scala
@@ -30,7 +30,7 @@
   val test: ScenarioBuilder = scenario(uniqueName)
       .feed(data)
       .exec(http(uniqueName).get("${url}")
-          .check(regex("\"" + memKey + "\": (\\d+)").saveAs(entriesKey)))
+          .check(regex("\"" + memKey + "\":(\\d+)").saveAs(entriesKey)))
       .exec(session => {
         if (consumer.nonEmpty) {
           consumer.get.entriesBeforeFlush(session(entriesKey).as[Int])
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/RestoreChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/RestoreChange.scala
new file mode 100644
index 0000000..81096b0
--- /dev/null
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/RestoreChange.scala
@@ -0,0 +1,74 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.scenarios
+
+import io.gatling.core.Predef.{atOnceUsers, _}
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import io.gatling.http.Predef.http
+
+import scala.collection.mutable
+import scala.concurrent.duration.DurationInt
+
+class RestoreChange extends GerritSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
+  private val projectName = className
+  private var numbersCopy: mutable.Queue[Int] = mutable.Queue[Int]()
+
+  override def relativeRuntimeWeight = 10
+
+  private val test: ScenarioBuilder = scenario(uniqueName)
+      .feed(data)
+      .exec(session => {
+        if (numbersCopy.isEmpty) {
+          numbersCopy = createChange.numbers.clone()
+        }
+        session.set(numberKey, numbersCopy.dequeue())
+      }
+      ).exec(http(uniqueName).post("${url}${" + numberKey + "}/restore"))
+
+  private val createProject = new CreateProject(projectName)
+  private val createChange = new CreateChange(projectName)
+  private val abandonChange = new AbandonChange(createChange)
+  private val deleteChange = new DeleteChange(createChange)
+  private val deleteProject = new DeleteProject(projectName)
+
+  setUp(
+    createProject.test.inject(
+      nothingFor(stepWaitTime(createProject) seconds),
+      atOnceUsers(single)
+    ),
+    createChange.test.inject(
+      nothingFor(stepWaitTime(createChange) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    abandonChange.test.inject(
+      nothingFor(stepWaitTime(abandonChange) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    test.inject(
+      nothingFor(stepWaitTime(this) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    deleteChange.test.inject(
+      nothingFor(stepWaitTime(deleteChange) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    deleteProject.test.inject(
+      nothingFor(stepWaitTime(deleteProject) seconds),
+      atOnceUsers(single)
+    ),
+  ).protocols(httpProtocol)
+}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala
index 067496a..20be28a 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala
@@ -36,9 +36,9 @@
   val test: ScenarioBuilder = scenario(uniqueName)
       .feed(data)
       .exec(session => {
-        session.set("number", createChange.number)
+        session.set(numberKey, createChange.number)
       })
-      .exec(http(uniqueName).post("${url}${number}/submit"))
+      .exec(http(uniqueName).post("${url}${" + numberKey + "}/submit"))
 
   private val createProject = new CreateProject(projectName)
   private val approveChange = new ApproveChange(createChange)
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChangeInBranch.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChangeInBranch.scala
new file mode 100644
index 0000000..9e1431b
--- /dev/null
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChangeInBranch.scala
@@ -0,0 +1,74 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.scenarios
+
+import io.gatling.core.Predef.{atOnceUsers, _}
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import io.gatling.http.Predef.http
+
+import scala.collection.mutable
+import scala.concurrent.duration._
+
+class SubmitChangeInBranch extends GerritSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
+  private var changesCopy: mutable.Queue[Int] = mutable.Queue[Int]()
+  private val projectName = className
+
+  override def relativeRuntimeWeight = 10
+
+  private val test: ScenarioBuilder = scenario(uniqueName)
+      .feed(data)
+      .exec(session => {
+        if (changesCopy.isEmpty) {
+          changesCopy = createChange.numbers.clone()
+        }
+        session.set(numberKey, changesCopy.dequeue())
+      })
+      .exec(http(uniqueName).post("${url}${" + numberKey + "}/submit"))
+
+  private val createProject = new CreateProject(projectName)
+  private val createBranch = new CreateBranch(projectName)
+  private val createChange = new CreateChange(projectName, createBranch)
+  private val approveChange = new ApproveChange(createChange)
+  private val deleteProject = new DeleteProject(projectName)
+
+  setUp(
+    createProject.test.inject(
+      nothingFor(stepWaitTime(createProject) seconds),
+      atOnceUsers(single)
+    ),
+    createBranch.test.inject(
+      nothingFor(stepWaitTime(createBranch) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    createChange.test.inject(
+      nothingFor(stepWaitTime(createChange) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    approveChange.test.inject(
+      nothingFor(stepWaitTime(approveChange) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    test.inject(
+      nothingFor(stepWaitTime(this) seconds),
+      atOnceUsers(numberOfUsers)
+    ),
+    deleteProject.test.inject(
+      nothingFor(stepWaitTime(deleteProject) seconds),
+      atOnceUsers(single)
+    ),
+  ).protocols(httpProtocol)
+}
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 2328697..ede488c 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -15,17 +15,20 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.OptionalSubject.optionals;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.entities.Patch.COMMIT_MSG;
+import static com.google.gerrit.entities.Patch.MERGE_LIST;
 import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
-import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
-import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
@@ -51,9 +54,16 @@
 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.PermissionRange;
 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.BooleanProjectConfig;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
@@ -78,14 +88,6 @@
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.EmailHeader;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -124,21 +126,23 @@
 import com.google.gerrit.server.plugins.TestServerPlugin;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.restapi.change.Revisions;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.util.git.DelegateSystemReader;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.SshMode;
+import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.jcraft.jsch.KeyPair;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -148,6 +152,8 @@
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -160,8 +166,6 @@
 import java.util.Optional;
 import java.util.regex.Pattern;
 import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -174,18 +178,20 @@
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.transport.FetchResult;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.Transport;
 import org.eclipse.jgit.transport.TransportBundleStream;
 import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Rule;
-import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
@@ -203,8 +209,6 @@
   @ConfigSuite.Parameter public Config baseConfig;
   @ConfigSuite.Name private String configName;
 
-  @Rule public ExpectedException exception = ExpectedException.none();
-
   @Rule
   public TestRule testRunner =
       new TestRule() {
@@ -219,7 +223,8 @@
               beforeTest(description);
               ProjectResetter.Config input = requireNonNull(resetProjects());
 
-              try (ProjectResetter resetter = projectResetter.builder().build(input)) {
+              try (ProjectResetter resetter =
+                  projectResetter != null ? projectResetter.builder().build(input) : null) {
                 AbstractDaemonTest.this.resetter = resetter;
                 base.evaluate();
               } finally {
@@ -291,12 +296,14 @@
   @Inject private PluginGuiceEnvironment pluginGuiceEnvironment;
   @Inject private PluginUser.Factory pluginUserFactory;
   @Inject private ProjectIndexCollection projectIndexes;
-  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private SitePaths sitePaths;
+  @Inject private ProjectOperations projectOperations;
 
   private ProjectResetter resetter;
   private List<Repository> toClose;
+  private String systemTimeZone;
+  private SystemReader oldSystemReader;
 
   @BeforeClass
   public static void enablePerThreadCacheStalenessCheck() {
@@ -306,12 +313,16 @@
 
   @Before
   public void clearSender() {
-    sender.clear();
+    if (sender != null) {
+      sender.clear();
+    }
   }
 
   @Before
   public void startEventRecorder() {
-    eventRecorder = eventRecorderFactory.create(admin);
+    if (eventRecorderFactory != null) {
+      eventRecorder = eventRecorderFactory.create(admin);
+    }
   }
 
   @Before
@@ -326,7 +337,9 @@
 
   @After
   public void closeEventRecorder() {
-    eventRecorder.close();
+    if (eventRecorder != null) {
+      eventRecorder.close();
+    }
   }
 
   @AfterClass
@@ -412,6 +425,10 @@
   }
 
   protected void beforeTest(Description description) throws Exception {
+    // SystemReader must be overridden before creating any repos, since they read the user/system
+    // configs at initialization time, and are then stored in the RepositoryCache forever.
+    oldSystemReader = setFakeSystemReader(temporaryFolder.getRoot());
+
     this.description = description;
     GerritServer.Description classDesc =
         GerritServer.Description.forTestClass(description, configName);
@@ -423,6 +440,9 @@
       baseConfig.setString("sshd", null, "listenAddress", "off");
     }
 
+    baseConfig.unset("gerrit", null, "canonicalWebUrl");
+    baseConfig.unset("httpd", null, "listenUrl");
+
     baseConfig.setInt("index", null, "batchThreads", -1);
 
     baseConfig.setInt("receive", null, "changeUpdateThreads", 4);
@@ -465,10 +485,63 @@
     atrScope.set(ctx);
     ProjectInput in = projectInput(description);
     gApi.projects().create(in);
-    project = new Project.NameKey(in.name);
+    project = Project.nameKey(in.name);
     if (!classDesc.skipProjectClone()) {
       testRepo = cloneProject(project, getCloneAsAccount(description));
     }
+
+    // Set the clock step last, so that the test setup isn't consuming any timestamps after the
+    // clock has been set.
+    setTimeSettings(classDesc.useSystemTime(), classDesc.useClockStep(), classDesc.useTimezone());
+    setTimeSettings(
+        methodDesc.useSystemTime(), methodDesc.useClockStep(), methodDesc.useTimezone());
+  }
+
+  private static SystemReader setFakeSystemReader(File tempDir) {
+    SystemReader oldSystemReader = SystemReader.getInstance();
+    SystemReader.setInstance(
+        new DelegateSystemReader(oldSystemReader) {
+          @Override
+          public FileBasedConfig openJGitConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "jgit.config"), FS.detect());
+          }
+
+          @Override
+          public FileBasedConfig openUserConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "user.config"), FS.detect());
+          }
+
+          @Override
+          public FileBasedConfig openSystemConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "system.config"), FS.detect());
+          }
+        });
+    return oldSystemReader;
+  }
+
+  private void setTimeSettings(
+      boolean useSystemTime,
+      @Nullable UseClockStep useClockStep,
+      @Nullable UseTimezone useTimezone) {
+    if (useSystemTime) {
+      TestTimeUtil.useSystemTime();
+    } else if (useClockStep != null) {
+      TestTimeUtil.resetWithClockStep(useClockStep.clockStep(), useClockStep.clockStepUnit());
+      if (useClockStep.startAtEpoch()) {
+        TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
+      }
+    }
+    if (useTimezone != null) {
+      systemTimeZone = System.setProperty("user.timezone", useTimezone.timezone());
+    }
+  }
+
+  private void resetTimeSettings() {
+    TestTimeUtil.useSystemTime();
+    if (systemTimeZone != null) {
+      System.setProperty("user.timezone", systemTimeZone);
+      systemTimeZone = null;
+    }
   }
 
   /** Override to bind an additional Guice module */
@@ -569,7 +642,7 @@
     in.submitType = submitType;
     in.createEmptyCommit = createEmptyCommit;
     gApi.projects().create(in);
-    return new Project.NameKey(in.name);
+    return Project.nameKey(in.name);
   }
 
   protected TestRepository<InMemoryRepository> cloneProject(Project.NameKey p) throws Exception {
@@ -601,10 +674,13 @@
       repo.close();
     }
     closeSsh();
+    resetTimeSettings();
     if (server != commonServer) {
       server.close();
       server = null;
     }
+    SystemReader.setInstance(oldSystemReader);
+    oldSystemReader = null;
   }
 
   protected void closeSsh() {
@@ -727,18 +803,18 @@
     return push.to("refs/for/" + branch + "%topic=" + name(topic));
   }
 
-  protected BranchApi createBranch(Branch.NameKey branch) throws Exception {
+  protected BranchApi createBranch(BranchNameKey branch) throws Exception {
     return gApi.projects()
-        .name(branch.getParentKey().get())
-        .branch(branch.get())
+        .name(branch.project().get())
+        .branch(branch.branch())
         .create(new BranchInput());
   }
 
-  protected BranchApi createBranchWithRevision(Branch.NameKey branch, String revision)
+  protected BranchApi createBranchWithRevision(BranchNameKey branch, String revision)
       throws Exception {
     BranchInput in = new BranchInput();
     in.revision = revision;
-    return gApi.projects().name(branch.getParentKey().get()).branch(branch.get()).create(in);
+    return gApi.projects().name(branch.project().get()).branch(branch.branch()).create(in);
   }
 
   private static final List<Character> RANDOM =
@@ -808,12 +884,15 @@
   }
 
   protected Account getAccount(Account.Id accountId) {
-    return getAccountState(accountId).getAccount();
+    return getAccountState(accountId).account();
   }
 
   protected AccountState getAccountState(Account.Id accountId) {
     Optional<AccountState> accountState = accountCache.get(accountId);
-    assertThat(accountState).named("account %s", accountId.get()).isPresent();
+    assertWithMessage("account %s", accountId.get())
+        .about(optionals())
+        .that(accountState)
+        .isPresent();
     return accountState.get();
   }
 
@@ -915,59 +994,6 @@
     return gApi.changes().id(r.getChangeId()).current();
   }
 
-  protected void allow(String ref, String permission, AccountGroup.UUID id) throws Exception {
-    allow(project, ref, permission, id);
-  }
-
-  protected void allow(Project.NameKey p, String ref, String permission, AccountGroup.UUID id)
-      throws Exception {
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.allow(u.getConfig(), permission, id, ref);
-      u.save();
-    }
-  }
-
-  protected void allowGlobalCapabilities(
-      AccountGroup.UUID id, int min, int max, String... capabilityNames) throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      for (String capabilityName : capabilityNames) {
-        Util.allow(
-            u.getConfig(), capabilityName, id, new PermissionRange(capabilityName, min, max));
-      }
-      u.save();
-    }
-  }
-
-  protected void allowGlobalCapabilities(AccountGroup.UUID id, String... capabilityNames)
-      throws Exception {
-    allowGlobalCapabilities(id, Arrays.asList(capabilityNames));
-  }
-
-  protected void allowGlobalCapabilities(AccountGroup.UUID id, Iterable<String> capabilityNames)
-      throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      for (String capabilityName : capabilityNames) {
-        Util.allow(u.getConfig(), capabilityName, id);
-      }
-      u.save();
-    }
-  }
-
-  protected void removeGlobalCapabilities(AccountGroup.UUID id, String... capabilityNames)
-      throws Exception {
-    removeGlobalCapabilities(id, Arrays.asList(capabilityNames));
-  }
-
-  protected void removeGlobalCapabilities(AccountGroup.UUID id, Iterable<String> capabilityNames)
-      throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      for (String capabilityName : capabilityNames) {
-        Util.remove(u.getConfig(), capabilityName, id);
-      }
-      u.save();
-    }
-  }
-
   protected void setUseSignedOffBy(InheritableBoolean value) throws Exception {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
       ProjectConfig config = projectConfigFactory.read(md);
@@ -986,125 +1012,14 @@
     }
   }
 
-  protected void deny(String ref, String permission, AccountGroup.UUID id) throws Exception {
-    deny(project, ref, permission, id);
-  }
-
-  protected void deny(Project.NameKey p, String ref, String permission, AccountGroup.UUID id)
-      throws Exception {
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.deny(u.getConfig(), permission, id, ref);
-      u.save();
-    }
-  }
-
-  protected PermissionRule block(String ref, String permission, AccountGroup.UUID id)
-      throws Exception {
-    return block(project, ref, permission, id);
-  }
-
-  protected PermissionRule block(
-      Project.NameKey project, String ref, String permission, AccountGroup.UUID id)
-      throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      PermissionRule rule = Util.block(u.getConfig(), permission, id, ref);
-      u.save();
-      return rule;
-    }
-  }
-
-  protected void blockLabel(
-      String label, int min, int max, AccountGroup.UUID id, String ref, Project.NameKey project)
-      throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.block(u.getConfig(), Permission.LABEL + label, min, max, id, ref);
-      u.save();
-    }
-  }
-
-  protected void grant(Project.NameKey project, String ref, String permission)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    grant(project, ref, permission, false);
-  }
-
-  protected void grant(Project.NameKey project, String ref, String permission, boolean force)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    grant(project, ref, permission, force, adminGroupUuid());
-  }
-
-  protected void grant(
-      Project.NameKey project,
-      String ref,
-      String permission,
-      boolean force,
-      AccountGroup.UUID groupUUID)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      md.setMessage(String.format("Grant %s on %s", permission, ref));
-      ProjectConfig config = projectConfigFactory.read(md);
-      AccessSection s = config.getAccessSection(ref, true);
-      Permission p = s.getPermission(permission, true);
-      PermissionRule rule = Util.newRule(config, groupUUID);
-      rule.setForce(force);
-      p.add(rule);
-      config.commit(md);
-      projectCache.evict(config.getProject());
-    }
-  }
-
-  protected void grantLabel(
-      String label,
-      int min,
-      int max,
-      Project.NameKey project,
-      String ref,
-      boolean force,
-      AccountGroup.UUID groupUUID,
-      boolean exclusive)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    String permission = Permission.LABEL + label;
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      md.setMessage(String.format("Grant %s on %s", permission, ref));
-      ProjectConfig config = projectConfigFactory.read(md);
-      AccessSection s = config.getAccessSection(ref, true);
-      Permission p = s.getPermission(permission, true);
-      p.setExclusiveGroup(exclusive);
-      PermissionRule rule = Util.newRule(config, groupUUID);
-      rule.setForce(force);
-      rule.setMin(min);
-      rule.setMax(max);
-      p.add(rule);
-      config.commit(md);
-      projectCache.evict(config.getProject());
-    }
-  }
-
-  protected void removePermission(Project.NameKey project, String ref, String permission)
-      throws IOException, ConfigInvalidException {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      md.setMessage(String.format("Remove %s on %s", permission, ref));
-      ProjectConfig config = projectConfigFactory.read(md);
-      AccessSection s = config.getAccessSection(ref, true);
-      Permission p = s.getPermission(permission, true);
-      p.clearRules();
-      config.commit(md);
-      projectCache.evict(config.getProject());
-    }
-  }
-
-  protected void blockRead(String ref) throws Exception {
-    block(ref, Permission.READ, REGISTERED_USERS);
-  }
-
   protected void blockAnonymousRead() throws Exception {
-    AccountGroup.UUID anonymous = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-    AccountGroup.UUID registered = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
     String allRefs = RefNames.REFS + "*";
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.block(u.getConfig(), Permission.READ, anonymous, allRefs);
-      Util.allow(u.getConfig(), Permission.READ, registered, allRefs);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(allRefs).group(ANONYMOUS_USERS))
+        .add(allow(Permission.READ).ref(allRefs).group(REGISTERED_USERS))
+        .update();
   }
 
   protected PushOneCommit.Result pushTo(String ref) throws Exception {
@@ -1113,11 +1028,11 @@
   }
 
   protected void approve(String id) throws Exception {
-    gApi.changes().id(id).revision("current").review(ReviewInput.approve());
+    gApi.changes().id(id).current().review(ReviewInput.approve());
   }
 
   protected void recommend(String id) throws Exception {
-    gApi.changes().id(id).revision("current").review(ReviewInput.recommend());
+    gApi.changes().id(id).current().review(ReviewInput.recommend());
   }
 
   protected void assertSubmittedTogether(String chId, String... expected) throws Exception {
@@ -1135,7 +1050,7 @@
   }
 
   protected PatchSet getPatchSet(PatchSet.Id psId) {
-    return changeDataFactory.create(project, psId.getParentKey()).patchSet(psId);
+    return changeDataFactory.create(project, psId.changeId()).patchSet(psId);
   }
 
   protected IdentifiedUser user(TestAccount testAccount) {
@@ -1155,7 +1070,7 @@
 
   protected RevisionResource parseRevisionResource(PushOneCommit.Result r) throws Exception {
     PatchSet.Id psId = r.getPatchSetId();
-    return parseRevisionResource(psId.getParentKey().toString(), psId.get());
+    return parseRevisionResource(psId.changeId().toString(), psId.get());
   }
 
   protected ChangeResource parseChangeResource(String changeId) throws Exception {
@@ -1171,22 +1086,13 @@
     }
   }
 
-  // TODO(hanwen): push this down.
-  protected RevCommit getRemoteHead(Project.NameKey project, String branch) throws Exception {
-    return projectOperations.project(project).getHead(branch);
-  }
-
-  protected RevCommit getRemoteHead() throws Exception {
-    return getRemoteHead(project, "master");
-  }
-
   protected void assertMailReplyTo(Message message, String email) throws Exception {
     assertThat(message.headers()).containsKey("Reply-To");
     EmailHeader.String replyTo = (EmailHeader.String) message.headers().get("Reply-To");
     assertThat(replyTo.getString()).contains(email);
   }
 
-  protected Map<Branch.NameKey, ObjectId> fetchFromSubmitPreview(String changeId) throws Exception {
+  protected Map<BranchNameKey, ObjectId> fetchFromSubmitPreview(String changeId) throws Exception {
     try (BinaryResult result = gApi.changes().id(changeId).current().submitPreview()) {
       return fetchFromBundles(result);
     }
@@ -1198,7 +1104,7 @@
    *
    * <p>Omits NoteDb meta refs.
    */
-  protected Map<Branch.NameKey, ObjectId> fetchFromBundles(BinaryResult bundles) throws Exception {
+  protected Map<BranchNameKey, ObjectId> fetchFromBundles(BinaryResult bundles) throws Exception {
     assertThat(bundles.getContentType()).isEqualTo("application/x-zip");
 
     FileSystem fs = Jimfs.newFileSystem();
@@ -1206,8 +1112,8 @@
     try (OutputStream out = Files.newOutputStream(previewPath)) {
       bundles.writeTo(out);
     }
-    Map<Branch.NameKey, ObjectId> ret = new HashMap<>();
-    try (FileSystem zipFs = FileSystems.newFileSystem(previewPath, null);
+    Map<BranchNameKey, ObjectId> ret = new HashMap<>();
+    try (FileSystem zipFs = FileSystems.newFileSystem(previewPath, (ClassLoader) null);
         DirectoryStream<Path> dirStream =
             Files.newDirectoryStream(Iterables.getOnlyElement(zipFs.getRootDirectories()))) {
       for (Path p : dirStream) {
@@ -1218,7 +1124,7 @@
         int len = bundleName.length();
         assertThat(bundleName).endsWith(".git");
         String repoName = bundleName.substring(0, len - 4);
-        Project.NameKey proj = new Project.NameKey(repoName);
+        Project.NameKey proj = Project.nameKey(repoName);
         TestRepository<?> localRepo = cloneProject(proj);
 
         try (InputStream bundleStream = Files.newInputStream(p);
@@ -1235,7 +1141,7 @@
               continue;
             }
             RevCommit c = localRepo.getRevWalk().parseCommit(r.getObjectId());
-            ret.put(new Branch.NameKey(proj, refName), c.getTree().copy());
+            ret.put(BranchNameKey.create(proj, refName), c.getTree().copy());
           }
         }
       }
@@ -1245,18 +1151,18 @@
   }
 
   /** Assert that the given branches have the given tree ids. */
-  protected void assertTrees(Project.NameKey proj, Map<Branch.NameKey, ObjectId> trees)
+  protected void assertTrees(Project.NameKey proj, Map<BranchNameKey, ObjectId> trees)
       throws Exception {
     TestRepository<?> localRepo = cloneProject(proj);
     GitUtil.fetch(localRepo, "refs/*:refs/*");
-    Map<Branch.NameKey, RevTree> refValues = new HashMap<>();
+    Map<BranchNameKey, RevTree> refValues = new HashMap<>();
 
-    for (Branch.NameKey b : trees.keySet()) {
-      if (!b.getParentKey().equals(proj)) {
+    for (BranchNameKey b : trees.keySet()) {
+      if (!b.project().equals(proj)) {
         continue;
       }
 
-      Ref r = localRepo.getRepository().exactRef(b.get());
+      Ref r = localRepo.getRepository().exactRef(b.branch());
       assertThat(r).isNotNull();
       RevWalk rw = localRepo.getRevWalk();
       RevCommit c = rw.parseCommit(r.getObjectId());
@@ -1360,7 +1266,7 @@
 
   protected InternalGroup group(AccountGroup.UUID groupUuid) {
     InternalGroup group = groupCache.get(groupUuid).orElse(null);
-    assertThat(group).named(groupUuid.get()).isNotNull();
+    assertWithMessage(groupUuid.get()).that(group).isNotNull();
     return group;
   }
 
@@ -1370,13 +1276,13 @@
   }
 
   protected InternalGroup group(String groupName) {
-    InternalGroup group = groupCache.get(new AccountGroup.NameKey(groupName)).orElse(null);
-    assertThat(group).named(groupName).isNotNull();
+    InternalGroup group = groupCache.get(AccountGroup.nameKey(groupName)).orElse(null);
+    assertWithMessage(groupName).that(group).isNotNull();
     return group;
   }
 
   protected GroupReference groupRef(String groupName) {
-    InternalGroup group = groupCache.get(new AccountGroup.NameKey(groupName)).orElse(null);
+    InternalGroup group = groupCache.get(AccountGroup.nameKey(groupName)).orElse(null);
     assertThat(group).isNotNull();
     return new GroupReference(group.getGroupUUID(), group.getName());
   }
@@ -1398,8 +1304,8 @@
   }
 
   protected void assertGroupDoesNotExist(String groupName) {
-    InternalGroup group = groupCache.get(new AccountGroup.NameKey(groupName)).orElse(null);
-    assertThat(group).named(groupName).isNull();
+    InternalGroup group = groupCache.get(AccountGroup.nameKey(groupName)).orElse(null);
+    assertWithMessage(groupName).that(group).isNull();
   }
 
   protected void assertNotifyTo(TestAccount expected) {
@@ -1551,7 +1457,7 @@
       LabelValue... value)
       throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType labelType = category(label, value);
+      LabelType labelType = label(label, value);
       labelType.setFunction(func);
       labelType.setRefPatterns(refPatterns);
       u.getConfig().getLabelSections().put(labelType.getName(), labelType);
@@ -1559,10 +1465,6 @@
     }
   }
 
-  protected void fail(@Nullable String format, Object... args) {
-    assert_().fail(format, args);
-  }
-
   protected void enableCreateNewChangeForAllNotInTarget() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig()
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index fa501e62..a372089 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -73,7 +73,11 @@
   }
 
   protected static FakeEmailSenderSubject assertThat(FakeEmailSender sender) {
-    return assertAbout(FakeEmailSenderSubject::new).that(sender);
+    return assertAbout(fakeEmailSenders()).that(sender);
+  }
+
+  protected static Subject.Factory<FakeEmailSenderSubject, FakeEmailSender> fakeEmailSenders() {
+    return FakeEmailSenderSubject::new;
   }
 
   protected void setEmailStrategy(TestAccount account, EmailStrategy strategy) throws Exception {
@@ -91,8 +95,8 @@
     gApi.accounts().self().setPreferences(prefs);
   }
 
-  protected static class FakeEmailSenderSubject
-      extends Subject<FakeEmailSenderSubject, FakeEmailSender> {
+  protected static class FakeEmailSenderSubject extends Subject {
+    private final FakeEmailSender fakeEmailSender;
     private Message message;
     private StagedUsers users;
     private Map<RecipientType, List<String>> recipients = new HashMap<>();
@@ -100,10 +104,11 @@
 
     FakeEmailSenderSubject(FailureMetadata failureMetadata, FakeEmailSender target) {
       super(failureMetadata, target);
+      fakeEmailSender = target;
     }
 
     public FakeEmailSenderSubject didNotSend() {
-      Message message = actual().peekMessage();
+      Message message = fakeEmailSender.peekMessage();
       if (message != null) {
         failWithoutActual(fact("expected no message", message));
       }
@@ -111,7 +116,7 @@
     }
 
     public FakeEmailSenderSubject sent(String messageType, StagedUsers users) {
-      message = actual().nextMessage();
+      message = fakeEmailSender.nextMessage();
       if (message == null) {
         failWithoutActual(fact("expected message", "not sent"));
       }
@@ -140,9 +145,7 @@
                     : header));
       }
 
-      // Return a named subject that displays a human-readable table of
-      // recipients.
-      return named(recipientMapToString(recipients, users::emailToName));
+      return this;
     }
 
     private static String recipientMapToString(
@@ -203,8 +206,9 @@
       if (recipients.get(type).contains(email) != expected) {
         failWithoutActual(
             fact(
-                expected ? "should notify" : "shouldn't notify",
-                type + ": " + users.emailToName(email)));
+                expected ? "expected to notify" : "expected not to notify",
+                type + ": " + users.emailToName(email)),
+            fact("but notified", recipientMapToString(recipients, users::emailToName)));
       }
       if (expected) {
         accountedFor.add(email);
@@ -429,7 +433,7 @@
               .reviewer(REVIEWER_BY_EMAIL)
               .reviewer(ccer.email(), ReviewerState.CC, false)
               .reviewer(CC_BY_EMAIL, ReviewerState.CC, false);
-      ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
+      ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
       supportReviewersByEmail = true;
       if (result.reviewers.values().stream().anyMatch(v -> v.error != null)) {
         supportReviewersByEmail = false;
@@ -437,7 +441,7 @@
             ReviewInput.noScore()
                 .reviewer(reviewer.email())
                 .reviewer(ccer.email(), ReviewerState.CC, false);
-        result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
+        result = gApi.changes().id(r.getChangeId()).current().review(in);
       }
       Truth.assertThat(result.reviewers.values().stream().allMatch(v -> v.error == null)).isTrue();
     }
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
index ccd30ab..020602b 100644
--- a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -22,11 +22,11 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
 import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.restapi.change.GetChange;
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index aeae2c2..75d0d2f 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -20,9 +20,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.GroupCache;
@@ -76,7 +76,7 @@
     if (account != null) {
       return account;
     }
-    Account.Id id = new Account.Id(sequences.nextAccountId());
+    Account.Id id = Account.id(sequences.nextAccountId());
 
     List<ExternalId> extIds = new ArrayList<>(2);
     String httpPass = null;
@@ -98,7 +98,7 @@
 
     if (groupNames != null) {
       for (String n : groupNames) {
-        AccountGroup.NameKey k = new AccountGroup.NameKey(n);
+        AccountGroup.NameKey k = AccountGroup.nameKey(n);
         Optional<InternalGroup> group = groupCache.get(k);
         if (!group.isPresent()) {
           throw new NoSuchGroupException(n);
diff --git a/java/com/google/gerrit/acceptance/AccountIndexedCounter.java b/java/com/google/gerrit/acceptance/AccountIndexedCounter.java
new file mode 100644
index 0000000..88b97c7
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/AccountIndexedCounter.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.acceptance;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.util.concurrent.AtomicLongMap;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.events.AccountIndexedListener;
+
+/** Checks if an account is indexed the correct number of times. */
+public class AccountIndexedCounter implements AccountIndexedListener {
+  private final AtomicLongMap<Integer> countsByAccount = AtomicLongMap.create();
+
+  @Override
+  public void onAccountIndexed(int id) {
+    countsByAccount.incrementAndGet(id);
+  }
+
+  public void clear() {
+    countsByAccount.clear();
+  }
+
+  public void assertReindexOf(TestAccount testAccount) {
+    assertReindexOf(testAccount, 1);
+  }
+
+  public void assertReindexOf(AccountInfo accountInfo) {
+    assertReindexOf(Account.id(accountInfo._accountId), 1);
+  }
+
+  public void assertReindexOf(TestAccount testAccount, long expectedCount) {
+    assertThat(countsByAccount.asMap()).containsExactly(testAccount.id().get(), expectedCount);
+    clear();
+  }
+
+  public void assertReindexOf(Account.Id accountId, long expectedCount) {
+    assertThat(countsByAccount.asMap()).containsEntry(accountId.get(), expectedCount);
+    countsByAccount.remove(accountId.get());
+  }
+
+  public void assertNoReindex() {
+    assertThat(countsByAccount.asMap()).isEmpty();
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 25d8df1..646d8f0 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -2,6 +2,82 @@
 load("//tools/bzl:java.bzl", "java_library2")
 load("//tools/bzl:javadoc.bzl", "java_doc")
 
+FUNCTION_SRCS = [
+    "testsuite/ThrowingConsumer.java",
+    "testsuite/ThrowingFunction.java",
+]
+
+DEPLOY_ENV = [
+    "//java/com/google/gerrit/exceptions",
+    "//java/com/google/gerrit/gpg",
+    "//java/com/google/gerrit/git",
+    "//java/com/google/gerrit/index:query_exception",
+    "//java/com/google/gerrit/launcher",
+    "//java/com/google/gerrit/lifecycle",
+    "//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/httpd",
+    "//java/com/google/gerrit/index",
+    "//java/com/google/gerrit/index/project",
+    "//java/com/google/gerrit/json",
+    "//java/com/google/gerrit/lucene",
+    "//java/com/google/gerrit/mail",
+    "//java/com/google/gerrit/metrics",
+    "//java/com/google/gerrit/server",
+    "//java/com/google/gerrit/server/audit",
+    "//java/com/google/gerrit/server/git/receive",
+    "//java/com/google/gerrit/server/logging",
+    "//java/com/google/gerrit/server/restapi",
+    "//java/com/google/gerrit/server/schema",
+    "//java/com/google/gerrit/server/util/git",
+    "//java/com/google/gerrit/server/util/time",
+    "//java/com/google/gerrit/sshd",
+    "//lib/auto:auto-value",
+    "//lib/auto:auto-value-annotations",
+    "//lib:args4j",
+    "//lib:gson",
+    "//lib:guava-retrying",
+    "//lib:jgit",
+    "//lib:jsch",
+    "//lib/commons:compress",
+    "//lib/commons:lang",
+    "//lib/flogger:api",
+    "//lib/guice",
+    "//lib/guice:guice-assistedinject",
+    "//lib/guice:guice-servlet",
+    "//lib/mail",
+    "//lib/mina:sshd",
+    "//lib:guava",
+    "//lib/bouncycastle:bcpg",
+    "//lib/bouncycastle:bcprov",
+    "//prolog:gerrit-prolog-common",
+]
+
+TEST_DEPS = [
+    "//java/com/google/gerrit/httpd/auth/openid",
+    "//java/com/google/gerrit/pgm",
+    "//java/com/google/gerrit/pgm/http/jetty",
+    "//java/com/google/gerrit/pgm/util",
+    "//java/com/google/gerrit/truth",
+    "//java/com/google/gerrit/acceptance/testsuite/project",
+    "//java/com/google/gerrit/server/group/testing",
+    "//java/com/google/gerrit/server/project/testing:project-test-util",
+    "//java/com/google/gerrit/testing:gerrit-test-util",
+    "//java/com/google/gerrit/extensions/common/testing:common-test-util",
+    "//java/com/google/gerrit/extensions/restapi/testing:restapi-test-util",
+    "//java/com/google/gerrit/gpg/testing:gpg-test-util",
+    "//java/com/google/gerrit/git/testing",
+]
+
+PGM_DEPLOY_ENV = [
+    "//lib:caffeine",
+    "//lib:caffeine-guava",
+    "//lib/jackson:jackson-core",
+    "//lib/prolog:cafeteria",
+]
+
 java_library(
     name = "lib",
     testonly = True,
@@ -10,125 +86,53 @@
     visibility = ["//visibility:public"],
     exports = [
         ":framework-lib",
-        "//java/com/google/gerrit/common:annotations",
-        "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/extensions/common/testing:common-test-util",
-        "//java/com/google/gerrit/extensions/restapi/testing:restapi-test-util",
-        "//java/com/google/gerrit/git/testing",
-        "//java/com/google/gerrit/gpg/testing:gpg-test-util",
-        "//java/com/google/gerrit/httpd",
-        "//java/com/google/gerrit/index",
-        "//java/com/google/gerrit/json",
-        "//java/com/google/gerrit/launcher",
-        "//java/com/google/gerrit/lucene",
-        "//java/com/google/gerrit/mail",
-        "//java/com/google/gerrit/metrics",
-        "//java/com/google/gerrit/pgm",
-        "//java/com/google/gerrit/pgm/init",
-        "//java/com/google/gerrit/pgm/util",
-        "//java/com/google/gerrit/reviewdb:server",
-        "//java/com/google/gerrit/server",
-        "//java/com/google/gerrit/server/audit",
-        "//java/com/google/gerrit/server/git/receive",
-        "//java/com/google/gerrit/server/project/testing:project-test-util",
-        "//java/com/google/gerrit/server/restapi",
-        "//java/com/google/gerrit/sshd",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//lib:args4j",
-        "//lib:gson",
-        "//lib:guava-retrying",
-        "//lib:h2",
-        "//lib:jimfs",
-        "//lib:jsch",
-        "//lib:servlet-api-without-neverlink",
-        "//lib/bouncycastle:bcpg",
-        "//lib/bouncycastle:bcprov",
-        "//lib/commons:compress",
-        "//lib/flogger:api",
-        "//lib/guice",
-        "//lib/guice:guice-assistedinject",
-        "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/mina:sshd",
-        "//prolog:gerrit-prolog-common",
-    ],
+    ] + DEPLOY_ENV + TEST_DEPS,
 )
 
 java_binary(
     name = "framework",
     testonly = True,
+    deploy_env = [":framework-deploy-env"],
     main_class = "Dummy",
     visibility = ["//visibility:public"],
     runtime_deps = [":framework-lib"],
 )
 
+java_binary(
+    name = "framework-deploy-env",
+    testonly = True,
+    main_class = "Dummy",
+    runtime_deps = DEPLOY_ENV + PGM_DEPLOY_ENV,
+)
+
 java_library2(
     name = "framework-lib",
     testonly = True,
-    srcs = glob(["**/*.java"]),
+    srcs = glob(
+        ["**/*.java"],
+        exclude = FUNCTION_SRCS,
+    ),
     exported_deps = [
-        "//java/com/google/gerrit/exceptions",
-        "//java/com/google/gerrit/gpg",
-        "//java/com/google/gerrit/httpd/auth/openid",
-        "//java/com/google/gerrit/index:query_exception",
-        "//java/com/google/gerrit/launcher",
-        "//java/com/google/gerrit/lifecycle",
-        "//java/com/google/gerrit/pgm:daemon",
-        "//java/com/google/gerrit/pgm/http/jetty",
-        "//java/com/google/gerrit/pgm/util",
-        "//java/com/google/gerrit/server/group/testing",
-        "//java/com/google/gerrit/server/project/testing:project-test-util",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//lib:guava",
+        ":function",
+        "//lib:jgit-junit",
         "//lib:jimfs",
-        "//lib/auto:auto-value",
-        "//lib/auto:auto-value-annotations",
-        "//lib/flogger:api",
+        "//lib:servlet-api",
         "//lib/httpcomponents:fluent-hc",
         "//lib/httpcomponents:httpclient",
         "//lib/httpcomponents:httpcore",
-        "//lib/jetty:servlet",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/mockito",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
-        "//prolog:gerrit-prolog-common",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        "//java/com/google/gerrit/common:annotations",
-        "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/httpd",
-        "//java/com/google/gerrit/index",
-        "//java/com/google/gerrit/index/project",
-        "//java/com/google/gerrit/json",
-        "//java/com/google/gerrit/lucene",
-        "//java/com/google/gerrit/mail",
-        "//java/com/google/gerrit/metrics",
-        "//java/com/google/gerrit/reviewdb:server",
-        "//java/com/google/gerrit/server",
-        "//java/com/google/gerrit/server/audit",
-        "//java/com/google/gerrit/server/git/receive",
-        "//java/com/google/gerrit/server/restapi",
-        "//java/com/google/gerrit/server/schema",
-        "//java/com/google/gerrit/server/util/time",
-        "//java/com/google/gerrit/sshd",
-        "//lib:args4j",
-        "//lib:gson",
-        "//lib:guava-retrying",
-        "//lib:jsch",
-        "//lib:servlet-api",
-        "//lib/commons:lang",
         "//lib/greenmail",
-        "//lib/guice",
-        "//lib/guice:guice-assistedinject",
-        "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/mail",
-        "//lib/mina:sshd",
-    ],
+    ] + TEST_DEPS,
+    visibility = ["//visibility:public"],
+    deps = DEPLOY_ENV,
+)
+
+java_library(
+    name = "function",
+    srcs = FUNCTION_SRCS,
+    visibility = ["//visibility:public"],
 )
 
 java_doc(
diff --git a/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java b/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
index 27ed603..1ff7d0e 100644
--- a/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
+++ b/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
@@ -45,9 +45,8 @@
     assertReindexOf(info, 1);
   }
 
-  public void assertReindexOf(ChangeInfo info, int expectedCount) {
-    assertThat(getCount(info)).isEqualTo(expectedCount);
-    assertThat(countsByChange).hasSize(1);
+  public void assertReindexOf(ChangeInfo info, long expectedCount) {
+    assertThat(countsByChange.asMap()).containsExactly(info._number, expectedCount);
     clear();
   }
 }
diff --git a/java/com/google/gerrit/acceptance/DisabledAccountIndex.java b/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
index 91baafb..271d15c 100644
--- a/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.account.AccountIndex;
 
diff --git a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
index a32c6d1..34f72f5c 100644
--- a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.query.change.ChangeData;
 import java.util.Optional;
diff --git a/java/com/google/gerrit/acceptance/DisabledProjectIndex.java b/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
index 2524a76..ed119ff 100644
--- a/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectIndex;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Project;
 
 /**
  * This class wraps an index and assumes the search index can't handle any queries. However, it does
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
new file mode 100644
index 0000000..eaf03b3
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -0,0 +1,253 @@
+// 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;
+
+import com.google.gerrit.extensions.api.changes.ActionVisitor;
+import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.events.AccountActivationListener;
+import com.google.gerrit.extensions.events.AccountIndexedListener;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
+import com.google.gerrit.extensions.events.CommentAddedListener;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.GroupIndexedListener;
+import com.google.gerrit.extensions.events.ProjectIndexedListener;
+import com.google.gerrit.extensions.events.RevisionCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.webui.FileHistoryWebLink;
+import com.google.gerrit.extensions.webui.PatchSetWebLink;
+import com.google.gerrit.server.ExceptionHook;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.change.ChangeETagComputation;
+import com.google.gerrit.server.git.ChangeMessageModifier;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
+import com.google.gerrit.server.git.validators.RefOperationValidationListener;
+import com.google.gerrit.server.logging.PerformanceLogger;
+import com.google.gerrit.server.rules.SubmitRule;
+import com.google.gerrit.server.validators.AccountActivationValidationListener;
+import com.google.gerrit.server.validators.ProjectCreationValidationListener;
+import com.google.inject.Inject;
+import com.google.inject.util.Providers;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ExtensionRegistry {
+  private final DynamicSet<AccountIndexedListener> accountIndexedListeners;
+  private final DynamicSet<ChangeIndexedListener> changeIndexedListeners;
+  private final DynamicSet<GroupIndexedListener> groupIndexedListeners;
+  private final DynamicSet<ProjectIndexedListener> projectIndexedListeners;
+  private final DynamicSet<CommitValidationListener> commitValidationListeners;
+  private final DynamicSet<ExceptionHook> exceptionHooks;
+  private final DynamicSet<PerformanceLogger> performanceLoggers;
+  private final DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
+  private final DynamicSet<SubmitRule> submitRules;
+  private final DynamicSet<ChangeMessageModifier> changeMessageModifiers;
+  private final DynamicSet<ChangeETagComputation> changeETagComputations;
+  private final DynamicSet<ActionVisitor> actionVisitors;
+  private final DynamicMap<DownloadScheme> downloadSchemes;
+  private final DynamicSet<RefOperationValidationListener> refOperationValidationListeners;
+  private final DynamicSet<CommentAddedListener> commentAddedListeners;
+  private final DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners;
+  private final DynamicSet<FileHistoryWebLink> fileHistoryWebLinks;
+  private final DynamicSet<PatchSetWebLink> patchSetWebLinks;
+  private final DynamicSet<RevisionCreatedListener> revisionCreatedListeners;
+  private final DynamicSet<GroupBackend> groupBackends;
+  private final DynamicSet<AccountActivationValidationListener>
+      accountActivationValidationListeners;
+  private final DynamicSet<AccountActivationListener> accountActivationListeners;
+  private final DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
+
+  @Inject
+  ExtensionRegistry(
+      DynamicSet<AccountIndexedListener> accountIndexedListeners,
+      DynamicSet<ChangeIndexedListener> changeIndexedListeners,
+      DynamicSet<GroupIndexedListener> groupIndexedListeners,
+      DynamicSet<ProjectIndexedListener> projectIndexedListeners,
+      DynamicSet<CommitValidationListener> commitValidationListeners,
+      DynamicSet<ExceptionHook> exceptionHooks,
+      DynamicSet<PerformanceLogger> performanceLoggers,
+      DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners,
+      DynamicSet<SubmitRule> submitRules,
+      DynamicSet<ChangeMessageModifier> changeMessageModifiers,
+      DynamicSet<ChangeETagComputation> changeETagComputations,
+      DynamicSet<ActionVisitor> actionVisitors,
+      DynamicMap<DownloadScheme> downloadSchemes,
+      DynamicSet<RefOperationValidationListener> refOperationValidationListeners,
+      DynamicSet<CommentAddedListener> commentAddedListeners,
+      DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners,
+      DynamicSet<FileHistoryWebLink> fileHistoryWebLinks,
+      DynamicSet<PatchSetWebLink> patchSetWebLinks,
+      DynamicSet<RevisionCreatedListener> revisionCreatedListeners,
+      DynamicSet<GroupBackend> groupBackends,
+      DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners,
+      DynamicSet<AccountActivationListener> accountActivationListeners,
+      DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners) {
+    this.accountIndexedListeners = accountIndexedListeners;
+    this.changeIndexedListeners = changeIndexedListeners;
+    this.groupIndexedListeners = groupIndexedListeners;
+    this.projectIndexedListeners = projectIndexedListeners;
+    this.commitValidationListeners = commitValidationListeners;
+    this.exceptionHooks = exceptionHooks;
+    this.performanceLoggers = performanceLoggers;
+    this.projectCreationValidationListeners = projectCreationValidationListeners;
+    this.submitRules = submitRules;
+    this.changeMessageModifiers = changeMessageModifiers;
+    this.changeETagComputations = changeETagComputations;
+    this.actionVisitors = actionVisitors;
+    this.downloadSchemes = downloadSchemes;
+    this.refOperationValidationListeners = refOperationValidationListeners;
+    this.commentAddedListeners = commentAddedListeners;
+    this.refUpdatedListeners = refUpdatedListeners;
+    this.fileHistoryWebLinks = fileHistoryWebLinks;
+    this.patchSetWebLinks = patchSetWebLinks;
+    this.revisionCreatedListeners = revisionCreatedListeners;
+    this.groupBackends = groupBackends;
+    this.accountActivationValidationListeners = accountActivationValidationListeners;
+    this.accountActivationListeners = accountActivationListeners;
+    this.onSubmitValidationListeners = onSubmitValidationListeners;
+  }
+
+  public Registration newRegistration() {
+    return new Registration();
+  }
+
+  @SuppressWarnings("FunctionalInterfaceClash")
+  public class Registration implements AutoCloseable {
+    private final List<RegistrationHandle> registrationHandles = new ArrayList<>();
+
+    public Registration add(AccountIndexedListener accountIndexedListener) {
+      return add(accountIndexedListeners, accountIndexedListener);
+    }
+
+    public Registration add(ChangeIndexedListener changeIndexedListener) {
+      return add(changeIndexedListeners, changeIndexedListener);
+    }
+
+    public Registration add(GroupIndexedListener groupIndexedListener) {
+      return add(groupIndexedListeners, groupIndexedListener);
+    }
+
+    public Registration add(ProjectIndexedListener projectIndexedListener) {
+      return add(projectIndexedListeners, projectIndexedListener);
+    }
+
+    public Registration add(CommitValidationListener commitValidationListener) {
+      return add(commitValidationListeners, commitValidationListener);
+    }
+
+    public Registration add(ExceptionHook exceptionHook) {
+      return add(exceptionHooks, exceptionHook);
+    }
+
+    public Registration add(PerformanceLogger performanceLogger) {
+      return add(performanceLoggers, performanceLogger);
+    }
+
+    public Registration add(ProjectCreationValidationListener projectCreationListener) {
+      return add(projectCreationValidationListeners, projectCreationListener);
+    }
+
+    public Registration add(SubmitRule submitRule) {
+      return add(submitRules, submitRule);
+    }
+
+    public Registration add(ChangeMessageModifier changeMessageModifier) {
+      return add(changeMessageModifiers, changeMessageModifier);
+    }
+
+    public Registration add(ChangeMessageModifier changeMessageModifier, String exportName) {
+      return add(changeMessageModifiers, changeMessageModifier, exportName);
+    }
+
+    public Registration add(ChangeETagComputation changeETagComputation) {
+      return add(changeETagComputations, changeETagComputation);
+    }
+
+    public Registration add(ActionVisitor actionVisitor) {
+      return add(actionVisitors, actionVisitor);
+    }
+
+    public Registration add(DownloadScheme downloadScheme, String exportName) {
+      return add(downloadSchemes, downloadScheme, exportName);
+    }
+
+    public Registration add(RefOperationValidationListener refOperationValidationListener) {
+      return add(refOperationValidationListeners, refOperationValidationListener);
+    }
+
+    public Registration add(CommentAddedListener commentAddedListener) {
+      return add(commentAddedListeners, commentAddedListener);
+    }
+
+    public Registration add(GitReferenceUpdatedListener refUpdatedListener) {
+      return add(refUpdatedListeners, refUpdatedListener);
+    }
+
+    public Registration add(FileHistoryWebLink fileHistoryWebLink) {
+      return add(fileHistoryWebLinks, fileHistoryWebLink);
+    }
+
+    public Registration add(PatchSetWebLink patchSetWebLink) {
+      return add(patchSetWebLinks, patchSetWebLink);
+    }
+
+    public Registration add(RevisionCreatedListener revisionCreatedListener) {
+      return add(revisionCreatedListeners, revisionCreatedListener);
+    }
+
+    public Registration add(GroupBackend groupBackend) {
+      return add(groupBackends, groupBackend);
+    }
+
+    public Registration add(
+        AccountActivationValidationListener accountActivationValidationListener) {
+      return add(accountActivationValidationListeners, accountActivationValidationListener);
+    }
+
+    public Registration add(AccountActivationListener accountDeactivatedListener) {
+      return add(accountActivationListeners, accountDeactivatedListener);
+    }
+
+    public Registration add(OnSubmitValidationListener onSubmitValidationListener) {
+      return add(onSubmitValidationListeners, onSubmitValidationListener);
+    }
+
+    private <T> Registration add(DynamicSet<T> dynamicSet, T extension) {
+      return add(dynamicSet, extension, "gerrit");
+    }
+
+    private <T> Registration add(DynamicSet<T> dynamicSet, T extension, String exportname) {
+      RegistrationHandle registrationHandle = dynamicSet.add(exportname, extension);
+      registrationHandles.add(registrationHandle);
+      return this;
+    }
+
+    private <T> Registration add(DynamicMap<T> dynamicMap, T extension, String exportName) {
+      RegistrationHandle registrationHandle =
+          ((PrivateInternals_DynamicMapImpl<T>) dynamicMap)
+              .put("myPlugin", exportName, Providers.of(extension));
+      registrationHandles.add(registrationHandle);
+      return this;
+    }
+
+    @Override
+    public void close() {
+      registrationHandles.forEach(h -> h.remove());
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/GcAssert.java b/java/com/google/gerrit/acceptance/GcAssert.java
index b9ef629..bef3323 100644
--- a/java/com/google/gerrit/acceptance/GcAssert.java
+++ b/java/com/google/gerrit/acceptance/GcAssert.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import java.io.File;
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 63ea4e3..805afc2 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.server.index.AutoFlush;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
 import com.google.gerrit.server.ssh.NoSshModule;
+import com.google.gerrit.server.util.ReplicaUtil;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.testing.FakeEmailSender;
@@ -105,6 +106,9 @@
           has(Sandboxed.class, testDesc.getTestClass()),
           has(SkipProjectClone.class, testDesc.getTestClass()),
           has(UseSsh.class, testDesc.getTestClass()),
+          false, // @UseSystemTime is only valid on methods.
+          get(UseClockStep.class, testDesc.getTestClass()),
+          get(UseTimezone.class, testDesc.getTestClass()),
           null, // @GerritConfig is only valid on methods.
           null, // @GerritConfigs is only valid on methods.
           null, // @GlobalPluginConfig is only valid on methods.
@@ -113,6 +117,15 @@
 
     public static Description forTestMethod(
         org.junit.runner.Description testDesc, String configName) {
+      UseClockStep useClockStep = testDesc.getAnnotation(UseClockStep.class);
+      if (testDesc.getAnnotation(UseSystemTime.class) == null && useClockStep == null) {
+        // Only read the UseClockStep from the class if on method level neither @UseSystemTime nor
+        // @UseClockStep have been used.
+        // If the method defines @UseSystemTime or @UseClockStep it should overwrite @UseClockStep
+        // on class level.
+        useClockStep = get(UseClockStep.class, testDesc.getTestClass());
+      }
+
       return new AutoValue_GerritServer_Description(
           testDesc,
           configName,
@@ -127,6 +140,11 @@
               || has(SkipProjectClone.class, testDesc.getTestClass()),
           testDesc.getAnnotation(UseSsh.class) != null
               || has(UseSsh.class, testDesc.getTestClass()),
+          testDesc.getAnnotation(UseSystemTime.class) != null,
+          useClockStep,
+          testDesc.getAnnotation(UseTimezone.class) != null
+              ? testDesc.getAnnotation(UseTimezone.class)
+              : get(UseTimezone.class, testDesc.getTestClass()),
           testDesc.getAnnotation(GerritConfig.class),
           testDesc.getAnnotation(GerritConfigs.class),
           testDesc.getAnnotation(GlobalPluginConfig.class),
@@ -142,6 +160,16 @@
       return false;
     }
 
+    @Nullable
+    private static <T extends Annotation> T get(Class<T> annotation, Class<?> clazz) {
+      for (; clazz != null; clazz = clazz.getSuperclass()) {
+        if (clazz.getAnnotation(annotation) != null) {
+          return clazz.getAnnotation(annotation);
+        }
+      }
+      return null;
+    }
+
     abstract org.junit.runner.Description testDescription();
 
     @Nullable
@@ -161,6 +189,14 @@
       return useSshAnnotation() && SshMode.useSsh();
     }
 
+    abstract boolean useSystemTime();
+
+    @Nullable
+    abstract UseClockStep useClockStep();
+
+    @Nullable
+    abstract UseTimezone useTimezone();
+
     @Nullable
     abstract GerritConfig config();
 
@@ -174,12 +210,15 @@
     abstract GlobalPluginConfigs pluginConfigs();
 
     private void checkValidAnnotations() {
+      if (useClockStep() != null && useSystemTime()) {
+        throw new IllegalStateException("Use either @UseClockStep or @UseSystemTime, not both");
+      }
       if (configs() != null && config() != null) {
-        throw new IllegalStateException("Use either @GerritConfigs or @GerritConfig not both");
+        throw new IllegalStateException("Use either @GerritConfigs or @GerritConfig, not both");
       }
       if (pluginConfigs() != null && pluginConfig() != null) {
         throw new IllegalStateException(
-            "Use either @GlobalPluginConfig or @GlobalPluginConfigs not both");
+            "Use either @GlobalPluginConfig or @GlobalPluginConfigs, not both");
       }
       if ((pluginConfigs() != null || pluginConfig() != null) && memory()) {
         throw new IllegalStateException("Must use @UseLocalDisk with @GlobalPluginConfig(s)");
@@ -362,7 +401,7 @@
       @Nullable InMemoryRepositoryManager inMemoryRepoManager)
       throws Exception {
     Config cfg = desc.buildConfig(baseConfig);
-    daemon.setSlave(isSlave(baseConfig) || cfg.getBoolean("container", "slave", false));
+    daemon.setReplica(ReplicaUtil.isReplica(baseConfig) || ReplicaUtil.isReplica(cfg));
     mergeTestConfig(cfg);
     // Set the log4j configuration to an invalid one to prevent system logs
     // from getting configured and creating log files.
@@ -376,7 +415,8 @@
         "accountPatchReviewDb", null, "url", JdbcAccountPatchReviewStore.TEST_IN_MEMORY_URL);
     daemon.setEnableHttpd(desc.httpd());
     daemon.setLuceneModule(
-        LuceneIndexModule.singleVersionAllLatest(0, isSlave(baseConfig), AutoFlush.ENABLED));
+        LuceneIndexModule.singleVersionAllLatest(
+            0, ReplicaUtil.isReplica(baseConfig), AutoFlush.ENABLED));
     daemon.setDatabaseForTesting(
         ImmutableList.of(
             new InMemoryTestingDatabaseModule(cfg, site, inMemoryRepoManager),
@@ -392,10 +432,6 @@
     return new GerritServer(desc, null, createTestInjector(daemon), daemon, null);
   }
 
-  private static boolean isSlave(Config baseConfig) {
-    return baseConfig.getBoolean("container", "slave", false);
-  }
-
   private static GerritServer startOnDisk(
       Description desc,
       Path site,
@@ -436,9 +472,13 @@
   private static void mergeTestConfig(Config cfg) {
     String forceEphemeralPort = String.format("%s:0", getLocalHost().getHostName());
     String url = "http://" + forceEphemeralPort + "/";
-    cfg.setString("gerrit", null, "canonicalWebUrl", url);
-    cfg.setString("httpd", null, "listenUrl", url);
 
+    if (cfg.getString("gerrit", null, "canonicalWebUrl") == null) {
+      cfg.setString("gerrit", null, "canonicalWebUrl", url);
+    }
+    if (cfg.getString("httpd", null, "listenUrl") == null) {
+      cfg.setString("httpd", null, "listenUrl", url);
+    }
     if (cfg.getString("sshd", null, "listenAddress") == null) {
       cfg.setString("sshd", null, "listenAddress", forceEphemeralPort);
     }
@@ -454,11 +494,13 @@
     cfg.setInt("sshd", null, "commandStartThreads", 1);
     cfg.setInt("receive", null, "threadPoolSize", 1);
     cfg.setInt("index", null, "threads", 1);
-    cfg.setBoolean("index", null, "reindexAfterRefUpdate", false);
+    if (cfg.getString("index", null, "reindexAfterRefUpdate") == null) {
+      cfg.setBoolean("index", null, "reindexAfterRefUpdate", false);
+    }
   }
 
   private static Injector createTestInjector(Daemon daemon) throws Exception {
-    Injector sysInjector = get(daemon, "sysInjector");
+    Injector sysInjector = getInjector(daemon, "sysInjector");
     Module module =
         new FactoryModule() {
           @Override
@@ -491,13 +533,14 @@
     return sysInjector.createChildInjector(module);
   }
 
-  @SuppressWarnings("unchecked")
-  private static <T> T get(Object obj, String field)
+  private static Injector getInjector(Object obj, String field)
       throws SecurityException, NoSuchFieldException, IllegalArgumentException,
           IllegalAccessException {
     Field f = obj.getClass().getDeclaredField(field);
     f.setAccessible(true);
-    return (T) f.get(obj);
+    Object v = f.get(obj);
+    checkArgument(v instanceof Injector, "not an Injector: %s", v);
+    return (Injector) f.get(obj);
   }
 
   private static InetAddress getLocalHost() {
@@ -557,7 +600,7 @@
     Path site = server.testInjector.getInstance(Key.get(Path.class, SitePath.class));
 
     Config cfg = server.testInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-    cfg.setBoolean("container", null, "slave", true);
+    cfg.setBoolean("container", null, "replica", true);
 
     InMemoryRepositoryManager inMemoryRepoManager = null;
     if (hasBinding(server.testInjector, InMemoryRepositoryManager.class)) {
diff --git a/java/com/google/gerrit/acceptance/GitClientVersion.java b/java/com/google/gerrit/acceptance/GitClientVersion.java
new file mode 100644
index 0000000..4c9a32d
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/GitClientVersion.java
@@ -0,0 +1,66 @@
+// 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;
+
+import static java.util.stream.Collectors.joining;
+
+import java.util.stream.IntStream;
+
+/** Class to parse and represent version of git-core client */
+public class GitClientVersion implements Comparable<GitClientVersion> {
+  private final int v[];
+
+  /**
+   * Constructor to represent instance for minimum supported git-core version
+   *
+   * @param parts version passed as single digits
+   */
+  public GitClientVersion(int... parts) {
+    this.v = parts;
+  }
+
+  /**
+   * Parse the git-core version as returned by git version command
+   *
+   * @param version String returned by git version command
+   */
+  public GitClientVersion(String version) {
+    // "git version x.y.z", at Google "git version x.y.z.gXXXXXXXXXX-goog"
+    String parts[] = version.split(" ")[2].split("\\.");
+    int numParts = Math.min(parts.length, 3); // ignore Google-specific part of the version
+    v = new int[numParts];
+    for (int i = 0; i < numParts; i++) {
+      v[i] = Integer.valueOf(parts[i]);
+    }
+  }
+
+  @Override
+  public int compareTo(GitClientVersion o) {
+    int m = Math.max(v.length, o.v.length);
+    for (int i = 0; i < m; i++) {
+      int l = i < v.length ? v[i] : 0;
+      int r = i < o.v.length ? o.v[i] : 0;
+      if (l != r) {
+        return l < r ? -1 : 1;
+      }
+    }
+    return 0;
+  }
+
+  @Override
+  public String toString() {
+    return IntStream.of(v).mapToObj(String::valueOf).collect(joining("."));
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/GitUtil.java b/java/com/google/gerrit/acceptance/GitUtil.java
index cdfdae7..ae72793 100644
--- a/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/java/com/google/gerrit/acceptance/GitUtil.java
@@ -15,13 +15,14 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.google.gerrit.common.FooterConstants;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.JSchException;
 import com.jcraft.jsch.KeyPair;
@@ -109,19 +110,14 @@
       throws Exception {
     DfsRepositoryDescription desc = new DfsRepositoryDescription("clone of " + project.get());
 
-    FS fs = FS.detect();
-
-    // Avoid leaking user state into our tests.
-    fs.setUserHome(null);
-
-    InMemoryRepository dest =
-        new InMemoryRepository.Builder()
-            .setRepositoryDescription(desc)
-            // SshTransport depends on a real FS to read ~/.ssh/config, but
-            // InMemoryRepository by default uses a null FS.
-            // TODO(dborowitz): Remove when we no longer depend on SSH.
-            .setFS(fs)
-            .build();
+    InMemoryRepository.Builder b = new InMemoryRepository.Builder().setRepositoryDescription(desc);
+    if (uri.startsWith("ssh://")) {
+      // SshTransport depends on a real FS to read ~/.ssh/config, but InMemoryRepository by default
+      // uses a null FS.
+      // Avoid leaking user state into our tests.
+      b.setFS(FS.detect().setUserHome(null));
+    }
+    InMemoryRepository dest = b.build();
     Config cfg = dest.getConfig();
     cfg.setString("remote", "origin", "url", uri);
     cfg.setString("remote", "origin", "fetch", "+refs/heads/*:refs/remotes/origin/*");
@@ -134,11 +130,6 @@
     return testRepo;
   }
 
-  public static TestRepository<InMemoryRepository> cloneProject(
-      Project.NameKey project, SshSession sshSession) throws Exception {
-    return cloneProject(project, sshSession.getUrl() + "/" + project.get());
-  }
-
   public static Ref createAnnotatedTag(TestRepository<?> testRepo, String name, PersonIdent tagger)
       throws GitAPIException {
     TagCommand cmd =
@@ -209,13 +200,13 @@
 
   public static void assertPushOk(PushResult result, String ref) {
     RemoteRefUpdate rru = result.getRemoteUpdate(ref);
-    assertThat(rru.getStatus()).named(rru.toString()).isEqualTo(RemoteRefUpdate.Status.OK);
+    assertWithMessage(rru.toString()).that(rru.getStatus()).isEqualTo(RemoteRefUpdate.Status.OK);
   }
 
   public static void assertPushRejected(PushResult result, String ref, String expectedMessage) {
     RemoteRefUpdate rru = result.getRemoteUpdate(ref);
-    assertThat(rru.getStatus())
-        .named(rru.toString())
+    assertWithMessage(rru.toString())
+        .that(rru.getStatus())
         .isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
     assertThat(rru.getMessage()).isEqualTo(expectedMessage);
   }
diff --git a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index a704d2f..a3207e2 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -55,8 +55,6 @@
   @Override
   protected void configure() {
     bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
-
-    // TODO(dborowitz): Use jimfs.
     bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
 
     if (repoManager != null) {
diff --git a/java/com/google/gerrit/acceptance/InProcessProtocol.java b/java/com/google/gerrit/acceptance/InProcessProtocol.java
index 83ed202..75e5a2e 100644
--- a/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -21,24 +21,24 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.InProcessProtocol.Context;
 import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.RequestCleanup;
 import com.google.gerrit.server.config.GerritRequestModule;
-import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
+import com.google.gerrit.server.git.PermissionAwareRepositoryManager;
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.UploadPackInitializer;
+import com.google.gerrit.server.git.UsersSelfAdvertiseRefsHook;
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -90,8 +90,6 @@
       @Provides
       @RemotePeer
       SocketAddress getSocketAddress() {
-        // TODO(dborowitz): Could potentially fake this with thread ID or
-        // something.
         throw new OutOfScopeException("No remote peer in acceptance tests");
       }
     };
@@ -203,6 +201,7 @@
     private final ThreadLocalRequestContext threadContext;
     private final ProjectCache projectCache;
     private final PermissionBackend permissionBackend;
+    private final UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook;
 
     @Inject
     Upload(
@@ -212,7 +211,8 @@
         UploadValidators.Factory uploadValidatorsFactory,
         ThreadLocalRequestContext threadContext,
         ProjectCache projectCache,
-        PermissionBackend permissionBackend) {
+        PermissionBackend permissionBackend,
+        UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook) {
       this.transferConfig = transferConfig;
       this.uploadPackInitializers = uploadPackInitializers;
       this.preUploadHooks = preUploadHooks;
@@ -220,6 +220,7 @@
       this.threadContext = threadContext;
       this.projectCache = projectCache;
       this.permissionBackend = permissionBackend;
+      this.usersSelfAdvertiseRefsHook = usersSelfAdvertiseRefsHook;
     }
 
     @Override
@@ -249,12 +250,17 @@
       if (projectState == null) {
         throw new RuntimeException("can't load project state for " + req.project.get());
       }
-      UploadPack up = new UploadPack(repo);
+      Repository permissionAwareRepository = PermissionAwareRepositoryManager.wrap(repo, perm);
+      UploadPack up = new UploadPack(permissionAwareRepository);
       up.setPackConfig(transferConfig.getPackConfig());
       up.setTimeout(transferConfig.getTimeout());
-      up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
+      if (projectState.isAllUsers()) {
+        up.setAdvertiseRefsHook(usersSelfAdvertiseRefsHook);
+      }
       List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks);
-      hooks.add(uploadValidatorsFactory.create(projectState.getProject(), repo, "localhost-test"));
+      hooks.add(
+          uploadValidatorsFactory.create(
+              projectState.getProject(), permissionAwareRepository, "localhost-test"));
       up.setPreUploadHook(PreUploadHookChain.newChain(hooks));
       uploadPackInitializers.runEach(initializer -> initializer.init(req.project, up));
       return up;
@@ -343,7 +349,7 @@
             ImmutableList.<PostReceiveHook>builder()
                 .add(
                     (pack, commands) -> {
-                      if (affectsSize(pack, commands)) {
+                      if (affectsSize(pack)) {
                         try {
                           quotaBackend
                               .user(identifiedUser)
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index ea958f6..a528974 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS;
+import static com.google.gerrit.entities.RefNames.REFS_USERS;
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.collect.ImmutableList;
@@ -23,11 +23,11 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.index.RefState;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupIncludeCache;
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index e15dd40..3ccbe4d 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static org.junit.Assert.assertEquals;
 
@@ -24,9 +25,9 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -395,15 +396,15 @@
     public void assertErrorStatus() {
       RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
       assertThat(refUpdate).isNotNull();
-      assertThat(refUpdate.getStatus())
-          .named(message(refUpdate))
+      assertWithMessage(message(refUpdate))
+          .that(refUpdate.getStatus())
           .isEqualTo(Status.REJECTED_OTHER_REASON);
     }
 
     private void assertStatus(Status expectedStatus, String expectedMessage) {
       RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
       assertThat(refUpdate).isNotNull();
-      assertThat(refUpdate.getStatus()).named(message(refUpdate)).isEqualTo(expectedStatus);
+      assertWithMessage(message(refUpdate)).that(refUpdate.getStatus()).isEqualTo(expectedStatus);
       if (expectedMessage == null) {
         assertThat(refUpdate.getMessage()).isNull();
       } else {
diff --git a/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java b/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
index 19910db..e943519 100644
--- a/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.query.change.ChangeData;
 
diff --git a/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
index bd8a926..b985e40 100644
--- a/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
+++ b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.gerrit.server.util.ReplicaUtil;
 import com.google.inject.Inject;
 import com.google.inject.Scopes;
 import java.io.IOException;
@@ -50,8 +51,8 @@
 
   @Override
   public void start() {
-    // Gerrit slaves without a reindex
-    if (cfg.getBoolean("container", "slave", false)
+    // Gerrit replicas without a reindex
+    if (ReplicaUtil.isReplica(cfg)
         && !cfg.getBoolean("index", "scheduledIndexer", "runOnStartup", true)) {
       return;
     }
diff --git a/java/com/google/gerrit/acceptance/RestResponse.java b/java/com/google/gerrit/acceptance/RestResponse.java
index e8de5c6..a045d80 100644
--- a/java/com/google/gerrit/acceptance/RestResponse.java
+++ b/java/com/google/gerrit/acceptance/RestResponse.java
@@ -17,13 +17,23 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.httpd.restapi.RestApiServlet.JSON_MAGIC;
+import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_CREATED;
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
+import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
+import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
 
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.net.URI;
-import org.apache.http.HttpStatus;
 
 public class RestResponse extends HttpResponse {
 
@@ -47,47 +57,47 @@
   }
 
   public void assertOK() throws Exception {
-    assertStatus(HttpStatus.SC_OK);
+    assertStatus(SC_OK);
   }
 
   public void assertNotFound() throws Exception {
-    assertStatus(HttpStatus.SC_NOT_FOUND);
+    assertStatus(SC_NOT_FOUND);
   }
 
   public void assertConflict() throws Exception {
-    assertStatus(HttpStatus.SC_CONFLICT);
+    assertStatus(SC_CONFLICT);
   }
 
   public void assertForbidden() throws Exception {
-    assertStatus(HttpStatus.SC_FORBIDDEN);
+    assertStatus(SC_FORBIDDEN);
   }
 
   public void assertNoContent() throws Exception {
-    assertStatus(HttpStatus.SC_NO_CONTENT);
+    assertStatus(SC_NO_CONTENT);
   }
 
   public void assertBadRequest() throws Exception {
-    assertStatus(HttpStatus.SC_BAD_REQUEST);
+    assertStatus(SC_BAD_REQUEST);
   }
 
   public void assertUnprocessableEntity() throws Exception {
-    assertStatus(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    assertStatus(SC_UNPROCESSABLE_ENTITY);
   }
 
   public void assertMethodNotAllowed() throws Exception {
-    assertStatus(HttpStatus.SC_METHOD_NOT_ALLOWED);
+    assertStatus(SC_METHOD_NOT_ALLOWED);
   }
 
   public void assertCreated() throws Exception {
-    assertStatus(HttpStatus.SC_CREATED);
+    assertStatus(SC_CREATED);
   }
 
   public void assertPreconditionFailed() throws Exception {
-    assertStatus(HttpStatus.SC_PRECONDITION_FAILED);
+    assertStatus(SC_PRECONDITION_FAILED);
   }
 
   public void assertTemporaryRedirect(String path) throws Exception {
-    assertStatus(HttpStatus.SC_MOVED_TEMPORARILY);
+    assertStatus(SC_MOVED_TEMPORARILY);
     assertThat(URI.create(getHeader("Location")).getPath()).isEqualTo(path);
   }
 }
diff --git a/java/com/google/gerrit/acceptance/SshdModule.java b/java/com/google/gerrit/acceptance/SshdModule.java
index 185d6e2..873ba177 100644
--- a/java/com/google/gerrit/acceptance/SshdModule.java
+++ b/java/com/google/gerrit/acceptance/SshdModule.java
@@ -34,7 +34,7 @@
     if (keys == null) {
       keys = new SimpleGeneratorHostKeyProvider();
       keys.setAlgorithm("RSA");
-      keys.loadKeys();
+      keys.loadKeys(null);
     }
     return keys;
   }
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index d20124a..43fe4eb 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.joining;
 import static org.junit.Assert.fail;
@@ -29,12 +29,12 @@
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.git.DelegateSystemReader;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Injector;
 import com.google.inject.Module;
@@ -67,10 +67,10 @@
     private ServerContext(GerritServer server) throws Exception {
       this.server = server;
       Injector i = server.getTestInjector();
-      if (adminId == null) {
-        adminId = i.getInstance(AccountCreator.class).admin().id();
+      if (admin == null) {
+        admin = i.getInstance(AccountCreator.class).admin();
       }
-      ctx = i.getInstance(OneOffRequestContext.class).openAs(adminId);
+      ctx = i.getInstance(OneOffRequestContext.class).openAs(admin.id());
       GerritApi gApi = i.getInstance(GerritApi.class);
 
       try {
@@ -125,7 +125,7 @@
   @Rule public RuleChain ruleChain = RuleChain.outerRule(tempSiteDir).around(testRunner);
 
   protected SitePaths sitePaths;
-  protected Account.Id adminId;
+  protected TestAccount admin;
 
   private GerritServer.Description serverDesc;
   private SystemReader oldSystemReader;
@@ -143,20 +143,10 @@
   private static SystemReader setFakeSystemReader(File tempDir) {
     SystemReader oldSystemReader = SystemReader.getInstance();
     SystemReader.setInstance(
-        new SystemReader() {
+        new DelegateSystemReader(oldSystemReader) {
           @Override
-          public String getHostname() {
-            return oldSystemReader.getHostname();
-          }
-
-          @Override
-          public String getenv(String variable) {
-            return oldSystemReader.getenv(variable);
-          }
-
-          @Override
-          public String getProperty(String key) {
-            return oldSystemReader.getProperty(key);
+          public FileBasedConfig openJGitConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "jgit.config"), FS.detect());
           }
 
           @Override
@@ -168,16 +158,6 @@
           public FileBasedConfig openSystemConfig(Config parent, FS fs) {
             return new FileBasedConfig(parent, new File(tempDir, "system.config"), FS.detect());
           }
-
-          @Override
-          public long getCurrentTime() {
-            return oldSystemReader.getCurrentTime();
-          }
-
-          @Override
-          public int getTimezone(long when) {
-            return oldSystemReader.getTimezone(when);
-          }
         });
     return oldSystemReader;
   }
@@ -214,8 +194,8 @@
     // Use invokeProgram with the current classloader, rather than mainImpl, which would create a
     // new classloader. This is necessary so that static state, particularly the SystemReader, is
     // shared with the test method.
-    assertThat(GerritLauncher.invokeProgram(StandaloneSiteTest.class.getClassLoader(), args))
-        .named("gerrit.war " + Arrays.stream(args).collect(joining(" ")))
+    assertWithMessage("gerrit.war " + Arrays.stream(args).collect(joining(" ")))
+        .that(GerritLauncher.invokeProgram(StandaloneSiteTest.class.getClassLoader(), args))
         .isEqualTo(0);
   }
 
diff --git a/java/com/google/gerrit/acceptance/TestAccount.java b/java/com/google/gerrit/acceptance/TestAccount.java
index c937aed..07bb739 100644
--- a/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/TestAccount.java
@@ -21,8 +21,8 @@
 import com.google.common.collect.Streams;
 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.reviewdb.client.Account;
 import java.net.InetSocketAddress;
 import java.util.Arrays;
 import org.apache.http.client.utils.URIBuilder;
diff --git a/java/com/google/gerrit/acceptance/UseClockStep.java b/java/com/google/gerrit/acceptance/UseClockStep.java
new file mode 100644
index 0000000..10a93fe
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/UseClockStep.java
@@ -0,0 +1,42 @@
+// 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;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Annotation to use a clock step for the execution of acceptance tests (the test class must inherit
+ * from {@link AbstractDaemonTest}).
+ *
+ * <p>Annotations on method level override annotations on class level.
+ */
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+public @interface UseClockStep {
+  /** Amount to increment clock by on each lookup. */
+  long clockStep() default 1L;
+
+  /** Time unit for {@link #clockStep()}. */
+  TimeUnit clockStepUnit() default TimeUnit.SECONDS;
+
+  /** Whether the clock should initially be set to {@link java.time.Instant#EPOCH}. */
+  boolean startAtEpoch() default false;
+}
diff --git a/java/com/google/gerrit/acceptance/UseSystemTime.java b/java/com/google/gerrit/acceptance/UseSystemTime.java
new file mode 100644
index 0000000..e9cbd47
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/UseSystemTime.java
@@ -0,0 +1,35 @@
+// 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;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to use the system time for the execution of acceptance tests (the test class must
+ * inherit from {@link AbstractDaemonTest}).
+ *
+ * <p>Can only be applied on method level, since using system time on class level is the default if
+ * {@link UseClockStep} is not used.
+ *
+ * <p>Intended to be used to use system time for single tests when the test class is annotated with
+ * {@link UseClockStep}.
+ */
+@Target(METHOD)
+@Retention(RUNTIME)
+public @interface UseSystemTime {}
diff --git a/java/com/google/gerrit/acceptance/UseTimezone.java b/java/com/google/gerrit/acceptance/UseTimezone.java
new file mode 100644
index 0000000..7412030
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/UseTimezone.java
@@ -0,0 +1,35 @@
+// 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;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to set a timezone for the execution of acceptance tests (the test class must inherit
+ * from {@link AbstractDaemonTest}).
+ *
+ * <p>Annotations on method level override annotations on class level.
+ */
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+public @interface UseTimezone {
+  /** The timezone that should be used for the test, e.g. "US/Eastern". */
+  String timezone();
+}
diff --git a/java/com/google/gerrit/acceptance/rest/CreateTestPlugin.java b/java/com/google/gerrit/acceptance/rest/CreateTestPlugin.java
index 85a2d7b..2a77d31 100644
--- a/java/com/google/gerrit/acceptance/rest/CreateTestPlugin.java
+++ b/java/com/google/gerrit/acceptance/rest/CreateTestPlugin.java
@@ -33,7 +33,8 @@
   }
 
   @Override
-  public Object apply(ConfigResource parentResource, IdString id, Input input) throws Exception {
+  public Response<?> apply(ConfigResource parentResource, IdString id, Input input)
+      throws Exception {
     return Response.created(input);
   }
 }
diff --git a/java/com/google/gerrit/acceptance/rest/GetTestPlugin.java b/java/com/google/gerrit/acceptance/rest/GetTestPlugin.java
index a31cc15..3b6da85 100644
--- a/java/com/google/gerrit/acceptance/rest/GetTestPlugin.java
+++ b/java/com/google/gerrit/acceptance/rest/GetTestPlugin.java
@@ -27,7 +27,7 @@
 public class GetTestPlugin implements RestReadView<PluginResource> {
 
   @Override
-  public Object apply(PluginResource resource)
+  public Response<?> apply(PluginResource resource)
       throws AuthException, BadRequestException, ResourceConflictException, Exception {
     return Response.ok("Foo");
   }
diff --git a/java/com/google/gerrit/acceptance/rest/ListTestPlugin.java b/java/com/google/gerrit/acceptance/rest/ListTestPlugin.java
index 00e071c..ce037a7 100644
--- a/java/com/google/gerrit/acceptance/rest/ListTestPlugin.java
+++ b/java/com/google/gerrit/acceptance/rest/ListTestPlugin.java
@@ -18,14 +18,15 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.ConfigResource;
 
 public class ListTestPlugin implements RestReadView<ConfigResource> {
 
   @Override
-  public Object apply(ConfigResource resource)
+  public Response<?> apply(ConfigResource resource)
       throws AuthException, BadRequestException, ResourceConflictException, Exception {
-    return ImmutableList.of();
+    return Response.ok(ImmutableList.of());
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
index 61b828e..efae223 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance.testsuite.account;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 
 /**
  * An aggregation of operations on accounts for test purposes.
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
index 7641e47..f1b840a 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Accounts;
@@ -61,14 +61,14 @@
   private Account.Id createAccount(TestAccountCreation accountCreation) throws Exception {
     AccountsUpdate.AccountUpdater accountUpdater =
         (account, updateBuilder) ->
-            fillBuilder(updateBuilder, accountCreation, account.getAccount().getId());
+            fillBuilder(updateBuilder, accountCreation, account.account().id());
     AccountState createdAccount = createAccount(accountUpdater);
-    return createdAccount.getAccount().getId();
+    return createdAccount.account().id();
   }
 
   private AccountState createAccount(AccountsUpdate.AccountUpdater accountUpdater)
       throws IOException, ConfigInvalidException {
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     return accountsUpdate.insert("Create Test Account", accountId, accountUpdater);
   }
 
@@ -129,13 +129,13 @@
     }
 
     private TestAccount toTestAccount(AccountState accountState) {
-      Account account = accountState.getAccount();
+      Account account = accountState.account();
       return TestAccount.builder()
-          .accountId(account.getId())
-          .preferredEmail(Optional.ofNullable(account.getPreferredEmail()))
-          .fullname(Optional.ofNullable(account.getFullName()))
-          .username(accountState.getUserName())
-          .active(accountState.getAccount().isActive())
+          .accountId(account.id())
+          .preferredEmail(Optional.ofNullable(account.preferredEmail()))
+          .fullname(Optional.ofNullable(account.fullName()))
+          .username(accountState.userName())
+          .active(accountState.account().isActive())
           .build();
     }
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java
index e7ffeec..2574d55 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance.testsuite.account;
 
 import com.google.auto.value.AutoValue;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import java.util.Optional;
 
 @AutoValue
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
index f2414e0..983fec0 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
@@ -16,7 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import java.util.Optional;
 
 @AutoValue
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
index 4847fdb..6c95360 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
@@ -19,7 +19,7 @@
 
 import com.google.gerrit.acceptance.SshEnabled;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java
index 533d06b..b9414e1 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance.testsuite.group;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 
 /**
  * An aggregation of operations on groups for test purposes.
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
index e0ddee5..fd5c003 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
@@ -16,9 +16,9 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.exceptions.NoSuchGroupException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.GroupUUID;
@@ -78,10 +78,10 @@
   }
 
   private InternalGroupCreation toInternalGroupCreation(TestGroupCreation groupCreation) {
-    AccountGroup.Id groupId = new AccountGroup.Id(seq.nextGroupId());
+    AccountGroup.Id groupId = AccountGroup.id(seq.nextGroupId());
     String groupName = groupCreation.name().orElse("group-with-id-" + groupId.get());
     AccountGroup.UUID groupUuid = GroupUUID.make(groupName, serverIdent);
-    AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
+    AccountGroup.NameKey nameKey = AccountGroup.nameKey(groupName);
     return InternalGroupCreation.builder()
         .setId(groupId)
         .setGroupUUID(groupUuid)
@@ -153,7 +153,7 @@
 
     private InternalGroupUpdate toInternalGroupUpdate(TestGroupUpdate groupUpdate) {
       InternalGroupUpdate.Builder builder = InternalGroupUpdate.builder();
-      groupUpdate.name().map(AccountGroup.NameKey::new).ifPresent(builder::setName);
+      groupUpdate.name().map(AccountGroup::nameKey).ifPresent(builder::setName);
       groupUpdate.description().ifPresent(builder::setDescription);
       groupUpdate.ownerGroupUuid().ifPresent(builder::setOwnerGroupUUID);
       groupUpdate.visibleToAll().ifPresent(builder::setVisibleToAll);
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
index b450304..c885353 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
@@ -16,8 +16,8 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import java.sql.Timestamp;
 import java.util.Optional;
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
index 612ce2a..8bb7b23 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
@@ -18,8 +18,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import java.util.Optional;
 import java.util.Set;
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
index bc9d569..47c7117 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
@@ -18,8 +18,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/BUILD b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
new file mode 100644
index 0000000..8a3a23a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
@@ -0,0 +1,22 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(default_testonly = 1)
+
+java_library(
+    name = "project",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/acceptance:function",
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/server",
+        "//lib:guava",
+        "//lib:jgit",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
+        "//lib/commons:lang",
+        "//lib/guice",
+    ],
+)
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
index 029d161..2db611b 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.acceptance.testsuite.project;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.project.ProjectConfig;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 /**
@@ -28,6 +30,9 @@
 
   PerProjectOperations project(Project.NameKey key);
 
+  /** Starts a fluent chain for updating All-Projects. */
+  TestProjectUpdate.Builder allProjectsForUpdate();
+
   interface PerProjectOperations {
     /**
      * Returns the commit for this project. branchName can either be shortened ("HEAD", "master") or
@@ -40,5 +45,33 @@
      * fully qualified refname ("refs/heads/master").
      */
     boolean hasHead(String branchName);
+
+    /** Returns a fresh {@link ProjectConfig} read from the tip of {@code refs/meta/config}. */
+    ProjectConfig getProjectConfig();
+
+    /**
+     * Returns a fresh JGit {@link Config} instance read from {@code project.config} at the tip of
+     * {@code refs/meta/config}. Does not have a base config, i.e. does not respect {@code
+     * $site_path/etc/project.config}.
+     */
+    Config getConfig();
+
+    /**
+     * Starts the fluent chain to update a project. The returned builder can be used to specify how
+     * the attributes of the project should be modified. To update the project for real, the {@link
+     * TestProjectUpdate.Builder#update()} must be called.
+     *
+     * <p>Example:
+     *
+     * <pre>
+     * projectOperations
+     *     .forUpdate()
+     *     .add(allow(ABANDON).ref("refs/*").group(REGISTERED_USERS))
+     *     .update();
+     * </pre>
+     *
+     * @return a builder to update the check.
+     */
+    TestProjectUpdate.Builder forUpdate();
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index 28be3f3..62a560b 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -14,30 +14,68 @@
 
 package com.google.gerrit.acceptance.testsuite.project;
 
-import com.google.common.base.Preconditions;
+import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
+import static com.google.gerrit.server.project.ProjectConfig.PROJECT_CONFIG;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectCreation.Builder;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestCapability;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestLabelPermission;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
+import com.google.gerrit.common.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.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCreator;
 import com.google.inject.Inject;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import org.apache.commons.lang.RandomStringUtils;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
 
 public class ProjectOperationsImpl implements ProjectOperations {
-  private final ProjectCreator projectCreator;
+  private final AllProjectsName allProjectsName;
   private final GitRepositoryManager repoManager;
+  private final MetaDataUpdate.Server metaDataUpdateFactory;
+  private final ProjectCache projectCache;
+  private final ProjectConfig.Factory projectConfigFactory;
+  private final ProjectCreator projectCreator;
 
   @Inject
-  ProjectOperationsImpl(GitRepositoryManager repoManager, ProjectCreator projectCreator) {
+  ProjectOperationsImpl(
+      AllProjectsName allProjectsName,
+      GitRepositoryManager repoManager,
+      MetaDataUpdate.Server metaDataUpdateFactory,
+      ProjectCache projectCache,
+      ProjectConfig.Factory projectConfigFactory,
+      ProjectCreator projectCreator) {
+    this.allProjectsName = allProjectsName;
     this.repoManager = repoManager;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.projectCache = projectCache;
+    this.projectConfigFactory = projectConfigFactory;
     this.projectCreator = projectCreator;
   }
 
@@ -58,7 +96,7 @@
     args.ownerIds = new ArrayList<>();
     projectCreation.submitType().ifPresent(st -> args.submitType = st);
     projectCreator.createProject(args);
-    return new Project.NameKey(name);
+    return Project.nameKey(name);
   }
 
   @Override
@@ -66,8 +104,12 @@
     return new PerProjectOperations(key);
   }
 
-  private class PerProjectOperations implements ProjectOperations.PerProjectOperations {
+  @Override
+  public TestProjectUpdate.Builder allProjectsForUpdate() {
+    return project(allProjectsName).forUpdate();
+  }
 
+  private class PerProjectOperations implements ProjectOperations.PerProjectOperations {
     Project.NameKey nameKey;
 
     PerProjectOperations(Project.NameKey nameKey) {
@@ -76,7 +118,7 @@
 
     @Override
     public RevCommit getHead(String branch) {
-      return Preconditions.checkNotNull(headOrNull(branch));
+      return requireNonNull(headOrNull(branch));
     }
 
     @Override
@@ -84,6 +126,91 @@
       return headOrNull(branch) != null;
     }
 
+    @Override
+    public TestProjectUpdate.Builder forUpdate() {
+      return TestProjectUpdate.builder(nameKey, allProjectsName, this::updateProject);
+    }
+
+    private void updateProject(TestProjectUpdate projectUpdate)
+        throws IOException, ConfigInvalidException {
+      try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create(nameKey)) {
+        ProjectConfig projectConfig = projectConfigFactory.read(metaDataUpdate);
+        if (projectUpdate.removeAllAccessSections()) {
+          projectConfig.getAccessSections().forEach(as -> projectConfig.remove(as));
+        }
+        removePermissions(projectConfig, projectUpdate.removedPermissions());
+        addCapabilities(projectConfig, projectUpdate.addedCapabilities());
+        addPermissions(projectConfig, projectUpdate.addedPermissions());
+        addLabelPermissions(projectConfig, projectUpdate.addedLabelPermissions());
+        setExclusiveGroupPermissions(projectConfig, projectUpdate.exclusiveGroupPermissions());
+        projectConfig.commit(metaDataUpdate);
+      }
+      projectCache.evict(nameKey);
+    }
+
+    private void removePermissions(
+        ProjectConfig projectConfig,
+        ImmutableList<TestProjectUpdate.TestPermissionKey> removedPermissions) {
+      for (TestProjectUpdate.TestPermissionKey p : removedPermissions) {
+        Permission permission =
+            projectConfig.getAccessSection(p.section(), true).getPermission(p.name(), true);
+        if (p.group().isPresent()) {
+          GroupReference group = new GroupReference(p.group().get(), p.group().get().get());
+          group = projectConfig.resolve(group);
+          permission.removeRule(group);
+        } else {
+          permission.clearRules();
+        }
+      }
+    }
+
+    private void addCapabilities(
+        ProjectConfig projectConfig, ImmutableList<TestCapability> addedCapabilities) {
+      for (TestCapability c : addedCapabilities) {
+        PermissionRule rule = newRule(projectConfig, c.group());
+        rule.setRange(c.min(), c.max());
+        projectConfig
+            .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
+            .getPermission(c.name(), true)
+            .add(rule);
+      }
+    }
+
+    private void addPermissions(
+        ProjectConfig projectConfig, ImmutableList<TestPermission> addedPermissions) {
+      for (TestPermission p : addedPermissions) {
+        PermissionRule rule = newRule(projectConfig, p.group());
+        rule.setAction(p.action());
+        rule.setForce(p.force());
+        projectConfig.getAccessSection(p.ref(), true).getPermission(p.name(), true).add(rule);
+      }
+    }
+
+    private void addLabelPermissions(
+        ProjectConfig projectConfig, ImmutableList<TestLabelPermission> addedLabelPermissions) {
+      for (TestLabelPermission p : addedLabelPermissions) {
+        PermissionRule rule = newRule(projectConfig, p.group());
+        rule.setAction(p.action());
+        rule.setRange(p.min(), p.max());
+        String permissionName =
+            p.impersonation() ? Permission.forLabelAs(p.name()) : Permission.forLabel(p.name());
+        Permission permission =
+            projectConfig.getAccessSection(p.ref(), true).getPermission(permissionName, true);
+        permission.add(rule);
+      }
+    }
+
+    private void setExclusiveGroupPermissions(
+        ProjectConfig projectConfig,
+        ImmutableMap<TestProjectUpdate.TestPermissionKey, Boolean> exclusiveGroupPermissions) {
+      exclusiveGroupPermissions.forEach(
+          (key, exclusive) ->
+              projectConfig
+                  .getAccessSection(key.section(), true)
+                  .getPermission(key.name(), true)
+                  .setExclusiveGroup(exclusive));
+    }
+
     private RevCommit headOrNull(String branch) {
       if (!branch.startsWith(Constants.R_REFS)) {
         branch = RefNames.REFS_HEADS + branch;
@@ -97,5 +224,45 @@
         throw new IllegalStateException(e);
       }
     }
+
+    @Override
+    public ProjectConfig getProjectConfig() {
+      try (Repository repo = repoManager.openRepository(nameKey)) {
+        ProjectConfig projectConfig = projectConfigFactory.create(nameKey);
+        projectConfig.load(nameKey, repo);
+        return projectConfig;
+      } catch (Exception e) {
+        throw new IllegalStateException(e);
+      }
+    }
+
+    @Override
+    public Config getConfig() {
+      try (Repository repo = repoManager.openRepository(nameKey);
+          RevWalk rw = new RevWalk(repo)) {
+        Ref ref = repo.exactRef(REFS_CONFIG);
+        if (ref == null) {
+          return new Config();
+        }
+        RevTree tree = rw.parseTree(ref.getObjectId());
+        TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), PROJECT_CONFIG, tree);
+        if (tw == null) {
+          return new Config();
+        }
+        ObjectLoader loader = rw.getObjectReader().open(tw.getObjectId(0));
+        String text = new String(loader.getCachedBytes(), UTF_8);
+        Config config = new Config();
+        config.fromText(text);
+        return config;
+      } catch (Exception e) {
+        throw new IllegalStateException(e);
+      }
+    }
+  }
+
+  private static PermissionRule newRule(ProjectConfig project, AccountGroup.UUID groupUUID) {
+    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
+    group = project.resolve(group);
+    return new PermissionRule(group);
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
index 31af1d2..99e045c 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
@@ -16,8 +16,8 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Project;
 import java.util.Optional;
 
 @AutoValue
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
new file mode 100644
index 0000000..739ed19
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
@@ -0,0 +1,449 @@
+// 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.testsuite.project;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.common.data.AccessSection.GLOBAL_CAPABILITIES;
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.LabelType;
+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.AccountGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Constants;
+
+@AutoValue
+public abstract class TestProjectUpdate {
+  /** Starts a builder for allowing a capability. */
+  public static TestCapability.Builder allowCapability(String name) {
+    return TestCapability.builder().name(name);
+  }
+
+  /** Records a global capability to be updated. */
+  @AutoValue
+  public abstract static class TestCapability {
+    private static Builder builder() {
+      return new AutoValue_TestProjectUpdate_TestCapability.Builder();
+    }
+
+    abstract String name();
+
+    abstract AccountGroup.UUID group();
+
+    abstract int min();
+
+    abstract int max();
+
+    /** Builder for {@link TestCapability}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+      /** Sets the name of the capability. */
+      public abstract Builder name(String name);
+
+      abstract String name();
+
+      /** Sets the group to which the capability applies. */
+      public abstract Builder group(AccountGroup.UUID group);
+
+      abstract Builder min(int min);
+
+      abstract Optional<Integer> min();
+
+      abstract Builder max(int max);
+
+      abstract Optional<Integer> max();
+
+      /** Sets the minimum and maximum values for the capability. */
+      public Builder range(int min, int max) {
+        checkNonInvertedRange(min, max);
+        return min(min).max(max);
+      }
+
+      /** Builds the {@link TestCapability}. */
+      abstract TestCapability autoBuild();
+
+      public TestCapability build() {
+        PermissionRange.WithDefaults withDefaults = GlobalCapability.getRange(name());
+        if (withDefaults != null) {
+          int min = min().orElse(withDefaults.getDefaultMin());
+          int max = max().orElse(withDefaults.getDefaultMax());
+          range(min, max);
+          // Don't enforce range is nonempty; this is allowed for e.g. batchChangesLimit.
+        } else {
+          checkArgument(
+              !min().isPresent() && !max().isPresent(),
+              "capability %s does not support ranges",
+              name());
+          range(0, 0);
+        }
+
+        return autoBuild();
+      }
+    }
+  }
+
+  /** Starts a builder for allowing a permission. */
+  public static TestPermission.Builder allow(String name) {
+    return TestPermission.builder().name(name).action(PermissionRule.Action.ALLOW);
+  }
+
+  /** Starts a builder for denying a permission. */
+  public static TestPermission.Builder deny(String name) {
+    return TestPermission.builder().name(name).action(PermissionRule.Action.DENY);
+  }
+
+  /** Starts a builder for blocking a permission. */
+  public static TestPermission.Builder block(String name) {
+    return TestPermission.builder().name(name).action(PermissionRule.Action.BLOCK);
+  }
+
+  /**
+   * Records a permission to be updated.
+   *
+   * <p>Not used for permissions that have ranges (label permissions) or global capabilities.
+   */
+  @AutoValue
+  public abstract static class TestPermission {
+    private static Builder builder() {
+      return new AutoValue_TestProjectUpdate_TestPermission.Builder().force(false);
+    }
+
+    abstract String name();
+
+    abstract String ref();
+
+    abstract AccountGroup.UUID group();
+
+    abstract PermissionRule.Action action();
+
+    abstract boolean force();
+
+    /** Builder for {@link TestPermission}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+      abstract Builder name(String name);
+
+      /** Sets the ref pattern used on the permission. */
+      public abstract Builder ref(String ref);
+
+      /** Sets the group to which the permission applies. */
+      public abstract Builder group(AccountGroup.UUID groupUuid);
+
+      abstract Builder action(PermissionRule.Action action);
+
+      /** Sets whether the permission is a force permission. */
+      public abstract Builder force(boolean force);
+
+      /** Builds the {@link TestPermission}. */
+      public abstract TestPermission build();
+    }
+  }
+
+  /** Starts a builder for allowing a label permission. */
+  public static TestLabelPermission.Builder allowLabel(String name) {
+    return TestLabelPermission.builder().name(name).action(PermissionRule.Action.ALLOW);
+  }
+
+  /** Starts a builder for denying a label permission. */
+  public static TestLabelPermission.Builder blockLabel(String name) {
+    return TestLabelPermission.builder().name(name).action(PermissionRule.Action.BLOCK);
+  }
+
+  /** Records a label permission to be updated. */
+  @AutoValue
+  public abstract static class TestLabelPermission {
+    private static Builder builder() {
+      return new AutoValue_TestProjectUpdate_TestLabelPermission.Builder().impersonation(false);
+    }
+
+    abstract String name();
+
+    abstract String ref();
+
+    abstract AccountGroup.UUID group();
+
+    abstract PermissionRule.Action action();
+
+    abstract int min();
+
+    abstract int max();
+
+    abstract boolean impersonation();
+
+    /** Builder for {@link TestLabelPermission}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+      abstract Builder name(String name);
+
+      /** Sets the ref pattern used on the permission. */
+      public abstract Builder ref(String ref);
+
+      /** Sets the group to which the permission applies. */
+      public abstract Builder group(AccountGroup.UUID group);
+
+      abstract Builder action(PermissionRule.Action action);
+
+      abstract Builder min(int min);
+
+      abstract Builder max(int max);
+
+      /** Sets the minimum and maximum values for the permission. */
+      public Builder range(int min, int max) {
+        checkArgument(min != 0 || max != 0, "empty range");
+        checkNonInvertedRange(min, max);
+        return min(min).max(max);
+      }
+
+      /** Sets whether this permission should be for impersonating another user's votes. */
+      public abstract Builder impersonation(boolean impersonation);
+
+      abstract TestLabelPermission autoBuild();
+
+      /** Builds the {@link TestPermission}. */
+      public TestLabelPermission build() {
+        TestLabelPermission result = autoBuild();
+        checkLabelName(result.name());
+        return result;
+      }
+    }
+  }
+
+  /**
+   * Starts a builder for describing a permission key for deletion. Not for label permissions or
+   * global capabilities.
+   */
+  public static TestPermissionKey.Builder permissionKey(String name) {
+    return TestPermissionKey.builder().name(name);
+  }
+
+  /** Starts a builder for describing a label permission key for deletion. */
+  public static TestPermissionKey.Builder labelPermissionKey(String name) {
+    checkLabelName(name);
+    return TestPermissionKey.builder().name(Permission.forLabel(name));
+  }
+
+  /** Starts a builder for describing a capability key for deletion. */
+  public static TestPermissionKey.Builder capabilityKey(String name) {
+    return TestPermissionKey.builder().name(name).section(GLOBAL_CAPABILITIES);
+  }
+
+  /** Records the key of a permission (of any type) for deletion. */
+  @AutoValue
+  public abstract static class TestPermissionKey {
+    private static Builder builder() {
+      return new AutoValue_TestProjectUpdate_TestPermissionKey.Builder();
+    }
+
+    abstract String section();
+
+    abstract String name();
+
+    abstract Optional<AccountGroup.UUID> group();
+
+    @AutoValue.Builder
+    public abstract static class Builder {
+      abstract Builder section(String section);
+
+      abstract Optional<String> section();
+
+      /** Sets the ref pattern used on the permission. Not for global capabilities. */
+      public Builder ref(String ref) {
+        requireNonNull(ref);
+        checkArgument(ref.startsWith(Constants.R_REFS), "must be a ref: %s", ref);
+        checkArgument(
+            !section().isPresent() || !section().get().equals(GLOBAL_CAPABILITIES),
+            "can't set ref on global capability");
+        return section(ref);
+      }
+
+      abstract Builder name(String name);
+
+      /** Sets the group to which the permission applies. */
+      public abstract Builder group(AccountGroup.UUID group);
+
+      /** Builds the {@link TestPermissionKey}. */
+      public abstract TestPermissionKey build();
+    }
+  }
+
+  static Builder builder(
+      Project.NameKey nameKey,
+      AllProjectsName allProjectsName,
+      ThrowingConsumer<TestProjectUpdate> projectUpdater) {
+    return new AutoValue_TestProjectUpdate.Builder()
+        .nameKey(nameKey)
+        .allProjectsName(allProjectsName)
+        .projectUpdater(projectUpdater)
+        .removeAllAccessSections(false);
+  }
+
+  /** Builder for {@link TestProjectUpdate}. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+    abstract Builder nameKey(Project.NameKey project);
+
+    abstract Builder allProjectsName(AllProjectsName allProjects);
+
+    abstract ImmutableList.Builder<TestPermission> addedPermissionsBuilder();
+
+    abstract ImmutableList.Builder<TestLabelPermission> addedLabelPermissionsBuilder();
+
+    abstract ImmutableList.Builder<TestCapability> addedCapabilitiesBuilder();
+
+    abstract ImmutableList.Builder<TestPermissionKey> removedPermissionsBuilder();
+
+    abstract ImmutableMap.Builder<TestPermissionKey, Boolean> exclusiveGroupPermissionsBuilder();
+
+    abstract Builder removeAllAccessSections(boolean value);
+
+    /**
+     * Removes all access sections. Useful when testing against a specific set of access sections or
+     * permissions.
+     */
+    public Builder removeAllAccessSections() {
+      return removeAllAccessSections(true);
+    }
+
+    /** Adds a permission to be included in this update. */
+    public Builder add(TestPermission testPermission) {
+      addedPermissionsBuilder().add(testPermission);
+      return this;
+    }
+
+    /** Adds a permission to be included in this update. */
+    public Builder add(TestPermission.Builder testPermissionBuilder) {
+      return add(testPermissionBuilder.build());
+    }
+
+    /** Adds a label permission to be included in this update. */
+    public Builder add(TestLabelPermission testLabelPermission) {
+      addedLabelPermissionsBuilder().add(testLabelPermission);
+      return this;
+    }
+
+    /** Adds a label permission to be included in this update. */
+    public Builder add(TestLabelPermission.Builder testLabelPermissionBuilder) {
+      return add(testLabelPermissionBuilder.build());
+    }
+
+    /** Adds a capability to be included in this update. */
+    public Builder add(TestCapability testCapability) {
+      addedCapabilitiesBuilder().add(testCapability);
+      return this;
+    }
+
+    /** Adds a capability to be included in this update. */
+    public Builder add(TestCapability.Builder testCapabilityBuilder) {
+      return add(testCapabilityBuilder.build());
+    }
+
+    /** Removes a permission, label permission, or capability as part of this update. */
+    public Builder remove(TestPermissionKey testPermissionKey) {
+      removedPermissionsBuilder().add(testPermissionKey);
+      return this;
+    }
+
+    /** Removes a permission, label permission, or capability as part of this update. */
+    public Builder remove(TestPermissionKey.Builder testPermissionKeyBuilder) {
+      return remove(testPermissionKeyBuilder.build());
+    }
+
+    /** Sets the exclusive bit bit for the given permission key. */
+    public Builder setExclusiveGroup(
+        TestPermissionKey.Builder testPermissionKeyBuilder, boolean exclusive) {
+      return setExclusiveGroup(testPermissionKeyBuilder.build(), exclusive);
+    }
+
+    /** Sets the exclusive bit bit for the given permission key. */
+    public Builder setExclusiveGroup(TestPermissionKey testPermissionKey, boolean exclusive) {
+      checkArgument(
+          !testPermissionKey.group().isPresent(),
+          "do not specify group for setExclusiveGroup: %s",
+          testPermissionKey);
+      checkArgument(
+          !testPermissionKey.section().equals(GLOBAL_CAPABILITIES),
+          "setExclusiveGroup not valid for global capabilities: %s",
+          testPermissionKey);
+      exclusiveGroupPermissionsBuilder().put(testPermissionKey, exclusive);
+      return this;
+    }
+
+    abstract Builder projectUpdater(ThrowingConsumer<TestProjectUpdate> projectUpdater);
+
+    abstract TestProjectUpdate autoBuild();
+
+    TestProjectUpdate build() {
+      TestProjectUpdate projectUpdate = autoBuild();
+      if (projectUpdate.hasCapabilityUpdates()) {
+        checkArgument(
+            projectUpdate.nameKey().equals(projectUpdate.allProjectsName()),
+            "cannot update global capabilities on %s, only %s: %s",
+            projectUpdate.nameKey(),
+            projectUpdate.allProjectsName(),
+            projectUpdate);
+      }
+      return projectUpdate;
+    }
+
+    /** Executes the update, updating the underlying project. */
+    public void update() {
+      TestProjectUpdate projectUpdate = build();
+      projectUpdate.projectUpdater().acceptAndThrowSilently(projectUpdate);
+    }
+  }
+
+  abstract Project.NameKey nameKey();
+
+  abstract AllProjectsName allProjectsName();
+
+  abstract ImmutableList<TestPermission> addedPermissions();
+
+  abstract ImmutableList<TestLabelPermission> addedLabelPermissions();
+
+  abstract ImmutableList<TestCapability> addedCapabilities();
+
+  abstract ImmutableList<TestPermissionKey> removedPermissions();
+
+  abstract ImmutableMap<TestPermissionKey, Boolean> exclusiveGroupPermissions();
+
+  abstract ThrowingConsumer<TestProjectUpdate> projectUpdater();
+
+  abstract boolean removeAllAccessSections();
+
+  boolean hasCapabilityUpdates() {
+    return !addedCapabilities().isEmpty()
+        || removedPermissions().stream().anyMatch(k -> k.section().equals(GLOBAL_CAPABILITIES));
+  }
+
+  private static void checkLabelName(String name) {
+    // "label-Code-Review" is technically a valid label name, and we don't prevent users from
+    // using it in production, but specifying it in a test is programmer error.
+    checkArgument(!Permission.isLabel(name), "expected label name, got permission name: %s", name);
+    LabelType.checkName(name);
+  }
+
+  private static void checkNonInvertedRange(int min, int max) {
+    checkArgument(min <= max, "inverted range: %s > %s", min, max);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
index 17d9294..a9914b3 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 
 /**
  * An aggregation of operations on Guice request scopes for test purposes.
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
index 5546422..db730a6 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 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/common/BUILD b/java/com/google/gerrit/common/BUILD
index 3b3d9c6..1099919 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -21,16 +21,15 @@
     visibility = ["//visibility:public"],
     deps = [
         ":annotations",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/prettify:server",
-        "//java/com/google/gerrit/reviewdb:server",
-        "//java/com/google/gwtorm",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:servlet-api",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/flogger:api",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
 
diff --git a/java/com/google/gerrit/common/FileUtil.java b/java/com/google/gerrit/common/FileUtil.java
index 04288bc..5b0925e 100644
--- a/java/com/google/gerrit/common/FileUtil.java
+++ b/java/com/google/gerrit/common/FileUtil.java
@@ -44,7 +44,6 @@
   }
 
   public static void chmod(int mode, Path path) {
-    // TODO(dborowitz): Is there a portable way to do this with NIO?
     chmod(mode, path.toFile());
   }
 
diff --git a/java/com/google/gerrit/common/PageLinks.java b/java/com/google/gerrit/common/PageLinks.java
index 2243eab..38de5b1 100644
--- a/java/com/google/gerrit/common/PageLinks.java
+++ b/java/com/google/gerrit/common/PageLinks.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.common;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtorm.client.KeyUtil;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Change.Status;
+import com.google.gerrit.entities.KeyUtil;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 
 public class PageLinks {
   public static final String PROJECT_CHANGE_DELIMITER = "/+/";
@@ -83,7 +83,7 @@
   }
 
   public static String toChange(@Nullable Project.NameKey project, PatchSet.Id ps) {
-    return toChange(project, ps.getParentKey()) + ps.getId();
+    return toChange(project, ps.changeId()) + ps.getId();
   }
 
   public static String toProject(Project.NameKey p) {
diff --git a/java/com/google/gerrit/common/UsedAt.java b/java/com/google/gerrit/common/UsedAt.java
index 44ed92b..9f8b255 100644
--- a/java/com/google/gerrit/common/UsedAt.java
+++ b/java/com/google/gerrit/common/UsedAt.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common;
 
+import static java.lang.annotation.ElementType.FIELD;
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -22,13 +23,13 @@
 import java.lang.annotation.Target;
 
 /**
- * A marker for a method that is public solely because it is called from inside a project or an
- * organisation using Gerrit.
+ * A marker to say a method/type/field is added or is increased to public solely because it is
+ * called from inside a project or an organisation using Gerrit.
  */
-@Target({METHOD, TYPE})
+@Target({METHOD, TYPE, FIELD})
 @Retention(RUNTIME)
 public @interface UsedAt {
-  /** Enumeration of projects that call a method that would otherwise be private. */
+  /** Enumeration of projects that call a method/type/field. */
   enum Project {
     GOOGLE,
     COLLABNET,
diff --git a/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java b/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
index 713fd4d..16dfb9b 100644
--- a/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
+++ b/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
@@ -18,5 +18,4 @@
   public static final String LASTID_COOKIE = "gerrit.last_openid";
 
   public static final String URL_LAUNCHPAD = "https://login.launchpad.net/+openid";
-  public static final String URL_YAHOO = "https://me.yahoo.com";
 }
diff --git a/java/com/google/gerrit/common/data/AccessSection.java b/java/com/google/gerrit/common/data/AccessSection.java
index 3670e96..0c9663b 100644
--- a/java/com/google/gerrit/common/data/AccessSection.java
+++ b/java/com/google/gerrit/common/data/AccessSection.java
@@ -18,7 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
diff --git a/java/com/google/gerrit/common/data/CommentDetail.java b/java/com/google/gerrit/common/data/CommentDetail.java
index 1ae246f..d69f0bb 100644
--- a/java/com/google/gerrit/common/data/CommentDetail.java
+++ b/java/com/google/gerrit/common/data/CommentDetail.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -42,7 +42,7 @@
   protected CommentDetail() {}
 
   public void include(Change.Id changeId, Comment p) {
-    PatchSet.Id psId = new PatchSet.Id(changeId, p.key.patchSetId);
+    PatchSet.Id psId = PatchSet.id(changeId, p.key.patchSetId);
     if (p.side == 0) {
       if (idA == null && idB.equals(psId)) {
         a.add(p);
diff --git a/java/com/google/gerrit/common/data/ContributorAgreement.java b/java/com/google/gerrit/common/data/ContributorAgreement.java
index a6e8cdd..bc106f0 100644
--- a/java/com/google/gerrit/common/data/ContributorAgreement.java
+++ b/java/com/google/gerrit/common/data/ContributorAgreement.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import java.util.ArrayList;
 import java.util.List;
 
diff --git a/java/com/google/gerrit/common/data/FilenameComparator.java b/java/com/google/gerrit/common/data/FilenameComparator.java
index e0a6569..ebf423c 100644
--- a/java/com/google/gerrit/common/data/FilenameComparator.java
+++ b/java/com/google/gerrit/common/data/FilenameComparator.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.entities.Patch;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashSet;
diff --git a/java/com/google/gerrit/common/data/GarbageCollectionResult.java b/java/com/google/gerrit/common/data/GarbageCollectionResult.java
index a6c534c..5ed0158 100644
--- a/java/com/google/gerrit/common/data/GarbageCollectionResult.java
+++ b/java/com/google/gerrit/common/data/GarbageCollectionResult.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import java.util.ArrayList;
 import java.util.List;
 
diff --git a/java/com/google/gerrit/common/data/GroupDescription.java b/java/com/google/gerrit/common/data/GroupDescription.java
index d22b94b..ed8b39d 100644
--- a/java/com/google/gerrit/common/data/GroupDescription.java
+++ b/java/com/google/gerrit/common/data/GroupDescription.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+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/common/data/GroupReference.java
index e5b0965..0af088e 100644
--- a/java/com/google/gerrit/common/data/GroupReference.java
+++ b/java/com/google/gerrit/common/data/GroupReference.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.common.data;
 
+import static java.util.Objects.requireNonNull;
+
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 
 /** Describes a group within a projects {@link AccessSection}s. */
 public class GroupReference implements Comparable<GroupReference> {
@@ -46,17 +48,27 @@
   /**
    * Create a group reference.
    *
-   * @param uuid UUID of the group, may be {@code null} if the group name couldn't be resolved
+   * @param uuid UUID of the group, must not be {@code null}
    * @param name the group name, must not be {@code null}
    */
-  public GroupReference(@Nullable AccountGroup.UUID uuid, String name) {
-    setUUID(uuid);
+  public GroupReference(AccountGroup.UUID uuid, String name) {
+    setUUID(requireNonNull(uuid));
+    setName(name);
+  }
+
+  /**
+   * Create a group reference where the group's name couldn't be resolved.
+   *
+   * @param name the group name, must not be {@code null}
+   */
+  public GroupReference(String name) {
+    setUUID(null);
     setName(name);
   }
 
   @Nullable
   public AccountGroup.UUID getUUID() {
-    return uuid != null ? new AccountGroup.UUID(uuid) : null;
+    return uuid != null ? AccountGroup.uuid(uuid) : null;
   }
 
   public void setUUID(@Nullable AccountGroup.UUID newUUID) {
diff --git a/java/com/google/gerrit/common/data/LabelFunction.java b/java/com/google/gerrit/common/data/LabelFunction.java
index 7d13c70..6af675b 100644
--- a/java/com/google/gerrit/common/data/LabelFunction.java
+++ b/java/com/google/gerrit/common/data/LabelFunction.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.entities.PatchSetApproval;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -98,18 +98,18 @@
     }
 
     for (PatchSetApproval a : approvals) {
-      if (a.getValue() == 0) {
+      if (a.value() == 0) {
         continue;
       }
 
       if (isBlock && labelType.isMaxNegative(a)) {
-        submitRecordLabel.appliedBy = a.getAccountId();
+        submitRecordLabel.appliedBy = a.accountId();
         submitRecordLabel.status = SubmitRecord.Label.Status.REJECT;
         return submitRecordLabel;
       }
 
       if (labelType.isMaxPositive(a) || !requiresMaxValue) {
-        submitRecordLabel.appliedBy = a.getAccountId();
+        submitRecordLabel.appliedBy = a.accountId();
 
         submitRecordLabel.status = SubmitRecord.Label.Status.MAY;
         if (isRequired) {
diff --git a/java/com/google/gerrit/common/data/LabelType.java b/java/com/google/gerrit/common/data/LabelType.java
index 42945c4..90b0930 100644
--- a/java/com/google/gerrit/common/data/LabelType.java
+++ b/java/com/google/gerrit/common/data/LabelType.java
@@ -19,8 +19,8 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSetApproval;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -34,6 +34,7 @@
   public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = false;
   public static final boolean DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = false;
   public static final boolean DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE = false;
+  public static final boolean DEF_COPY_ANY_SCORE = false;
   public static final boolean DEF_COPY_MAX_SCORE = false;
   public static final boolean DEF_COPY_MIN_SCORE = false;
   public static final boolean DEF_IGNORE_SELF_APPROVAL = false;
@@ -96,6 +97,7 @@
 
   protected LabelFunction function;
 
+  protected boolean copyAnyScore;
   protected boolean copyMinScore;
   protected boolean copyMaxScore;
   protected boolean copyAllScoresOnMergeFirstParentUpdate;
@@ -139,6 +141,7 @@
     setCopyAllScoresIfNoCodeChange(DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE);
     setCopyAllScoresOnTrivialRebase(DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
     setCopyAllScoresOnMergeFirstParentUpdate(DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE);
+    setCopyAnyScore(DEF_COPY_ANY_SCORE);
     setCopyMaxScore(DEF_COPY_MAX_SCORE);
     setCopyMinScore(DEF_COPY_MIN_SCORE);
     setAllowPostSubmit(DEF_ALLOW_POST_SUBMIT);
@@ -155,7 +158,7 @@
   }
 
   public boolean matches(PatchSetApproval psa) {
-    return psa.getLabelId().get().equalsIgnoreCase(name);
+    return psa.labelId().get().equalsIgnoreCase(name);
   }
 
   public LabelFunction getFunction() {
@@ -229,6 +232,14 @@
     this.defaultValue = defaultValue;
   }
 
+  public boolean isCopyAnyScore() {
+    return copyAnyScore;
+  }
+
+  public void setCopyAnyScore(boolean copyAnyScore) {
+    this.copyAnyScore = copyAnyScore;
+  }
+
   public boolean isCopyMinScore() {
     return copyMinScore;
   }
@@ -279,11 +290,11 @@
   }
 
   public boolean isMaxNegative(PatchSetApproval ca) {
-    return maxNegative == ca.getValue();
+    return maxNegative == ca.value();
   }
 
   public boolean isMaxPositive(PatchSetApproval ca) {
-    return maxPositive == ca.getValue();
+    return maxPositive == ca.value();
   }
 
   public LabelValue getValue(short value) {
@@ -291,11 +302,11 @@
   }
 
   public LabelValue getValue(PatchSetApproval ca) {
-    return byValue.get(ca.getValue());
+    return byValue.get(ca.value());
   }
 
   public LabelId getLabelId() {
-    return new LabelId(name);
+    return LabelId.create(name);
   }
 
   @Override
diff --git a/java/com/google/gerrit/common/data/LabelTypes.java b/java/com/google/gerrit/common/data/LabelTypes.java
index d5891d1..1647658 100644
--- a/java/com/google/gerrit/common/data/LabelTypes.java
+++ b/java/com/google/gerrit/common/data/LabelTypes.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.entities.LabelId;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
diff --git a/java/com/google/gerrit/common/data/PatchScript.java b/java/com/google/gerrit/common/data/PatchScript.java
index 3428580..c177e35 100644
--- a/java/com/google/gerrit/common/data/PatchScript.java
+++ b/java/com/google/gerrit/common/data/PatchScript.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Patch.ChangeType;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import java.util.List;
 import java.util.Set;
 import org.eclipse.jgit.diff.Edit;
@@ -56,7 +56,6 @@
   private CommentDetail comments;
   private List<Patch> history;
   private boolean hugeFile;
-  private boolean intralineDifference;
   private boolean intralineFailure;
   private boolean intralineTimeout;
   private boolean binary;
@@ -83,7 +82,6 @@
       CommentDetail cd,
       List<Patch> hist,
       boolean hf,
-      boolean id,
       boolean idf,
       boolean idt,
       boolean bin,
@@ -108,7 +106,6 @@
     comments = cd;
     history = hist;
     hugeFile = hf;
-    intralineDifference = id;
     intralineFailure = idf;
     intralineTimeout = idt;
     binary = bin;
@@ -178,10 +175,6 @@
     return diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE;
   }
 
-  public boolean hasIntralineDifference() {
-    return intralineDifference;
-  }
-
   public boolean hasIntralineFailure() {
     return intralineFailure;
   }
diff --git a/java/com/google/gerrit/common/data/PermissionRange.java b/java/com/google/gerrit/common/data/PermissionRange.java
index 2f05854..97c3731 100644
--- a/java/com/google/gerrit/common/data/PermissionRange.java
+++ b/java/com/google/gerrit/common/data/PermissionRange.java
@@ -17,6 +17,10 @@
 import java.util.ArrayList;
 import java.util.List;
 
+/**
+ * Represents a closed interval [min, max] with a name. The special value [0, 0] is understood to be
+ * the empty range.
+ */
 public class PermissionRange implements Comparable<PermissionRange> {
   public static class WithDefaults extends PermissionRange {
     protected int defaultMin;
diff --git a/java/com/google/gerrit/common/data/SubmitRecord.java b/java/com/google/gerrit/common/data/SubmitRecord.java
index 22861b2..fe5843ad 100644
--- a/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
diff --git a/java/com/google/gerrit/common/data/SubscribeSection.java b/java/com/google/gerrit/common/data/SubscribeSection.java
index aaf0798..6ac4695 100644
--- a/java/com/google/gerrit/common/data/SubscribeSection.java
+++ b/java/com/google/gerrit/common/data/SubscribeSection.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -60,14 +60,14 @@
    * @param branch the branch to check
    * @return if the branch could trigger a superproject update
    */
-  public boolean appliesTo(Branch.NameKey branch) {
+  public boolean appliesTo(BranchNameKey branch) {
     for (RefSpec r : matchingRefSpecs) {
-      if (r.matchSource(branch.get())) {
+      if (r.matchSource(branch.branch())) {
         return true;
       }
     }
     for (RefSpec r : multiMatchRefSpecs) {
-      if (r.matchSource(branch.get())) {
+      if (r.matchSource(branch.branch())) {
         return true;
       }
     }
diff --git a/java/com/google/gerrit/common/data/testing/BUILD b/java/com/google/gerrit/common/data/testing/BUILD
index 8ab01de..b9ec30b 100644
--- a/java/com/google/gerrit/common/data/testing/BUILD
+++ b/java/com/google/gerrit/common/data/testing/BUILD
@@ -7,7 +7,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
index 265d590..d841aa6 100644
--- a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
+++ b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
@@ -21,9 +21,9 @@
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 
-public class GroupReferenceSubject extends Subject<GroupReferenceSubject, GroupReference> {
+public class GroupReferenceSubject extends Subject {
 
   public static GroupReferenceSubject assertThat(GroupReference group) {
     return assertAbout(groupReferences()).that(group);
@@ -33,19 +33,20 @@
     return GroupReferenceSubject::new;
   }
 
+  private final GroupReference group;
+
   private GroupReferenceSubject(FailureMetadata metadata, GroupReference group) {
     super(metadata, group);
+    this.group = group;
   }
 
-  public ComparableSubject<?, AccountGroup.UUID> groupUuid() {
+  public ComparableSubject<AccountGroup.UUID> groupUuid() {
     isNotNull();
-    GroupReference group = actual();
-    return check("groupUuid()").that(group.getUUID());
+    return check("getUUID()").that(group.getUUID());
   }
 
   public StringSubject name() {
     isNotNull();
-    GroupReference group = actual();
-    return check("name()").that(group.getName());
+    return check("getName()").that(group.getName());
   }
 }
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 8ed9729..ed32ce5 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.elasticsearch.builders.QueryBuilder;
 import com.google.gerrit.elasticsearch.builders.SearchSourceBuilder;
 import com.google.gerrit.elasticsearch.bulk.DeleteRequest;
+import com.google.gerrit.entities.converter.ProtoConverter;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.FieldType;
@@ -45,7 +46,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.ResultSet;
 import com.google.gerrit.proto.Protos;
-import com.google.gerrit.reviewdb.converter.ProtoConverter;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gson.Gson;
@@ -127,7 +127,6 @@
   private final SitePaths sitePaths;
   private final String indexNameRaw;
 
-  protected final String type;
   protected final ElasticRestClientProvider client;
   protected final String indexName;
   protected final Gson gson;
@@ -147,7 +146,6 @@
     this.indexName = config.getIndexName(indexName, schema.getVersion());
     this.indexNameRaw = indexName;
     this.client = client;
-    this.type = client.adapter().getType();
   }
 
   @Override
@@ -167,7 +165,7 @@
 
   @Override
   public void delete(K id) {
-    String uri = getURI(type, BULK);
+    String uri = getURI(BULK);
     Response response = postRequest(uri, getDeleteActions(id), getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
@@ -192,10 +190,8 @@
     }
 
     // Recreate the index.
-    String indexCreationFields = concatJsonString(getSettings(client.adapter()), getMappings());
-    response =
-        performRequest(
-            "PUT", indexName + client.adapter().includeTypeNameParam(), indexCreationFields);
+    String indexCreationFields = concatJsonString(getSettings(), getMappings());
+    response = performRequest("PUT", indexName, indexCreationFields);
     statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
       String error = String.format("Failed to create index %s: %s", indexName, statusCode);
@@ -207,26 +203,20 @@
 
   protected abstract String getMappings();
 
-  private String getSettings(ElasticQueryAdapter adapter) {
-    return gson.toJson(ImmutableMap.of(SETTINGS, ElasticSetting.createSetting(config, adapter)));
+  private String getSettings() {
+    return gson.toJson(ImmutableMap.of(SETTINGS, ElasticSetting.createSetting(config)));
   }
 
   protected abstract String getId(V v);
 
   protected String getMappingsForSingleType(MappingProperties properties) {
-    return getMappingsFor(client.adapter().getType(), properties);
+    return getMappingsFor(properties);
   }
 
-  protected String getMappingsFor(String type, MappingProperties properties) {
+  protected String getMappingsFor(MappingProperties properties) {
     JsonObject mappings = new JsonObject();
 
-    if (client.adapter().omitType()) {
-      mappings.add(MAPPINGS, gson.toJsonTree(properties));
-    } else {
-      JsonObject mappingType = new JsonObject();
-      mappingType.add(type, gson.toJsonTree(properties));
-      mappings.add(MAPPINGS, gson.toJsonTree(mappingType));
-    }
+    mappings.add(MAPPINGS, gson.toJsonTree(properties));
     return gson.toJson(mappings);
   }
 
@@ -305,15 +295,9 @@
     return sortArray;
   }
 
-  protected String getURI(String type, String request) {
+  protected String getURI(String request) {
     try {
-      String encodedIndexName = URLEncoder.encode(indexName, UTF_8.toString());
-      if (SEARCH.equals(request) && client.adapter().omitType()) {
-        return encodedIndexName + "/" + request;
-      }
-      String encodedTypeIfAny =
-          client.adapter().omitType() ? "" : "/" + URLEncoder.encode(type, UTF_8.toString());
-      return encodedIndexName + encodedTypeIfAny + "/" + request;
+      return URLEncoder.encode(indexName, UTF_8.toString()) + "/" + request;
     } catch (UnsupportedEncodingException e) {
       throw new StorageException(e);
     }
@@ -359,12 +343,10 @@
   protected class ElasticQuerySource implements DataSource<V> {
     private final QueryOptions opts;
     private final String search;
-    private final String index;
 
-    ElasticQuerySource(Predicate<V> p, QueryOptions opts, String index, JsonArray sortArray)
+    ElasticQuerySource(Predicate<V> p, QueryOptions opts, JsonArray sortArray)
         throws QueryParseException {
       this.opts = opts;
-      this.index = index;
       QueryBuilder qb = queryBuilder.toQueryBuilder(p);
       SearchSourceBuilder searchSource =
           new SearchSourceBuilder(client.adapter())
@@ -392,7 +374,7 @@
 
     private <T> ResultSet<T> readImpl(Function<JsonObject, T> mapper) {
       try {
-        String uri = getURI(index, SEARCH);
+        String uri = getURI(SEARCH);
         Response response =
             performRequest(HttpPost.METHOD_NAME, uri, search, Collections.emptyMap());
         StatusLine statusLine = response.getStatusLine();
diff --git a/java/com/google/gerrit/elasticsearch/BUILD b/java/com/google/gerrit/elasticsearch/BUILD
index a9b145b..edbd82c 100644
--- a/java/com/google/gerrit/elasticsearch/BUILD
+++ b/java/com/google/gerrit/elasticsearch/BUILD
@@ -6,6 +6,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
@@ -13,10 +14,10 @@
         "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/proto",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:gson",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:protobuf",
         "//lib/commons:codec",
         "//lib/commons:lang",
@@ -29,6 +30,5 @@
         "//lib/httpcomponents:httpcore",
         "//lib/httpcomponents:httpcore-nio",
         "//lib/jackson:jackson-core",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
index f646efc..bde3ad5 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -14,19 +14,17 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.gerrit.server.index.account.AccountField.ID;
-
 import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.SitePaths;
@@ -76,22 +74,29 @@
   public void replace(AccountState as) {
     BulkRequest bulk = new IndexRequest(getId(as), indexName).add(new UpdateRequest<>(schema, as));
 
-    String uri = getURI(type, BULK);
+    String uri = getURI(BULK);
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
       throw new StorageException(
           String.format(
               "Failed to replace account %s in index %s: %s",
-              as.getAccount().getId(), indexName, statusCode));
+              as.account().id(), indexName, statusCode));
     }
   }
 
   @Override
   public DataSource<AccountState> getSource(Predicate<AccountState> p, QueryOptions opts)
       throws QueryParseException {
-    JsonArray sortArray = getSortArray(AccountField.ID.getName());
-    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::accountFields), type, sortArray);
+    JsonArray sortArray =
+        getSortArray(
+            schema.useLegacyNumericFields()
+                ? AccountField.ID.getName()
+                : AccountField.ID_STR.getName());
+    return new ElasticQuerySource(
+        p,
+        opts.filterFields(o -> IndexUtils.accountFields(o, schema.useLegacyNumericFields())),
+        sortArray);
   }
 
   @Override
@@ -106,7 +111,7 @@
 
   @Override
   protected String getId(AccountState as) {
-    return as.getAccount().getId().toString();
+    return as.account().id().toString();
   }
 
   @Override
@@ -116,7 +121,15 @@
       source = json.getAsJsonObject().get("fields");
     }
 
-    Account.Id id = new Account.Id(source.getAsJsonObject().get(ID.getName()).getAsInt());
+    Account.Id id =
+        Account.id(
+            source
+                .getAsJsonObject()
+                .get(
+                    schema.useLegacyNumericFields()
+                        ? AccountField.ID.getName()
+                        : AccountField.ID_STR.getName())
+                .getAsInt());
     // Use the AccountCache rather than depending on any stored fields in the document (of which
     // there shouldn't be any). The most expensive part to compute anyway is the effective group
     // IDs, and we don't have a good way to reindex when those change.
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index ca2b1a8..2be1585 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
-import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 
@@ -23,25 +21,25 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
 import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.converter.ChangeProtoConverter;
+import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
+import com.google.gerrit.entities.converter.PatchSetProtoConverter;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.converter.ChangeProtoConverter;
-import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
-import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.StarredChangesUtil;
@@ -49,7 +47,6 @@
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
-import com.google.gerrit.server.index.change.ChangeIndexRewriter;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gson.JsonArray;
@@ -58,7 +55,6 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.Collections;
-import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import org.apache.http.HttpStatus;
@@ -81,12 +77,11 @@
   }
 
   private static final String CHANGES = "changes";
-  private static final String OPEN_CHANGES = "open_" + CHANGES;
-  private static final String CLOSED_CHANGES = "closed_" + CHANGES;
 
   private final ChangeMapping mapping;
   private final ChangeData.Factory changeDataFactory;
   private final Schema<ChangeData> schema;
+  private final FieldDef<ChangeData, ?> idField;
 
   @Inject
   ElasticChangeIndex(
@@ -98,14 +93,16 @@
     super(cfg, sitePaths, schema, clientBuilder, CHANGES);
     this.changeDataFactory = changeDataFactory;
     this.schema = schema;
-    mapping = new ChangeMapping(schema, client.adapter());
+    this.mapping = new ChangeMapping(schema, client.adapter());
+    this.idField =
+        this.schema.useLegacyNumericFields() ? ChangeField.LEGACY_ID : ChangeField.LEGACY_ID_STR;
   }
 
   @Override
   public void replace(ChangeData cd) {
     BulkRequest bulk = new IndexRequest(getId(cd), indexName).add(new UpdateRequest<>(schema, cd));
 
-    String uri = getURI(type, BULK);
+    String uri = getURI(BULK);
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
@@ -118,26 +115,9 @@
   @Override
   public DataSource<ChangeData> getSource(Predicate<ChangeData> p, QueryOptions opts)
       throws QueryParseException {
-    Set<Change.Status> statuses = ChangeIndexRewriter.getPossibleStatus(p);
-    List<String> indexes = Lists.newArrayListWithCapacity(2);
-    if (!client.adapter().omitType()) {
-      if (client.adapter().useV6Type()) {
-        if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()
-            || !Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
-          indexes.add(ElasticQueryAdapter.V6_TYPE);
-        }
-      } else {
-        if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
-          indexes.add(OPEN_CHANGES);
-        }
-        if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
-          indexes.add(CLOSED_CHANGES);
-        }
-      }
-    }
-
-    QueryOptions filteredOpts = opts.filterFields(IndexUtils::changeFields);
-    return new ElasticQuerySource(p, filteredOpts, getURI(indexes), getSortArray());
+    QueryOptions filteredOpts =
+        opts.filterFields(o -> IndexUtils.changeFields(o, schema.useLegacyNumericFields()));
+    return new ElasticQuerySource(p, filteredOpts, getSortArray());
   }
 
   private JsonArray getSortArray() {
@@ -146,14 +126,10 @@
 
     JsonArray sortArray = new JsonArray();
     addNamedElement(ChangeField.UPDATED.getName(), properties, sortArray);
-    addNamedElement(ChangeField.LEGACY_ID.getName(), properties, sortArray);
+    addNamedElement(idField.getName(), properties, sortArray);
     return sortArray;
   }
 
-  private String getURI(List<String> types) {
-    return String.join(",", types);
-  }
-
   @Override
   protected String getDeleteActions(Change.Id c) {
     return getDeleteRequest(c);
@@ -161,7 +137,7 @@
 
   @Override
   protected String getMappings() {
-    return getMappingsFor(client.adapter().getType(), mapping.changes);
+    return getMappingsFor(mapping.changes);
   }
 
   @Override
@@ -179,10 +155,10 @@
     JsonElement c = source.get(ChangeField.CHANGE.getName());
 
     if (c == null) {
-      int id = source.get(ChangeField.LEGACY_ID.getName()).getAsInt();
+      int id = source.get(idField.getName()).getAsInt();
       // IndexUtils#changeFields ensures either CHANGE or PROJECT is always present.
       String projectName = requireNonNull(source.get(ChangeField.PROJECT.getName()).getAsString());
-      return changeDataFactory.create(new Project.NameKey(projectName), new Change.Id(id));
+      return changeDataFactory.create(Project.nameKey(projectName), Change.id(id));
     }
 
     ChangeData cd =
@@ -252,7 +228,7 @@
           if (reviewedBy.size() == 1 && aId == ChangeField.NOT_REVIEWED) {
             break;
           }
-          accounts.add(new Account.Id(aId));
+          accounts.add(Account.id(aId));
         }
         cd.setReviewedBy(accounts);
       }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java b/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
index 35c33cb..06b128c 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
@@ -44,7 +44,7 @@
 
   static final String DEFAULT_PORT = "9200";
   static final String DEFAULT_USERNAME = "elastic";
-  static final int DEFAULT_NUMBER_OF_SHARDS = 0;
+  static final int DEFAULT_NUMBER_OF_SHARDS = 1;
   static final int DEFAULT_NUMBER_OF_REPLICAS = 1;
   static final int DEFAULT_MAX_RESULT_WINDOW = 10000;
 
@@ -107,10 +107,7 @@
     return String.format("%s%s_%04d", prefix, name, schemaVersion);
   }
 
-  int getNumberOfShards(ElasticQueryAdapter adapter) {
-    if (numberOfShards == DEFAULT_NUMBER_OF_SHARDS) {
-      return adapter.getDefaultNumberOfShards();
-    }
+  int getNumberOfShards() {
     return numberOfShards;
   }
 }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index eff3d52..241e7fd 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -18,13 +18,13 @@
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.group.InternalGroup;
@@ -75,7 +75,7 @@
     BulkRequest bulk =
         new IndexRequest(getId(group), indexName).add(new UpdateRequest<>(schema, group));
 
-    String uri = getURI(type, BULK);
+    String uri = getURI(BULK);
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
@@ -90,7 +90,7 @@
   public DataSource<InternalGroup> getSource(Predicate<InternalGroup> p, QueryOptions opts)
       throws QueryParseException {
     JsonArray sortArray = getSortArray(GroupField.UUID.getName());
-    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::groupFields), type, sortArray);
+    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::groupFields), sortArray);
   }
 
   @Override
@@ -116,8 +116,7 @@
     }
 
     AccountGroup.UUID uuid =
-        new AccountGroup.UUID(
-            source.getAsJsonObject().get(GroupField.UUID.getName()).getAsString());
+        AccountGroup.uuid(source.getAsJsonObject().get(GroupField.UUID.getName()).getAsString());
     // Use the GroupCache rather than depending on any stored fields in the
     // document (of which there shouldn't be any).
     return groupCache.get().get(uuid).orElse(null);
diff --git a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
index a0ebb07..36eeca1 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
@@ -27,7 +28,6 @@
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.project.ProjectCache;
@@ -76,7 +76,7 @@
         new IndexRequest(projectState.getProject().getName(), indexName)
             .add(new UpdateRequest<>(schema, projectState));
 
-    String uri = getURI(type, BULK);
+    String uri = getURI(BULK);
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
@@ -91,7 +91,7 @@
   public DataSource<ProjectData> getSource(Predicate<ProjectData> p, QueryOptions opts)
       throws QueryParseException {
     JsonArray sortArray = getSortArray(ProjectField.NAME.getName());
-    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::projectFields), type, sortArray);
+    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::projectFields), sortArray);
   }
 
   @Override
@@ -117,8 +117,7 @@
     }
 
     Project.NameKey nameKey =
-        new Project.NameKey(
-            source.getAsJsonObject().get(ProjectField.NAME.getName()).getAsString());
+        Project.nameKey(source.getAsJsonObject().get(ProjectField.NAME.getName()).getAsString());
     return projectCache.get().get(nameKey).toProjectData();
   }
 }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
index 779d433..19d9901 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
@@ -14,42 +14,23 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.gerrit.elasticsearch.ElasticVersion.V6_8;
-
 public class ElasticQueryAdapter {
-  static final String V6_TYPE = "_doc";
-
-  private static final String INCLUDE_TYPE = "include_type_name=true";
   private static final String INDICES = "?allow_no_indices=false";
 
-  private final boolean useV6Type;
-  private final boolean omitType;
-  private final int defaultNumberOfShards;
-
   private final String searchFilteringName;
-  private final String indicesExistParams;
   private final String exactFieldType;
   private final String stringFieldType;
   private final String indexProperty;
   private final String rawFieldsKey;
   private final String versionDiscoveryUrl;
-  private final String includeTypeNameParam;
 
-  ElasticQueryAdapter(ElasticVersion version) {
-    this.useV6Type = version.isV6();
-    this.omitType = version.isV7OrLater();
-    this.defaultNumberOfShards = version.isV7OrLater() ? 1 : 5;
-    this.versionDiscoveryUrl = version.isV6OrLater() ? "/%s*" : "/%s*/_aliases";
+  ElasticQueryAdapter() {
+    this.versionDiscoveryUrl = "/%s*";
     this.searchFilteringName = "_source";
     this.exactFieldType = "keyword";
     this.stringFieldType = "text";
     this.indexProperty = "true";
     this.rawFieldsKey = "_source";
-
-    // Since v6.7 (end-of-life), in fact, for these two parameters:
-    this.indicesExistParams =
-        version.isAtLeastMinorVersion(V6_8) ? INDICES + "&" + INCLUDE_TYPE : INDICES;
-    this.includeTypeNameParam = version.isAtLeastMinorVersion(V6_8) ? "?" + INCLUDE_TYPE : "";
   }
 
   public String searchFilteringName() {
@@ -57,7 +38,7 @@
   }
 
   String indicesExistParams() {
-    return indicesExistParams;
+    return INDICES;
   }
 
   String exactFieldType() {
@@ -76,27 +57,7 @@
     return rawFieldsKey;
   }
 
-  boolean useV6Type() {
-    return useV6Type;
-  }
-
-  boolean omitType() {
-    return omitType;
-  }
-
-  int getDefaultNumberOfShards() {
-    return defaultNumberOfShards;
-  }
-
-  String getType() {
-    return useV6Type() ? V6_TYPE : "";
-  }
-
   String getVersionDiscoveryUrl(String name) {
     return String.format(versionDiscoveryUrl, name);
   }
-
-  String includeTypeNameParam() {
-    return includeTypeNameParam;
-  }
 }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java b/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
index 394158d..d05e91c 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
@@ -145,7 +145,7 @@
     String name = p.getField().getName();
     String value = p.getValue();
 
-    if (value.isEmpty()) {
+    if (!p.getField().isRepeatable() && value.isEmpty()) {
       return new BoolQueryBuilder().mustNot(QueryBuilders.existsQuery(name));
     } else if (p instanceof RegexPredicate) {
       if (value.startsWith("^")) {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java b/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
index a67de44..f635b23 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
@@ -65,7 +65,7 @@
           client = build();
           ElasticVersion version = getVersion();
           logger.atInfo().log("Elasticsearch integration version %s", version);
-          adapter = new ElasticQueryAdapter(version);
+          adapter = new ElasticQueryAdapter();
         }
       }
     }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticSetting.java b/java/com/google/gerrit/elasticsearch/ElasticSetting.java
index e016efb..7ec0566 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticSetting.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticSetting.java
@@ -22,18 +22,18 @@
   private static final ImmutableMap<String, String> CUSTOM_CHAR_MAPPING =
       ImmutableMap.of("\\u002E", "\\u0020", "\\u005F", "\\u0020");
 
-  static SettingProperties createSetting(ElasticConfiguration config, ElasticQueryAdapter adapter) {
-    return new ElasticSetting.Builder().addCharFilter().addAnalyzer().build(config, adapter);
+  static SettingProperties createSetting(ElasticConfiguration config) {
+    return new ElasticSetting.Builder().addCharFilter().addAnalyzer().build(config);
   }
 
   static class Builder {
     private final ImmutableMap.Builder<String, FieldProperties> fields =
         new ImmutableMap.Builder<>();
 
-    SettingProperties build(ElasticConfiguration config, ElasticQueryAdapter adapter) {
+    SettingProperties build(ElasticConfiguration config) {
       SettingProperties properties = new SettingProperties();
       properties.analysis = fields.build();
-      properties.numberOfShards = config.getNumberOfShards(adapter);
+      properties.numberOfShards = config.getNumberOfShards();
       properties.numberOfReplicas = config.numberOfReplicas;
       properties.maxResultWindow = config.maxResultWindow;
       return properties;
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index b3f1471..5e72780 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -18,11 +18,6 @@
 import java.util.regex.Pattern;
 
 public enum ElasticVersion {
-  V6_8("6.8.*"),
-  V7_0("7.0.*"),
-  V7_1("7.1.*"),
-  V7_2("7.2.*"),
-  V7_3("7.3.*"),
   V7_4("7.4.*"),
   V7_5("7.5.*"),
   V7_6("7.6.*"),
@@ -67,34 +62,6 @@
     return Joiner.on(", ").join(ElasticVersion.values());
   }
 
-  public boolean isV6() {
-    return getMajor() == 6;
-  }
-
-  public boolean isV6OrLater() {
-    return isAtLeastVersion(6);
-  }
-
-  public boolean isV7OrLater() {
-    return isAtLeastVersion(7);
-  }
-
-  private boolean isAtLeastVersion(int major) {
-    return getMajor() >= major;
-  }
-
-  public boolean isAtLeastMinorVersion(ElasticVersion version) {
-    return getMajor().equals(version.getMajor()) && getMinor() >= version.getMinor();
-  }
-
-  private Integer getMajor() {
-    return Integer.valueOf(version.split("\\.")[0]);
-  }
-
-  private Integer getMinor() {
-    return Integer.valueOf(version.split("\\.")[1]);
-  }
-
   @Override
   public String toString() {
     return version;
diff --git a/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java b/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java
index a693f6d..2f0bd01 100644
--- a/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java
+++ b/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java
@@ -40,11 +40,7 @@
       for (Values<V> values : schema.buildFields(v)) {
         String name = values.getField().getName();
         if (values.getField().isRepeatable()) {
-          builder.field(
-              name,
-              Streams.stream(values.getValues())
-                  .filter(e -> shouldAddElement(e))
-                  .collect(toList()));
+          builder.field(name, Streams.stream(values.getValues()).collect(toList()));
         } else {
           Object element = Iterables.getOnlyElement(values.getValues(), "");
           if (shouldAddElement(element)) {
diff --git a/java/com/google/gerrit/reviewdb/client/Account.java b/java/com/google/gerrit/entities/Account.java
similarity index 63%
rename from java/com/google/gerrit/reviewdb/client/Account.java
rename to java/com/google/gerrit/entities/Account.java
index fa3abed..809db1e 100644
--- a/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -12,15 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_DRAFT_COMMENTS;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_STARRED_CHANGES;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS;
+import static com.google.gerrit.entities.RefNames.REFS_DRAFT_COMMENTS;
+import static com.google.gerrit.entities.RefNames.REFS_STARRED_CHANGES;
+import static com.google.gerrit.entities.RefNames.REFS_USERS;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gwtorm.client.IntKey;
 import java.sql.Timestamp;
 import java.util.Optional;
 
@@ -37,44 +38,24 @@
  *   <li>ExternalId: OpenID identities and email addresses known to be registered to this user.
  *       Multiple records can exist when the user has more than one public identity, such as a work
  *       and a personal email address.
- *   <li>{@link AccountGroupMember}: membership of the user in a specific human managed {@link
- *       AccountGroup}. Multiple records can exist when the user is a member of more than one group.
  *   <li>AccountSshKey: user's public SSH keys, for authentication through the internal SSH daemon.
  *       One record per SSH key uploaded by the user, keys are checked in random order until a match
  *       is found.
  *   <li>{@link DiffPreferencesInfo}: user's preferences for rendering side-to-side and unified diff
  * </ul>
  */
-public final class Account {
+@AutoValue
+public abstract class Account {
   public static Id id(int id) {
-    return new Id(id);
+    return new AutoValue_Account_Id(id);
   }
 
   /** Key local to Gerrit to identify a user. */
-  public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    protected int id;
-
-    protected Id() {}
-
-    public Id(int id) {
-      this.id = id;
-    }
-
-    @Override
-    public int get() {
-      return id;
-    }
-
-    @Override
-    protected void set(int newValue) {
-      id = newValue;
-    }
-
+  @AutoValue
+  public abstract static class Id implements Comparable<Id> {
     /** Parse an Account.Id out of a string representation. */
     public static Optional<Id> tryParse(String str) {
-      return Optional.ofNullable(Ints.tryParse(str)).map(Id::new);
+      return Optional.ofNullable(Ints.tryParse(str)).map(Account::id);
     }
 
     public static Id fromRef(String name) {
@@ -99,12 +80,12 @@
      */
     public static Id fromRefPart(String name) {
       Integer id = RefNames.parseShardedRefPart(name);
-      return id != null ? new Account.Id(id) : null;
+      return id != null ? Account.id(id) : null;
     }
 
     public static Id parseAfterShardedRefPart(String name) {
       Integer id = RefNames.parseAfterShardedRefPart(name);
-      return id != null ? new Account.Id(id) : null;
+      return id != null ? Account.id(id) : null;
     }
 
     /**
@@ -119,34 +100,52 @@
      */
     public static Id fromRefSuffix(String name) {
       Integer id = RefNames.parseRefSuffix(name);
-      return id != null ? new Account.Id(id) : null;
+      return id != null ? Account.id(id) : null;
+    }
+
+    abstract int id();
+
+    public int get() {
+      return id();
+    }
+
+    @Override
+    public final int compareTo(Id o) {
+      return Integer.compare(id(), o.id());
+    }
+
+    @Override
+    public final String toString() {
+      return Integer.toString(get());
     }
   }
 
-  private Id accountId;
+  public abstract Id id();
 
   /** Date and time the user registered with the review server. */
-  private Timestamp registeredOn;
+  public abstract Timestamp registeredOn();
 
   /** Full name of the user ("Given-name Surname" style). */
-  private String fullName;
+  @Nullable
+  public abstract String fullName();
 
   /** Email address the user prefers to be contacted through. */
-  private String preferredEmail;
+  @Nullable
+  public abstract String preferredEmail();
 
   /**
    * Is this user inactive? This is used to avoid showing some users (eg. former employees) in
    * auto-suggest.
    */
-  private boolean inactive;
+  public abstract boolean inactive();
 
   /** The user-settable status of this account (e.g. busy, OOO, available) */
-  private String status;
+  @Nullable
+  public abstract String status();
 
   /** ID of the user branch from which the account was read. */
-  private String metaId;
-
-  protected Account() {}
+  @Nullable
+  public abstract String metaId();
 
   /**
    * Create a new account.
@@ -154,38 +153,11 @@
    * @param newId unique id, see {@link com.google.gerrit.server.notedb.Sequences#nextAccountId()}.
    * @param registeredOn when the account was registered.
    */
-  public Account(Account.Id newId, Timestamp registeredOn) {
-    this.accountId = newId;
-    this.registeredOn = registeredOn;
-  }
-
-  /** Get local id of this account, to link with in other entities */
-  public Account.Id getId() {
-    return accountId;
-  }
-
-  /** Get the full name of the user ("Given-name Surname" style). */
-  public String getFullName() {
-    return fullName;
-  }
-
-  /** Set the full name of the user ("Given-name Surname" style). */
-  public void setFullName(String name) {
-    if (name != null && !name.trim().isEmpty()) {
-      fullName = name.trim();
-    } else {
-      fullName = null;
-    }
-  }
-
-  /** Email address the user prefers to be contacted through. */
-  public String getPreferredEmail() {
-    return preferredEmail;
-  }
-
-  /** Set the email address the user prefers to be contacted through. */
-  public void setPreferredEmail(String addr) {
-    preferredEmail = addr;
+  public static Account.Builder builder(Account.Id newId, Timestamp registeredOn) {
+    return new AutoValue_Account.Builder()
+        .setInactive(false)
+        .setId(newId)
+        .setRegisteredOn(registeredOn);
   }
 
   /**
@@ -201,13 +173,13 @@
    *     generic string containing the accountId.
    */
   public String getName() {
-    if (fullName != null) {
-      return fullName;
+    if (fullName() != null) {
+      return fullName();
     }
-    if (preferredEmail != null) {
-      return preferredEmail;
+    if (preferredEmail() != null) {
+      return preferredEmail();
     }
-    return getName(accountId);
+    return getName(id());
   }
 
   public static String getName(Account.Id accountId) {
@@ -227,62 +199,70 @@
    * </ul>
    */
   public String getNameEmail(String anonymousCowardName) {
-    String name = fullName != null ? fullName : anonymousCowardName;
+    String name = fullName() != null ? fullName() : anonymousCowardName;
     StringBuilder b = new StringBuilder();
     b.append(name);
-    if (preferredEmail != null) {
+    if (preferredEmail() != null) {
       b.append(" <");
-      b.append(preferredEmail);
+      b.append(preferredEmail());
       b.append(">");
     } else {
       b.append(" (");
-      b.append(accountId.get());
+      b.append(id().get());
       b.append(")");
     }
     return b.toString();
   }
 
-  /** Get the date and time the user first registered. */
-  public Timestamp getRegisteredOn() {
-    return registeredOn;
-  }
-
-  public String getMetaId() {
-    return metaId;
-  }
-
-  public void setMetaId(String metaId) {
-    this.metaId = metaId;
-  }
-
   public boolean isActive() {
-    return !inactive;
+    return !inactive();
   }
 
-  public void setActive(boolean active) {
-    inactive = !active;
-  }
+  public abstract Builder toBuilder();
 
-  public String getStatus() {
-    return status;
-  }
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Id id();
 
-  public void setStatus(String status) {
-    this.status = status;
+    abstract Builder setId(Id id);
+
+    public abstract Timestamp registeredOn();
+
+    abstract Builder setRegisteredOn(Timestamp registeredOn);
+
+    @Nullable
+    public abstract String fullName();
+
+    public abstract Builder setFullName(String fullName);
+
+    @Nullable
+    public abstract String preferredEmail();
+
+    public abstract Builder setPreferredEmail(String preferredEmail);
+
+    public abstract boolean inactive();
+
+    public abstract Builder setInactive(boolean inactive);
+
+    public Builder setActive(boolean active) {
+      return setInactive(!active);
+    }
+
+    @Nullable
+    public abstract String status();
+
+    public abstract Builder setStatus(String status);
+
+    @Nullable
+    public abstract String metaId();
+
+    public abstract Builder setMetaId(@Nullable String metaId);
+
+    public abstract Account build();
   }
 
   @Override
-  public boolean equals(Object o) {
-    return o instanceof Account && ((Account) o).getId().equals(getId());
-  }
-
-  @Override
-  public int hashCode() {
-    return getId().get();
-  }
-
-  @Override
-  public String toString() {
+  public final String toString() {
     return getName();
   }
 }
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/java/com/google/gerrit/entities/AccountGroup.java
similarity index 82%
rename from java/com/google/gerrit/reviewdb/client/AccountGroup.java
rename to java/com/google/gerrit/entities/AccountGroup.java
index 0db7bbd..c10edc2 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/java/com/google/gerrit/entities/AccountGroup.java
@@ -12,11 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
+import com.google.auto.value.AutoValue;
 import com.google.gerrit.common.Nullable;
-import com.google.gwtorm.client.IntKey;
-import com.google.gwtorm.client.StringKey;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.Objects;
@@ -34,63 +33,45 @@
   }
 
   public static NameKey nameKey(String n) {
-    return new NameKey(n);
+    return new AutoValue_AccountGroup_NameKey(n);
   }
 
   /** Group name key */
-  public static class NameKey extends StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
+  @AutoValue
+  public abstract static class NameKey implements Comparable<NameKey> {
+    abstract String name();
 
-    protected String name;
-
-    protected NameKey() {}
-
-    public NameKey(String n) {
-      name = n;
-    }
-
-    @Override
     public String get() {
-      return name;
+      return name();
     }
 
     @Override
-    protected void set(String newValue) {
-      name = newValue;
+    public final int compareTo(NameKey o) {
+      return name().compareTo(o.name());
+    }
+
+    @Override
+    public final String toString() {
+      return KeyUtil.encode(get());
     }
   }
 
   public static UUID uuid(String n) {
-    return new UUID(n);
+    return new AutoValue_AccountGroup_UUID(n);
   }
 
   /** Globally unique identifier. */
-  public static class UUID extends StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
+  @AutoValue
+  public abstract static class UUID implements Comparable<UUID> {
+    abstract String uuid();
 
-    protected String uuid;
-
-    protected UUID() {}
-
-    public UUID(String n) {
-      uuid = n;
-    }
-
-    @Override
     public String get() {
-      return uuid;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      uuid = newValue;
+      return uuid();
     }
 
     /** Parse an {@link AccountGroup.UUID} out of a string representation. */
     public static UUID parse(String str) {
-      final UUID r = new UUID();
-      r.fromString(str);
-      return r;
+      return AccountGroup.uuid(KeyUtil.decode(str));
     }
 
     /** Parse an {@link AccountGroup.UUID} out of a ref-name. */
@@ -112,7 +93,17 @@
      */
     public static UUID fromRefPart(String refPart) {
       String uuid = RefNames.parseShardedUuidFromRefPart(refPart);
-      return uuid != null ? new AccountGroup.UUID(uuid) : null;
+      return uuid != null ? AccountGroup.uuid(uuid) : null;
+    }
+
+    @Override
+    public final int compareTo(UUID o) {
+      return uuid().compareTo(o.uuid());
+    }
+
+    @Override
+    public final String toString() {
+      return KeyUtil.encode(get());
     }
   }
 
@@ -122,36 +113,26 @@
   }
 
   public static Id id(int id) {
-    return new Id(id);
+    return new AutoValue_AccountGroup_Id(id);
   }
 
   /** Synthetic key to link to within the database */
-  public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
+  @AutoValue
+  public abstract static class Id {
+    abstract int id();
 
-    protected int id;
-
-    protected Id() {}
-
-    public Id(int id) {
-      this.id = id;
-    }
-
-    @Override
     public int get() {
-      return id;
-    }
-
-    @Override
-    protected void set(int newValue) {
-      id = newValue;
+      return id();
     }
 
     /** Parse an AccountGroup.Id out of a string representation. */
     public static Id parse(String str) {
-      final Id r = new Id();
-      r.fromString(str);
-      return r;
+      return AccountGroup.id(Integer.parseInt(str));
+    }
+
+    @Override
+    public final String toString() {
+      return Integer.toString(get());
     }
   }
 
diff --git a/java/com/google/gerrit/entities/AccountGroupByIdAudit.java b/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
new file mode 100644
index 0000000..17ddf51
--- /dev/null
+++ b/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2011 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.entities;
+
+import com.google.auto.value.AutoValue;
+import java.sql.Timestamp;
+import java.util.Optional;
+
+/** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
+@AutoValue
+public abstract class AccountGroupByIdAudit {
+  public static Builder builder() {
+    return new AutoValue_AccountGroupByIdAudit.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder groupId(AccountGroup.Id groupId);
+
+    public abstract Builder includeUuid(AccountGroup.UUID includeUuid);
+
+    public abstract Builder addedBy(Account.Id addedBy);
+
+    public abstract Builder addedOn(Timestamp addedOn);
+
+    abstract Builder removedBy(Account.Id removedBy);
+
+    abstract Builder removedOn(Timestamp removedOn);
+
+    public Builder removed(Account.Id removedBy, Timestamp removedOn) {
+      return removedBy(removedBy).removedOn(removedOn);
+    }
+
+    public abstract AccountGroupByIdAudit build();
+  }
+
+  public abstract AccountGroup.Id groupId();
+
+  public abstract AccountGroup.UUID includeUuid();
+
+  public abstract Account.Id addedBy();
+
+  public abstract Timestamp addedOn();
+
+  public abstract Optional<Account.Id> removedBy();
+
+  public abstract Optional<Timestamp> removedOn();
+
+  public abstract Builder toBuilder();
+
+  public boolean isActive() {
+    return !removedOn().isPresent();
+  }
+}
diff --git a/java/com/google/gerrit/entities/AccountGroupMemberAudit.java b/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
new file mode 100644
index 0000000..4d191b8
--- /dev/null
+++ b/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2009 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.entities;
+
+import com.google.auto.value.AutoValue;
+import java.sql.Timestamp;
+import java.util.Optional;
+
+/** Membership of an {@link Account} in an {@link AccountGroup}. */
+@AutoValue
+public abstract class AccountGroupMemberAudit {
+  public static Builder builder() {
+    return new AutoValue_AccountGroupMemberAudit.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder groupId(AccountGroup.Id groupId);
+
+    public abstract Builder memberId(Account.Id accountId);
+
+    public abstract Builder addedBy(Account.Id addedBy);
+
+    abstract Account.Id addedBy();
+
+    public abstract Builder addedOn(Timestamp addedOn);
+
+    abstract Timestamp addedOn();
+
+    abstract Builder removedBy(Account.Id removedBy);
+
+    abstract Builder removedOn(Timestamp removedOn);
+
+    public Builder removed(Account.Id removedBy, Timestamp removedOn) {
+      return removedBy(removedBy).removedOn(removedOn);
+    }
+
+    public Builder removedLegacy() {
+      return removed(addedBy(), addedOn());
+    }
+
+    public abstract AccountGroupMemberAudit build();
+  }
+
+  public abstract AccountGroup.Id groupId();
+
+  public abstract Account.Id memberId();
+
+  public abstract Account.Id addedBy();
+
+  public abstract Timestamp addedOn();
+
+  public abstract Optional<Account.Id> removedBy();
+
+  public abstract Optional<Timestamp> removedOn();
+
+  public abstract Builder toBuilder();
+
+  public boolean isActive() {
+    return !removedOn().isPresent();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/BUILD b/java/com/google/gerrit/entities/BUILD
similarity index 69%
rename from java/com/google/gerrit/reviewdb/BUILD
rename to java/com/google/gerrit/entities/BUILD
index 3bc6528..8784bd8 100644
--- a/java/com/google/gerrit/reviewdb/BUILD
+++ b/java/com/google/gerrit/entities/BUILD
@@ -5,14 +5,17 @@
 )
 
 java_library(
-    name = "server",
+    name = "entities",
     srcs = glob(["**/*.java"]),
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gwtorm",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:protobuf",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
+        "//lib/errorprone:annotations",
         "//proto:entities_java_proto",
     ],
 )
diff --git a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java b/java/com/google/gerrit/entities/BooleanProjectConfig.java
similarity index 97%
rename from java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
rename to java/com/google/gerrit/entities/BooleanProjectConfig.java
index a70d254..5201f6d 100644
--- a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
+++ b/java/com/google/gerrit/entities/BooleanProjectConfig.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 /**
  * Contains all inheritable boolean project configs and maps internal representations to API
diff --git a/java/com/google/gerrit/entities/BranchNameKey.java b/java/com/google/gerrit/entities/BranchNameKey.java
new file mode 100644
index 0000000..cbb2e25
--- /dev/null
+++ b/java/com/google/gerrit/entities/BranchNameKey.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.entities;
+
+import com.google.auto.value.AutoValue;
+
+/** Branch name key */
+@AutoValue
+public abstract class BranchNameKey implements Comparable<BranchNameKey> {
+  public static BranchNameKey create(Project.NameKey projectName, String branchName) {
+    return new AutoValue_BranchNameKey(projectName, RefNames.fullName(branchName));
+  }
+
+  public static BranchNameKey create(String projectName, String branchName) {
+    return create(Project.nameKey(projectName), branchName);
+  }
+
+  public abstract Project.NameKey project();
+
+  public abstract String branch();
+
+  public String shortName() {
+    return RefNames.shortName(branch());
+  }
+
+  @Override
+  public final int compareTo(BranchNameKey o) {
+    // TODO(dborowitz): Only compares branch name in order to match old StringKey behavior.
+    // Consider comparing project name first.
+    return branch().compareTo(o.branch());
+  }
+
+  @Override
+  public final String toString() {
+    return project() + "," + KeyUtil.encode(branch());
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/client/Change.java b/java/com/google/gerrit/entities/Change.java
similarity index 85%
rename from java/com/google/gerrit/reviewdb/client/Change.java
rename to java/com/google/gerrit/entities/Change.java
index 79739dc..739bd38 100644
--- a/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -12,19 +12,26 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeStatus;
-import com.google.gwtorm.client.IntKey;
-import com.google.gwtorm.client.StringKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
 import java.sql.Timestamp;
 import java.util.Arrays;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 
 /**
- * A change proposed to be merged into a {@link Branch}.
+ * A change proposed to be merged into a branch.
  *
  * <p>The data graph rooted below a Change can be quite complex:
  *
@@ -37,7 +44,7 @@
  *          |
  *          +- {@link PatchSetApproval}: a +/- vote on the change's current state.
  *          |
- *          +- {@link PatchLineComment}: comment about a specific line
+ *          +- {@link Comment}: comment about a specific line
  * </pre>
  *
  * <p>
@@ -53,8 +60,8 @@
  * commit can contain zero patches, if the merge has no conflicts, or has no impact other than to
  * cut off a line of development.
  *
- * <p>Each PatchLineComment is a draft or a published comment about a single line of the associated
- * file. These are the inline comment entities created by users as they perform a review.
+ * <p>Each Comment is a draft or a published comment about a single line of the associated file.
+ * These are the inline comment entities created by users as they perform a review.
  *
  * <p>When additional PatchSets appear under a change, these PatchSets reference <i>replacement</i>
  * commits; alternative commits that could be made to the project instead of the original commit
@@ -69,10 +76,10 @@
  * <h5>ChangeMessage</h5>
  *
  * <p>The ChangeMessage entity is a general free-form comment about the whole change, rather than
- * PatchLineComment's file and line specific context. The ChangeMessage appears at the start of any
- * email generated by Gerrit, and is shown on the change overview page, rather than in a
- * file-specific context. Users often use this entity to describe general remarks about the overall
- * concept proposed by the change.
+ * Comment's file and line specific context. The ChangeMessage appears at the start of any email
+ * generated by Gerrit, and is shown on the change overview page, rather than in a file-specific
+ * context. Users often use this entity to describe general remarks about the overall concept
+ * proposed by the change.
  *
  * <p>
  *
@@ -93,49 +100,27 @@
  * notice of a replacement patch set is sent, or when notice of the change submission occurs.
  */
 public final class Change {
-  public static Id id(int id) {
-    return new Id(id);
+  private static final SecureRandom rng;
+
+  static {
+    try {
+      rng = SecureRandom.getInstance("SHA1PRNG");
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException("Cannot create RNG for Change-Id generator", e);
+    }
   }
 
-  public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
+  public static Id id(int id) {
+    return new AutoValue_Change_Id(id);
+  }
 
-    public int id;
-
-    protected Id() {}
-
-    public Id(int id) {
-      this.id = id;
-    }
-
-    @Override
-    public int get() {
-      return id;
-    }
-
-    @Override
-    protected void set(int newValue) {
-      id = newValue;
-    }
-
-    public String toRefPrefix() {
-      return refPrefixBuilder().toString();
-    }
-
-    StringBuilder refPrefixBuilder() {
-      StringBuilder r = new StringBuilder(32).append(REFS_CHANGES);
-      int m = id % 100;
-      if (m < 10) {
-        r.append('0');
-      }
-      return r.append(m).append('/').append(id).append('/');
-    }
-
+  @AutoValue
+  public abstract static class Id {
     /** Parse a Change.Id out of a string representation. */
     public static Id parse(String str) {
-      final Id r = new Id();
-      r.fromString(str);
-      return r;
+      Integer id = Ints.tryParse(str);
+      checkArgument(id != null, "invalid change ID: %s", str);
+      return Change.id(id);
     }
 
     public static Id fromRef(String ref) {
@@ -150,7 +135,7 @@
       if (ref.substring(ce).equals(RefNames.META_SUFFIX)
           || ref.substring(ce).equals(RefNames.ROBOT_COMMENTS_SUFFIX)
           || PatchSet.Id.fromRef(ref, ce) >= 0) {
-        return new Change.Id(Integer.parseInt(ref.substring(cs, ce)));
+        return Change.id(Integer.parseInt(ref.substring(cs, ce)));
       }
       return null;
     }
@@ -173,7 +158,7 @@
       }
       int ce = nextNonDigit(ref, cs);
       if (ce < ref.length() && ref.charAt(ce) == '/' && isNumeric(ref, ce + 1)) {
-        return new Change.Id(Integer.parseInt(ref.substring(cs, ce)));
+        return Change.id(Integer.parseInt(ref.substring(cs, ce)));
       }
       return null;
     }
@@ -195,14 +180,14 @@
       int endChangeId = nextNonDigit(ref, startChangeId);
       String id = ref.substring(startChangeId, endChangeId);
       if (id != null && !id.isEmpty()) {
-        return new Change.Id(Integer.parseInt(id));
+        return Change.id(Integer.parseInt(id));
       }
       return null;
     }
 
     public static Id fromRefPart(String ref) {
       Integer id = RefNames.parseShardedRefPart(ref);
-      return id != null ? new Change.Id(id) : null;
+      return id != null ? Change.id(id) : null;
     }
 
     static int startIndex(String ref) {
@@ -253,35 +238,66 @@
       }
       return i;
     }
+
+    abstract int id();
+
+    public int get() {
+      return id();
+    }
+
+    public String toRefPrefix() {
+      return refPrefixBuilder().toString();
+    }
+
+    StringBuilder refPrefixBuilder() {
+      StringBuilder r = new StringBuilder(32).append(REFS_CHANGES);
+      int m = get() % 100;
+      if (m < 10) {
+        r.append('0');
+      }
+      return r.append(m).append('/').append(get()).append('/');
+    }
+
+    @Override
+    public final String toString() {
+      return Integer.toString(get());
+    }
+  }
+
+  public static ObjectId generateChangeId() {
+    byte[] rand = new byte[Constants.OBJECT_ID_STRING_LENGTH];
+    rng.nextBytes(rand);
+    String randomString = new String(rand, UTF_8);
+
+    try (ObjectInserter f = new ObjectInserter.Formatter()) {
+      return f.idFor(Constants.OBJ_COMMIT, Constants.encode(randomString));
+    }
+  }
+
+  public static Key generateKey() {
+    return key("I" + generateChangeId().name());
   }
 
   public static Key key(String key) {
-    return new Key(key);
+    return new AutoValue_Change_Key(key);
   }
 
   /**
    * Globally unique identification of this change. This generally takes the form of a string
    * "Ixxxxxx...", and is stored in the Change-Id footer of a commit.
    */
-  public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    protected String id;
-
-    protected Key() {}
-
-    public Key(String id) {
-      this.id = id;
+  @AutoValue
+  public abstract static class Key {
+    // TODO(dborowitz): This hardly seems worth it: why would someone pass a URL-encoded change key?
+    // Ideally the standard key() factory method would enforce the format and throw IAE.
+    public static Key parse(String str) {
+      return Change.key(KeyUtil.decode(str));
     }
 
-    @Override
+    abstract String key();
+
     public String get() {
-      return id;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      id = newValue;
+      return key();
     }
 
     /** Construct a key that is after all keys prefixed by this key. */
@@ -289,7 +305,7 @@
       final StringBuilder revEnd = new StringBuilder(get().length() + 1);
       revEnd.append(get());
       revEnd.append('\u9fa5');
-      return new Key(revEnd.toString());
+      return Change.key(revEnd.toString());
     }
 
     /** Obtain a shorter version of this key string, using a leading prefix. */
@@ -298,11 +314,9 @@
       return s.substring(0, Math.min(s.length(), 9));
     }
 
-    /** Parse a Change.Key out of a string representation. */
-    public static Key parse(String str) {
-      final Key r = new Key();
-      r.fromString(str);
-      return r;
+    @Override
+    public final String toString() {
+      return get();
     }
   }
 
@@ -412,13 +426,6 @@
         }
       }
 
-      // TODO(davido): Remove in 3.0, after all sites upgraded to version,
-      // where DRAFT status was removed. This code path is still needed,
-      // when changes are deserialized from the secondary index, during
-      // the online migration to the new schema version wasn't completed.
-      if (c == 'd') {
-        return Status.NEW;
-      }
       return null;
     }
 
@@ -456,7 +463,7 @@
   protected Account.Id owner;
 
   /** The branch (and project) this change merges into. */
-  protected Branch.NameKey dest;
+  protected BranchNameKey dest;
 
   // DELETED: id = 9 (open)
 
@@ -512,7 +519,7 @@
       Change.Key newKey,
       Change.Id newId,
       Account.Id ownedBy,
-      Branch.NameKey forBranch,
+      BranchNameKey forBranch,
       Timestamp ts) {
     changeKey = newKey;
     changeId = newId;
@@ -599,16 +606,16 @@
     this.owner = owner;
   }
 
-  public Branch.NameKey getDest() {
+  public BranchNameKey getDest() {
     return dest;
   }
 
-  public void setDest(Branch.NameKey dest) {
+  public void setDest(BranchNameKey dest) {
     this.dest = dest;
   }
 
   public Project.NameKey getProject() {
-    return dest.getParentKey();
+    return dest.project();
   }
 
   public String getSubject() {
@@ -626,7 +633,7 @@
   /** Get the id of the most current {@link PatchSet} in this change. */
   public PatchSet.Id currentPatchSetId() {
     if (currentPatchSetId > 0) {
-      return new PatchSet.Id(changeId, currentPatchSetId);
+      return PatchSet.id(changeId, currentPatchSetId);
     }
     return null;
   }
@@ -649,7 +656,7 @@
   }
 
   public void setCurrentPatchSet(PatchSet.Id psId, String subject, String originalSubject) {
-    if (!psId.getParentKey().equals(changeId)) {
+    if (!psId.changeId().equals(changeId)) {
       throw new IllegalArgumentException("patch set ID " + psId + " is not for change " + changeId);
     }
     currentPatchSetId = psId.get();
diff --git a/java/com/google/gerrit/reviewdb/client/ChangeMessage.java b/java/com/google/gerrit/entities/ChangeMessage.java
similarity index 82%
rename from java/com/google/gerrit/reviewdb/client/ChangeMessage.java
rename to java/com/google/gerrit/entities/ChangeMessage.java
index de318bd..f34cc7d 100644
--- a/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
+++ b/java/com/google/gerrit/entities/ChangeMessage.java
@@ -12,57 +12,24 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
+import com.google.auto.value.AutoValue;
 import com.google.gerrit.common.Nullable;
-import com.google.gwtorm.client.StringKey;
 import java.sql.Timestamp;
 import java.util.Objects;
 
 /** A message attached to a {@link Change}. */
 public final class ChangeMessage {
   public static Key key(Change.Id changeId, String uuid) {
-    return new Key(changeId, uuid);
+    return new AutoValue_ChangeMessage_Key(changeId, uuid);
   }
 
-  public static class Key extends StringKey<Change.Id> {
-    private static final long serialVersionUID = 1L;
+  @AutoValue
+  public abstract static class Key {
+    public abstract Change.Id changeId();
 
-    protected Change.Id changeId;
-
-    protected String uuid;
-
-    protected Key() {
-      changeId = new Change.Id();
-    }
-
-    public Key(Change.Id change, String uuid) {
-      this.changeId = change;
-      this.uuid = uuid;
-    }
-
-    @Override
-    public Change.Id getParentKey() {
-      return changeId;
-    }
-
-    public Change.Id changeId() {
-      return getParentKey();
-    }
-
-    @Override
-    public String get() {
-      return uuid;
-    }
-
-    public String uuid() {
-      return get();
-    }
-
-    @Override
-    public void set(String newValue) {
-      uuid = newValue;
-    }
+    public abstract String uuid();
   }
 
   protected Key key;
diff --git a/java/com/google/gerrit/reviewdb/client/CodedEnum.java b/java/com/google/gerrit/entities/CodedEnum.java
similarity index 94%
rename from java/com/google/gerrit/reviewdb/client/CodedEnum.java
rename to java/com/google/gerrit/entities/CodedEnum.java
index 11e7efa..90629a0 100644
--- a/java/com/google/gerrit/reviewdb/client/CodedEnum.java
+++ b/java/com/google/gerrit/entities/CodedEnum.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 /** Extension of Enum which provides distinct character code values. */
 public interface CodedEnum {
diff --git a/java/com/google/gerrit/reviewdb/client/Comment.java b/java/com/google/gerrit/entities/Comment.java
similarity index 62%
rename from java/com/google/gerrit/reviewdb/client/Comment.java
rename to java/com/google/gerrit/entities/Comment.java
index e03d0fa..55d739a 100644
--- a/java/com/google/gerrit/reviewdb/client/Comment.java
+++ b/java/com/google/gerrit/entities/Comment.java
@@ -12,11 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.gerrit.common.Nullable;
 import java.sql.Timestamp;
 import java.util.Comparator;
 import java.util.Objects;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
 
 /**
  * This class represents inline comments in NoteDb. This means it determines the JSON format for
@@ -24,30 +29,33 @@
  *
  * <p>Changing fields in this class changes the storage format of inline comments in NoteDb and may
  * require a corresponding data migration (adding new optional fields is generally okay).
- *
- * <p>{@link PatchLineComment} historically represented comments in ReviewDb. There are a few
- * notable differences:
- *
- * <ul>
- *   <li>PatchLineComment knows the comment status (published or draft). For comments in NoteDb the
- *       status is determined by the branch in which they are stored (published comments are stored
- *       in the change meta ref; draft comments are store in refs/draft-comments branches in
- *       All-Users). Hence Comment doesn't need to contain the status, but the status is implicitly
- *       known by where the comments are read from.
- *   <li>PatchLineComment knows the change ID. For comments in NoteDb, the change ID is determined
- *       by the branch in which they are stored (the ref name contains the change ID). Hence Comment
- *       doesn't need to contain the change ID, but the change ID is implicitly known by where the
- *       comments are read from.
- * </ul>
- *
- * <p>For all utility classes and middle layer functionality using Comment over PatchLineComment is
- * preferred, as ReviewDb is gone so PatchLineComment is slated for deletion as well. This means
- * Comment should be used everywhere and only for storing inline comment in ReviewDb a conversion to
- * PatchLineComment is done. Converting Comments to PatchLineComments and vice verse is done by
- * CommentsUtil#toPatchLineComments(Change.Id, PatchLineComment.Status, Iterable) and
- * CommentsUtil#toComments(String, Iterable).
  */
 public class Comment {
+  public enum Status {
+    DRAFT('d'),
+
+    PUBLISHED('P');
+
+    private final char code;
+
+    Status(char c) {
+      code = c;
+    }
+
+    public char getCode() {
+      return code;
+    }
+
+    public static Status forCode(char c) {
+      for (Status s : Status.values()) {
+        if (s.code == c) {
+          return s;
+        }
+      }
+      return null;
+    }
+  }
+
   public static class Key {
     public String uuid;
     public String filename;
@@ -65,17 +73,10 @@
 
     @Override
     public String toString() {
-      return new StringBuilder()
-          .append("Comment.Key{")
-          .append("uuid=")
-          .append(uuid)
-          .append(',')
-          .append("filename=")
-          .append(filename)
-          .append(',')
-          .append("patchSetId=")
-          .append(patchSetId)
-          .append('}')
+      return MoreObjects.toStringHelper(this)
+          .add("uuid", uuid)
+          .add("filename", filename)
+          .add("patchSetId", patchSetId)
           .toString();
     }
 
@@ -104,7 +105,7 @@
     }
 
     public Account.Id getId() {
-      return new Account.Id(id);
+      return Account.id(id);
     }
 
     @Override
@@ -122,15 +123,30 @@
 
     @Override
     public String toString() {
-      return new StringBuilder()
-          .append("Comment.Identity{")
-          .append("id=")
-          .append(id)
-          .append('}')
-          .toString();
+      return MoreObjects.toStringHelper(this).add("id", id).toString();
     }
   }
 
+  /**
+   * The Range class defines continuous range of character.
+   *
+   * <p>The pair (startLine, startChar) defines the first character in the range. The pair (endLine,
+   * endChar) defines the first character AFTER the range (i.e. it doesn't belong the range).
+   * (endLine, endChar) must be a valid character inside text, except EOF case.
+   *
+   * <p>Special cases:
+   *
+   * <ul>
+   *   <li>Zero length range: (startLine, startChar) = (endLine, endChar). Range defines insert
+   *       position right before the (startLine, startChar) character (for {@link FixReplacement})
+   *   <li>EOF case - range includes the last character in the file:
+   *       <ul>
+   *         <li>if a file ends with EOL mark, then (endLine, endChar) = (num_of_lines + 1, 0)
+   *         <li>if a file doesn't end with EOL mark, then (endLine, endChar) = (num_of_lines,
+   *             num_of_chars_in_last_line)
+   *       </ul>
+   * </ul>
+   */
   public static class Range implements Comparable<Range> {
     private static final Comparator<Range> RANGE_COMPARATOR =
         Comparator.<Range>comparingInt(range -> range.startLine)
@@ -138,10 +154,10 @@
             .thenComparingInt(range -> range.endLine)
             .thenComparingInt(range -> range.endChar);
 
-    public int startLine; // 1-based, inclusive
-    public int startChar; // 0-based, inclusive
-    public int endLine; // 1-based, exclusive
-    public int endChar; // 0-based, exclusive
+    public int startLine; // 1-based
+    public int startChar; // 0-based
+    public int endLine; // 1-based
+    public int endChar; // 0-based
 
     public Range(Range r) {
       this(r.startLine, r.startChar, r.endLine, r.endChar);
@@ -177,20 +193,11 @@
 
     @Override
     public String toString() {
-      return new StringBuilder()
-          .append("Comment.Range{")
-          .append("startLine=")
-          .append(startLine)
-          .append(',')
-          .append("startChar=")
-          .append(startChar)
-          .append(',')
-          .append("endLine=")
-          .append(endLine)
-          .append(',')
-          .append("endChar=")
-          .append(endChar)
-          .append('}')
+      return MoreObjects.toStringHelper(this)
+          .add("startLine", startLine)
+          .add("startChar", startChar)
+          .add("endLine", endLine)
+          .add("endChar", endChar)
           .toString();
     }
 
@@ -201,7 +208,9 @@
   }
 
   public Key key;
+  /** The line number (1-based) to which the comment refers, or 0 for a file comment. */
   public int lineNbr;
+
   public Identity author;
   protected Identity realAuthor;
   public Timestamp writtenOn;
@@ -211,17 +220,15 @@
   public Range range;
   public String tag;
 
-  // Hex commit SHA1 of the commit of the patchset to which this comment applies.
-  public String revId;
+  // Hex commit SHA1 of the commit of the patchset to which this comment applies. Other classes call
+  // this "commitId", but this class uses the old ReviewDb term "revId", and this field name is
+  // serialized into JSON in NoteDb, so it can't easily be changed. Callers do not access this field
+  // directly, and instead use the public getter/setter that wraps an ObjectId.
+  private String revId;
+
   public String serverId;
   public boolean unresolved;
 
-  /**
-   * Whether the comment was parsed from a JSON representation (false) or the legacy custom notes
-   * format (true).
-   */
-  public transient boolean legacyFormat;
-
   public Comment(Comment c) {
     this(
         new Key(c.key),
@@ -269,8 +276,13 @@
     this.range = range != null ? range.asCommentRange() : null;
   }
 
-  public void setRevId(RevId revId) {
-    this.revId = revId != null ? revId.get() : null;
+  @Nullable
+  public ObjectId getCommitId() {
+    return revId != null ? ObjectId.fromString(revId) : null;
+  }
+
+  public void setCommitId(@Nullable AnyObjectId commitId) {
+    this.revId = commitId != null ? commitId.name() : null;
   }
 
   public void setRealAuthor(Account.Id id) {
@@ -322,44 +334,22 @@
 
   @Override
   public String toString() {
-    return new StringBuilder()
-        .append("Comment{")
-        .append("key=")
-        .append(key)
-        .append(',')
-        .append("lineNbr=")
-        .append(lineNbr)
-        .append(',')
-        .append("author=")
-        .append(author.getId().get())
-        .append(',')
-        .append("realAuthor=")
-        .append(realAuthor != null ? realAuthor.getId().get() : "")
-        .append(',')
-        .append("writtenOn=")
-        .append(writtenOn.toString())
-        .append(',')
-        .append("side=")
-        .append(side)
-        .append(',')
-        .append("message=")
-        .append(Objects.toString(message, ""))
-        .append(',')
-        .append("parentUuid=")
-        .append(Objects.toString(parentUuid, ""))
-        .append(',')
-        .append("range=")
-        .append(Objects.toString(range, ""))
-        .append(',')
-        .append("revId=")
-        .append(revId != null ? revId : "")
-        .append(',')
-        .append("tag=")
-        .append(Objects.toString(tag, ""))
-        .append(',')
-        .append("unresolved=")
-        .append(unresolved)
-        .append('}')
-        .toString();
+    return toStringHelper().toString();
+  }
+
+  protected ToStringHelper toStringHelper() {
+    return MoreObjects.toStringHelper(this)
+        .add("key", key)
+        .add("lineNbr", lineNbr)
+        .add("author", author.getId())
+        .add("realAuthor", realAuthor != null ? realAuthor.getId() : "")
+        .add("writtenOn", writtenOn)
+        .add("side", side)
+        .add("message", Objects.toString(message, ""))
+        .add("parentUuid", Objects.toString(parentUuid, ""))
+        .add("range", Objects.toString(range, ""))
+        .add("revId", Objects.toString(revId, ""))
+        .add("tag", Objects.toString(tag, ""))
+        .add("unresolved", unresolved);
   }
 }
diff --git a/java/com/google/gerrit/reviewdb/client/CommentRange.java b/java/com/google/gerrit/entities/CommentRange.java
similarity index 97%
rename from java/com/google/gerrit/reviewdb/client/CommentRange.java
rename to java/com/google/gerrit/entities/CommentRange.java
index e6c5078..f58780f 100644
--- a/java/com/google/gerrit/reviewdb/client/CommentRange.java
+++ b/java/com/google/gerrit/entities/CommentRange.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 public class CommentRange {
 
diff --git a/java/com/google/gerrit/reviewdb/client/CoreDownloadSchemes.java b/java/com/google/gerrit/entities/CoreDownloadSchemes.java
similarity index 95%
rename from java/com/google/gerrit/reviewdb/client/CoreDownloadSchemes.java
rename to java/com/google/gerrit/entities/CoreDownloadSchemes.java
index 2ca89c8..37c10f1 100644
--- a/java/com/google/gerrit/reviewdb/client/CoreDownloadSchemes.java
+++ b/java/com/google/gerrit/entities/CoreDownloadSchemes.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 /** Download scheme string constants supported by the download-commands core plugin. */
 public class CoreDownloadSchemes {
diff --git a/java/com/google/gerrit/reviewdb/client/FixReplacement.java b/java/com/google/gerrit/entities/FixReplacement.java
similarity index 96%
rename from java/com/google/gerrit/reviewdb/client/FixReplacement.java
rename to java/com/google/gerrit/entities/FixReplacement.java
index 66630e4..046300e 100644
--- a/java/com/google/gerrit/reviewdb/client/FixReplacement.java
+++ b/java/com/google/gerrit/entities/FixReplacement.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 public class FixReplacement {
   public String path;
diff --git a/java/com/google/gerrit/reviewdb/client/FixSuggestion.java b/java/com/google/gerrit/entities/FixSuggestion.java
similarity index 96%
rename from java/com/google/gerrit/reviewdb/client/FixSuggestion.java
rename to java/com/google/gerrit/entities/FixSuggestion.java
index d766a3a..ac4e720 100644
--- a/java/com/google/gerrit/reviewdb/client/FixSuggestion.java
+++ b/java/com/google/gerrit/entities/FixSuggestion.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 import java.util.List;
 
diff --git a/java/com/google/gwtorm/client/StandardKeyEncoder.java b/java/com/google/gerrit/entities/KeyUtil.java
similarity index 91%
rename from java/com/google/gwtorm/client/StandardKeyEncoder.java
rename to java/com/google/gerrit/entities/KeyUtil.java
index 1ceb0dc..40fb757 100644
--- a/java/com/google/gwtorm/client/StandardKeyEncoder.java
+++ b/java/com/google/gerrit/entities/KeyUtil.java
@@ -1,4 +1,4 @@
-// Copyright 2008 Google Inc.
+// 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.
@@ -12,13 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gwtorm.client;
+package com.google.gerrit.entities;
 
-import com.google.gwtorm.client.KeyUtil.Encoder;
 import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
 
-public class StandardKeyEncoder extends Encoder {
+public class KeyUtil {
   private static final char[] hexc = {
     '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
   };
@@ -49,8 +48,7 @@
     for (char i = 'a'; i <= 'f'; i++) hexb[i] = (byte) ((i - 'a') + 10);
   }
 
-  @Override
-  public String encode(final String e) {
+  public static String encode(final String e) {
     final byte[] b;
     try {
       b = e.getBytes("UTF-8");
@@ -73,8 +71,7 @@
     return r.toString();
   }
 
-  @Override
-  public String decode(final String e) {
+  public static String decode(final String e) {
     if (e.indexOf('%') < 0) {
       return e.replace('+', ' ');
     }
diff --git a/java/com/google/gerrit/reviewdb/client/LabelId.java b/java/com/google/gerrit/entities/LabelId.java
similarity index 62%
rename from java/com/google/gerrit/reviewdb/client/LabelId.java
rename to java/com/google/gerrit/entities/LabelId.java
index abf131b..1cc45c8 100644
--- a/java/com/google/gerrit/reviewdb/client/LabelId.java
+++ b/java/com/google/gerrit/entities/LabelId.java
@@ -12,38 +12,25 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
-import com.google.gwtorm.client.StringKey;
+import com.google.auto.value.AutoValue;
 
-public class LabelId extends StringKey<com.google.gwtorm.client.Key<?>> {
-  private static final long serialVersionUID = 1L;
-
+@AutoValue
+public abstract class LabelId {
   static final String LEGACY_SUBMIT_NAME = "SUBM";
 
   public static LabelId create(String n) {
-    return new LabelId(n);
+    return new AutoValue_LabelId(n);
   }
 
   public static LabelId legacySubmit() {
-    return new LabelId(LEGACY_SUBMIT_NAME);
+    return create(LEGACY_SUBMIT_NAME);
   }
 
-  public String id;
+  abstract String id();
 
-  public LabelId() {}
-
-  public LabelId(String n) {
-    id = n;
-  }
-
-  @Override
   public String get() {
-    return id;
-  }
-
-  @Override
-  protected void set(String newValue) {
-    id = newValue;
+    return id();
   }
 }
diff --git a/java/com/google/gerrit/reviewdb/client/Patch.java b/java/com/google/gerrit/entities/Patch.java
similarity index 85%
rename from java/com/google/gerrit/reviewdb/client/Patch.java
rename to java/com/google/gerrit/entities/Patch.java
index d192df0..cc38bda 100644
--- a/java/com/google/gerrit/reviewdb/client/Patch.java
+++ b/java/com/google/gerrit/entities/Patch.java
@@ -12,9 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
-import com.google.gwtorm.client.StringKey;
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.primitives.Ints;
+import java.util.List;
 
 /** A single modified file in a {@link PatchSet}. */
 public final class Patch {
@@ -36,58 +41,29 @@
   }
 
   public static Key key(PatchSet.Id patchSetId, String fileName) {
-    return new Key(patchSetId, fileName);
+    return new AutoValue_Patch_Key(patchSetId, fileName);
   }
 
-  public static class Key extends StringKey<PatchSet.Id> {
-    private static final long serialVersionUID = 1L;
-
-    protected PatchSet.Id patchSetId;
-
-    protected String fileName;
-
-    protected Key() {
-      patchSetId = new PatchSet.Id();
-    }
-
-    public Key(PatchSet.Id ps, String name) {
-      this.patchSetId = ps;
-      this.fileName = name;
-    }
-
-    @Override
-    public PatchSet.Id getParentKey() {
-      return patchSetId;
-    }
-
-    public PatchSet.Id patchSetId() {
-      return getParentKey();
-    }
-
-    @Override
-    public String get() {
-      return fileName;
-    }
-
-    public String fileName() {
-      return get();
-    }
-
-    @Override
-    protected void set(String newValue) {
-      fileName = newValue;
-    }
-
-    /** Parse a Patch.Id out of a string representation. */
+  @AutoValue
+  public abstract static class Key {
+    /** Parse a Patch.Key out of a string representation. */
     public static Key parse(String str) {
-      final Key r = new Key();
-      r.fromString(str);
-      return r;
+      List<String> parts = Splitter.on(',').limit(3).splitToList(str);
+      checkKeyFormat(parts.size() == 3, str);
+      Integer changeId = Ints.tryParse(parts.get(0));
+      checkKeyFormat(changeId != null, str);
+      Integer patchSetNum = Ints.tryParse(parts.get(1));
+      checkKeyFormat(patchSetNum != null, str);
+      return key(PatchSet.id(Change.id(changeId), patchSetNum), parts.get(2));
     }
 
-    public String getFileName() {
-      return get();
+    private static void checkKeyFormat(boolean test, String input) {
+      checkArgument(test, "invalid patch key: %s", input);
     }
+
+    public abstract PatchSet.Id patchSetId();
+
+    public abstract String fileName();
   }
 
   /** Type of modification made to the file path. */
@@ -271,7 +247,7 @@
   }
 
   public String getFileName() {
-    return key.fileName;
+    return key.fileName();
   }
 
   public String getSourceFileName() {
diff --git a/java/com/google/gerrit/entities/PatchSet.java b/java/com/google/gerrit/entities/PatchSet.java
new file mode 100644
index 0000000..8b93dbc
--- /dev/null
+++ b/java/com/google/gerrit/entities/PatchSet.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2008 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.entities;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
+import com.google.common.primitives.Ints;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** A single revision of a {@link Change}. */
+@AutoValue
+public abstract class PatchSet {
+  /** Is the reference name a change reference? */
+  public static boolean isChangeRef(String name) {
+    return Id.fromRef(name) != null;
+  }
+
+  /**
+   * Is the reference name a change reference?
+   *
+   * @deprecated use isChangeRef instead.
+   */
+  @Deprecated
+  public static boolean isRef(String name) {
+    return isChangeRef(name);
+  }
+
+  public static String joinGroups(List<String> groups) {
+    requireNonNull(groups);
+    for (String group : groups) {
+      checkArgument(!group.contains(","), "group may not contain ',': %s", group);
+    }
+    return String.join(",", groups);
+  }
+
+  public static ImmutableList<String> splitGroups(String joinedGroups) {
+    return Streams.stream(Splitter.on(',').split(joinedGroups)).collect(toImmutableList());
+  }
+
+  public static Id id(Change.Id changeId, int id) {
+    return new AutoValue_PatchSet_Id(changeId, id);
+  }
+
+  @AutoValue
+  public abstract static class Id {
+    /** Parse a PatchSet.Id out of a string representation. */
+    public static Id parse(String str) {
+      List<String> parts = Splitter.on(',').splitToList(str);
+      checkIdFormat(parts.size() == 2, str);
+      Integer changeId = Ints.tryParse(parts.get(0));
+      checkIdFormat(changeId != null, str);
+      Integer id = Ints.tryParse(parts.get(1));
+      checkIdFormat(id != null, str);
+      return PatchSet.id(Change.id(changeId), id);
+    }
+
+    private static void checkIdFormat(boolean test, String input) {
+      checkArgument(test, "invalid patch set ID: %s", input);
+    }
+
+    /** Parse a PatchSet.Id from a {@link #refName()} result. */
+    public static Id fromRef(String ref) {
+      int cs = Change.Id.startIndex(ref);
+      if (cs < 0) {
+        return null;
+      }
+      int ce = Change.Id.nextNonDigit(ref, cs);
+      int patchSetId = fromRef(ref, ce);
+      if (patchSetId < 0) {
+        return null;
+      }
+      int changeId = Integer.parseInt(ref.substring(cs, ce));
+      return PatchSet.id(Change.id(changeId), patchSetId);
+    }
+
+    static int fromRef(String ref, int changeIdEnd) {
+      // Patch set ID.
+      int ps = changeIdEnd + 1;
+      if (ps >= ref.length() || ref.charAt(ps) == '0') {
+        return -1;
+      }
+      for (int i = ps; i < ref.length(); i++) {
+        if (ref.charAt(i) < '0' || ref.charAt(i) > '9') {
+          return -1;
+        }
+      }
+      return Integer.parseInt(ref.substring(ps));
+    }
+
+    public static String toId(int number) {
+      return number == 0 ? "edit" : String.valueOf(number);
+    }
+
+    public String getId() {
+      return toId(id());
+    }
+
+    public abstract Change.Id changeId();
+
+    abstract int id();
+
+    public int get() {
+      return id();
+    }
+
+    public String toRefName() {
+      return changeId().refPrefixBuilder().append(id()).toString();
+    }
+
+    @Override
+    public final String toString() {
+      return changeId().toString() + ',' + id();
+    }
+  }
+
+  public static Builder builder() {
+    return new AutoValue_PatchSet.Builder().groups(ImmutableList.of());
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder id(Id id);
+
+    public abstract Id id();
+
+    public abstract Builder commitId(ObjectId commitId);
+
+    public abstract Optional<ObjectId> commitId();
+
+    public abstract Builder uploader(Account.Id uploader);
+
+    public abstract Builder createdOn(Timestamp createdOn);
+
+    public abstract Builder groups(Iterable<String> groups);
+
+    public abstract ImmutableList<String> groups();
+
+    public abstract Builder pushCertificate(Optional<String> pushCertificate);
+
+    public abstract Builder pushCertificate(String pushCertificate);
+
+    public abstract Builder description(Optional<String> description);
+
+    public abstract Builder description(String description);
+
+    public abstract Optional<String> description();
+
+    public abstract PatchSet build();
+  }
+
+  /** ID of the patch set. */
+  public abstract Id id();
+
+  /**
+   * Commit ID of the patch set, also known as the revision.
+   *
+   * <p>If this is a deserialized instance that was originally serialized by an older version of
+   * Gerrit, and the old data erroneously did not include a {@code commitId}, then this method will
+   * return {@link ObjectId#zeroId()}.
+   */
+  public abstract ObjectId commitId();
+
+  /**
+   * Account that uploaded the patch set.
+   *
+   * <p>If this is a deserialized instance that was originally serialized by an older version of
+   * Gerrit, and the old data erroneously did not include an {@code uploader}, then this method will
+   * return an account ID of 0.
+   */
+  public abstract Account.Id uploader();
+
+  /**
+   * When this patch set was first introduced onto the change.
+   *
+   * <p>If this is a deserialized instance that was originally serialized by an older version of
+   * Gerrit, and the old data erroneously did not include a {@code createdOn}, then this method will
+   * return a timestamp of 0.
+   */
+  public abstract Timestamp createdOn();
+
+  /**
+   * Opaque group identifier, usually assigned during creation.
+   *
+   * <p>This field is actually a comma-separated list of values, as in rare cases involving merge
+   * commits a patch set may belong to multiple groups.
+   *
+   * <p>Changes on the same branch having patch sets with intersecting groups are considered
+   * related, as in the "Related Changes" tab.
+   */
+  public abstract ImmutableList<String> groups();
+
+  /** Certificate sent with a push that created this patch set. */
+  public abstract Optional<String> pushCertificate();
+
+  /**
+   * Optional user-supplied description for this patch set.
+   *
+   * <p>When this field is an empty {@code Optional}, the description was never set on the patch
+   * set. When this field is present but an empty string, the description was set and later cleared.
+   */
+  public abstract Optional<String> description();
+
+  /** Patch set number. */
+  public int number() {
+    return id().get();
+  }
+
+  /** Name of the corresponding patch set ref. */
+  public String refName() {
+    return id().toRefName();
+  }
+}
diff --git a/java/com/google/gerrit/entities/PatchSetApproval.java b/java/com/google/gerrit/entities/PatchSetApproval.java
new file mode 100644
index 0000000..a4bb251
--- /dev/null
+++ b/java/com/google/gerrit/entities/PatchSetApproval.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2008 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.entities;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.primitives.Shorts;
+import java.sql.Timestamp;
+import java.util.Date;
+import java.util.Optional;
+
+/** An approval (or negative approval) on a patch set. */
+@AutoValue
+public abstract class PatchSetApproval {
+  public static Key key(PatchSet.Id patchSetId, Account.Id accountId, LabelId labelId) {
+    return new AutoValue_PatchSetApproval_Key(patchSetId, accountId, labelId);
+  }
+
+  @AutoValue
+  public abstract static class Key {
+    public abstract PatchSet.Id patchSetId();
+
+    public abstract Account.Id accountId();
+
+    public abstract LabelId labelId();
+
+    public boolean isLegacySubmit() {
+      return LabelId.LEGACY_SUBMIT_NAME.equals(labelId().get());
+    }
+  }
+
+  public static Builder builder() {
+    return new AutoValue_PatchSetApproval.Builder().postSubmit(false);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder key(Key key);
+
+    public abstract Key key();
+
+    public abstract Builder value(short value);
+
+    public Builder value(int value) {
+      return value(Shorts.checkedCast(value));
+    }
+
+    public abstract Builder granted(Timestamp granted);
+
+    public Builder granted(Date granted) {
+      return granted(new Timestamp(granted.getTime()));
+    }
+
+    public abstract Builder tag(String tag);
+
+    public abstract Builder tag(Optional<String> tag);
+
+    public abstract Builder realAccountId(Account.Id realAccountId);
+
+    abstract Optional<Account.Id> realAccountId();
+
+    public abstract Builder postSubmit(boolean isPostSubmit);
+
+    abstract PatchSetApproval autoBuild();
+
+    public PatchSetApproval build() {
+      if (!realAccountId().isPresent()) {
+        realAccountId(key().accountId());
+      }
+      return autoBuild();
+    }
+  }
+
+  public abstract Key key();
+
+  /**
+   * Value assigned by the user.
+   *
+   * <p>The precise meaning of "value" is up to each category.
+   *
+   * <p>In general:
+   *
+   * <ul>
+   *   <li><b>&lt; 0:</b> The approval is rejected/revoked.
+   *   <li><b>= 0:</b> No indication either way is provided.
+   *   <li><b>&gt; 0:</b> The approval is approved/positive.
+   * </ul>
+   *
+   * and in the negative and positive direction a magnitude can be assumed.The further from 0 the
+   * more assertive the approval.
+   */
+  public abstract short value();
+
+  public abstract Timestamp granted();
+
+  public abstract Optional<String> tag();
+
+  /** Real user that made this approval on behalf of the user recorded in {@link Key#accountId}. */
+  public abstract Account.Id realAccountId();
+
+  public abstract boolean postSubmit();
+
+  public abstract Builder toBuilder();
+
+  public PatchSetApproval copyWithPatchSet(PatchSet.Id psId) {
+    return toBuilder().key(key(psId, key().accountId(), key().labelId())).build();
+  }
+
+  public PatchSet.Id patchSetId() {
+    return key().patchSetId();
+  }
+
+  public Account.Id accountId() {
+    return key().accountId();
+  }
+
+  public LabelId labelId() {
+    return key().labelId();
+  }
+
+  public String label() {
+    return labelId().get();
+  }
+
+  public boolean isLegacySubmit() {
+    return key().isLegacySubmit();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java b/java/com/google/gerrit/entities/PatchSetInfo.java
similarity index 81%
rename from java/com/google/gerrit/reviewdb/client/PatchSetInfo.java
rename to java/com/google/gerrit/entities/PatchSetInfo.java
index f949013..e3c6613 100644
--- a/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java
+++ b/java/com/google/gerrit/entities/PatchSetInfo.java
@@ -12,19 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
+
+import static java.util.Objects.requireNonNull;
 
 import java.util.List;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
 
 /** Additional data about a {@link PatchSet} not normally loaded. */
 public final class PatchSetInfo {
   public static class ParentInfo {
-    public RevId id;
+    public ObjectId commitId;
     public String shortMessage;
 
-    public ParentInfo(RevId id, String shortMessage) {
-      this.id = id;
-      this.shortMessage = shortMessage;
+    public ParentInfo(AnyObjectId commitId, String shortMessage) {
+      this.commitId = commitId.copy();
+      this.shortMessage = requireNonNull(shortMessage);
     }
 
     protected ParentInfo() {}
@@ -47,8 +51,8 @@
   /** List of parents of the patch set. */
   protected List<ParentInfo> parents;
 
-  /** SHA-1 of commit */
-  protected String revId;
+  /** ID of commit. */
+  protected ObjectId commitId;
 
   /** Optional user-supplied description for the patch set. */
   protected String description;
@@ -107,12 +111,12 @@
     return parents;
   }
 
-  public void setRevId(String s) {
-    revId = s;
+  public void setCommitId(AnyObjectId commitId) {
+    this.commitId = commitId.copy();
   }
 
-  public String getRevId() {
-    return revId;
+  public ObjectId getCommitId() {
+    return commitId;
   }
 
   public void setDescription(String description) {
diff --git a/java/com/google/gerrit/reviewdb/client/Project.java b/java/com/google/gerrit/entities/Project.java
similarity index 82%
rename from java/com/google/gerrit/reviewdb/client/Project.java
rename to java/com/google/gerrit/entities/Project.java
index e025c0f..867b14d 100644
--- a/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
+
+import static java.util.Objects.requireNonNull;
 
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gwtorm.client.StringKey;
+import java.io.Serializable;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
@@ -35,50 +37,60 @@
     return new NameKey(name);
   }
 
-  /** Project name key */
-  public static class NameKey extends StringKey<com.google.gwtorm.client.Key<?>> {
+  /**
+   * Project name key.
+   *
+   * <p>This class has subclasses such as {@code AllProjectsName}, which make Guice injection more
+   * convenient. Subclasses must compare equal if they have the same name, regardless of the
+   * specific class. This implies that subclasses may not add additional fields.
+   *
+   * <p>Because of this unusual subclassing behavior, this class is not an {@code @AutoValue},
+   * unlike other key types in this package. However, this is strictly an implementation detail; its
+   * interface and semantics are otherwise analogous to the {@code @AutoValue} types.
+   */
+  public static class NameKey implements Serializable, Comparable<NameKey> {
     private static final long serialVersionUID = 1L;
 
-    protected String name;
-
-    protected NameKey() {}
-
-    public NameKey(String n) {
-      name = n;
+    /** Parse a Project.NameKey out of a string representation. */
+    public static NameKey parse(String str) {
+      return nameKey(KeyUtil.decode(str));
     }
 
-    @Override
+    public static String asStringOrNull(NameKey key) {
+      return key == null ? null : key.get();
+    }
+
+    private final String name;
+
+    protected NameKey(String name) {
+      this.name = requireNonNull(name);
+    }
+
     public String get() {
       return name;
     }
 
     @Override
-    protected void set(String newValue) {
-      name = newValue;
-    }
-
-    @Override
-    public int hashCode() {
+    public final int hashCode() {
       return get().hashCode();
     }
 
     @Override
-    public boolean equals(Object b) {
+    public final boolean equals(Object b) {
       if (b instanceof NameKey) {
         return get().equals(((NameKey) b).get());
       }
       return false;
     }
 
-    /** Parse a Project.NameKey out of a string representation. */
-    public static NameKey parse(String str) {
-      final NameKey r = new NameKey();
-      r.fromString(str);
-      return r;
+    @Override
+    public final int compareTo(NameKey o) {
+      return get().compareTo(o.get());
     }
 
-    public static String asStringOrNull(NameKey key) {
-      return key == null ? null : key.get();
+    @Override
+    public final String toString() {
+      return KeyUtil.encode(get());
     }
   }
 
@@ -220,7 +232,7 @@
   }
 
   public void setParentName(String n) {
-    parent = n != null ? new NameKey(n) : null;
+    parent = n != null ? nameKey(n) : null;
   }
 
   public void setParentName(NameKey n) {
diff --git a/java/com/google/gerrit/reviewdb/client/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
similarity index 98%
rename from java/com/google/gerrit/reviewdb/client/RefNames.java
rename to java/com/google/gerrit/entities/RefNames.java
index 984e43e..5595bc7 100644
--- a/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 import com.google.gerrit.common.UsedAt;
 
@@ -128,6 +128,11 @@
     return shard(id.changeId().get(), r).append('/').append(id.get()).toString();
   }
 
+  public static String changeRefPrefix(Change.Id id) {
+    StringBuilder r = newStringBuilder().append(REFS_CHANGES);
+    return shard(id.get(), r).append('/').toString();
+  }
+
   public static String robotCommentsRef(Change.Id id) {
     StringBuilder r = newStringBuilder().append(REFS_CHANGES);
     return shard(id.get(), r).append(ROBOT_COMMENTS_SUFFIX).toString();
diff --git a/java/com/google/gerrit/entities/RobotComment.java b/java/com/google/gerrit/entities/RobotComment.java
new file mode 100644
index 0000000..a7951ad
--- /dev/null
+++ b/java/com/google/gerrit/entities/RobotComment.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class RobotComment extends Comment {
+  public String robotId;
+  public String robotRunId;
+  public String url;
+  public Map<String, String> properties;
+  public List<FixSuggestion> fixSuggestions;
+
+  public RobotComment(
+      Key key,
+      Account.Id author,
+      Timestamp writtenOn,
+      short side,
+      String message,
+      String serverId,
+      String robotId,
+      String robotRunId) {
+    super(key, author, writtenOn, side, message, serverId, false);
+    this.robotId = robotId;
+    this.robotRunId = robotRunId;
+  }
+
+  @Override
+  public String toString() {
+    return toStringHelper()
+        .add("robotId", robotId)
+        .add("robotRunId", robotRunId)
+        .add("url", url)
+        .add("properties", Objects.toString(properties, ""))
+        .add("fixSuggestions", Objects.toString(fixSuggestions, ""))
+        .toString();
+  }
+}
diff --git a/java/com/google/gerrit/entities/SubmoduleSubscription.java b/java/com/google/gerrit/entities/SubmoduleSubscription.java
new file mode 100644
index 0000000..5ea1b1e
--- /dev/null
+++ b/java/com/google/gerrit/entities/SubmoduleSubscription.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2011 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.entities;
+
+import java.util.Objects;
+
+/**
+ * Defining a project/branch subscription to a project/branch project.
+ *
+ * <p>This means a class instance represents a repo/branch subscription to a project/branch (the
+ * subscriber).
+ *
+ * <p>A subscriber operates a submodule in defined path.
+ */
+public final class SubmoduleSubscription {
+  protected BranchNameKey superProject;
+
+  protected String submodulePath;
+
+  protected BranchNameKey submodule;
+
+  public SubmoduleSubscription(BranchNameKey superProject, BranchNameKey submodule, String path) {
+    this.superProject = superProject;
+    this.submodule = submodule;
+    this.submodulePath = path;
+  }
+
+  /**
+   * Indicates the super project, aka subscriber: the project owner of the gitlinks to the
+   * submodules.
+   */
+  public BranchNameKey getSuperProject() {
+    return superProject;
+  }
+
+  public String getPath() {
+    return submodulePath;
+  }
+
+  public BranchNameKey getSubmodule() {
+    return submodule;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof SubmoduleSubscription) {
+      SubmoduleSubscription s = (SubmoduleSubscription) o;
+      return superProject.equals(s.superProject)
+          && submodulePath.equals(s.submodulePath)
+          && submodule.equals(s.submodule);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(superProject, submodulePath, submodule);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(getSuperProject()).append(':').append(getPath());
+    sb.append(" follows ");
+    sb.append(getSubmodule());
+    return sb.toString();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/client/UserIdentity.java b/java/com/google/gerrit/entities/UserIdentity.java
similarity index 97%
rename from java/com/google/gerrit/reviewdb/client/UserIdentity.java
rename to java/com/google/gerrit/entities/UserIdentity.java
index 0b7aee3..e07d21a 100644
--- a/java/com/google/gerrit/reviewdb/client/UserIdentity.java
+++ b/java/com/google/gerrit/entities/UserIdentity.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 import java.sql.Timestamp;
 
diff --git a/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java b/java/com/google/gerrit/entities/converter/AccountIdProtoConverter.java
similarity index 85%
rename from java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java
rename to java/com/google/gerrit/entities/converter/AccountIdProtoConverter.java
index 8dd1794..1e846fb 100644
--- a/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/AccountIdProtoConverter.java
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum AccountIdProtoConverter implements ProtoConverter<Entities.Account_Id, Account.Id> {
   INSTANCE;
 
@@ -28,7 +30,7 @@
 
   @Override
   public Account.Id fromProto(Entities.Account_Id proto) {
-    return new Account.Id(proto.getId());
+    return Account.id(proto.getId());
   }
 
   @Override
diff --git a/java/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/BranchNameKeyProtoConverter.java
similarity index 63%
rename from java/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverter.java
rename to java/com/google/gerrit/entities/converter/BranchNameKeyProtoConverter.java
index 4558f9b..4d0e306 100644
--- a/java/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/BranchNameKeyProtoConverter.java
@@ -12,32 +12,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum BranchNameKeyProtoConverter
-    implements ProtoConverter<Entities.Branch_NameKey, Branch.NameKey> {
+    implements ProtoConverter<Entities.Branch_NameKey, BranchNameKey> {
   INSTANCE;
 
   private final ProtoConverter<Entities.Project_NameKey, Project.NameKey> projectNameConverter =
       ProjectNameKeyProtoConverter.INSTANCE;
 
   @Override
-  public Entities.Branch_NameKey toProto(Branch.NameKey nameKey) {
+  public Entities.Branch_NameKey toProto(BranchNameKey nameKey) {
     return Entities.Branch_NameKey.newBuilder()
-        .setProjectName(projectNameConverter.toProto(nameKey.getParentKey()))
-        .setBranchName(nameKey.get())
+        .setProject(projectNameConverter.toProto(nameKey.project()))
+        .setBranch(nameKey.branch())
         .build();
   }
 
   @Override
-  public Branch.NameKey fromProto(Entities.Branch_NameKey proto) {
-    return new Branch.NameKey(
-        projectNameConverter.fromProto(proto.getProjectName()), proto.getBranchName());
+  public BranchNameKey fromProto(Entities.Branch_NameKey proto) {
+    return BranchNameKey.create(
+        projectNameConverter.fromProto(proto.getProject()), proto.getBranch());
   }
 
   @Override
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeIdProtoConverter.java
similarity index 85%
rename from java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java
rename to java/com/google/gerrit/entities/converter/ChangeIdProtoConverter.java
index 14ed59c..0d4ec70 100644
--- a/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeIdProtoConverter.java
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum ChangeIdProtoConverter implements ProtoConverter<Entities.Change_Id, Change.Id> {
   INSTANCE;
 
@@ -28,7 +30,7 @@
 
   @Override
   public Change.Id fromProto(Entities.Change_Id proto) {
-    return new Change.Id(proto.getId());
+    return Change.id(proto.getId());
   }
 
   @Override
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeKeyProtoConverter.java
similarity index 85%
rename from java/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverter.java
rename to java/com/google/gerrit/entities/converter/ChangeKeyProtoConverter.java
index dccd7d9..f3ccdfa 100644
--- a/java/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeKeyProtoConverter.java
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum ChangeKeyProtoConverter implements ProtoConverter<Entities.Change_Key, Change.Key> {
   INSTANCE;
 
@@ -28,7 +30,7 @@
 
   @Override
   public Change.Key fromProto(Entities.Change_Key proto) {
-    return new Change.Key(proto.getId());
+    return Change.key(proto.getId());
   }
 
   @Override
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeMessageKeyProtoConverter.java
similarity index 76%
rename from java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java
rename to java/com/google/gerrit/entities/converter/ChangeMessageKeyProtoConverter.java
index bb532df..3e93c5a 100644
--- a/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeMessageKeyProtoConverter.java
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum ChangeMessageKeyProtoConverter
     implements ProtoConverter<Entities.ChangeMessage_Key, ChangeMessage.Key> {
   INSTANCE;
@@ -29,14 +31,14 @@
   @Override
   public Entities.ChangeMessage_Key toProto(ChangeMessage.Key messageKey) {
     return Entities.ChangeMessage_Key.newBuilder()
-        .setChangeId(changeIdConverter.toProto(messageKey.getParentKey()))
-        .setUuid(messageKey.get())
+        .setChangeId(changeIdConverter.toProto(messageKey.changeId()))
+        .setUuid(messageKey.uuid())
         .build();
   }
 
   @Override
   public ChangeMessage.Key fromProto(Entities.ChangeMessage_Key proto) {
-    return new ChangeMessage.Key(changeIdConverter.fromProto(proto.getChangeId()), proto.getUuid());
+    return ChangeMessage.key(changeIdConverter.fromProto(proto.getChangeId()), proto.getUuid());
   }
 
   @Override
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java
similarity index 93%
rename from java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java
rename to java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java
index 0895d8d..19c121249 100644
--- a/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java
@@ -12,16 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.protobuf.Parser;
 import java.sql.Timestamp;
 import java.util.Objects;
 
+@Immutable
 public enum ChangeMessageProtoConverter
     implements ProtoConverter<Entities.ChangeMessage, ChangeMessage> {
   INSTANCE;
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
similarity index 90%
rename from java/com/google/gerrit/reviewdb/converter/ChangeProtoConverter.java
rename to java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
index 30b6d27..5b066ea 100644
--- a/java/com/google/gerrit/reviewdb/converter/ChangeProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
@@ -12,16 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.protobuf.Parser;
 import java.sql.Timestamp;
 
+@Immutable
 public enum ChangeProtoConverter implements ProtoConverter<Entities.Change, Change> {
   INSTANCE;
 
@@ -31,7 +33,7 @@
       ChangeKeyProtoConverter.INSTANCE;
   private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
       AccountIdProtoConverter.INSTANCE;
-  private final ProtoConverter<Entities.Branch_NameKey, Branch.NameKey> branchNameConverter =
+  private final ProtoConverter<Entities.Branch_NameKey, BranchNameKey> branchNameConverter =
       BranchNameKeyProtoConverter.INSTANCE;
 
   @Override
@@ -86,7 +88,7 @@
         proto.hasChangeKey() ? changeKeyConverter.fromProto(proto.getChangeKey()) : null;
     Account.Id owner =
         proto.hasOwnerAccountId() ? accountIdConverter.fromProto(proto.getOwnerAccountId()) : null;
-    Branch.NameKey destination =
+    BranchNameKey destination =
         proto.hasDest() ? branchNameConverter.fromProto(proto.getDest()) : null;
     Change change =
         new Change(key, changeId, owner, destination, new Timestamp(proto.getCreatedOn()));
@@ -100,7 +102,7 @@
     String subject = proto.hasSubject() ? proto.getSubject() : null;
     String originalSubject = proto.hasOriginalSubject() ? proto.getOriginalSubject() : null;
     change.setCurrentPatchSet(
-        new PatchSet.Id(changeId, proto.getCurrentPatchSetId()), subject, originalSubject);
+        PatchSet.id(changeId, proto.getCurrentPatchSetId()), subject, originalSubject);
     if (proto.hasTopic()) {
       change.setTopic(proto.getTopic());
     }
diff --git a/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java b/java/com/google/gerrit/entities/converter/LabelIdProtoConverter.java
similarity index 84%
rename from java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java
rename to java/com/google/gerrit/entities/converter/LabelIdProtoConverter.java
index 274f23b..a1894ac 100644
--- a/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/LabelIdProtoConverter.java
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum LabelIdProtoConverter implements ProtoConverter<Entities.LabelId, LabelId> {
   INSTANCE;
 
@@ -28,7 +30,7 @@
 
   @Override
   public LabelId fromProto(Entities.LabelId proto) {
-    return new LabelId(proto.getId());
+    return LabelId.create(proto.getId());
   }
 
   @Override
diff --git a/java/com/google/gerrit/entities/converter/ObjectIdProtoConverter.java b/java/com/google/gerrit/entities/converter/ObjectIdProtoConverter.java
new file mode 100644
index 0000000..6c80528
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/ObjectIdProtoConverter.java
@@ -0,0 +1,52 @@
+// 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.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Proto converter for {@code ObjectId}s.
+ *
+ * <p>This converter uses the hex representation of object IDs embedded in a wrapper proto type,
+ * rather than a more parsimonious implementation (e.g. a raw byte array), for two reasons:
+ *
+ * <ul>
+ *   <li>Hex strings are easier to read and work with when reading and writing protos in text
+ *       formats, for example in test failure messages, or when using command-line tools.
+ *   <li>This maintains backwards wire compatibility with a pre-NoteDb implementation.
+ * </ul>
+ */
+@Immutable
+public enum ObjectIdProtoConverter implements ProtoConverter<Entities.ObjectId, ObjectId> {
+  INSTANCE;
+
+  @Override
+  public Entities.ObjectId toProto(ObjectId objectId) {
+    return Entities.ObjectId.newBuilder().setName(objectId.name()).build();
+  }
+
+  @Override
+  public ObjectId fromProto(Entities.ObjectId proto) {
+    return ObjectId.fromString(proto.getName());
+  }
+
+  @Override
+  public Parser<Entities.ObjectId> getParser() {
+    return Entities.ObjectId.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetApprovalKeyProtoConverter.java
similarity index 75%
rename from java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java
rename to java/com/google/gerrit/entities/converter/PatchSetApprovalKeyProtoConverter.java
index 3538301..c7d1714 100644
--- a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetApprovalKeyProtoConverter.java
@@ -12,15 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum PatchSetApprovalKeyProtoConverter
     implements ProtoConverter<Entities.PatchSetApproval_Key, PatchSetApproval.Key> {
   INSTANCE;
@@ -35,18 +37,18 @@
   @Override
   public Entities.PatchSetApproval_Key toProto(PatchSetApproval.Key key) {
     return Entities.PatchSetApproval_Key.newBuilder()
-        .setPatchSetId(patchSetIdConverter.toProto(key.getParentKey()))
-        .setAccountId(accountIdConverter.toProto(key.getAccountId()))
-        .setCategoryId(labelIdConverter.toProto(key.getLabelId()))
+        .setPatchSetId(patchSetIdConverter.toProto(key.patchSetId()))
+        .setAccountId(accountIdConverter.toProto(key.accountId()))
+        .setLabelId(labelIdConverter.toProto(key.labelId()))
         .build();
   }
 
   @Override
   public PatchSetApproval.Key fromProto(Entities.PatchSetApproval_Key proto) {
-    return new PatchSetApproval.Key(
+    return PatchSetApproval.key(
         patchSetIdConverter.fromProto(proto.getPatchSetId()),
         accountIdConverter.fromProto(proto.getAccountId()),
-        labelIdConverter.fromProto(proto.getCategoryId()));
+        labelIdConverter.fromProto(proto.getLabelId()));
   }
 
   @Override
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
similarity index 67%
rename from java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java
rename to java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
index 418076f..78a35ff 100644
--- a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
@@ -12,15 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.protobuf.Parser;
 import java.sql.Timestamp;
 import java.util.Objects;
 
+@Immutable
 public enum PatchSetApprovalProtoConverter
     implements ProtoConverter<Entities.PatchSetApproval, PatchSetApproval> {
   INSTANCE;
@@ -34,21 +36,18 @@
   public Entities.PatchSetApproval toProto(PatchSetApproval patchSetApproval) {
     Entities.PatchSetApproval.Builder builder =
         Entities.PatchSetApproval.newBuilder()
-            .setKey(patchSetApprovalKeyProtoConverter.toProto(patchSetApproval.getKey()))
-            .setValue(patchSetApproval.getValue())
-            .setGranted(patchSetApproval.getGranted().getTime())
-            .setPostSubmit(patchSetApproval.isPostSubmit());
+            .setKey(patchSetApprovalKeyProtoConverter.toProto(patchSetApproval.key()))
+            .setValue(patchSetApproval.value())
+            .setGranted(patchSetApproval.granted().getTime())
+            .setPostSubmit(patchSetApproval.postSubmit());
 
-    String tag = patchSetApproval.getTag();
-    if (tag != null) {
-      builder.setTag(tag);
-    }
-    Account.Id realAccountId = patchSetApproval.getRealAccountId();
+    patchSetApproval.tag().ifPresent(builder::setTag);
+    Account.Id realAccountId = patchSetApproval.realAccountId();
     // PatchSetApproval#getRealAccountId automatically delegates to PatchSetApproval#getAccountId if
     // the real author is not set. However, the previous protobuf representation kept
     // 'realAccountId' empty if it wasn't set. To ensure binary compatibility, simulate the previous
     // behavior.
-    if (realAccountId != null && !Objects.equals(realAccountId, patchSetApproval.getAccountId())) {
+    if (realAccountId != null && !Objects.equals(realAccountId, patchSetApproval.accountId())) {
       builder.setRealAccountId(accountIdConverter.toProto(realAccountId));
     }
 
@@ -57,21 +56,19 @@
 
   @Override
   public PatchSetApproval fromProto(Entities.PatchSetApproval proto) {
-    PatchSetApproval patchSetApproval =
-        new PatchSetApproval(
-            patchSetApprovalKeyProtoConverter.fromProto(proto.getKey()),
-            (short) proto.getValue(),
-            new Timestamp(proto.getGranted()));
+    PatchSetApproval.Builder builder =
+        PatchSetApproval.builder()
+            .key(patchSetApprovalKeyProtoConverter.fromProto(proto.getKey()))
+            .value(proto.getValue())
+            .granted(new Timestamp(proto.getGranted()))
+            .postSubmit(proto.getPostSubmit());
     if (proto.hasTag()) {
-      patchSetApproval.setTag(proto.getTag());
+      builder.tag(proto.getTag());
     }
     if (proto.hasRealAccountId()) {
-      patchSetApproval.setRealAccountId(accountIdConverter.fromProto(proto.getRealAccountId()));
+      builder.realAccountId(accountIdConverter.fromProto(proto.getRealAccountId()));
     }
-    if (proto.hasPostSubmit()) {
-      patchSetApproval.setPostSubmit(proto.getPostSubmit());
-    }
-    return patchSetApproval;
+    return builder.build();
   }
 
   @Override
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetIdProtoConverter.java
similarity index 76%
rename from java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java
rename to java/com/google/gerrit/entities/converter/PatchSetIdProtoConverter.java
index a2b95bd..60c13f1 100644
--- a/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetIdProtoConverter.java
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum PatchSetIdProtoConverter implements ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> {
   INSTANCE;
 
@@ -28,14 +30,14 @@
   @Override
   public Entities.PatchSet_Id toProto(PatchSet.Id patchSetId) {
     return Entities.PatchSet_Id.newBuilder()
-        .setChangeId(changeIdConverter.toProto(patchSetId.getParentKey()))
-        .setPatchSetId(patchSetId.get())
+        .setChangeId(changeIdConverter.toProto(patchSetId.changeId()))
+        .setId(patchSetId.get())
         .build();
   }
 
   @Override
   public PatchSet.Id fromProto(Entities.PatchSet_Id proto) {
-    return new PatchSet.Id(changeIdConverter.fromProto(proto.getChangeId()), proto.getPatchSetId());
+    return PatchSet.id(changeIdConverter.fromProto(proto.getChangeId()), proto.getId());
   }
 
   @Override
diff --git a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
new file mode 100644
index 0000000..13a6e71
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
@@ -0,0 +1,96 @@
+// 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.
+
+package com.google.gerrit.entities.converter;
+
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+import java.sql.Timestamp;
+import java.util.List;
+import org.eclipse.jgit.lib.ObjectId;
+
+@Immutable
+public enum PatchSetProtoConverter implements ProtoConverter<Entities.PatchSet, PatchSet> {
+  INSTANCE;
+
+  private final ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
+      PatchSetIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.ObjectId, ObjectId> objectIdConverter =
+      ObjectIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
+      AccountIdProtoConverter.INSTANCE;
+
+  @Override
+  public Entities.PatchSet toProto(PatchSet patchSet) {
+    Entities.PatchSet.Builder builder =
+        Entities.PatchSet.newBuilder()
+            .setId(patchSetIdConverter.toProto(patchSet.id()))
+            .setCommitId(objectIdConverter.toProto(patchSet.commitId()))
+            .setUploaderAccountId(accountIdConverter.toProto(patchSet.uploader()))
+            .setCreatedOn(patchSet.createdOn().getTime());
+    List<String> groups = patchSet.groups();
+    if (!groups.isEmpty()) {
+      builder.setGroups(PatchSet.joinGroups(groups));
+    }
+    patchSet.pushCertificate().ifPresent(builder::setPushCertificate);
+    patchSet.description().ifPresent(builder::setDescription);
+    return builder.build();
+  }
+
+  @Override
+  public PatchSet fromProto(Entities.PatchSet proto) {
+    PatchSet.Builder builder =
+        PatchSet.builder()
+            .id(patchSetIdConverter.fromProto(proto.getId()))
+            .groups(
+                proto.hasGroups() ? PatchSet.splitGroups(proto.getGroups()) : ImmutableList.of());
+    if (proto.hasPushCertificate()) {
+      builder.pushCertificate(proto.getPushCertificate());
+    }
+    if (proto.hasDescription()) {
+      builder.description(proto.getDescription());
+    }
+
+    // The following fields used to theoretically be nullable in PatchSet, but in practice no
+    // production codepath should have ever serialized an instance that was missing one of these
+    // fields.
+    //
+    // However, since some protos may theoretically be missing these fields, we need to support
+    // them. Populate specific sentinel values for each field as documented in the PatchSet javadoc.
+    // Callers that encounter one of these sentinels will likely fail, for example by failing to
+    // look up the zeroId. They would have also failed back when the fields were nullable, for
+    // example with NPE; the current behavior just fails slightly differently.
+    builder
+        .commitId(
+            proto.hasCommitId()
+                ? objectIdConverter.fromProto(proto.getCommitId())
+                : ObjectId.zeroId())
+        .uploader(
+            proto.hasUploaderAccountId()
+                ? accountIdConverter.fromProto(proto.getUploaderAccountId())
+                : Account.id(0))
+        .createdOn(proto.hasCreatedOn() ? new Timestamp(proto.getCreatedOn()) : new Timestamp(0));
+
+    return builder.build();
+  }
+
+  @Override
+  public Parser<Entities.PatchSet> getParser() {
+    return Entities.PatchSet.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
similarity index 85%
rename from java/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverter.java
rename to java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
index f7d809e..6bb0f79 100644
--- a/java/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum ProjectNameKeyProtoConverter
     implements ProtoConverter<Entities.Project_NameKey, Project.NameKey> {
   INSTANCE;
@@ -29,7 +31,7 @@
 
   @Override
   public Project.NameKey fromProto(Entities.Project_NameKey proto) {
-    return new Project.NameKey(proto.getName());
+    return Project.nameKey(proto.getName());
   }
 
   @Override
diff --git a/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java b/java/com/google/gerrit/entities/converter/ProtoConverter.java
similarity index 88%
rename from java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
rename to java/com/google/gerrit/entities/converter/ProtoConverter.java
index 568759c..a5c4052 100644
--- a/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ProtoConverter.java
@@ -12,11 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.protobuf.MessageLite;
 import com.google.protobuf.Parser;
 
+@Immutable
 public interface ProtoConverter<P extends MessageLite, C> {
 
   P toProto(C valueClass);
diff --git a/java/com/google/gerrit/exceptions/BUILD b/java/com/google/gerrit/exceptions/BUILD
index e08c3fd..ef59be1 100644
--- a/java/com/google/gerrit/exceptions/BUILD
+++ b/java/com/google/gerrit/exceptions/BUILD
@@ -4,5 +4,5 @@
     name = "exceptions",
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
-    deps = ["//java/com/google/gerrit/reviewdb:server"],
+    deps = ["//java/com/google/gerrit/entities"],
 )
diff --git a/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java b/java/com/google/gerrit/exceptions/InvalidMergeStrategyException.java
similarity index 62%
copy from java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
copy to java/com/google/gerrit/exceptions/InvalidMergeStrategyException.java
index 568759c..d9c5776 100644
--- a/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
+++ b/java/com/google/gerrit/exceptions/InvalidMergeStrategyException.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2018 The Android Open Source Project
+// 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.
@@ -12,16 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.exceptions;
 
-import com.google.protobuf.MessageLite;
-import com.google.protobuf.Parser;
+public class InvalidMergeStrategyException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
 
-public interface ProtoConverter<P extends MessageLite, C> {
-
-  P toProto(C valueClass);
-
-  C fromProto(P proto);
-
-  Parser<P> getParser();
+  public InvalidMergeStrategyException(String strategy) {
+    super("invalid merge strategy: " + strategy);
+  }
 }
diff --git a/java/com/google/gerrit/exceptions/NoSuchGroupException.java b/java/com/google/gerrit/exceptions/NoSuchGroupException.java
index dca28cb..95efac3 100644
--- a/java/com/google/gerrit/exceptions/NoSuchGroupException.java
+++ b/java/com/google/gerrit/exceptions/NoSuchGroupException.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.exceptions;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 
 /** Indicates the account group does not exist. */
 public class NoSuchGroupException extends Exception {
diff --git a/java/com/google/gerrit/extensions/BUILD b/java/com/google/gerrit/extensions/BUILD
index b5e5863..da5dc8b 100644
--- a/java/com/google/gerrit/extensions/BUILD
+++ b/java/com/google/gerrit/extensions/BUILD
@@ -1,8 +1,11 @@
 load("@rules_java//java:defs.bzl", "java_binary", "java_library")
 load("//lib:guava.bzl", "GUAVA_DOC_URL")
-load("//lib/jgit:jgit.bzl", "JGIT_DOC_URL")
 load("//tools/bzl:javadoc.bzl", "java_doc")
 
+_DOC_VERS = "5.5.0.201909110433-r"
+
+JGIT_DOC_URL = "https://download.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
+
 java_binary(
     name = "extension-api",
     main_class = "Dummy",
@@ -23,7 +26,6 @@
     ],
 )
 
-#TODO(davido): There is no provided_deps argument to java_library rule
 java_library(
     name = "api",
     srcs = glob(["**/*.java"]),
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 3d5fccc..f0462c6 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ListChangesOption;
+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.extensions.common.ChangeMessageInfo;
@@ -209,6 +210,10 @@
     return suggestReviewers().withQuery(query);
   }
 
+  default SuggestedReviewersRequest suggestCcs(String query) throws RestApiException {
+    return suggestReviewers().forCc().withQuery(query);
+  }
+
   /**
    * Retrieve reviewers ({@code ReviewerState.REVIEWER} and {@code ReviewerState.CC}) on the change.
    */
@@ -241,12 +246,16 @@
    * <ul>
    *   <li>{@code CHECK} is omitted, to skip consistency checks.
    *   <li>{@code SKIP_MERGEABLE} is omitted, so the {@code mergeable} bit <em>is</em> set.
+   *   <li>{@code SKIP_DIFFSTAT} is omitted to ensure diffstat calculations.
    * </ul>
    */
   default ChangeInfo get() throws RestApiException {
     return get(
         EnumSet.complementOf(
-            EnumSet.of(ListChangesOption.CHECK, ListChangesOption.SKIP_MERGEABLE)));
+            EnumSet.of(
+                ListChangesOption.CHECK,
+                ListChangesOption.SKIP_MERGEABLE,
+                ListChangesOption.SKIP_DIFFSTAT)));
   }
 
   /** {@link #get(ListChangesOption...)} with no options included. */
@@ -377,6 +386,8 @@
   abstract class SuggestedReviewersRequest {
     private String query;
     private int limit;
+    private boolean excludeGroups;
+    private ReviewerState reviewerState = ReviewerState.REVIEWER;
 
     public abstract List<SuggestedReviewerInfo> get() throws RestApiException;
 
@@ -390,6 +401,16 @@
       return this;
     }
 
+    public SuggestedReviewersRequest excludeGroups(boolean excludeGroups) {
+      this.excludeGroups = excludeGroups;
+      return this;
+    }
+
+    public SuggestedReviewersRequest forCc() {
+      this.reviewerState = ReviewerState.CC;
+      return this;
+    }
+
     public String getQuery() {
       return query;
     }
@@ -397,6 +418,14 @@
     public int getLimit() {
       return limit;
     }
+
+    public boolean getExcludeGroups() {
+      return excludeGroups;
+    }
+
+    public ReviewerState getReviewerState() {
+      return reviewerState;
+    }
   }
 
   /**
diff --git a/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java b/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java
index ef49651..dd29635 100644
--- a/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java
+++ b/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java
@@ -14,13 +14,38 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.common.base.MoreObjects;
 import java.util.List;
+import java.util.Objects;
 
 /** Detailed information about who should be notified about an update. */
 public class NotifyInfo {
   public List<String> accounts;
 
+  /**
+   * @param accounts may be either just a list of: account IDs, Full names, usernames, or emails.
+   *     Also could be a list of those: "Full name <email@example.com>" or "Full name (<ID>)"
+   */
   public NotifyInfo(List<String> accounts) {
     this.accounts = accounts;
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof NotifyInfo)) {
+      return false;
+    }
+    NotifyInfo other = (NotifyInfo) o;
+    return Objects.equals(other.accounts, accounts);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(accounts);
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).add("accounts", accounts).toString();
+  }
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/RevertInput.java b/java/com/google/gerrit/extensions/api/changes/RevertInput.java
index c1be9b0..c4272e4 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevertInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevertInput.java
@@ -24,4 +24,6 @@
   public NotifyHandling notify = NotifyHandling.ALL;
 
   public Map<RecipientType, NotifyInfo> notifyDetails;
+
+  public String topic;
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 68575ca..f854d4a 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -136,7 +136,7 @@
 
   SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException;
 
-  List<TestSubmitRuleInfo> testSubmitRule(TestSubmitRuleInput in) throws RestApiException;
+  TestSubmitRuleInfo testSubmitRule(TestSubmitRuleInput in) throws RestApiException;
 
   MergeListRequest getMergeList() throws RestApiException;
 
@@ -341,7 +341,7 @@
     }
 
     @Override
-    public List<TestSubmitRuleInfo> testSubmitRule(TestSubmitRuleInput in) throws RestApiException {
+    public TestSubmitRuleInfo testSubmitRule(TestSubmitRuleInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
diff --git a/java/com/google/gerrit/extensions/api/groups/GroupInput.java b/java/com/google/gerrit/extensions/api/groups/GroupInput.java
index ab38434..30af08f 100644
--- a/java/com/google/gerrit/extensions/api/groups/GroupInput.java
+++ b/java/com/google/gerrit/extensions/api/groups/GroupInput.java
@@ -18,6 +18,7 @@
 
 public class GroupInput {
   public String name;
+  public String uuid;
   public String description;
   public Boolean visibleToAll;
   public String ownerId;
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 3d70996..c6d9dee 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -201,6 +201,9 @@
    */
   void index(boolean indexChildren) throws RestApiException;
 
+  /** Reindexes all changes of the project. */
+  void indexChanges() throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -370,5 +373,10 @@
     public void index(boolean indexChildren) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void indexChanges() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/extensions/client/Comment.java b/java/com/google/gerrit/extensions/client/Comment.java
index 3bca4bb..d5fbf89 100644
--- a/java/com/google/gerrit/extensions/client/Comment.java
+++ b/java/com/google/gerrit/extensions/client/Comment.java
@@ -30,7 +30,9 @@
   public String path;
   public Side side;
   public Integer parent;
-  public Integer line; // value 0 or null indicates a file comment, normal lines start at 1
+  /** Value 0 or null indicates a file comment, normal lines start at 1. */
+  public Integer line;
+
   public Range range;
   public String inReplyTo;
   public Timestamp updated;
diff --git a/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java b/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
index 652abcc..ed01a4d 100644
--- a/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -38,7 +38,7 @@
     IGNORE_NONE,
     IGNORE_TRAILING,
     IGNORE_LEADING_AND_TRAILING,
-    IGNORE_ALL;
+    IGNORE_ALL
   }
 
   public Integer context;
@@ -73,17 +73,12 @@
     i.fontSize = DEFAULT_FONT_SIZE;
     i.lineLength = DEFAULT_LINE_LENGTH;
     i.cursorBlinkRate = 0;
-    i.ignoreWhitespace = Whitespace.IGNORE_NONE;
     i.expandAllComments = false;
     i.intralineDifference = true;
     i.manualReview = false;
-    i.retainHeader = false;
     i.showLineEndings = true;
     i.showTabs = true;
     i.showWhitespaceErrors = true;
-    i.skipDeleted = false;
-    i.skipUnchanged = false;
-    i.skipUncommented = false;
     i.syntaxHighlighting = true;
     i.hideTopMenu = false;
     i.autoHideDiffTableHeader = true;
@@ -92,6 +87,11 @@
     i.hideEmptyPane = false;
     i.matchBrackets = false;
     i.lineWrapping = false;
+    i.ignoreWhitespace = Whitespace.IGNORE_NONE;
+    i.retainHeader = false;
+    i.skipDeleted = false;
+    i.skipUnchanged = false;
+    i.skipUncommented = false;
     return i;
   }
 }
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 8eb54e1..212f6da 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -67,14 +67,6 @@
     }
   }
 
-  public enum ReviewCategoryStrategy {
-    NONE,
-    NAME,
-    EMAIL,
-    USERNAME,
-    ABBREV
-  }
-
   public enum DiffView {
     SIDE_BY_SIDE,
     UNIFIED_DIFF
@@ -130,10 +122,6 @@
 
   /** Number of changes to show in a screen. */
   public Integer changesPerPage;
-  /** Should the site header be displayed when logged in ? */
-  public Boolean showSiteHeader;
-  /** Should the Flash helper movie be used to copy text to the clipboard? */
-  public Boolean useFlashClipboard;
   /** Type of download URL the user prefers to use. */
   public String downloadScheme;
 
@@ -145,20 +133,15 @@
   public DiffView diffView;
   public Boolean sizeBarInChangeTable;
   public Boolean legacycidInChangeTable;
-  public ReviewCategoryStrategy reviewCategoryStrategy;
   public Boolean muteCommonPathPrefixes;
   public Boolean signedOffBy;
-  public List<MenuItem> my;
-  public List<String> changeTable;
   public EmailStrategy emailStrategy;
   public EmailFormat emailFormat;
   public DefaultBase defaultBaseForMerges;
   public Boolean publishCommentsOnPush;
   public Boolean workInProgressByDefault;
-
-  public boolean isShowInfoInReviewCategory() {
-    return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE;
-  }
+  public List<MenuItem> my;
+  public List<String> changeTable;
 
   public DateFormat getDateFormat() {
     if (dateFormat == null) {
@@ -174,13 +157,6 @@
     return timeFormat;
   }
 
-  public ReviewCategoryStrategy getReviewCategoryStrategy() {
-    if (reviewCategoryStrategy == null) {
-      return ReviewCategoryStrategy.NONE;
-    }
-    return reviewCategoryStrategy;
-  }
-
   public DiffView getDiffView() {
     if (diffView == null) {
       return DiffView.SIDE_BY_SIDE;
@@ -205,11 +181,6 @@
   public static GeneralPreferencesInfo defaults() {
     GeneralPreferencesInfo p = new GeneralPreferencesInfo();
     p.changesPerPage = DEFAULT_PAGESIZE;
-    p.showSiteHeader = true;
-    p.useFlashClipboard = true;
-    p.emailStrategy = EmailStrategy.ENABLED;
-    p.emailFormat = EmailFormat.HTML_PLAINTEXT;
-    p.reviewCategoryStrategy = ReviewCategoryStrategy.NONE;
     p.downloadScheme = null;
     p.dateFormat = DateFormat.STD;
     p.timeFormat = TimeFormat.HHMM_12;
@@ -221,6 +192,8 @@
     p.legacycidInChangeTable = false;
     p.muteCommonPathPrefixes = true;
     p.signedOffBy = false;
+    p.emailStrategy = EmailStrategy.ENABLED;
+    p.emailFormat = EmailFormat.HTML_PLAINTEXT;
     p.defaultBaseForMerges = DefaultBase.FIRST_PARENT;
     p.publishCommentsOnPush = false;
     p.workInProgressByDefault = false;
diff --git a/java/com/google/gerrit/extensions/client/ListChangesOption.java b/java/com/google/gerrit/extensions/client/ListChangesOption.java
index c842adc..425265b 100644
--- a/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -75,7 +75,13 @@
   TRACKING_IDS(21),
 
   /** Skip mergeability data */
-  SKIP_MERGEABLE(22);
+  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);
 
   private final int value;
 
diff --git a/java/com/google/gerrit/extensions/common/AccountDetailInfo.java b/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
index a498ab0..3ffa97a 100644
--- a/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
@@ -18,7 +18,6 @@
 
 public class AccountDetailInfo extends AccountInfo {
   public Timestamp registeredOn;
-  public Boolean inactive;
 
   public AccountDetailInfo(Integer id) {
     super(id);
diff --git a/java/com/google/gerrit/extensions/common/AccountInfo.java b/java/com/google/gerrit/extensions/common/AccountInfo.java
index f20509b..d1bbe88 100644
--- a/java/com/google/gerrit/extensions/common/AccountInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.common;
 
+import com.google.common.base.MoreObjects;
 import java.util.List;
 import java.util.Objects;
 
@@ -26,6 +27,7 @@
   public List<AvatarInfo> avatars;
   public Boolean _moreAccounts;
   public String status;
+  public Boolean inactive;
 
   public AccountInfo(Integer id) {
     this._accountId = id;
@@ -54,6 +56,16 @@
   }
 
   @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("id", _accountId)
+        .add("name", name)
+        .add("email", email)
+        .add("username", username)
+        .toString();
+  }
+
+  @Override
   public int hashCode() {
     return Objects.hash(
         _accountId, name, email, secondaryEmails, username, avatars, _moreAccounts, status);
diff --git a/java/com/google/gerrit/extensions/common/AvatarInfo.java b/java/com/google/gerrit/extensions/common/AvatarInfo.java
index 00f1819..de609eb 100644
--- a/java/com/google/gerrit/extensions/common/AvatarInfo.java
+++ b/java/com/google/gerrit/extensions/common/AvatarInfo.java
@@ -21,7 +21,7 @@
    * <p>The web UI prefers avatar images to be square, both the height and width of the image should
    * be this size. The height is the more important dimension to match than the width.
    */
-  public static final int DEFAULT_SIZE = 26;
+  public static final int DEFAULT_SIZE = 32;
 
   public String url;
   public Integer height;
diff --git a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
index 1e822e3..e8aeb40 100644
--- a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
@@ -23,4 +23,5 @@
   public String replyTooltip;
   public int updateDelay;
   public Boolean submitWholeTopic;
+  public Boolean excludeMergeableInChangeInfo;
 }
diff --git a/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java b/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
index 3e6f762..711337a 100644
--- a/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
+++ b/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.common;
 
 import java.sql.Timestamp;
+import java.util.Optional;
 
 public abstract class GroupAuditEventInfo {
   public enum Type {
@@ -30,35 +31,35 @@
 
   public static UserMemberAuditEventInfo createAddUserEvent(
       AccountInfo user, Timestamp date, AccountInfo member) {
-    return new UserMemberAuditEventInfo(Type.ADD_USER, user, date, member);
+    return new UserMemberAuditEventInfo(Type.ADD_USER, user, Optional.of(date), member);
   }
 
   public static UserMemberAuditEventInfo createRemoveUserEvent(
-      AccountInfo user, Timestamp date, AccountInfo member) {
+      AccountInfo user, Optional<Timestamp> date, AccountInfo member) {
     return new UserMemberAuditEventInfo(Type.REMOVE_USER, user, date, member);
   }
 
   public static GroupMemberAuditEventInfo createAddGroupEvent(
       AccountInfo user, Timestamp date, GroupInfo member) {
-    return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, date, member);
+    return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, Optional.of(date), member);
   }
 
   public static GroupMemberAuditEventInfo createRemoveGroupEvent(
-      AccountInfo user, Timestamp date, GroupInfo member) {
+      AccountInfo user, Optional<Timestamp> date, GroupInfo member) {
     return new GroupMemberAuditEventInfo(Type.REMOVE_GROUP, user, date, member);
   }
 
-  protected GroupAuditEventInfo(Type type, AccountInfo user, Timestamp date) {
+  protected GroupAuditEventInfo(Type type, AccountInfo user, Optional<Timestamp> date) {
     this.type = type;
     this.user = user;
-    this.date = date;
+    this.date = date.orElse(null);
   }
 
   public static class UserMemberAuditEventInfo extends GroupAuditEventInfo {
     public AccountInfo member;
 
-    public UserMemberAuditEventInfo(
-        Type type, AccountInfo user, Timestamp date, AccountInfo member) {
+    private UserMemberAuditEventInfo(
+        Type type, AccountInfo user, Optional<Timestamp> date, AccountInfo member) {
       super(type, user, date);
       this.member = member;
     }
@@ -67,8 +68,8 @@
   public static class GroupMemberAuditEventInfo extends GroupAuditEventInfo {
     public GroupInfo member;
 
-    public GroupMemberAuditEventInfo(
-        Type type, AccountInfo user, Timestamp date, GroupInfo member) {
+    private GroupMemberAuditEventInfo(
+        Type type, AccountInfo user, Optional<Timestamp> date, GroupInfo member) {
       super(type, user, date);
       this.member = member;
     }
diff --git a/java/com/google/gerrit/extensions/common/testing/BUILD b/java/com/google/gerrit/extensions/common/testing/BUILD
index 9cecb66..c976de0 100644
--- a/java/com/google/gerrit/extensions/common/testing/BUILD
+++ b/java/com/google/gerrit/extensions/common/testing/BUILD
@@ -9,7 +9,7 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/truth",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
         "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
index f0f5516..d6fcb37 100644
--- a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.truth.ListSubject;
 
-public class CommitInfoSubject extends Subject<CommitInfoSubject, CommitInfo> {
+public class CommitInfoSubject extends Subject {
 
   public static CommitInfoSubject assertThat(CommitInfo commitInfo) {
     return assertAbout(commits()).that(commitInfo);
@@ -34,37 +34,35 @@
     return CommitInfoSubject::new;
   }
 
+  private final CommitInfo commitInfo;
+
   private CommitInfoSubject(FailureMetadata failureMetadata, CommitInfo commitInfo) {
     super(failureMetadata, commitInfo);
+    this.commitInfo = commitInfo;
   }
 
   public StringSubject commit() {
     isNotNull();
-    CommitInfo commitInfo = actual();
-    return check("commit()").that(commitInfo.commit);
+    return check("commit").that(commitInfo.commit);
   }
 
   public ListSubject<CommitInfoSubject, CommitInfo> parents() {
     isNotNull();
-    CommitInfo commitInfo = actual();
-    return check("parents()").about(elements()).thatCustom(commitInfo.parents, commits());
+    return check("parents").about(elements()).thatCustom(commitInfo.parents, commits());
   }
 
   public GitPersonSubject committer() {
     isNotNull();
-    CommitInfo commitInfo = actual();
-    return check("committer()").about(gitPersons()).that(commitInfo.committer);
+    return check("committer").about(gitPersons()).that(commitInfo.committer);
   }
 
   public GitPersonSubject author() {
     isNotNull();
-    CommitInfo commitInfo = actual();
-    return check("author()").about(gitPersons()).that(commitInfo.author);
+    return check("author").about(gitPersons()).that(commitInfo.author);
   }
 
   public StringSubject message() {
     isNotNull();
-    CommitInfo commitInfo = actual();
     return check("message").that(commitInfo.message);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
index 25750c1..b55f7c2 100644
--- a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
@@ -27,7 +27,7 @@
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
 import com.google.gerrit.truth.ListSubject;
 
-public class ContentEntrySubject extends Subject<ContentEntrySubject, ContentEntry> {
+public class ContentEntrySubject extends Subject {
 
   public static ContentEntrySubject assertThat(ContentEntry contentEntry) {
     return assertAbout(contentEntries()).that(contentEntry);
@@ -37,13 +37,15 @@
     return ContentEntrySubject::new;
   }
 
+  private final ContentEntry contentEntry;
+
   private ContentEntrySubject(FailureMetadata failureMetadata, ContentEntry contentEntry) {
     super(failureMetadata, contentEntry);
+    this.contentEntry = contentEntry;
   }
 
   public void isDueToRebase() {
     isNotNull();
-    ContentEntry contentEntry = actual();
     if (contentEntry.dueToRebase == null || !contentEntry.dueToRebase) {
       failWithActual(simpleFact("expected entry to be marked 'dueToRebase'"));
     }
@@ -51,7 +53,6 @@
 
   public void isNotDueToRebase() {
     isNotNull();
-    ContentEntry contentEntry = actual();
     if (contentEntry.dueToRebase != null && contentEntry.dueToRebase) {
       failWithActual(simpleFact("expected entry not to be marked 'dueToRebase'"));
     }
@@ -59,7 +60,6 @@
 
   public ListSubject<StringSubject, String> commonLines() {
     isNotNull();
-    ContentEntry contentEntry = actual();
     return check("commonLines()")
         .about(elements())
         .that(contentEntry.ab, StandardSubjectBuilder::that);
@@ -67,31 +67,26 @@
 
   public ListSubject<StringSubject, String> linesOfA() {
     isNotNull();
-    ContentEntry contentEntry = actual();
     return check("linesOfA()").about(elements()).that(contentEntry.a, StandardSubjectBuilder::that);
   }
 
   public ListSubject<StringSubject, String> linesOfB() {
     isNotNull();
-    ContentEntry contentEntry = actual();
     return check("linesOfB()").about(elements()).that(contentEntry.b, StandardSubjectBuilder::that);
   }
 
   public IterableSubject intralineEditsOfA() {
     isNotNull();
-    ContentEntry contentEntry = actual();
     return check("intralineEditsOfA()").that(contentEntry.editA);
   }
 
   public IterableSubject intralineEditsOfB() {
     isNotNull();
-    ContentEntry contentEntry = actual();
     return check("intralineEditsOfB()").that(contentEntry.editB);
   }
 
   public IntegerSubject numberOfSkippedLines() {
     isNotNull();
-    ContentEntry contentEntry = actual();
     return check("numberOfSkippedLines()").that(contentEntry.skip);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
index ee37bde..8853a30 100644
--- a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
@@ -26,39 +26,38 @@
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
 import com.google.gerrit.truth.ListSubject;
 
-public class DiffInfoSubject extends Subject<DiffInfoSubject, DiffInfo> {
+public class DiffInfoSubject extends Subject {
 
   public static DiffInfoSubject assertThat(DiffInfo diffInfo) {
     return assertAbout(DiffInfoSubject::new).that(diffInfo);
   }
 
+  private final DiffInfo diffInfo;
+
   private DiffInfoSubject(FailureMetadata failureMetadata, DiffInfo diffInfo) {
     super(failureMetadata, diffInfo);
+    this.diffInfo = diffInfo;
   }
 
   public ListSubject<ContentEntrySubject, ContentEntry> content() {
     isNotNull();
-    DiffInfo diffInfo = actual();
-    return check("content()")
+    return check("content")
         .about(elements())
         .thatCustom(diffInfo.content, ContentEntrySubject.contentEntries());
   }
 
-  public ComparableSubject<?, ChangeType> changeType() {
+  public ComparableSubject<ChangeType> changeType() {
     isNotNull();
-    DiffInfo diffInfo = actual();
-    return check("changeType()").that(diffInfo.changeType);
+    return check("changeType").that(diffInfo.changeType);
   }
 
   public FileMetaSubject metaA() {
     isNotNull();
-    DiffInfo diffInfo = actual();
-    return check("metaA()").about(fileMetas()).that(diffInfo.metaA);
+    return check("metaA").about(fileMetas()).that(diffInfo.metaA);
   }
 
   public FileMetaSubject metaB() {
     isNotNull();
-    DiffInfo diffInfo = actual();
-    return check("metaB()").about(fileMetas()).that(diffInfo.metaB);
+    return check("metaB").about(fileMetas()).that(diffInfo.metaB);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
index 1c99141..b5622e0 100644
--- a/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.truth.OptionalSubject;
 import java.util.Optional;
 
-public class EditInfoSubject extends Subject<EditInfoSubject, EditInfo> {
+public class EditInfoSubject extends Subject {
 
   public static EditInfoSubject assertThat(EditInfo editInfo) {
     return assertAbout(edits()).that(editInfo);
@@ -39,19 +39,20 @@
     return OptionalSubject.assertThat(editInfoOptional, edits());
   }
 
+  private final EditInfo editInfo;
+
   private EditInfoSubject(FailureMetadata failureMetadata, EditInfo editInfo) {
     super(failureMetadata, editInfo);
+    this.editInfo = editInfo;
   }
 
   public CommitInfoSubject commit() {
     isNotNull();
-    EditInfo editInfo = actual();
-    return check("commit()").about(commits()).that(editInfo.commit);
+    return check("commit").about(commits()).that(editInfo.commit);
   }
 
   public StringSubject baseRevision() {
     isNotNull();
-    EditInfo editInfo = actual();
-    return check("baseRevision()").that(editInfo.baseRevision);
+    return check("baseRevision").that(editInfo.baseRevision);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
index 3ebf838..d011d5d 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
@@ -22,31 +22,31 @@
 import com.google.common.truth.Subject;
 import com.google.gerrit.extensions.common.FileInfo;
 
-public class FileInfoSubject extends Subject<FileInfoSubject, FileInfo> {
+public class FileInfoSubject extends Subject {
 
   public static FileInfoSubject assertThat(FileInfo fileInfo) {
     return assertAbout(FileInfoSubject::new).that(fileInfo);
   }
 
+  private final FileInfo fileInfo;
+
   private FileInfoSubject(FailureMetadata failureMetadata, FileInfo fileInfo) {
     super(failureMetadata, fileInfo);
+    this.fileInfo = fileInfo;
   }
 
   public IntegerSubject linesInserted() {
     isNotNull();
-    FileInfo fileInfo = actual();
-    return check("linesInserted()").that(fileInfo.linesInserted);
+    return check("linesInserted").that(fileInfo.linesInserted);
   }
 
   public IntegerSubject linesDeleted() {
     isNotNull();
-    FileInfo fileInfo = actual();
-    return check("linesDeleted()").that(fileInfo.linesDeleted);
+    return check("linesDeleted").that(fileInfo.linesDeleted);
   }
 
-  public ComparableSubject<?, Character> status() {
+  public ComparableSubject<Character> status() {
     isNotNull();
-    FileInfo fileInfo = actual();
-    return check("status()").that(fileInfo.status);
+    return check("status").that(fileInfo.status);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
index d1b2031..fb09a1f 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
@@ -21,7 +21,7 @@
 import com.google.common.truth.Subject;
 import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
 
-public class FileMetaSubject extends Subject<FileMetaSubject, FileMeta> {
+public class FileMetaSubject extends Subject {
 
   public static FileMetaSubject assertThat(FileMeta fileMeta) {
     return assertAbout(fileMetas()).that(fileMeta);
@@ -31,13 +31,15 @@
     return FileMetaSubject::new;
   }
 
+  private final FileMeta fileMeta;
+
   private FileMetaSubject(FailureMetadata failureMetadata, FileMeta fileMeta) {
     super(failureMetadata, fileMeta);
+    this.fileMeta = fileMeta;
   }
 
   public IntegerSubject totalLineCount() {
     isNotNull();
-    FileMeta fileMeta = actual();
     return check("totalLineCount()").that(fileMeta.lines);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
index 6ba5f8b..9ba69dc 100644
--- a/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
@@ -22,8 +22,7 @@
 import com.google.common.truth.Subject;
 import com.google.gerrit.extensions.common.FixReplacementInfo;
 
-public class FixReplacementInfoSubject
-    extends Subject<FixReplacementInfoSubject, FixReplacementInfo> {
+public class FixReplacementInfoSubject extends Subject {
 
   public static FixReplacementInfoSubject assertThat(FixReplacementInfo fixReplacementInfo) {
     return assertAbout(fixReplacements()).that(fixReplacementInfo);
@@ -33,23 +32,26 @@
     return FixReplacementInfoSubject::new;
   }
 
+  private final FixReplacementInfo fixReplacementInfo;
+
   private FixReplacementInfoSubject(
       FailureMetadata failureMetadata, FixReplacementInfo fixReplacementInfo) {
     super(failureMetadata, fixReplacementInfo);
+    this.fixReplacementInfo = fixReplacementInfo;
   }
 
   public StringSubject path() {
     isNotNull();
-    return check("path()").that(actual().path);
+    return check("path").that(fixReplacementInfo.path);
   }
 
   public RangeSubject range() {
     isNotNull();
-    return check("range()").about(ranges()).that(actual().range);
+    return check("range").about(ranges()).that(fixReplacementInfo.range);
   }
 
   public StringSubject replacement() {
     isNotNull();
-    return check("replacement()").that(actual().replacement);
+    return check("replacement").that(fixReplacementInfo.replacement);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
index 98dac38..4ac725a 100644
--- a/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
@@ -21,12 +21,11 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.FixReplacementInfo;
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import com.google.gerrit.truth.ListSubject;
 
-public class FixSuggestionInfoSubject extends Subject<FixSuggestionInfoSubject, FixSuggestionInfo> {
+public class FixSuggestionInfoSubject extends Subject {
 
   public static FixSuggestionInfoSubject assertThat(FixSuggestionInfo fixSuggestionInfo) {
     return assertAbout(fixSuggestions()).that(fixSuggestionInfo);
@@ -36,20 +35,23 @@
     return FixSuggestionInfoSubject::new;
   }
 
+  private final FixSuggestionInfo fixSuggestionInfo;
+
   private FixSuggestionInfoSubject(
       FailureMetadata failureMetadata, FixSuggestionInfo fixSuggestionInfo) {
     super(failureMetadata, fixSuggestionInfo);
+    this.fixSuggestionInfo = fixSuggestionInfo;
   }
 
   public StringSubject fixId() {
-    return Truth.assertThat(actual().fixId).named("fixId");
+    return check("fixId").that(fixSuggestionInfo.fixId);
   }
 
   public ListSubject<FixReplacementInfoSubject, FixReplacementInfo> replacements() {
     isNotNull();
-    return check("replacements()")
+    return check("replacements")
         .about(elements())
-        .thatCustom(actual().replacements, fixReplacements());
+        .thatCustom(fixSuggestionInfo.replacements, fixReplacements());
   }
 
   public FixReplacementInfoSubject onlyReplacement() {
@@ -58,6 +60,6 @@
 
   public StringSubject description() {
     isNotNull();
-    return check("description()").that(actual().description);
+    return check("description").that(fixSuggestionInfo.description);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index dee0636..d827d5d 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.extensions.common.testing;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertAbout;
+import static java.util.Objects.requireNonNull;
 
 import com.google.common.truth.ComparableSubject;
 import com.google.common.truth.FailureMetadata;
@@ -27,7 +27,7 @@
 import java.util.Date;
 import org.eclipse.jgit.lib.PersonIdent;
 
-public class GitPersonSubject extends Subject<GitPersonSubject, GitPerson> {
+public class GitPersonSubject extends Subject {
 
   public static GitPersonSubject assertThat(GitPerson gitPerson) {
     return assertAbout(gitPersons()).that(gitPerson);
@@ -37,36 +37,35 @@
     return GitPersonSubject::new;
   }
 
+  private final GitPerson gitPerson;
+
   private GitPersonSubject(FailureMetadata failureMetadata, GitPerson gitPerson) {
     super(failureMetadata, gitPerson);
+    this.gitPerson = gitPerson;
   }
 
   public StringSubject name() {
     isNotNull();
-    GitPerson gitPerson = actual();
-    return check("name()").that(gitPerson.name);
+    return check("name").that(gitPerson.name);
   }
 
   public StringSubject email() {
     isNotNull();
-    GitPerson gitPerson = actual();
-    return check("email()").that(gitPerson.email);
+    return check("email").that(gitPerson.email);
   }
 
-  public ComparableSubject<?, Timestamp> date() {
+  public ComparableSubject<Timestamp> date() {
     isNotNull();
-    GitPerson gitPerson = actual();
-    return check("date()").that(gitPerson.date);
+    return check("date").that(gitPerson.date);
   }
 
   public IntegerSubject tz() {
     isNotNull();
-    GitPerson gitPerson = actual();
-    return check("tz()").that(gitPerson.tz);
+    return check("tz").that(gitPerson.tz);
   }
 
   public void hasSameDateAs(GitPerson other) {
-    checkNotNull(other, "'other' GitPerson must not be null");
+    requireNonNull(other, "'other' GitPerson must not be null");
     isNotNull();
     date().isEqualTo(other.date);
     tz().isEqualTo(other.tz);
@@ -76,7 +75,7 @@
     isNotNull();
     name().isEqualTo(ident.getName());
     email().isEqualTo(ident.getEmailAddress());
-    check("roundedDate()").that(new Date(actual().date.getTime())).isEqualTo(ident.getWhen());
+    check("roundedDate()").that(new Date(gitPerson.date.getTime())).isEqualTo(ident.getWhen());
     tz().isEqualTo(ident.getTimeZoneOffset());
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/RangeSubject.java b/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
index 12acb8d..10abca2 100644
--- a/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
@@ -22,7 +22,7 @@
 import com.google.common.truth.Subject;
 import com.google.gerrit.extensions.client.Comment;
 
-public class RangeSubject extends Subject<RangeSubject, Comment.Range> {
+public class RangeSubject extends Subject {
 
   public static RangeSubject assertThat(Comment.Range range) {
     return assertAbout(ranges()).that(range);
@@ -32,36 +32,39 @@
     return RangeSubject::new;
   }
 
+  private final Comment.Range range;
+
   private RangeSubject(FailureMetadata failureMetadata, Comment.Range range) {
     super(failureMetadata, range);
+    this.range = range;
   }
 
   public IntegerSubject startLine() {
-    return check("startLine()").that(actual().startLine);
+    return check("startLine").that(range.startLine);
   }
 
   public IntegerSubject startCharacter() {
-    return check("startCharacter()").that(actual().startCharacter);
+    return check("startCharacter").that(range.startCharacter);
   }
 
   public IntegerSubject endLine() {
-    return check("endLine()").that(actual().endLine);
+    return check("endLine").that(range.endLine);
   }
 
   public IntegerSubject endCharacter() {
-    return check("endCharacter()").that(actual().endCharacter);
+    return check("endCharacter").that(range.endCharacter);
   }
 
   public void isValid() {
     isNotNull();
-    if (!actual().isValid()) {
+    if (!range.isValid()) {
       failWithActual(simpleFact("expected to be valid"));
     }
   }
 
   public void isInvalid() {
     isNotNull();
-    if (actual().isValid()) {
+    if (range.isValid()) {
       failWithActual(simpleFact("expected to be invalid"));
     }
   }
diff --git a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
index 033f54b..0698735 100644
--- a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
@@ -24,11 +24,11 @@
 import com.google.gerrit.truth.ListSubject;
 import java.util.List;
 
-public class RobotCommentInfoSubject extends Subject<RobotCommentInfoSubject, RobotCommentInfo> {
+public class RobotCommentInfoSubject extends Subject {
 
   public static ListSubject<RobotCommentInfoSubject, RobotCommentInfo> assertThatList(
       List<RobotCommentInfo> robotCommentInfos) {
-    return ListSubject.assertThat(robotCommentInfos, robotComments()).named("robotCommentInfos");
+    return ListSubject.assertThat(robotCommentInfos, robotComments());
   }
 
   public static RobotCommentInfoSubject assertThat(RobotCommentInfo robotCommentInfo) {
@@ -39,15 +39,18 @@
     return RobotCommentInfoSubject::new;
   }
 
+  private final RobotCommentInfo robotCommentInfo;
+
   private RobotCommentInfoSubject(
       FailureMetadata failureMetadata, RobotCommentInfo robotCommentInfo) {
     super(failureMetadata, robotCommentInfo);
+    this.robotCommentInfo = robotCommentInfo;
   }
 
   public ListSubject<FixSuggestionInfoSubject, FixSuggestionInfo> fixSuggestions() {
-    return check("fixSuggestions()")
+    return check("fixSuggestions")
         .about(elements())
-        .thatCustom(actual().fixSuggestions, FixSuggestionInfoSubject.fixSuggestions());
+        .thatCustom(robotCommentInfo.fixSuggestions, FixSuggestionInfoSubject.fixSuggestions());
   }
 
   public FixSuggestionInfoSubject onlyFixSuggestion() {
diff --git a/java/com/google/gerrit/extensions/events/AccountActivationListener.java b/java/com/google/gerrit/extensions/events/AccountActivationListener.java
new file mode 100644
index 0000000..b45533b
--- /dev/null
+++ b/java/com/google/gerrit/extensions/events/AccountActivationListener.java
@@ -0,0 +1,42 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Notified whenever an account got activated or deactivated.
+ *
+ * <p>This listener is called only after an account got (de)activated and hence cannot cancel the
+ * (de)activation. See {@link
+ * com.google.gerrit.server.validators.AccountActivationValidationListener} for a listener that can
+ * cancel a (de)activation.
+ */
+@ExtensionPoint
+public interface AccountActivationListener {
+  /**
+   * Invoked after an account got activated
+   *
+   * @param id of the account
+   */
+  default void onAccountActivated(int id) {}
+
+  /**
+   * Invoked after an account got deactivated
+   *
+   * @param id of the account
+   */
+  default void onAccountDeactivated(int id) {}
+}
diff --git a/java/com/google/gerrit/extensions/events/ChangeEvent.java b/java/com/google/gerrit/extensions/events/ChangeEvent.java
index 0b51052..def75b7 100644
--- a/java/com/google/gerrit/extensions/events/ChangeEvent.java
+++ b/java/com/google/gerrit/extensions/events/ChangeEvent.java
@@ -20,6 +20,11 @@
 
 /** Interface to be extended by Events with a Change. */
 public interface ChangeEvent extends GerritEvent {
+  /**
+   * Information about the change. Some fields might be null.
+   *
+   * @see com.google.gerrit.server.extensions.events.EventUtil
+   */
   ChangeInfo getChange();
 
   AccountInfo getWho();
diff --git a/java/com/google/gerrit/extensions/events/ChangeIndexedListener.java b/java/com/google/gerrit/extensions/events/ChangeIndexedListener.java
index 8dd64ed..64bc022 100644
--- a/java/com/google/gerrit/extensions/events/ChangeIndexedListener.java
+++ b/java/com/google/gerrit/extensions/events/ChangeIndexedListener.java
@@ -20,6 +20,21 @@
 @ExtensionPoint
 public interface ChangeIndexedListener {
   /**
+   * Invoked when a change is scheduled for indexing.
+   *
+   * @param projectName project containing the change
+   * @param id id of the change that was scheduled for indexing
+   */
+  default void onChangeScheduledForIndexing(String projectName, int id) {}
+
+  /**
+   * Invoked when a change is scheduled for deletion from indexing.
+   *
+   * @param id id of the change that was scheduled for deletion from indexing
+   */
+  default void onChangeScheduledForDeletionFromIndex(int id) {}
+
+  /**
    * Invoked when a change is indexed.
    *
    * @param projectName project containing the change
diff --git a/java/com/google/gerrit/extensions/events/RevisionEvent.java b/java/com/google/gerrit/extensions/events/RevisionEvent.java
index f0cfa2c..db7830e 100644
--- a/java/com/google/gerrit/extensions/events/RevisionEvent.java
+++ b/java/com/google/gerrit/extensions/events/RevisionEvent.java
@@ -18,5 +18,11 @@
 
 /** Interface to be extended by Events with a Revision. */
 public interface RevisionEvent extends ChangeEvent {
+
+  /**
+   * Information about the revision. Some fields might be null.
+   *
+   * @see com.google.gerrit.server.extensions.events.EventUtil
+   */
   RevisionInfo getRevision();
 }
diff --git a/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java b/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
index fa2288a..270a040 100644
--- a/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
+++ b/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
@@ -38,9 +38,8 @@
     super("Not found: " + id.get(), cause);
   }
 
-  @SuppressWarnings("unchecked")
-  @Override
   public ResourceNotFoundException caching(CacheControl c) {
-    return super.caching(c);
+    setCaching(c);
+    return this;
   }
 }
diff --git a/java/com/google/gerrit/extensions/restapi/Response.java b/java/com/google/gerrit/extensions/restapi/Response.java
index 8f2dd5f..5504cfd 100644
--- a/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/java/com/google/gerrit/extensions/restapi/Response.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 /** Special return value to mean specific HTTP status codes in a REST API. */
@@ -51,24 +55,48 @@
     return new Redirect(location);
   }
 
+  /**
+   * HTTP 500 Internal Server Error: failure due to an unexpected exception.
+   *
+   * <p>Can be returned from REST endpoints, instead of throwing the exception, if additional
+   * properties (e.g. a traceId) should be set on the response.
+   *
+   * @param cause the exception that caused the request to fail, must not be a {@link
+   *     RestApiException} because such an exception would result in a 4XX response code
+   */
+  public static <T> InternalServerError<T> internalServerError(Exception cause) {
+    return new InternalServerError<>(cause);
+  }
+
   /** Arbitrary status code with wrapped result. */
   public static <T> Response<T> withStatusCode(int statusCode, T value) {
     return new Impl<>(statusCode, value);
   }
 
   @SuppressWarnings({"unchecked", "rawtypes"})
-  public static <T> T unwrap(T obj) {
+  public static <T> T unwrap(T obj) throws Exception {
     while (obj instanceof Response) {
       obj = (T) ((Response) obj).value();
     }
     return obj;
   }
 
+  private String traceId;
+
+  public Response<T> traceId(@Nullable String traceId) {
+    this.traceId = traceId;
+    return this;
+  }
+
+  public Optional<String> traceId() {
+    return Optional.ofNullable(traceId);
+  }
+
   public abstract boolean isNone();
 
   public abstract int statusCode();
 
-  public abstract T value();
+  public abstract T value() throws Exception;
 
   public abstract CacheControl caching();
 
@@ -154,13 +182,38 @@
   }
 
   /** An HTTP redirect to another location. */
-  public static final class Redirect {
+  public static final class Redirect extends Response<Object> {
     private final String location;
 
     private Redirect(String url) {
       this.location = url;
     }
 
+    @Override
+    public boolean isNone() {
+      return false;
+    }
+
+    @Override
+    public int statusCode() {
+      return 302;
+    }
+
+    @Override
+    public Object value() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public CacheControl caching() {
+      return CacheControl.NONE;
+    }
+
+    @Override
+    public Response<Object> caching(CacheControl c) {
+      throw new UnsupportedOperationException();
+    }
+
     public String location() {
       return location;
     }
@@ -182,13 +235,38 @@
   }
 
   /** Accepted as task for asynchronous execution. */
-  public static final class Accepted {
+  public static final class Accepted extends Response<Object> {
     private final String location;
 
     private Accepted(String url) {
       this.location = url;
     }
 
+    @Override
+    public boolean isNone() {
+      return false;
+    }
+
+    @Override
+    public int statusCode() {
+      return 202;
+    }
+
+    @Override
+    public Object value() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public CacheControl caching() {
+      return CacheControl.NONE;
+    }
+
+    @Override
+    public Response<Object> caching(CacheControl c) {
+      throw new UnsupportedOperationException();
+    }
+
     public String location() {
       return location;
     }
@@ -208,4 +286,57 @@
       return String.format("[202 Accepted] %s", location);
     }
   }
+
+  public static final class InternalServerError<T> extends Response<T> {
+    private final Exception cause;
+
+    private InternalServerError(Exception cause) {
+      checkArgument(!(cause instanceof RestApiException), "cause must not be a RestApiException");
+      this.cause = cause;
+    }
+
+    @Override
+    public boolean isNone() {
+      return false;
+    }
+
+    @Override
+    public int statusCode() {
+      return 500;
+    }
+
+    @Override
+    public T value() throws Exception {
+      throw cause();
+    }
+
+    @Override
+    public CacheControl caching() {
+      return CacheControl.NONE;
+    }
+
+    @Override
+    public Response<T> caching(CacheControl c) {
+      throw new UnsupportedOperationException();
+    }
+
+    public Exception cause() {
+      return cause;
+    }
+
+    @Override
+    public int hashCode() {
+      return cause.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof InternalServerError && ((InternalServerError<?>) o).cause.equals(cause);
+    }
+
+    @Override
+    public String toString() {
+      return String.format("[500 Internal Server Error] %s", cause.getClass());
+    }
+  }
 }
diff --git a/java/com/google/gerrit/extensions/restapi/RestApiException.java b/java/com/google/gerrit/extensions/restapi/RestApiException.java
index b09723e..f3d7dec 100644
--- a/java/com/google/gerrit/extensions/restapi/RestApiException.java
+++ b/java/com/google/gerrit/extensions/restapi/RestApiException.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import static java.util.Objects.requireNonNull;
+
 /** Root exception type for REST API failures. */
 public class RestApiException extends Exception {
   private static final long serialVersionUID = 1L;
@@ -33,9 +35,7 @@
     return caching;
   }
 
-  @SuppressWarnings("unchecked")
-  public <T extends RestApiException> T caching(CacheControl c) {
-    caching = c;
-    return (T) this;
+  protected void setCaching(CacheControl caching) {
+    this.caching = requireNonNull(caching);
   }
 }
diff --git a/java/com/google/gerrit/extensions/restapi/RestCollectionCreateView.java b/java/com/google/gerrit/extensions/restapi/RestCollectionCreateView.java
index 25cdb76..72ca74b 100644
--- a/java/com/google/gerrit/extensions/restapi/RestCollectionCreateView.java
+++ b/java/com/google/gerrit/extensions/restapi/RestCollectionCreateView.java
@@ -33,13 +33,24 @@
   /**
    * Process the view operation by creating the resource.
    *
+   * <p>The returned response defines the status code that is returned to the client. For
+   * RestCollectionCreateViews this is usually {@code 201 Created} because a resource is created,
+   * but other 2XX or 3XX status codes are also possible (e.g. {@link Response.Redirect} can be
+   * returned for {@code 302 Found}).
+   *
+   * <p>The value of the returned response is automatically converted to JSON unless it is a {@link
+   * BinaryResult}.
+   *
+   * <p>Throwing a subclass of {@link RestApiException} results in a 4XX response to the client. For
+   * any other exception the client will get a {@code 500 Internal Server Error} response.
+   *
    * @param parentResource parent resource of the resource that should be created
+   * @param id the ID of the child resource that should be created
    * @param input input after parsing from request.
-   * @return result to return to the client. Use {@link BinaryResult} to avoid automatic conversion
-   *     to JSON.
+   * @return response to return to the client
    * @throws RestApiException if the resource creation is rejected
    * @throws Exception the implementation of the view failed. The exception will be logged and HTTP
    *     500 Internal Server Error will be returned to the client.
    */
-  Object apply(P parentResource, IdString id, I input) throws Exception;
+  Response<?> apply(P parentResource, IdString id, I input) throws Exception;
 }
diff --git a/java/com/google/gerrit/extensions/restapi/RestCollectionDeleteMissingView.java b/java/com/google/gerrit/extensions/restapi/RestCollectionDeleteMissingView.java
index 7e5649c..c08d06a 100644
--- a/java/com/google/gerrit/extensions/restapi/RestCollectionDeleteMissingView.java
+++ b/java/com/google/gerrit/extensions/restapi/RestCollectionDeleteMissingView.java
@@ -37,13 +37,25 @@
   /**
    * Process the view operation by deleting the resource.
    *
+   * <p>The returned response defines the status code that is returned to the client. For
+   * RestCollectionDeleteMissingViews this is usually {@code 204 No Content} because a resource is
+   * deleted, but other 2XX or 3XX status codes are also possible (e.g. {@code 200 OK}, {@code 302
+   * Found} for a redirect).
+   *
+   * <p>The returned response usually does not have any value (status code {@code 204 No Content}).
+   * If a value in the returned response is set it is automatically converted to JSON unless it is a
+   * {@link BinaryResult}.
+   *
+   * <p>Throwing a subclass of {@link RestApiException} results in a 4XX response to the client. For
+   * any other exception the client will get a {@code 500 Internal Server Error} response.
+   *
    * @param parentResource parent resource of the resource that should be deleted
-   * @param input input after parsing from request.
-   * @return result to return to the client. Use {@link BinaryResult} to avoid automatic conversion
-   *     to JSON.
+   * @param id the ID of the child resource that should be deleted
+   * @param input input after parsing from request
+   * @return response to return to the client
    * @throws RestApiException if the resource creation is rejected
    * @throws Exception the implementation of the view failed. The exception will be logged and HTTP
    *     500 Internal Server Error will be returned to the client.
    */
-  Object apply(P parentResource, IdString id, I input) throws Exception;
+  Response<?> apply(P parentResource, IdString id, I input) throws Exception;
 }
diff --git a/java/com/google/gerrit/extensions/restapi/RestCollectionModifyView.java b/java/com/google/gerrit/extensions/restapi/RestCollectionModifyView.java
index acabf96..fcaa15b 100644
--- a/java/com/google/gerrit/extensions/restapi/RestCollectionModifyView.java
+++ b/java/com/google/gerrit/extensions/restapi/RestCollectionModifyView.java
@@ -28,5 +28,25 @@
 public interface RestCollectionModifyView<P extends RestResource, C extends RestResource, I>
     extends RestCollectionView<P, C, I> {
 
-  Object apply(P parentResource, I input) throws Exception;
+  /**
+   * Process the modification on the collection resource.
+   *
+   * <p>The value of the returned response is automatically converted to JSON unless it is a {@link
+   * BinaryResult}.
+   *
+   * <p>The returned response defines the status code that is returned to the client. For
+   * RestCollectionModifyViews this is usually {@code 200 OK}, but other 2XX or 3XX status codes are
+   * also possible (e.g. {@code 201 Created} if a resource was created, {@code 202 Accepted} if a
+   * background task was scheduled, {@code 204 No Content} if no content is returned, {@code 302
+   * Found} for a redirect).
+   *
+   * <p>Throwing a subclass of {@link RestApiException} results in a 4XX response to the client. For
+   * any other exception the client will get a {@code 500 Internal Server Error} response.
+   *
+   * @param parentResource the collection resource on which the modification is done
+   * @return response to return to the client
+   * @throws Exception the implementation of the view failed. The exception will be logged and HTTP
+   *     500 Internal Server Error will be returned to the client.
+   */
+  Response<?> apply(P parentResource, I input) throws Exception;
 }
diff --git a/java/com/google/gerrit/extensions/restapi/RestModifyView.java b/java/com/google/gerrit/extensions/restapi/RestModifyView.java
index 79053dd..e397bd0 100644
--- a/java/com/google/gerrit/extensions/restapi/RestModifyView.java
+++ b/java/com/google/gerrit/extensions/restapi/RestModifyView.java
@@ -28,11 +28,21 @@
   /**
    * Process the view operation by altering the resource.
    *
-   * @param resource resource to modify.
-   * @param input input after parsing from request.
-   * @return result to return to the client. Use {@link BinaryResult} to avoid automatic conversion
-   *     to JSON.
-   * @throws AuthException the client is not permitted to access this view.
+   * <p>The value of the returned response is automatically converted to JSON unless it is a {@link
+   * BinaryResult}.
+   *
+   * <p>The returned response defines the status code that is returned to the client. For
+   * RestModifyViews this is usually {@code 200 OK}, but other 2XX or 3XX status codes are also
+   * possible (e.g. {@code 202 Accepted} if a background task was scheduled, {@code 204 No Content}
+   * if no content is returned, {@code 302 Found} for a redirect).
+   *
+   * <p>Throwing a subclass of {@link RestApiException} results in a 4XX response to the client. For
+   * any other exception the client will get a {@code 500 Internal Server Error} response.
+   *
+   * @param resource resource to modify
+   * @param input input after parsing from request
+   * @return response to return to the client
+   * @throws AuthException the caller is not permitted to access this view.
    * @throws BadRequestException the request was incorrectly specified and cannot be handled by this
    *     view.
    * @throws ResourceConflictException the resource state does not permit this view to make the
@@ -40,6 +50,6 @@
    * @throws Exception the implementation of the view failed. The exception will be logged and HTTP
    *     500 Internal Server Error will be returned to the client.
    */
-  Object apply(R resource, I input)
+  Response<?> apply(R resource, I input)
       throws AuthException, BadRequestException, ResourceConflictException, Exception;
 }
diff --git a/java/com/google/gerrit/extensions/restapi/RestReadView.java b/java/com/google/gerrit/extensions/restapi/RestReadView.java
index a3c31d3..8991f0b 100644
--- a/java/com/google/gerrit/extensions/restapi/RestReadView.java
+++ b/java/com/google/gerrit/extensions/restapi/RestReadView.java
@@ -17,16 +17,27 @@
 /**
  * RestView to read a resource without modification.
  *
+ * <p>RestReadViews are invoked by the HTTP GET method.
+ *
  * @param <R> type of resource the view reads.
  */
 public interface RestReadView<R extends RestResource> extends RestView<R> {
   /**
    * Process the view operation by reading from the resource.
    *
-   * @param resource resource to read.
-   * @return result to return to the client. Use {@link BinaryResult} to avoid automatic conversion
-   *     to JSON.
-   * @throws AuthException the client is not permitted to access this view.
+   * <p>The value of the returned response is automatically converted to JSON unless it is a {@link
+   * BinaryResult}.
+   *
+   * <p>The returned response defines the status code that is returned to the client. For
+   * RestReadViews this is usually {@code 200 OK}, but other 2XX or 3XX status codes are also
+   * possible (e.g. {@link Response.Redirect} can be returned for {@code 302 Found}).
+   *
+   * <p>Throwing a subclass of {@link RestApiException} results in a 4XX response to the client. For
+   * any other exception the client will get a {@code 500 Internal Server Error} response.
+   *
+   * @param resource resource to read
+   * @return response to return to the client
+   * @throws AuthException the caller is not permitted to access this view.
    * @throws BadRequestException the request was incorrectly specified and cannot be handled by this
    *     view.
    * @throws ResourceConflictException the resource state does not permit this view to make the
@@ -34,6 +45,6 @@
    * @throws Exception the implementation of the view failed. The exception will be logged and HTTP
    *     500 Internal Server Error will be returned to the client.
    */
-  Object apply(R resource)
+  Response<?> apply(R resource)
       throws AuthException, BadRequestException, ResourceConflictException, Exception;
 }
diff --git a/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java b/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
index 5109205..c5304e3 100644
--- a/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
+++ b/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
@@ -26,7 +26,7 @@
 import java.io.IOException;
 import java.util.Optional;
 
-public class BinaryResultSubject extends Subject<BinaryResultSubject, BinaryResult> {
+public class BinaryResultSubject extends Subject {
 
   public static BinaryResultSubject assertThat(BinaryResult binaryResult) {
     return assertAbout(binaryResults()).that(binaryResult);
@@ -41,8 +41,11 @@
     return OptionalSubject.assertThat(binaryResultOptional, binaryResults());
   }
 
+  private final BinaryResult binaryResult;
+
   private BinaryResultSubject(FailureMetadata failureMetadata, BinaryResult binaryResult) {
     super(failureMetadata, binaryResult);
+    this.binaryResult = binaryResult;
   }
 
   public StringSubject asString() throws IOException {
@@ -50,7 +53,6 @@
     // We shouldn't close the BinaryResult within this method as it might still
     // be used afterwards. Besides, closing it doesn't have an effect for most
     // implementations of a BinaryResult.
-    BinaryResult binaryResult = actual();
     return check("asString()").that(binaryResult.asString());
   }
 
@@ -59,7 +61,6 @@
     // We shouldn't close the BinaryResult within this method as it might still
     // be used afterwards. Besides, closing it doesn't have an effect for most
     // implementations of a BinaryResult.
-    BinaryResult binaryResult = actual();
     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
     binaryResult.writeTo(byteArrayOutputStream);
     byte[] bytes = byteArrayOutputStream.toByteArray();
diff --git a/java/com/google/gerrit/extensions/validators/CommentForValidation.java b/java/com/google/gerrit/extensions/validators/CommentForValidation.java
new file mode 100644
index 0000000..51ae5ae
--- /dev/null
+++ b/java/com/google/gerrit/extensions/validators/CommentForValidation.java
@@ -0,0 +1,48 @@
+// 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.extensions.validators;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Holds a comment's text and {@link CommentType} in order to pass it to a validation plugin.
+ *
+ * @see CommentValidator
+ */
+@AutoValue
+public abstract class CommentForValidation {
+
+  /** The type of comment. */
+  public enum CommentType {
+    /** A regular (inline) comment. */
+    INLINE_COMMENT,
+    /** A file comment. */
+    FILE_COMMENT,
+    /** A change message. */
+    CHANGE_MESSAGE
+  }
+
+  public static CommentForValidation create(CommentType type, String text) {
+    return new AutoValue_CommentForValidation(type, text);
+  }
+
+  public abstract CommentType getType();
+
+  public abstract String getText();
+
+  public CommentValidationFailure failValidation(String message) {
+    return CommentValidationFailure.create(this, message);
+  }
+}
diff --git a/java/com/google/gerrit/extensions/validators/CommentValidationFailure.java b/java/com/google/gerrit/extensions/validators/CommentValidationFailure.java
new file mode 100644
index 0000000..1a832760
--- /dev/null
+++ b/java/com/google/gerrit/extensions/validators/CommentValidationFailure.java
@@ -0,0 +1,32 @@
+// 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.extensions.validators;
+
+import com.google.auto.value.AutoValue;
+
+/** A comment or review message was rejected by a {@link CommentValidator}. */
+@AutoValue
+public abstract class CommentValidationFailure {
+  static CommentValidationFailure create(
+      CommentForValidation commentForValidation, String message) {
+    return new AutoValue_CommentValidationFailure(commentForValidation, message);
+  }
+
+  /** Returns the offending comment. */
+  public abstract CommentForValidation getComment();
+
+  /** A friendly message set by the {@link CommentValidator}. */
+  public abstract String getMessage();
+}
diff --git a/java/com/google/gerrit/extensions/validators/CommentValidator.java b/java/com/google/gerrit/extensions/validators/CommentValidator.java
new file mode 100644
index 0000000..cfefdef
--- /dev/null
+++ b/java/com/google/gerrit/extensions/validators/CommentValidator.java
@@ -0,0 +1,34 @@
+// 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.extensions.validators;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Validates review comments and messages. Rejecting any comment/message will prevent all comments
+ * from being published.
+ */
+@ExtensionPoint
+public interface CommentValidator {
+
+  /**
+   * Validate the specified comments.
+   *
+   * @return An empty list if all comments are valid, or else a list of validation failures.
+   */
+  ImmutableList<CommentValidationFailure> validateComments(
+      ImmutableList<CommentForValidation> comments);
+}
diff --git a/java/com/google/gerrit/git/BUILD b/java/com/google/gerrit/git/BUILD
index 4c4d5bc..0574716 100644
--- a/java/com/google/gerrit/git/BUILD
+++ b/java/com/google/gerrit/git/BUILD
@@ -5,7 +5,10 @@
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
     ],
 )
diff --git a/java/com/google/gerrit/git/GitUpdateFailureException.java b/java/com/google/gerrit/git/GitUpdateFailureException.java
new file mode 100644
index 0000000..76ef217
--- /dev/null
+++ b/java/com/google/gerrit/git/GitUpdateFailureException.java
@@ -0,0 +1,95 @@
+// 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.git;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Thrown when updating a ref in Git fails. */
+public class GitUpdateFailureException extends IOException {
+  private static final long serialVersionUID = 1L;
+
+  private final ImmutableList<GitUpdateFailure> failures;
+
+  public GitUpdateFailureException(String message, RefUpdate refUpdate) {
+    super(message);
+    this.failures = ImmutableList.of(GitUpdateFailure.create(refUpdate));
+  }
+
+  public GitUpdateFailureException(String message, BatchRefUpdate batchRefUpdate) {
+    super(message);
+    this.failures =
+        batchRefUpdate.getCommands().stream()
+            .filter(c -> c.getResult() != ReceiveCommand.Result.OK)
+            .map(GitUpdateFailure::create)
+            .collect(toImmutableList());
+  }
+
+  /** @return the names of the refs for which the update failed. */
+  public ImmutableList<String> getFailedRefs() {
+    return failures.stream().map(GitUpdateFailure::ref).collect(toImmutableList());
+  }
+
+  /** @return the failures that caused this exception. */
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public ImmutableList<GitUpdateFailure> getFailures() {
+    return failures;
+  }
+
+  @AutoValue
+  public abstract static class GitUpdateFailure {
+    private static GitUpdateFailure create(RefUpdate refUpdate) {
+      return builder().ref(refUpdate.getName()).result(refUpdate.getResult().name()).build();
+    }
+
+    private static GitUpdateFailure create(ReceiveCommand receiveCommand) {
+      return builder()
+          .ref(receiveCommand.getRefName())
+          .result(receiveCommand.getResult().name())
+          .message(receiveCommand.getMessage())
+          .build();
+    }
+
+    public abstract String ref();
+
+    public abstract String result();
+
+    public abstract Optional<String> message();
+
+    public static GitUpdateFailure.Builder builder() {
+      return new AutoValue_GitUpdateFailureException_GitUpdateFailure.Builder();
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+      abstract Builder ref(String ref);
+
+      abstract Builder result(String result);
+
+      abstract Builder message(@Nullable String message);
+
+      abstract GitUpdateFailure build();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/git/LockFailureException.java b/java/com/google/gerrit/git/LockFailureException.java
index 9e67d70..371488d 100644
--- a/java/com/google/gerrit/git/LockFailureException.java
+++ b/java/com/google/gerrit/git/LockFailureException.java
@@ -14,36 +14,18 @@
 
 package com.google.gerrit.git;
 
-import static com.google.common.collect.ImmutableList.toImmutableList;
-
-import com.google.common.collect.ImmutableList;
-import java.io.IOException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 /** Thrown when updating a ref in Git fails with LOCK_FAILURE. */
-public class LockFailureException extends IOException {
+public class LockFailureException extends GitUpdateFailureException {
   private static final long serialVersionUID = 1L;
 
-  private final ImmutableList<String> refs;
-
   public LockFailureException(String message, RefUpdate refUpdate) {
-    super(message);
-    refs = ImmutableList.of(refUpdate.getName());
+    super(message, refUpdate);
   }
 
   public LockFailureException(String message, BatchRefUpdate batchRefUpdate) {
-    super(message);
-    refs =
-        batchRefUpdate.getCommands().stream()
-            .filter(c -> c.getResult() == ReceiveCommand.Result.LOCK_FAILURE)
-            .map(ReceiveCommand::getRefName)
-            .collect(toImmutableList());
-  }
-
-  /** Subset of ref names that caused the lock failure. */
-  public ImmutableList<String> getFailedRefs() {
-    return refs;
+    super(message, batchRefUpdate);
   }
 }
diff --git a/java/com/google/gerrit/git/ObjectIds.java b/java/com/google/gerrit/git/ObjectIds.java
new file mode 100644
index 0000000..4d83046
--- /dev/null
+++ b/java/com/google/gerrit/git/ObjectIds.java
@@ -0,0 +1,131 @@
+// 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.git;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.common.Nullable;
+import java.io.IOException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+
+/** Static utilities for working with {@code ObjectId}s. */
+public class ObjectIds {
+  /** Length of a binary SHA-1 byte array. */
+  public static final int LEN = Constants.OBJECT_ID_LENGTH;
+
+  /** Length of a hex SHA-1 string. */
+  public static final int STR_LEN = Constants.OBJECT_ID_STRING_LENGTH;
+
+  /** Default abbreviated length of a hex SHA-1 string. */
+  public static final int ABBREV_STR_LEN = 7;
+
+  /**
+   * Abbreviate an ID's hex string representation to 7 chars.
+   *
+   * @param id object ID.
+   * @return abbreviated hex string representation, exactly 7 chars.
+   */
+  public static String abbreviateName(AnyObjectId id) {
+    return abbreviateName(id, ABBREV_STR_LEN);
+  }
+
+  /**
+   * Abbreviate an ID's hex string representation to {@code n} chars.
+   *
+   * @param id object ID.
+   * @param n number of hex chars, 1 to 40.
+   * @return abbreviated hex string representation, exactly {@code n} chars.
+   */
+  public static String abbreviateName(AnyObjectId id, int n) {
+    checkValidLength(n);
+    return requireNonNull(id).abbreviate(n).name();
+  }
+
+  /**
+   * Abbreviate an ID's hex string representation uniquely to at least 7 chars.
+   *
+   * @param id object ID.
+   * @param reader object reader for determining uniqueness.
+   * @return abbreviated hex string representation, unique according to {@code reader} at least 7
+   *     chars.
+   * @throws IOException if an error occurs while looking for ambiguous objects.
+   */
+  public static String abbreviateName(AnyObjectId id, ObjectReader reader) throws IOException {
+    return abbreviateName(id, ABBREV_STR_LEN, reader);
+  }
+
+  /**
+   * Abbreviate an ID's hex string representation uniquely to at least {@code n} chars.
+   *
+   * @param id object ID.
+   * @param n minimum number of hex chars, 1 to 40.
+   * @param reader object reader for determining uniqueness.
+   * @return abbreviated hex string representation, unique according to {@code reader} at least
+   *     {@code n} chars.
+   * @throws IOException if an error occurs while looking for ambiguous objects.
+   */
+  public static String abbreviateName(AnyObjectId id, int n, ObjectReader reader)
+      throws IOException {
+    checkValidLength(n);
+    return reader.abbreviate(id, n).name();
+  }
+
+  /**
+   * Copy a nullable ID, preserving null.
+   *
+   * @param id object ID.
+   * @return {@link AnyObjectId#copy} of {@code id}, or {@code null} if {@code id} is null.
+   */
+  @Nullable
+  public static ObjectId copyOrNull(@Nullable AnyObjectId id) {
+    return id != null ? id.copy() : null;
+  }
+
+  /**
+   * Copy a nullable ID, converting null to {@code zeroId}.
+   *
+   * @param id object ID.
+   * @return {@link AnyObjectId#copy} of {@code id}, or {@link ObjectId#zeroId} if {@code id} is
+   *     null.
+   */
+  public static ObjectId copyOrZero(@Nullable AnyObjectId id) {
+    return id != null ? id.copy() : ObjectId.zeroId();
+  }
+
+  /**
+   * Return whether the given ID matches the given abbreviation.
+   *
+   * @param id object ID.
+   * @param abbreviation abbreviated hex object ID. May not be null, but may be an invalid hex SHA-1
+   *     abbreviation string.
+   * @return true if {@code id} is not null and {@code abbreviation} is a valid hex SHA-1
+   *     abbreviation that matches {@code id}, false otherwise.
+   */
+  public static boolean matchesAbbreviation(@Nullable AnyObjectId id, String abbreviation) {
+    requireNonNull(abbreviation);
+    return id != null && id.name().startsWith(abbreviation);
+  }
+
+  private static void checkValidLength(int n) {
+    checkArgument(n > 0);
+    checkArgument(n <= STR_LEN);
+  }
+
+  private ObjectIds() {}
+}
diff --git a/java/com/google/gerrit/git/RefUpdateUtil.java b/java/com/google/gerrit/git/RefUpdateUtil.java
index 5eaf253..bd88962 100644
--- a/java/com/google/gerrit/git/RefUpdateUtil.java
+++ b/java/com/google/gerrit/git/RefUpdateUtil.java
@@ -99,7 +99,7 @@
     if (lockFailure + aborted == bru.getCommands().size()) {
       throw new LockFailureException("Update aborted with one or more lock failures: " + bru, bru);
     } else if (failure > 0) {
-      throw new IOException("Update failed: " + bru);
+      throw new GitUpdateFailureException("Update failed: " + bru, bru);
     }
   }
 
@@ -130,7 +130,8 @@
       case REJECTED_CURRENT_BRANCH:
       case REJECTED_MISSING_OBJECT:
       case REJECTED_OTHER_REASON:
-        throw new IOException("Failed to update " + ru.getName() + ": " + ru.getResult());
+        throw new GitUpdateFailureException(
+            "Failed to update " + ru.getName() + ": " + ru.getResult(), ru);
     }
   }
 
@@ -175,7 +176,8 @@
       case REJECTED_MISSING_OBJECT:
       case REJECTED_OTHER_REASON:
       default:
-        throw new IOException("Failed to delete " + refName + ": " + ru.getResult());
+        throw new GitUpdateFailureException(
+            "Failed to delete " + refName + ": " + ru.getResult(), ru);
     }
   }
 
diff --git a/java/com/google/gerrit/git/testing/BUILD b/java/com/google/gerrit/git/testing/BUILD
index 13fddc1..fda9aff 100644
--- a/java/com/google/gerrit/git/testing/BUILD
+++ b/java/com/google/gerrit/git/testing/BUILD
@@ -9,7 +9,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
     ],
diff --git a/java/com/google/gerrit/git/testing/CommitSubject.java b/java/com/google/gerrit/git/testing/CommitSubject.java
index 0873107..41eb45b 100644
--- a/java/com/google/gerrit/git/testing/CommitSubject.java
+++ b/java/com/google/gerrit/git/testing/CommitSubject.java
@@ -24,7 +24,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 
 /** Subject over JGit {@link RevCommit}s. */
-public class CommitSubject extends Subject<CommitSubject, RevCommit> {
+public class CommitSubject extends Subject {
 
   /**
    * Constructs a new subject.
@@ -56,8 +56,11 @@
     commitSubject.hasSha1(expectedSha1);
   }
 
-  private CommitSubject(FailureMetadata metadata, RevCommit actual) {
-    super(metadata, actual);
+  private final RevCommit commit;
+
+  private CommitSubject(FailureMetadata metadata, RevCommit commit) {
+    super(metadata, commit);
+    this.commit = commit;
   }
 
   /**
@@ -67,8 +70,7 @@
    */
   public void hasCommitMessage(String expectedCommitMessage) {
     isNotNull();
-    RevCommit commit = actual();
-    check("commitMessage()").that(commit.getFullMessage()).isEqualTo(expectedCommitMessage);
+    check("getFullMessage()").that(commit.getFullMessage()).isEqualTo(expectedCommitMessage);
   }
 
   /**
@@ -78,7 +80,6 @@
    */
   public void hasCommitTimestamp(Timestamp expectedCommitTimestamp) {
     isNotNull();
-    RevCommit commit = actual();
     long timestampDiffMs =
         Math.abs(commit.getCommitTime() * 1000L - expectedCommitTimestamp.getTime());
     check("commitTimestampDiff()").that(timestampDiffMs).isAtMost(SECONDS.toMillis(1));
@@ -91,7 +92,6 @@
    */
   public void hasSha1(ObjectId expectedSha1) {
     isNotNull();
-    RevCommit commit = actual();
     check("sha1()").that(commit).isEqualTo(expectedSha1);
   }
 }
diff --git a/java/com/google/gerrit/git/testing/ObjectIdSubject.java b/java/com/google/gerrit/git/testing/ObjectIdSubject.java
index 5fe91f9..0cfc563 100644
--- a/java/com/google/gerrit/git/testing/ObjectIdSubject.java
+++ b/java/com/google/gerrit/git/testing/ObjectIdSubject.java
@@ -20,7 +20,7 @@
 import com.google.common.truth.Subject;
 import org.eclipse.jgit.lib.ObjectId;
 
-public class ObjectIdSubject extends Subject<ObjectIdSubject, ObjectId> {
+public class ObjectIdSubject extends Subject {
   public static ObjectIdSubject assertThat(ObjectId objectId) {
     return assertAbout(objectIds()).that(objectId);
   }
@@ -29,13 +29,15 @@
     return ObjectIdSubject::new;
   }
 
-  private ObjectIdSubject(FailureMetadata metadata, ObjectId actual) {
-    super(metadata, actual);
+  private final ObjectId objectId;
+
+  private ObjectIdSubject(FailureMetadata metadata, ObjectId objectId) {
+    super(metadata, objectId);
+    this.objectId = objectId;
   }
 
   public void hasName(String expectedName) {
     isNotNull();
-    ObjectId objectId = actual();
-    check("name()").that(objectId.getName()).isEqualTo(expectedName);
+    check("getName()").that(objectId.getName()).isEqualTo(expectedName);
   }
 }
diff --git a/java/com/google/gerrit/git/testing/PushResultSubject.java b/java/com/google/gerrit/git/testing/PushResultSubject.java
index f5c9810..9a46632 100644
--- a/java/com/google/gerrit/git/testing/PushResultSubject.java
+++ b/java/com/google/gerrit/git/testing/PushResultSubject.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.git.testing;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Fact.fact;
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.git.testing.PushResultSubject.RemoteRefUpdateSubject.refs;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
@@ -26,42 +28,44 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StreamSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.common.Nullable;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 
-public class PushResultSubject extends Subject<PushResultSubject, PushResult> {
+public class PushResultSubject extends Subject {
   public static PushResultSubject assertThat(PushResult actual) {
     return assertAbout(PushResultSubject::new).that(actual);
   }
 
-  private PushResultSubject(FailureMetadata metadata, PushResult actual) {
-    super(metadata, actual);
+  private final PushResult pushResult;
+
+  private PushResultSubject(FailureMetadata metadata, PushResult pushResult) {
+    super(metadata, pushResult);
+    this.pushResult = pushResult;
   }
 
   public void hasNoMessages() {
-    check("hasNoMessages()")
-        .withMessage("expected no messages")
-        .that(Strings.nullToEmpty(trimMessages()))
-        .isEqualTo("");
+    isNotNull();
+    check("hasNoMessages()").that(Strings.nullToEmpty(getTrimmedMessages())).isEqualTo("");
   }
 
   public void hasMessages(String... expectedLines) {
     checkArgument(expectedLines.length > 0, "use hasNoMessages()");
     isNotNull();
-    check("messages()").that(trimMessages()).isEqualTo(String.join("\n", expectedLines));
+    check("getTrimmedMessages()")
+        .that(getTrimmedMessages())
+        .isEqualTo(String.join("\n", expectedLines));
   }
 
   public void containsMessages(String... expectedLines) {
     checkArgument(expectedLines.length > 0, "use hasNoMessages()");
     isNotNull();
-    Iterable<String> got = Splitter.on("\n").split(trimMessages());
-    check("messages()").that(got).containsAtLeastElementsIn(expectedLines).inOrder();
+    Iterable<String> got = Splitter.on("\n").split(getTrimmedMessages());
+    check("getTrimmedMessages()").that(got).containsAtLeastElementsIn(expectedLines).inOrder();
   }
 
-  private String trimMessages() {
-    return trimMessages(actual().getMessages());
+  private String getTrimmedMessages() {
+    return trimMessages(pushResult.getMessages());
   }
 
   @VisibleForTesting
@@ -78,15 +82,16 @@
   }
 
   public void hasProcessed(ImmutableMap<String, Integer> expected) {
+    isNotNull();
     ImmutableMap<String, Integer> actual;
-    String messages = actual().getMessages();
+    String messages = pushResult.getMessages();
     try {
       actual = parseProcessed(messages);
     } catch (RuntimeException e) {
-      Truth.assert_()
-          .fail(
-              "failed to parse \"Processing changes\" line from messages: %s\n%s",
-              messages, Throwables.getStackTraceAsString(e));
+      failWithActual(
+          fact(
+              "failed to parse \"Processing changes\" line from messages, reason:",
+              Throwables.getStackTraceAsString(e)));
       return;
     }
     check("processedCommands()").that(actual).containsExactlyEntriesIn(expected).inOrder();
@@ -119,57 +124,60 @@
   }
 
   public RemoteRefUpdateSubject ref(String refName) {
-    return assertAbout(
-            (FailureMetadata m, RemoteRefUpdate a) -> new RemoteRefUpdateSubject(refName, m, a))
-        .that(actual().getRemoteUpdate(refName));
+    isNotNull();
+    return check("getRemoteUpdate(%s)", refName)
+        .about(refs())
+        .that(pushResult.getRemoteUpdate(refName));
   }
 
   public RemoteRefUpdateSubject onlyRef(String refName) {
+    isNotNull();
     check("setOfRefs()")
         .about(StreamSubject.streams())
-        .that(actual().getRemoteUpdates().stream().map(RemoteRefUpdate::getRemoteName))
-        .named("set of refs")
+        .that(pushResult.getRemoteUpdates().stream().map(RemoteRefUpdate::getRemoteName))
         .containsExactly(refName);
     return ref(refName);
   }
 
-  public static class RemoteRefUpdateSubject
-      extends Subject<RemoteRefUpdateSubject, RemoteRefUpdate> {
-    private final String refName;
+  public static class RemoteRefUpdateSubject extends Subject {
+    private final RemoteRefUpdate remoteRefUpdate;
 
-    private RemoteRefUpdateSubject(
-        String refName, FailureMetadata metadata, RemoteRefUpdate actual) {
-      super(metadata, actual);
-      this.refName = refName;
-      named("ref update for %s", refName).isNotNull();
+    private RemoteRefUpdateSubject(FailureMetadata metadata, RemoteRefUpdate remoteRefUpdate) {
+      super(metadata, remoteRefUpdate);
+      this.remoteRefUpdate = remoteRefUpdate;
+    }
+
+    static Factory<RemoteRefUpdateSubject, RemoteRefUpdate> refs() {
+      return RemoteRefUpdateSubject::new;
     }
 
     public void hasStatus(RemoteRefUpdate.Status status) {
-      RemoteRefUpdate u = actual();
-      Truth.assertThat(u.getStatus())
-          .named(
-              "status of ref update for %s%s",
-              refName, u.getMessage() != null ? ": " + u.getMessage() : "")
+      isNotNull();
+      RemoteRefUpdate u = remoteRefUpdate;
+      check("getStatus()")
+          .withMessage(
+              "status message: %s", u.getMessage() != null ? ": " + u.getMessage() : "<emtpy>")
+          .that(u.getStatus())
           .isEqualTo(status);
     }
 
     public void hasNoMessage() {
-      Truth.assertThat(actual().getMessage())
-          .named("message of ref update for %s", refName)
-          .isNull();
+      isNotNull();
+      check("getMessage()").that(remoteRefUpdate.getMessage()).isNull();
     }
 
     public void hasMessage(String expected) {
-      Truth.assertThat(actual().getMessage())
-          .named("message of ref update for %s", refName)
-          .isEqualTo(expected);
+      isNotNull();
+      check("getMessage()").that(remoteRefUpdate.getMessage()).isEqualTo(expected);
     }
 
     public void isOk() {
+      isNotNull();
       hasStatus(RemoteRefUpdate.Status.OK);
     }
 
     public void isRejected(String expectedMessage) {
+      isNotNull();
       hasStatus(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
       hasMessage(expectedMessage);
     }
diff --git a/java/com/google/gerrit/gpg/BUILD b/java/com/google/gerrit/gpg/BUILD
index f11b9b9..9f804c4 100644
--- a/java/com/google/gerrit/gpg/BUILD
+++ b/java/com/google/gerrit/gpg/BUILD
@@ -5,18 +5,18 @@
     srcs = glob(["**/*.java"]),
     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/exceptions",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/api",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/bouncycastle:bcpg-neverlink",
         "//lib/bouncycastle:bcprov-neverlink",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index 9c08857..9477cb6 100644
--- a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -201,7 +201,7 @@
   private Set<String> getAllowedUserIds(IdentifiedUser user) {
     Set<String> result = new HashSet<>();
     result.addAll(user.getEmailAddresses());
-    for (ExternalId extId : user.state().getExternalIds()) {
+    for (ExternalId extId : user.state().externalIds()) {
       if (extId.isScheme(SCHEME_GPGKEY)) {
         continue; // Omit GPG keys.
       }
diff --git a/java/com/google/gerrit/gpg/PublicKeyStore.java b/java/com/google/gerrit/gpg/PublicKeyStore.java
index 7dd01d9..519c400 100644
--- a/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -16,10 +16,12 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.base.Preconditions;
 import com.google.common.io.ByteStreams;
+import com.google.gerrit.git.ObjectIds;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -45,7 +47,6 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -75,9 +76,6 @@
  * after checking with a {@link PublicKeyChecker}.
  */
 public class PublicKeyStore implements AutoCloseable {
-  private static final ObjectId EMPTY_TREE =
-      ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904");
-
   /** Ref where GPG public keys are stored. */
   public static final String REFS_GPG_KEYS = "refs/meta/gpg-keys";
 
@@ -360,7 +358,7 @@
         deleteFromNotes(ins, fp);
       }
       cb.setTreeId(notes.writeTree(ins));
-      if (cb.getTreeId().equals(tip != null ? tip.getTree() : EMPTY_TREE)) {
+      if (cb.getTreeId().equals(tip != null ? tip.getTree() : EMPTY_TREE_ID)) {
         return RefUpdate.Result.NO_CHANGE;
       }
 
@@ -516,7 +514,7 @@
   }
 
   static ObjectId keyObjectId(long keyId) {
-    byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
+    byte[] buf = new byte[ObjectIds.LEN];
     NB.encodeInt64(buf, 0, keyId);
     return ObjectId.fromRaw(buf);
   }
diff --git a/java/com/google/gerrit/gpg/SignedPushModule.java b/java/com/google/gerrit/gpg/SignedPushModule.java
index a051861..c11c4e3 100644
--- a/java/com/google/gerrit/gpg/SignedPushModule.java
+++ b/java/com/google/gerrit/gpg/SignedPushModule.java
@@ -16,9 +16,9 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.EnableSignedPush;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
diff --git a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
index b7b03db..652afea 100644
--- a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.gpg.api;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
 import com.google.gerrit.extensions.api.accounts.GpgKeysInput;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
@@ -65,9 +67,11 @@
   public Map<String, GpgKeyInfo> listGpgKeys(AccountResource account)
       throws RestApiException, GpgException {
     try {
-      return gpgKeys.get().list().apply(account);
+      return gpgKeys.get().list().apply(account).value();
     } catch (PGPException | IOException e) {
       throw new GpgException(e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list GPG keys", e);
     }
   }
 
@@ -79,9 +83,11 @@
     in.add = add;
     in.delete = delete;
     try {
-      return postGpgKeys.get().apply(account, in);
+      return postGpgKeys.get().apply(account, in).value();
     } catch (PGPException | IOException | ConfigInvalidException e) {
       throw new GpgException(e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot put GPG keys", e);
     }
   }
 
diff --git a/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java b/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
index cf09acf..0ff12e8 100644
--- a/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.gpg.api;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.Input;
@@ -46,9 +48,9 @@
   @Override
   public GpgKeyInfo get() throws RestApiException {
     try {
-      return get.apply(rsrc);
-    } catch (IOException e) {
-      throw new RestApiException("Cannot get GPG key", e);
+      return get.apply(rsrc).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get GPG key", e);
     }
   }
 
@@ -57,7 +59,7 @@
     try {
       delete.apply(rsrc, new Input());
     } catch (PGPException | IOException | ConfigInvalidException e) {
-      throw new RestApiException("Cannot delete GPG key", e);
+      throw asRestApiException("Cannot delete GPG key", e);
     }
   }
 }
diff --git a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index 68bd2d9..1be37f5 100644
--- a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -105,7 +105,7 @@
           } catch (EmailException e) {
             logger.atSevere().withCause(e).log(
                 "Cannot send GPG key deletion message to %s",
-                rsrc.getUser().getAccount().getPreferredEmail());
+                rsrc.getUser().getAccount().preferredEmail());
           }
           break;
         case LOCK_FAILURE:
diff --git a/java/com/google/gerrit/gpg/server/GpgKeys.java b/java/com/google/gerrit/gpg/server/GpgKeys.java
index 16592f8..b3a2f53 100644
--- a/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -27,6 +27,7 @@
 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.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.gpg.BouncyCastleUtil;
@@ -140,7 +141,7 @@
 
   public class ListGpgKeys implements RestReadView<AccountResource> {
     @Override
-    public Map<String, GpgKeyInfo> apply(AccountResource rsrc)
+    public Response<Map<String, GpgKeyInfo>> apply(AccountResource rsrc)
         throws PGPException, IOException, ResourceNotFoundException {
       checkVisible(self, rsrc);
       Map<String, GpgKeyInfo> keys = new HashMap<>();
@@ -165,7 +166,7 @@
           }
         }
       }
-      return keys;
+      return Response.ok(keys);
     }
   }
 
@@ -181,12 +182,13 @@
     }
 
     @Override
-    public GpgKeyInfo apply(GpgKey rsrc) throws IOException {
+    public Response<GpgKeyInfo> apply(GpgKey rsrc) throws IOException {
       try (PublicKeyStore store = storeProvider.get()) {
-        return toJson(
-            rsrc.getKeyRing().getPublicKey(),
-            checkerFactory.create().setExpectedUser(rsrc.getUser()),
-            store);
+        return Response.ok(
+            toJson(
+                rsrc.getKeyRing().getPublicKey(),
+                checkerFactory.create().setExpectedUser(rsrc.getUser()),
+                store));
       }
     }
   }
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 0bb4ff7..5396e1c 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -18,15 +18,18 @@
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Joiner;
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.accounts.GpgKeysInput;
@@ -34,13 +37,15 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+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.git.LockFailureException;
 import com.google.gerrit.gpg.CheckResult;
 import com.google.gerrit.gpg.Fingerprint;
 import com.google.gerrit.gpg.GerritPublicKeyChecker;
 import com.google.gerrit.gpg.PublicKeyChecker;
 import com.google.gerrit.gpg.PublicKeyStore;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -53,6 +58,8 @@
 import com.google.gerrit.server.mail.send.AddKeySender;
 import com.google.gerrit.server.mail.send.DeleteKeySender;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryHelper.ActionType;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -87,6 +94,7 @@
   private final Provider<InternalAccountQuery> accountQueryProvider;
   private final ExternalIds externalIds;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
+  private final RetryHelper retryHelper;
 
   @Inject
   PostGpgKeys(
@@ -98,7 +106,8 @@
       DeleteKeySender.Factory deleteKeySenderFactory,
       Provider<InternalAccountQuery> accountQueryProvider,
       ExternalIds externalIds,
-      @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
+      @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
+      RetryHelper retryHelper) {
     this.serverIdent = serverIdent;
     this.self = self;
     this.storeProvider = storeProvider;
@@ -108,12 +117,12 @@
     this.accountQueryProvider = accountQueryProvider;
     this.externalIds = externalIds;
     this.accountsUpdateProvider = accountsUpdateProvider;
+    this.retryHelper = retryHelper;
   }
 
   @Override
-  public Map<String, GpgKeyInfo> apply(AccountResource rsrc, GpgKeysInput input)
-      throws ResourceNotFoundException, BadRequestException, ResourceConflictException,
-          PGPException, IOException, ConfigInvalidException {
+  public Response<Map<String, GpgKeyInfo>> apply(AccountResource rsrc, GpgKeysInput input)
+      throws RestApiException, PGPException, IOException, ConfigInvalidException {
     GpgKeys.checkVisible(self, rsrc);
 
     Collection<ExternalId> existingExtIds =
@@ -129,7 +138,7 @@
         ExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
         Account account = getAccountByExternalId(extIdKey);
         if (account != null) {
-          if (!account.getId().equals(rsrc.getUser().getAccountId())) {
+          if (!account.id().equals(rsrc.getUser().getAccountId())) {
             throw new ResourceConflictException("GPG key already associated with another account");
           }
         } else {
@@ -145,7 +154,7 @@
               "Update GPG Keys via API",
               rsrc.getUser().getAccountId(),
               u -> u.replaceExternalIds(toRemove.keySet(), newExtIds));
-      return toJson(newKeys, fingerprintsToRemove, store, rsrc.getUser());
+      return Response.ok(toJson(newKeys, fingerprintsToRemove, store, rsrc.getUser()));
     }
   }
 
@@ -196,7 +205,24 @@
 
   private void storeKeys(
       AccountResource rsrc, List<PGPPublicKeyRing> keyRings, Collection<Fingerprint> toRemove)
-      throws BadRequestException, PGPException, IOException {
+      throws RestApiException, PGPException, IOException {
+    try {
+      retryHelper.execute(
+          ActionType.ACCOUNT_UPDATE,
+          () -> tryStoreKeys(rsrc, keyRings, toRemove),
+          LockFailureException.class::isInstance);
+    } catch (Exception e) {
+      Throwables.throwIfUnchecked(e);
+      Throwables.throwIfInstanceOf(e, RestApiException.class);
+      Throwables.throwIfInstanceOf(e, IOException.class);
+      Throwables.throwIfInstanceOf(e, PGPException.class);
+      throw new StorageException(e);
+    }
+  }
+
+  private Void tryStoreKeys(
+      AccountResource rsrc, List<PGPPublicKeyRing> keyRings, Collection<Fingerprint> toRemove)
+      throws RestApiException, PGPException, IOException {
     try (PublicKeyStore store = storeProvider.get()) {
       List<String> addedKeys = new ArrayList<>();
       IdentifiedUser user = rsrc.getUser();
@@ -232,7 +258,7 @@
             } catch (EmailException e) {
               logger.atSevere().withCause(e).log(
                   "Cannot send GPG key added message to %s",
-                  rsrc.getUser().getAccount().getPreferredEmail());
+                  rsrc.getUser().getAccount().preferredEmail());
             }
           }
           if (!toRemove.isEmpty()) {
@@ -242,8 +268,7 @@
                   .send();
             } catch (EmailException e) {
               logger.atSevere().withCause(e).log(
-                  "Cannot send GPG key deleted message to %s",
-                  user.getAccount().getPreferredEmail());
+                  "Cannot send GPG key deleted message to %s", user.getAccount().preferredEmail());
             }
           }
           break;
@@ -261,6 +286,7 @@
           throw new StorageException(String.format("Failed to save public keys: %s", saveResult));
       }
     }
+    return null;
   }
 
   private ExternalId.Key toExtIdKey(byte[] fp) {
@@ -275,15 +301,15 @@
     }
 
     if (accountStates.size() > 1) {
-      StringBuilder msg = new StringBuilder();
-      msg.append("GPG key ")
-          .append(extIdKey.get())
-          .append(" associated with multiple accounts: ")
-          .append(Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
-      throw new IllegalStateException(msg.toString());
+      String msg = "GPG key " + extIdKey.get() + " associated with multiple accounts: [";
+      msg =
+          accountStates.stream()
+              .map(a -> a.account().id().toString())
+              .collect(joining(", ", msg, "]"));
+      throw new IllegalStateException(msg);
     }
 
-    return accountStates.get(0).getAccount();
+    return accountStates.get(0).account();
   }
 
   private Map<String, GpgKeyInfo> toJson(
diff --git a/java/com/google/gerrit/gpg/testing/BUILD b/java/com/google/gerrit/gpg/testing/BUILD
index b227dd5..dc39071 100644
--- a/java/com/google/gerrit/gpg/testing/BUILD
+++ b/java/com/google/gerrit/gpg/testing/BUILD
@@ -8,7 +8,7 @@
     deps = [
         "//java/com/google/gerrit/gpg",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/bouncycastle:bcpg-neverlink",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java b/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java
index 68bbd98..af8234f 100644
--- a/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java
+++ b/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.httpd;
 
 import com.google.auto.value.AutoValue;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
 
 @AutoValue
 abstract class AdvertisedObjectsCacheKey {
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index 1d4189f..bcb2a2a 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -12,14 +12,13 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/exceptions",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/json",
         "//java/com/google/gerrit/launcher",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/audit",
         "//java/com/google/gerrit/server/git/receive",
@@ -32,6 +31,8 @@
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib:jgit-servlet",
         "//lib:jsch",
         "//lib:servlet-api",
         "//lib:soy",
@@ -43,7 +44,5 @@
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 177be75..3a84a29 100644
--- a/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -19,11 +19,11 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.httpd.WebSessionManager.Key;
 import com.google.gerrit.httpd.WebSessionManager.Val;
 import com.google.gerrit.httpd.restapi.ParameterParser;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
@@ -75,29 +75,27 @@
     this.identified = identified;
     this.byIdCache = byIdCache;
 
-    if (request.getRequestURI() == null || !GitSmartHttpTools.isGitClient(request)) {
-      String cookie = readCookie(request);
-      if (cookie != null) {
-        authFromCookie(cookie);
-      } else {
-        String token;
-        try {
-          token = ParameterParser.getQueryParams(request).accessToken();
-        } catch (BadRequestException e) {
-          token = null;
-        }
-        if (token != null) {
-          authFromQueryParameter(token);
-        }
+    String cookie = readCookie(request);
+    if (cookie != null) {
+      authFromCookie(cookie);
+    } else if (request.getRequestURI() == null || !GitSmartHttpTools.isGitClient(request)) {
+      String token;
+      try {
+        token = ParameterParser.getQueryParams(request).accessToken();
+      } catch (BadRequestException e) {
+        token = null;
       }
-      if (val != null && !checkAccountStatus(val.getAccountId())) {
-        val = null;
-        okPaths.clear();
+      if (token != null) {
+        authFromQueryParameter(token);
       }
-      if (val != null && val.needsCookieRefresh()) {
-        // Session is more than half old; update cache entry with new expiration date.
-        val = manager.createVal(key, val);
-      }
+    }
+    if (val != null && !checkAccountStatus(val.getAccountId())) {
+      val = null;
+      okPaths.clear();
+    }
+    if (val != null && val.needsCookieRefresh()) {
+      // Session is more than half old; update cache entry with new expiration date.
+      val = manager.createVal(key, val);
     }
   }
 
@@ -222,7 +220,7 @@
   }
 
   private boolean checkAccountStatus(Account.Id id) {
-    return byIdCache.get(id).filter(as -> as.getAccount().isActive()).isPresent();
+    return byIdCache.get(id).filter(as -> as.account().isActive()).isPresent();
   }
 
   private void saveCookie() {
diff --git a/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index d13f2f6..517d5db 100644
--- a/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -112,13 +112,13 @@
       username = username.toLowerCase(Locale.US);
     }
     Optional<AccountState> who =
-        accountCache.getByUsername(username).filter(a -> a.getAccount().isActive());
+        accountCache.getByUsername(username).filter(a -> a.account().isActive());
     if (!who.isPresent()) {
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     }
     WebSession ws = session.get();
-    ws.setUserAccountId(who.get().getAccount().getId());
+    ws.setUserAccountId(who.get().account().id());
     ws.setAccessPathOk(AccessPath.GIT, true);
     ws.setAccessPathOk(AccessPath.REST_API, true);
     return true;
diff --git a/java/com/google/gerrit/httpd/DirectChangeByCommit.java b/java/com/google/gerrit/httpd/DirectChangeByCommit.java
index 152a83d..a9be2aa 100644
--- a/java/com/google/gerrit/httpd/DirectChangeByCommit.java
+++ b/java/com/google/gerrit/httpd/DirectChangeByCommit.java
@@ -6,11 +6,11 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -47,7 +47,7 @@
       // If exactly one change matches, link to that change.
       // TODO Link to a specific patch set, if one matched.
       ChangeInfo ci = results.iterator().next();
-      token = PageLinks.toChange(new Project.NameKey(ci.project), new Change.Id(ci._number));
+      token = PageLinks.toChange(Project.nameKey(ci.project), Change.id(ci._number));
     } else {
       // Otherwise, link to the query page.
       token = PageLinks.toChangeQuery(query);
diff --git a/java/com/google/gerrit/httpd/GitOverHttpModule.java b/java/com/google/gerrit/httpd/GitOverHttpModule.java
index 8400d60..cbcfb0b 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.httpd.GitOverHttpServlet.URL_REGEX;
 
-import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
+import com.google.gerrit.entities.CoreDownloadSchemes;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.DownloadConfig;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index bd8ba62..520ce4e 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -20,23 +20,27 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.audit.HttpAuditEvent;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.PermissionAwareRepositoryManager;
+import com.google.gerrit.server.git.TracingHook;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.UploadPackInitializer;
+import com.google.gerrit.server.git.UsersSelfAdvertiseRefsHook;
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.group.GroupAuditService;
+import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -51,6 +55,7 @@
 import com.google.inject.name.Named;
 import java.io.IOException;
 import java.time.Duration;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Map;
@@ -279,7 +284,7 @@
       user.setAccessPath(AccessPath.GIT);
 
       try {
-        Project.NameKey nameKey = new Project.NameKey(projectName);
+        Project.NameKey nameKey = Project.nameKey(projectName);
         ProjectState state = projectCache.checkedGet(nameKey);
         if (state == null || !state.statePermitsRead()) {
           throw new RepositoryNotFoundException(nameKey.get());
@@ -307,27 +312,38 @@
     private final DynamicSet<PreUploadHook> preUploadHooks;
     private final DynamicSet<PostUploadHook> postUploadHooks;
     private final PluginSetContext<UploadPackInitializer> uploadPackInitializers;
+    private final PermissionBackend permissionBackend;
 
     @Inject
     UploadFactory(
         TransferConfig tc,
         DynamicSet<PreUploadHook> preUploadHooks,
         DynamicSet<PostUploadHook> postUploadHooks,
-        PluginSetContext<UploadPackInitializer> uploadPackInitializers) {
+        PluginSetContext<UploadPackInitializer> uploadPackInitializers,
+        PermissionBackend permissionBackend) {
       this.config = tc;
       this.preUploadHooks = preUploadHooks;
       this.postUploadHooks = postUploadHooks;
       this.uploadPackInitializers = uploadPackInitializers;
+      this.permissionBackend = permissionBackend;
     }
 
     @Override
     public UploadPack create(HttpServletRequest req, Repository repo) {
-      UploadPack up = new UploadPack(repo);
+      ProjectState state = (ProjectState) req.getAttribute(ATT_STATE);
+      UploadPack up =
+          new UploadPack(
+              PermissionAwareRepositoryManager.wrap(
+                  repo, permissionBackend.currentUser().project(state.getNameKey())));
       up.setPackConfig(config.getPackConfig());
       up.setTimeout(config.getTimeout());
       up.setPreUploadHook(PreUploadHookChain.newChain(Lists.newArrayList(preUploadHooks)));
       up.setPostUploadHook(PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
-      ProjectState state = (ProjectState) req.getAttribute(ATT_STATE);
+      String header = req.getHeader("Git-Protocol");
+      if (header != null) {
+        String[] params = header.split(":");
+        up.setExtraParameters(Arrays.asList(params));
+      }
       uploadPackInitializers.runEach(initializer -> initializer.init(state.getNameKey(), up));
       return up;
     }
@@ -339,6 +355,8 @@
     private final Provider<CurrentUser> userProvider;
     private final GroupAuditService groupAuditService;
     private final Metrics metrics;
+    private final PluginSetContext<RequestListener> requestListeners;
+    private final UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook;
     private final Provider<WebSession> sessionProvider;
 
     @Inject
@@ -346,15 +364,19 @@
         UploadValidators.Factory uploadValidatorsFactory,
         PermissionBackend permissionBackend,
         Provider<CurrentUser> userProvider,
-        Provider<WebSession> sessionProvider,
         GroupAuditService groupAuditService,
-        Metrics metrics) {
+        Metrics metrics,
+        PluginSetContext<RequestListener> requestListeners,
+        UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook,
+        Provider<WebSession> sessionProvider) {
       this.uploadValidatorsFactory = uploadValidatorsFactory;
       this.permissionBackend = permissionBackend;
       this.userProvider = userProvider;
-      this.sessionProvider = sessionProvider;
       this.groupAuditService = groupAuditService;
       this.metrics = metrics;
+      this.requestListeners = requestListeners;
+      this.usersSelfAdvertiseRefsHook = usersSelfAdvertiseRefsHook;
+      this.sessionProvider = sessionProvider;
     }
 
     @Override
@@ -372,7 +394,14 @@
       HttpServletRequest httpRequest = (HttpServletRequest) request;
       String sessionId = getSessionIdOrNull(sessionProvider);
 
-      try {
+      try (TraceContext traceContext = TraceContext.open()) {
+        RequestInfo requestInfo =
+            RequestInfo.builder(
+                    RequestInfo.RequestType.GIT_UPLOAD, userProvider.get(), traceContext)
+                .project(state.getNameKey())
+                .build();
+        requestListeners.runEach(l -> l.onRequest(requestInfo));
+
         try {
           perm.check(ProjectPermission.RUN_UPLOAD_PACK);
         } catch (AuthException e) {
@@ -394,8 +423,14 @@
         up.setPreUploadHook(
             PreUploadHookChain.newChain(
                 Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
-        up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
-        next.doFilter(httpRequest, responseWrapper);
+        if (state.isAllUsers()) {
+          up.setAdvertiseRefsHook(usersSelfAdvertiseRefsHook);
+        }
+
+        try (TracingHook tracingHook = new TracingHook()) {
+          up.setProtocolV2Hook(tracingHook);
+          next.doFilter(httpRequest, responseWrapper);
+        }
       } finally {
         groupAuditService.dispatch(
             new HttpAuditEvent(
@@ -467,15 +502,15 @@
         @Named(ID_CACHE) Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache,
         PermissionBackend permissionBackend,
         Provider<CurrentUser> userProvider,
-        Provider<WebSession> sessionProvider,
         GroupAuditService groupAuditService,
-        Metrics metrics) {
+        Metrics metrics,
+        Provider<WebSession> sessionProvider) {
       this.cache = cache;
       this.permissionBackend = permissionBackend;
       this.userProvider = userProvider;
-      this.sessionProvider = sessionProvider;
       this.groupAuditService = groupAuditService;
       this.metrics = metrics;
+      this.sessionProvider = sessionProvider;
     }
 
     @Override
diff --git a/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java b/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
index 397d093..4ae7e5e 100644
--- a/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
+++ b/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
@@ -67,7 +67,7 @@
     headers.put(name, value);
   }
 
-  @SuppressWarnings("all")
+  @SuppressWarnings({"all", "MissingOverride"})
   // @Override is omitted for backwards compatibility with servlet-api 2.5
   // TODO: Remove @SuppressWarnings and add @Override when Google upgrades
   //       to servlet-api 3.1
diff --git a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
index d53a5c5..89ad878 100644
--- a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
+++ b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
@@ -61,7 +61,7 @@
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
       return;
     } catch (PermissionBackendException | RuntimeException e) {
-      throw new IOException("Unable to lookup change " + id.id, e);
+      throw new IOException("Unable to lookup change " + id.get(), e);
     }
     String path =
         PageLinks.toChange(changeResource.getProject(), changeResource.getChange().getId());
diff --git a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index eb9d1d7..1974ba7 100644
--- a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -21,9 +21,10 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
@@ -32,6 +33,7 @@
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.AuthenticationFailedException;
+import com.google.gerrit.server.account.externalids.PasswordVerifier;
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
 import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.AuthConfig;
@@ -50,6 +52,7 @@
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponseWrapper;
 import org.apache.commons.codec.binary.Base64;
+import org.eclipse.jgit.http.server.GitSmartHttpTools;
 
 /**
  * Authenticates the current user by HTTP basic authentication.
@@ -98,11 +101,21 @@
     HttpServletRequest req = (HttpServletRequest) request;
     Response rsp = new Response((HttpServletResponse) response);
 
-    if (verify(req, rsp)) {
+    if (isSignedInGitRequest(req) || verify(req, rsp)) {
       chain.doFilter(req, rsp);
     }
   }
 
+  private boolean isSignedInGitRequest(HttpServletRequest req) {
+    boolean isGitRequest = req.getRequestURI() != null && GitSmartHttpTools.isGitClient(req);
+    boolean isAlreadySignedIn = session.get().isSignedIn();
+    boolean res = isAlreadySignedIn && isGitRequest;
+    logger.atFine().log(
+        "HTTP:%s %s signedIn=%s (isAlreadySignedIn=%s, isGitRequest=%s)",
+        req.getMethod(), req.getRequestURI(), res, isAlreadySignedIn, isGitRequest);
+    return res;
+  }
+
   private boolean verify(HttpServletRequest req, Response rsp) throws IOException {
     final String hdr = req.getHeader(AUTHORIZATION);
     if (hdr == null || !hdr.startsWith(LIT_BASIC)) {
@@ -130,7 +143,7 @@
     }
 
     Optional<AccountState> accountState =
-        accountCache.getByUsername(username).filter(a -> a.getAccount().isActive());
+        accountCache.getByUsername(username).filter(a -> a.account().isActive());
     if (!accountState.isPresent()) {
       logger.atWarning().log(
           "Authentication failed for %s: account inactive or not provisioned in Gerrit", username);
@@ -142,8 +155,11 @@
     GitBasicAuthPolicy gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
     if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP
         || gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP) {
-      if (who.checkPassword(password, username)) {
-        return succeedAuthentication(who);
+      if (PasswordVerifier.checkPassword(who.externalIds(), username, password)) {
+        logger.atFine().log(
+            "HTTP:%s %s username/password authentication succeeded",
+            req.getMethod(), req.getRequestURI());
+        return succeedAuthentication(who, null);
       }
     }
 
@@ -156,11 +172,13 @@
 
     try {
       AuthResult whoAuthResult = accountManager.authenticate(whoAuth);
-      setUserIdentified(whoAuthResult.getAccountId());
+      setUserIdentified(whoAuthResult.getAccountId(), whoAuthResult);
+      logger.atFine().log(
+          "HTTP:%s %s Realm authentication succeeded", req.getMethod(), req.getRequestURI());
       return true;
     } catch (NoSuchUserException e) {
-      if (who.checkPassword(password, username)) {
-        return succeedAuthentication(who);
+      if (PasswordVerifier.checkPassword(who.externalIds(), username, password)) {
+        return succeedAuthentication(who, null);
       }
       logger.atWarning().withCause(e).log(authenticationFailedMsg(username, req));
       rsp.sendError(SC_UNAUTHORIZED);
@@ -182,8 +200,8 @@
     }
   }
 
-  private boolean succeedAuthentication(AccountState who) {
-    setUserIdentified(who.getAccount().getId());
+  private boolean succeedAuthentication(AccountState who, @Nullable AuthResult whoAuthResult) {
+    setUserIdentified(who.account().id(), whoAuthResult);
     return true;
   }
 
@@ -200,11 +218,15 @@
     return String.format("Authentication from %s failed for %s", req.getRemoteAddr(), username);
   }
 
-  private void setUserIdentified(Account.Id id) {
+  private void setUserIdentified(Account.Id id, @Nullable AuthResult whoAuthResult) {
     WebSession ws = session.get();
     ws.setUserAccountId(id);
     ws.setAccessPathOk(AccessPath.GIT, true);
     ws.setAccessPathOk(AccessPath.REST_API, true);
+
+    if (whoAuthResult != null) {
+      ws.login(whoAuthResult, false);
+    }
   }
 
   private String encoding(HttpServletRequest req) {
diff --git a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
index 4461a52..8300823 100644
--- a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -22,11 +22,11 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.Extension;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
@@ -152,7 +152,7 @@
     }
 
     Optional<AccountState> who =
-        accountCache.getByUsername(authInfo.username).filter(a -> a.getAccount().isActive());
+        accountCache.getByUsername(authInfo.username).filter(a -> a.account().isActive());
     if (!who.isPresent()) {
       logger.atWarning().log(
           authenticationFailedMsg(authInfo.username, req)
@@ -161,10 +161,10 @@
       return false;
     }
 
-    Account account = who.get().getAccount();
+    Account account = who.get().account();
     AuthRequest authRequest = AuthRequest.forExternalUser(authInfo.username);
-    authRequest.setEmailAddress(account.getPreferredEmail());
-    authRequest.setDisplayName(account.getFullName());
+    authRequest.setEmailAddress(account.preferredEmail());
+    authRequest.setDisplayName(account.fullName());
     authRequest.setPassword(authInfo.tokenOrSecret);
     authRequest.setAuthPlugin(authInfo.pluginName);
     authRequest.setAuthProvider(authInfo.exportName);
diff --git a/java/com/google/gerrit/httpd/RequestMetrics.java b/java/com/google/gerrit/httpd/RequestMetrics.java
index cab4a92..e0f9b6a 100644
--- a/java/com/google/gerrit/httpd/RequestMetrics.java
+++ b/java/com/google/gerrit/httpd/RequestMetrics.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -28,15 +29,20 @@
 
   @Inject
   public RequestMetrics(MetricMaker metricMaker) {
+    Field<Integer> statusCodeField =
+        Field.ofInteger("status", Metadata.Builder::httpStatus)
+            .description("HTTP status code")
+            .build();
+
     errors =
         metricMaker.newCounter(
             "http/server/error_count",
             new Description("Rate of REST API error responses").setRate().setUnit("errors"),
-            Field.ofInteger("status", "HTTP status code"));
+            statusCodeField);
     successes =
         metricMaker.newCounter(
             "http/server/success_count",
             new Description("Rate of REST API success responses").setRate().setUnit("successes"),
-            Field.ofInteger("status", "HTTP status code"));
+            statusCodeField);
   }
 }
diff --git a/java/com/google/gerrit/httpd/RunAsFilter.java b/java/com/google/gerrit/httpd/RunAsFilter.java
index 15dbcab..135de42 100644
--- a/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -19,10 +19,10 @@
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.config.AuthConfig;
@@ -105,7 +105,7 @@
 
       Account.Id target;
       try {
-        target = accountResolver.resolve(runas).asUnique().getAccount().getId();
+        target = accountResolver.resolve(runas).asUnique().account().id();
       } catch (UnprocessableEntityException e) {
         replyError(req, res, SC_FORBIDDEN, "no account matches " + RUN_AS, null);
         return;
diff --git a/java/com/google/gerrit/httpd/UrlModule.java b/java/com/google/gerrit/httpd/UrlModule.java
index 993a042..ac73d22 100644
--- a/java/com/google/gerrit/httpd/UrlModule.java
+++ b/java/com/google/gerrit/httpd/UrlModule.java
@@ -18,7 +18,10 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.httpd.raw.AuthorizationCheckServlet;
 import com.google.gerrit.httpd.raw.CatServlet;
 import com.google.gerrit.httpd.raw.SshInfoServlet;
 import com.google.gerrit.httpd.raw.ToolServlet;
@@ -28,8 +31,6 @@
 import com.google.gerrit.httpd.restapi.ConfigRestApiServlet;
 import com.google.gerrit.httpd.restapi.GroupsRestApiServlet;
 import com.google.gerrit.httpd.restapi.ProjectsRestApiServlet;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Key;
 import com.google.inject.internal.UniqueAnnotations;
@@ -82,6 +83,9 @@
 
     serveRegex("^/(?:a/)?tools/(.*)$").with(ToolServlet.class);
 
+    // Serve auth check. Mainly used by PolyGerrit for checking if a user is still logged in.
+    serveRegex("^/(?:a/)?auth-check$").with(AuthorizationCheckServlet.class);
+
     // Bind servlets for REST root collections.
     // The '/plugins/' root collection is already handled by HttpPluginServlet
     // which is bound in HttpPluginModule. We cannot bind it here again although
@@ -147,7 +151,7 @@
             while (name.endsWith("/")) {
               name = name.substring(0, name.length() - 1);
             }
-            Project.NameKey project = new Project.NameKey(name);
+            Project.NameKey project = Project.nameKey(name);
             toGerrit(
                 PageLinks.toChangeQuery(PageLinks.projectQuery(project, Change.Status.NEW)),
                 req,
diff --git a/java/com/google/gerrit/httpd/WebSession.java b/java/com/google/gerrit/httpd/WebSession.java
index e476f15..e8b54fe 100644
--- a/java/com/google/gerrit/httpd/WebSession.java
+++ b/java/com/google/gerrit/httpd/WebSession.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AuthResult;
diff --git a/java/com/google/gerrit/httpd/WebSessionManager.java b/java/com/google/gerrit/httpd/WebSessionManager.java
index d09b4dd..c0900ec 100644
--- a/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -30,7 +30,7 @@
 
 import com.google.common.cache.Cache;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -286,7 +286,7 @@
           case 0:
             break PARSE;
           case 1:
-            accountId = new Account.Id(readVarInt32(in));
+            accountId = Account.id(readVarInt32(in));
             continue;
           case 2:
             refreshCookieAt = readFixInt64(in);
diff --git a/java/com/google/gerrit/httpd/XsrfCookieFilter.java b/java/com/google/gerrit/httpd/XsrfCookieFilter.java
index d15ecac..079efa4 100644
--- a/java/com/google/gerrit/httpd/XsrfCookieFilter.java
+++ b/java/com/google/gerrit/httpd/XsrfCookieFilter.java
@@ -32,6 +32,7 @@
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.http.server.GitSmartHttpTools;
 
 @Singleton
 public class XsrfCookieFilter implements Filter {
@@ -50,8 +51,11 @@
   @Override
   public void doFilter(ServletRequest req, ServletResponse rsp, FilterChain chain)
       throws IOException, ServletException {
-    WebSession s = user.get().isIdentifiedUser() ? session.get() : null;
-    setXsrfTokenCookie((HttpServletRequest) req, (HttpServletResponse) rsp, s);
+    HttpServletRequest httpRequest = (HttpServletRequest) req;
+    if (!GitSmartHttpTools.isGitClient(httpRequest)) {
+      WebSession s = user.get().isIdentifiedUser() ? session.get() : null;
+      setXsrfTokenCookie(httpRequest, (HttpServletResponse) rsp, s);
+    }
     chain.doFilter(req, rsp);
   }
 
diff --git a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 552e667..97bb44b 100644
--- a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -18,12 +18,12 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
 
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.template.SiteHeaderFooter;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -153,20 +153,20 @@
       if (!accountState.isPresent()) {
         continue;
       }
-      Account account = accountState.get().getAccount();
+      Account account = accountState.get().account();
       String displayName;
-      if (accountState.get().getUserName().isPresent()) {
-        displayName = accountState.get().getUserName().get();
-      } else if (account.getFullName() != null && !account.getFullName().isEmpty()) {
-        displayName = account.getFullName();
-      } else if (account.getPreferredEmail() != null) {
-        displayName = account.getPreferredEmail();
+      if (accountState.get().userName().isPresent()) {
+        displayName = accountState.get().userName().get();
+      } else if (account.fullName() != null && !account.fullName().isEmpty()) {
+        displayName = account.fullName();
+      } else if (account.preferredEmail() != null) {
+        displayName = account.preferredEmail();
       } else {
         displayName = accountId.toString();
       }
 
       Element linkElement = doc.createElement("a");
-      linkElement.setAttribute("href", "?account_id=" + account.getId().toString());
+      linkElement.setAttribute("href", "?account_id=" + account.id().toString());
       linkElement.setTextContent(displayName);
       userlistElement.appendChild(linkElement);
       userlistElement.appendChild(doc.createElement("br"));
@@ -176,7 +176,7 @@
   }
 
   private Optional<AuthResult> auth(Optional<AccountState> account) {
-    return account.map(a -> new AuthResult(a.getAccount().getId(), null, false));
+    return account.map(a -> new AuthResult(a.account().id(), null, false));
   }
 
   private AuthResult auth(Account.Id account) {
@@ -196,7 +196,7 @@
       getServletContext().log("Multiple accounts with username " + userName + " found");
       return null;
     }
-    return auth(accountStates.get(0).getAccount().getId());
+    return auth(accountStates.get(0).account().id());
   }
 
   private Optional<AuthResult> byPreferredEmail(String email) {
diff --git a/java/com/google/gerrit/httpd/auth/oauth/BUILD b/java/com/google/gerrit/httpd/auth/oauth/BUILD
index 2d9345f..dd4549e 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/BUILD
+++ b/java/com/google/gerrit/httpd/auth/oauth/BUILD
@@ -8,18 +8,17 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
-        "//java/com/google/gerrit/exceptions",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/httpd",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:gson",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:servlet-api",
         "//lib/commons:codec",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
index 0c8a1a10..fcaef5e 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -19,6 +19,7 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
@@ -27,7 +28,6 @@
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -237,7 +237,7 @@
     try {
       return SecureRandom.getInstance("SHA1PRNG");
     } catch (NoSuchAlgorithmException e) {
-      throw new IllegalArgumentException("No SecureRandom available for GitHub authentication", e);
+      throw new IllegalStateException("No SecureRandom available for GitHub authentication", e);
     }
   }
 
diff --git a/java/com/google/gerrit/httpd/auth/openid/BUILD b/java/com/google/gerrit/httpd/auth/openid/BUILD
index 2206397..94f436b 100644
--- a/java/com/google/gerrit/httpd/auth/openid/BUILD
+++ b/java/com/google/gerrit/httpd/auth/openid/BUILD
@@ -10,20 +10,18 @@
         # We want all these deps to be provided_deps
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/httpd",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/util/http",
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//java/com/google/gwtorm",
         "//lib:servlet-api",
         "//lib/commons:codec",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
         "//lib/openid:consumer",
     ],
 )
diff --git a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index 283cd50..0b6008c 100644
--- a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -59,9 +59,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final ImmutableMap<String, String> ALL_PROVIDERS =
-      ImmutableMap.of(
-          "launchpad", OpenIdUrls.URL_LAUNCHPAD,
-          "yahoo", OpenIdUrls.URL_YAHOO);
+      ImmutableMap.of("launchpad", OpenIdUrls.URL_LAUNCHPAD);
 
   private final ImmutableSet<String> suggestProviders;
   private final Provider<String> urlProvider;
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
index 08f2d52..37250b4 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
@@ -27,7 +28,6 @@
 import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -222,7 +222,7 @@
     try {
       return SecureRandom.getInstance("SHA1PRNG");
     } catch (NoSuchAlgorithmException e) {
-      throw new IllegalArgumentException("No SecureRandom available for GitHub authentication", e);
+      throw new IllegalStateException("No SecureRandom available for GitHub authentication", e);
     }
   }
 
diff --git a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 8c3dc10..b685011 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -17,12 +17,13 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.KeyUtil;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.httpd.ProxyProperties;
 import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.UrlEncoded;
 import com.google.gerrit.server.account.AccountException;
@@ -32,7 +33,6 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.client.KeyUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -477,8 +477,9 @@
     final StringBuilder rdr = new StringBuilder();
     rdr.append(urlProvider.get(req));
     String nextToken = Url.decode(token);
-    if (isNew && !token.startsWith(PageLinks.REGISTER + "/")) {
-      rdr.append('#' + PageLinks.REGISTER);
+    String registerUri = PageLinks.REGISTER + "/";
+    if (isNew && !token.startsWith(registerUri)) {
+      rdr.append('#' + registerUri);
       if (nextToken.startsWith("#")) {
         // Need to strip the leading # off the token to fix registration page redirect
         nextToken = nextToken.substring(1);
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index b438c00..4fabb18 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -36,10 +36,10 @@
 import com.google.common.base.Splitter;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -174,10 +174,8 @@
     }
     Path myconf = Files.createTempFile(site.tmp_dir, "gitweb_config", ".perl");
 
-    // To make our configuration file only readable or writable by us;
-    // this reduces the chances of someone tampering with the file.
-    //
-    // TODO(dborowitz): Is there a portable way to do this with NIO?
+    // To make our configuration file only readable or writable by us; this reduces the chances of
+    // someone tampering with the file.
     File myconfFile = myconf.toFile();
     myconfFile.setWritable(false, false /* all */);
     myconfFile.setReadable(false, false /* all */);
@@ -414,7 +412,7 @@
       name = name.substring(0, name.length() - 4);
     }
 
-    Project.NameKey nameKey = new Project.NameKey(name);
+    Project.NameKey nameKey = Project.nameKey(name);
     ProjectState projectState;
     try {
       projectState = projectCache.checkedGet(nameKey);
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index 97475d1..222041a 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -11,6 +11,7 @@
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/httpd/auth/oauth",
         "//java/com/google/gerrit/httpd/auth/openid",
+        "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/lucene",
         "//java/com/google/gerrit/metrics/dropwizard",
@@ -26,10 +27,10 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/sshd",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:servlet-api",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index c9dfc58..7d0b42b 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -38,7 +38,9 @@
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.httpd.raw.StaticModule;
+import com.google.gerrit.index.IndexType;
 import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
 import com.google.gerrit.pgm.util.LogFileCompressor;
@@ -72,10 +74,10 @@
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.GitRepositoryManagerModule;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
+import com.google.gerrit.server.git.SystemReaderInstaller;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.AutoFlush;
 import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.OnlineUpgrader;
 import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
@@ -262,6 +264,13 @@
 
     Module configModule = new GerritServerConfigModule();
     modules.add(configModule);
+    modules.add(
+        new LifecycleModule() {
+          @Override
+          protected void configure() {
+            listener().to(SystemReaderInstaller.class);
+          }
+        });
     modules.add(new DropWizardMetricMaker.ApiModule());
     return Guice.createInjector(PRODUCTION, modules);
   }
@@ -343,13 +352,12 @@
   }
 
   private Module createIndexModule() {
-    switch (indexType) {
-      case LUCENE:
-        return LuceneIndexModule.latestVersion(false, AutoFlush.ENABLED);
-      case ELASTICSEARCH:
-        return ElasticIndexModule.latestVersion(false);
-      default:
-        throw new IllegalStateException("unsupported index.type = " + indexType);
+    if (indexType.isLucene()) {
+      return LuceneIndexModule.latestVersion(false, AutoFlush.ENABLED);
+    } else if (indexType.isElasticsearch()) {
+      return ElasticIndexModule.latestVersion(false);
+    } else {
+      throw new IllegalStateException("unsupported index.type = " + indexType);
     }
   }
 
diff --git a/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java b/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java
new file mode 100644
index 0000000..e8f173c
--- /dev/null
+++ b/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java
@@ -0,0 +1,52 @@
+// 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.
+
+package com.google.gerrit.httpd.raw;
+
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.util.http.CacheHeaders;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Offers a dedicated endpoint for checking if a user is still logged in. Returns {@code 204
+ * NO_CONTENT} for logged-in users, {@code 403 FORBIDDEN} otherwise.
+ *
+ * <p>Mainly used by PolyGerrit to check if a user is still logged in.
+ */
+@Singleton
+public class AuthorizationCheckServlet extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+  private final Provider<CurrentUser> user;
+
+  @Inject
+  AuthorizationCheckServlet(Provider<CurrentUser> user) {
+    this.user = user;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+    CacheHeaders.setNotCacheable(res);
+    if (user.get().isIdentifiedUser()) {
+      res.setStatus(HttpServletResponse.SC_NO_CONTENT);
+    } else {
+      res.setStatus(HttpServletResponse.SC_FORBIDDEN);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/httpd/raw/CatServlet.java b/java/com/google/gerrit/httpd/raw/CatServlet.java
index 1d0e7d8b..a295213 100644
--- a/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.httpd.raw;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
@@ -118,13 +118,13 @@
       }
     }
 
-    final Change.Id changeId = patchKey.getParentKey().getParentKey();
+    final Change.Id changeId = patchKey.patchSetId().changeId();
     String revision;
     try {
       ChangeNotes notes = changeNotesFactory.createChecked(changeId);
       permissionBackend.currentUser().change(notes).check(ChangePermission.READ);
       projectCache.checkedGet(notes.getProjectName()).checkStatePermitsRead();
-      if (patchKey.getParentKey().get() == 0) {
+      if (patchKey.patchSetId().get() == 0) {
         // change edit
         Optional<ChangeEdit> edit = changeEditUtil.byChange(notes);
         if (edit.isPresent()) {
@@ -134,12 +134,12 @@
           return;
         }
       } else {
-        PatchSet patchSet = psUtil.get(notes, patchKey.getParentKey());
+        PatchSet patchSet = psUtil.get(notes, patchKey.patchSetId());
         if (patchSet == null) {
           rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
           return;
         }
-        revision = patchSet.getRevision().get();
+        revision = patchSet.commitId().name();
       }
     } catch (ResourceConflictException | NoSuchChangeException | AuthException e) {
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
@@ -150,7 +150,7 @@
       return;
     }
 
-    String path = patchKey.getFileName();
+    String path = patchKey.fileName();
     String restUrl =
         String.format(
             "%s/changes/%d/revisions/%s/files/%s/download?parent=%d",
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
new file mode 100644
index 0000000..8d81d62
--- /dev/null
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -0,0 +1,148 @@
+// 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.httpd.raw;
+
+import static com.google.template.soy.data.ordainers.GsonOrdainer.serializeObject;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.accounts.AccountApi;
+import com.google.gerrit.extensions.api.config.Server;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.json.OutputFormat;
+import com.google.gson.Gson;
+import com.google.template.soy.data.SanitizedContent;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+/** Helper for generating parts of {@code index.html}. */
+public class IndexHtmlUtil {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  /**
+   * Returns both static and dynamic parameters of {@code index.html}. The result is to be used when
+   * rendering the soy template.
+   */
+  public static ImmutableMap<String, Object> templateData(
+      GerritApi gerritApi,
+      String canonicalURL,
+      String cdnPath,
+      String faviconPath,
+      Map<String, String[]> urlParameterMap,
+      Function<String, SanitizedContent> urlInScriptTagOrdainer)
+      throws URISyntaxException, RestApiException {
+    return ImmutableMap.<String, Object>builder()
+        .putAll(
+            staticTemplateData(
+                canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer))
+        .putAll(dynamicTemplateData(gerritApi))
+        .build();
+  }
+
+  /** Returns dynamic parameters of {@code index.html}. */
+  @UsedAt(Project.GOOGLE)
+  public static Map<String, Map<String, SanitizedContent>> dynamicTemplateData(GerritApi gerritApi)
+      throws RestApiException {
+    Gson gson = OutputFormat.JSON_COMPACT.newGson();
+    Map<String, SanitizedContent> initialData = new HashMap<>();
+    Server serverApi = gerritApi.config().server();
+    initialData.put("\"/config/server/info\"", serializeObject(gson, serverApi.getInfo()));
+    initialData.put("\"/config/server/version\"", serializeObject(gson, serverApi.getVersion()));
+    initialData.put("\"/config/server/top-menus\"", serializeObject(gson, serverApi.topMenus()));
+
+    try {
+      AccountApi accountApi = gerritApi.accounts().self();
+      initialData.put("\"/accounts/self/detail\"", serializeObject(gson, accountApi.get()));
+      initialData.put(
+          "\"/accounts/self/preferences\"", serializeObject(gson, accountApi.getPreferences()));
+      initialData.put(
+          "\"/accounts/self/preferences.diff\"",
+          serializeObject(gson, accountApi.getDiffPreferences()));
+      initialData.put(
+          "\"/accounts/self/preferences.edit\"",
+          serializeObject(gson, accountApi.getEditPreferences()));
+    } catch (AuthException e) {
+      logger.atFine().log("Can't inline account-related data because user is unauthenticated");
+      // Don't render data
+      // TODO(hiesel): Tell the client that the user is not authenticated so that it doesn't have to
+      // fetch anyway. This requires more client side modifications.
+    }
+    return ImmutableMap.of("gerritInitialData", initialData);
+  }
+
+  /** Returns all static parameters of {@code index.html}. */
+  static Map<String, Object> staticTemplateData(
+      String canonicalURL,
+      String cdnPath,
+      String faviconPath,
+      Map<String, String[]> urlParameterMap,
+      Function<String, SanitizedContent> urlInScriptTagOrdainer)
+      throws URISyntaxException {
+    String canonicalPath = computeCanonicalPath(canonicalURL);
+
+    String staticPath = "";
+    if (cdnPath != null) {
+      staticPath = cdnPath;
+    } else if (canonicalPath != null) {
+      staticPath = canonicalPath;
+    }
+
+    SanitizedContent sanitizedStaticPath = urlInScriptTagOrdainer.apply(staticPath);
+    ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
+
+    if (canonicalPath != null) {
+      data.put("canonicalPath", canonicalPath);
+    }
+    if (sanitizedStaticPath != null) {
+      data.put("staticResourcePath", sanitizedStaticPath);
+    }
+    if (faviconPath != null) {
+      data.put("faviconPath", faviconPath);
+    }
+    if (urlParameterMap.containsKey("ce")) {
+      data.put("polyfillCE", "true");
+    }
+    if (urlParameterMap.containsKey("sd")) {
+      data.put("polyfillSD", "true");
+    }
+    if (urlParameterMap.containsKey("sc")) {
+      data.put("polyfillSC", "true");
+    }
+    return data.build();
+  }
+
+  private static String computeCanonicalPath(@Nullable String canonicalURL)
+      throws URISyntaxException {
+    if (Strings.isNullOrEmpty(canonicalURL)) {
+      return "";
+    }
+
+    // If we serving from a sub-directory rather than root, determine the path
+    // from the cannonical web URL.
+    URI uri = new URI(canonicalURL);
+    return uri.getPath().replaceAll("/$", "");
+  }
+
+  private IndexHtmlUtil() {}
+}
diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
index b6594bc..a0b41b21 100644
--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
+++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
@@ -17,91 +17,73 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 
-import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.Resources;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.template.soy.SoyFileSet;
 import com.google.template.soy.data.SanitizedContent;
 import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
-import com.google.template.soy.tofu.SoyTofu;
+import com.google.template.soy.jbcsrc.api.SoySauce;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Map;
+import java.util.function.Function;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 public class IndexServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
-  protected final byte[] indexSource;
+
+  @Nullable private final String canonicalUrl;
+  @Nullable private final String cdnPath;
+  @Nullable private final String faviconPath;
+  private final GerritApi gerritApi;
+  private final SoySauce soySauce;
+  private final Function<String, SanitizedContent> urlOrdainer;
 
   IndexServlet(
-      @Nullable String canonicalURL, @Nullable String cdnPath, @Nullable String faviconPath)
-      throws URISyntaxException {
-    String resourcePath = "com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy";
-    SoyFileSet.Builder builder = SoyFileSet.builder();
-    builder.add(Resources.getResource(resourcePath));
-    SoyTofu.Renderer renderer =
-        builder
+      @Nullable String canonicalUrl,
+      @Nullable String cdnPath,
+      @Nullable String faviconPath,
+      GerritApi gerritApi) {
+    this.canonicalUrl = canonicalUrl;
+    this.cdnPath = cdnPath;
+    this.faviconPath = faviconPath;
+    this.gerritApi = gerritApi;
+    this.soySauce =
+        SoyFileSet.builder()
+            .add(Resources.getResource("com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy"))
             .build()
-            .compileToTofu()
-            .newRenderer("com.google.gerrit.httpd.raw.Index")
-            .setContentKind(SanitizedContent.ContentKind.HTML)
-            .setData(getTemplateData(canonicalURL, cdnPath, faviconPath));
-    indexSource = renderer.render().getBytes(UTF_8);
+            .compileTemplates();
+    this.urlOrdainer =
+        (s) ->
+            UnsafeSanitizedContentOrdainer.ordainAsSafe(
+                s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
   }
 
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
+    SoySauce.Renderer renderer;
+    try {
+      Map<String, String[]> parameterMap = req.getParameterMap();
+      // TODO(hiesel): Remove URL ordainer as parameter once Soy is consistent
+      ImmutableMap<String, Object> templateData =
+          IndexHtmlUtil.templateData(
+              gerritApi, canonicalUrl, cdnPath, faviconPath, parameterMap, urlOrdainer);
+      renderer = soySauce.renderTemplate("com.google.gerrit.httpd.raw.Index").setData(templateData);
+    } catch (URISyntaxException | RestApiException e) {
+      throw new IOException(e);
+    }
+
     rsp.setCharacterEncoding(UTF_8.name());
     rsp.setContentType("text/html");
     rsp.setStatus(SC_OK);
     try (OutputStream w = rsp.getOutputStream()) {
-      w.write(indexSource);
+      w.write(renderer.renderHtml().get().toString().getBytes(UTF_8));
     }
   }
-
-  static String computeCanonicalPath(@Nullable String canonicalURL) throws URISyntaxException {
-    if (Strings.isNullOrEmpty(canonicalURL)) {
-      return "";
-    }
-
-    // If we serving from a sub-directory rather than root, determine the path
-    // from the cannonical web URL.
-    URI uri = new URI(canonicalURL);
-    return uri.getPath().replaceAll("/$", "");
-  }
-
-  static Map<String, Object> getTemplateData(
-      String canonicalURL, String cdnPath, String faviconPath) throws URISyntaxException {
-    String canonicalPath = computeCanonicalPath(canonicalURL);
-
-    String staticPath = "";
-    if (cdnPath != null) {
-      staticPath = cdnPath;
-    } else if (canonicalPath != null) {
-      staticPath = canonicalPath;
-    }
-
-    // The resource path must be typed as safe for use in a script src.
-    // TODO(wyatta): Upgrade this to use an appropriate safe URL type.
-    SanitizedContent sanitizedStaticPath =
-        UnsafeSanitizedContentOrdainer.ordainAsSafe(
-            staticPath, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
-
-    ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
-    if (canonicalPath != null) {
-      data.put("canonicalPath", canonicalPath);
-    }
-    if (sanitizedStaticPath != null) {
-      data.put("staticResourcePath", sanitizedStaticPath);
-    }
-    if (faviconPath != null) {
-      data.put("faviconPath", faviconPath);
-    }
-    return data.build();
-  }
 }
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 06ac886..7f2161d 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.httpd.XsrfCookieFilter;
 import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
 import com.google.gerrit.launcher.GerritLauncher;
@@ -41,7 +42,6 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.net.URISyntaxException;
 import java.nio.file.FileSystem;
 import java.nio.file.Path;
 import javax.servlet.Filter;
@@ -54,6 +54,7 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
 import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.http.server.GitSmartHttpTools;
 import org.eclipse.jgit.lib.Config;
 
 public class StaticModule extends ServletModule {
@@ -78,11 +79,6 @@
           "/groups/self",
           "/settings/*",
           "/Documentation/q/*");
-  // TODO(dborowitz): These fragments conflict with the REST API
-  // namespace, so they will need to use a different path.
-  // "/groups/*",
-  // "/projects/*");
-  //
 
   /**
    * Paths that should be treated as static assets when serving PolyGerrit.
@@ -224,11 +220,12 @@
     @Singleton
     @Named(POLYGERRIT_INDEX_SERVLET)
     HttpServlet getPolyGerritUiIndexServlet(
-        @CanonicalWebUrl @Nullable String canonicalUrl, @GerritServerConfig Config cfg)
-        throws URISyntaxException {
+        @CanonicalWebUrl @Nullable String canonicalUrl,
+        @GerritServerConfig Config cfg,
+        GerritApi gerritApi) {
       String cdnPath = cfg.getString("gerrit", null, "cdnPath");
       String faviconPath = cfg.getString("gerrit", null, "faviconPath");
-      return new IndexServlet(canonicalUrl, cdnPath, faviconPath);
+      return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi);
     }
 
     @Provides
@@ -409,32 +406,34 @@
       HttpServletRequest req = (HttpServletRequest) request;
       HttpServletResponse res = (HttpServletResponse) response;
 
-      GuiceFilterRequestWrapper reqWrapper = new GuiceFilterRequestWrapper(req);
-      String path = pathInfo(req);
+      if (!GitSmartHttpTools.isGitClient(req)) {
+        GuiceFilterRequestWrapper reqWrapper = new GuiceFilterRequestWrapper(req);
+        String path = pathInfo(req);
 
-      // Special case assets during development that are built by Bazel and not
-      // served out of the source tree.
-      //
-      // In the war case, these are either inlined, or live under
-      // /polygerrit_ui in the war file, so we can just treat them as normal
-      // assets.
-      if (paths.isDev()) {
-        if (path.startsWith("/bower_components/")) {
-          bowerComponentServlet.service(reqWrapper, res);
-          return;
-        } else if (path.startsWith("/fonts/")) {
-          fontServlet.service(reqWrapper, res);
+        // Special case assets during development that are built by Bazel and not
+        // served out of the source tree.
+        //
+        // In the war case, these are either inlined, or live under
+        // /polygerrit_ui in the war file, so we can just treat them as normal
+        // assets.
+        if (paths.isDev()) {
+          if (path.startsWith("/bower_components/")) {
+            bowerComponentServlet.service(reqWrapper, res);
+            return;
+          } else if (path.startsWith("/fonts/")) {
+            fontServlet.service(reqWrapper, res);
+            return;
+          }
+        }
+
+        if (isPolyGerritIndex(path)) {
+          polyGerritIndex.service(reqWrapper, res);
           return;
         }
-      }
-
-      if (isPolyGerritIndex(path)) {
-        polyGerritIndex.service(reqWrapper, res);
-        return;
-      }
-      if (isPolyGerritAsset(path)) {
-        polygerritUI.service(reqWrapper, res);
-        return;
+        if (isPolyGerritAsset(path)) {
+          polygerritUI.service(reqWrapper, res);
+          return;
+        }
       }
 
       chain.doFilter(req, res);
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
index 562687b..fc099a6 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.metrics.Histogram1;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -41,19 +42,24 @@
 
   @Inject
   RestApiMetrics(MetricMaker metrics) {
-    Field<String> view = Field.ofString("view", "view implementation class");
+    Field<String> viewField =
+        Field.ofString("view", Metadata.Builder::className)
+            .description("view implementation class")
+            .build();
     count =
         metrics.newCounter(
             "http/server/rest_api/count",
             new Description("REST API calls by view").setRate(),
-            view);
+            viewField);
 
     errorCount =
         metrics.newCounter(
             "http/server/rest_api/error_count",
             new Description("REST API errors by view").setRate(),
-            view,
-            Field.ofInteger("error_code", "HTTP status code"));
+            viewField,
+            Field.ofInteger("error_code", Metadata.Builder::httpStatus)
+                .description("HTTP status code")
+                .build());
 
     serverLatency =
         metrics.newTimer(
@@ -61,7 +67,7 @@
             new Description("REST API call latency by view")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            view);
+            viewField);
 
     responseBytes =
         metrics.newHistogram(
@@ -69,7 +75,7 @@
             new Description("Size of response on network (may be gzip compressed)")
                 .setCumulative()
                 .setUnit(Units.BYTES),
-            view);
+            viewField);
   }
 
   String view(ViewData viewData) {
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index bb05fa8..1296c49 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -34,23 +34,19 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.joining;
-import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
-import static javax.servlet.http.HttpServletResponse.SC_CREATED;
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
-import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
 import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
@@ -67,8 +63,10 @@
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -105,16 +103,25 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OptionUtil;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
 import com.google.gerrit.server.cache.PerThreadCache;
+import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.GroupAuditService;
+import com.google.gerrit.server.logging.PerformanceLogContext;
+import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.quota.QuotaException;
+import com.google.gerrit.server.restapi.change.ChangesCollection;
+import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.util.http.CacheHeaders;
@@ -126,7 +133,6 @@
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonParseException;
-import com.google.gson.JsonPrimitive;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonToken;
 import com.google.gson.stream.JsonWriter;
@@ -158,6 +164,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
@@ -186,9 +193,6 @@
 
   @VisibleForTesting public static final String X_GERRIT_TRACE = "X-Gerrit-Trace";
 
-  // HTTP 422 Unprocessable Entity.
-  // TODO: Remove when HttpServletResponse.SC_UNPROCESSABLE_ENTITY is available
-  private static final int SC_UNPROCESSABLE_ENTITY = 422;
   private static final String X_REQUESTED_WITH = "X-Requested-With";
   private static final String X_GERRIT_AUTH = "X-Gerrit-Auth";
   static final ImmutableSet<String> ALLOWED_CORS_METHODS =
@@ -201,6 +205,8 @@
   public static final String XD_AUTHORIZATION = "access_token";
   public static final String XD_CONTENT_TYPE = "$ct";
   public static final String XD_METHOD = "$m";
+  public static final int SC_UNPROCESSABLE_ENTITY = 422;
+  public static final int SC_TOO_MANY_REQUESTS = 429;
 
   private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
   private static final String PLAIN_TEXT = "text/plain";
@@ -224,30 +230,41 @@
     final Provider<CurrentUser> currentUser;
     final DynamicItem<WebSession> webSession;
     final Provider<ParameterParser> paramParser;
+    final PluginSetContext<RequestListener> requestListeners;
     final PermissionBackend permissionBackend;
     final GroupAuditService auditService;
     final RestApiMetrics metrics;
     final Pattern allowOrigin;
     final RestApiQuotaEnforcer quotaChecker;
+    final Config config;
+    final DynamicSet<PerformanceLogger> performanceLoggers;
+    final ChangeFinder changeFinder;
 
     @Inject
     Globals(
         Provider<CurrentUser> currentUser,
         DynamicItem<WebSession> webSession,
         Provider<ParameterParser> paramParser,
+        PluginSetContext<RequestListener> requestListeners,
         PermissionBackend permissionBackend,
         GroupAuditService auditService,
         RestApiMetrics metrics,
         RestApiQuotaEnforcer quotaChecker,
-        @GerritServerConfig Config cfg) {
+        @GerritServerConfig Config config,
+        DynamicSet<PerformanceLogger> performanceLoggers,
+        ChangeFinder changeFinder) {
       this.currentUser = currentUser;
       this.webSession = webSession;
       this.paramParser = paramParser;
+      this.requestListeners = requestListeners;
       this.permissionBackend = permissionBackend;
       this.auditService = auditService;
       this.metrics = metrics;
       this.quotaChecker = quotaChecker;
-      allowOrigin = makeAllowOrigin(cfg);
+      this.config = config;
+      this.performanceLoggers = performanceLoggers;
+      this.changeFinder = changeFinder;
+      allowOrigin = makeAllowOrigin(config);
     }
 
     private static Pattern makeAllowOrigin(Config cfg) {
@@ -261,6 +278,7 @@
 
   private final Globals globals;
   private final Provider<RestCollection<RestResource, RestResource>> members;
+  private Optional<String> traceId = Optional.empty();
 
   public RestApiServlet(
       Globals globals, RestCollection<? extends RestResource, ? extends RestResource> members) {
@@ -286,272 +304,288 @@
     res.setHeader("X-Content-Type-Options", "nosniff");
     int status = SC_OK;
     long responseBytes = -1;
-    Object result = null;
+    Response<?> response = null;
     QueryParams qp = null;
     Object inputRequestBody = null;
     RestResource rsrc = TopLevelResource.INSTANCE;
     ViewData viewData = null;
 
     try (TraceContext traceContext = enableTracing(req, res)) {
+      List<IdString> path = splitPath(req);
+
+      RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
+      globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
+
       try (PerThreadCache ignored = PerThreadCache.create(req)) {
-        logger.atFinest().log(
-            "Received REST request: %s %s (parameters: %s)",
-            req.getMethod(), req.getRequestURI(), getParameterNames(req));
-        logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName());
+        // It's important that the PerformanceLogContext is closed before the response is sent to
+        // the client. Only this way it is ensured that the invocation of the PerformanceLogger
+        // plugins happens before the client sees the response. This is needed for being able to
+        // test performance logging from an acceptance test (see
+        // TraceIT#performanceLoggingForRestCall()).
+        try (PerformanceLogContext performanceLogContext =
+            new PerformanceLogContext(globals.config, globals.performanceLoggers)) {
+          logger.atFinest().log(
+              "Received REST request: %s %s (parameters: %s)",
+              req.getMethod(), req.getRequestURI(), getParameterNames(req));
+          logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName());
+          logger.atFinest().log(
+              "Groups: %s",
+              lazy(() -> globals.currentUser.get().getEffectiveGroups().getKnownGroups()));
 
-        if (isCorsPreflight(req)) {
-          doCorsPreflight(req, res);
-          return;
-        }
-
-        qp = ParameterParser.getQueryParams(req);
-        checkCors(req, res, qp.hasXdOverride());
-        if (qp.hasXdOverride()) {
-          req = applyXdOverrides(req, qp);
-        }
-        checkUserSession(req);
-
-        List<IdString> path = splitPath(req);
-        RestCollection<RestResource, RestResource> rc = members.get();
-        globals
-            .permissionBackend
-            .currentUser()
-            .checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
-
-        viewData = new ViewData(null, null);
-
-        if (path.isEmpty()) {
-          globals.quotaChecker.enforce(req);
-          if (rc instanceof NeedsParams) {
-            ((NeedsParams) rc).setParams(qp.params());
+          if (isCorsPreflight(req)) {
+            doCorsPreflight(req, res);
+            return;
           }
 
-          if (isRead(req)) {
-            viewData = new ViewData(null, rc.list());
-          } else if (isPost(req)) {
-            RestView<RestResource> restCollectionView =
-                rc.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
-            if (restCollectionView != null) {
-              viewData = new ViewData(null, restCollectionView);
-            } else {
-              throw methodNotAllowed(req);
-            }
-          } else {
-            // DELETE on root collections is not supported
-            throw methodNotAllowed(req);
+          qp = ParameterParser.getQueryParams(req);
+          checkCors(req, res, qp.hasXdOverride());
+          if (qp.hasXdOverride()) {
+            req = applyXdOverrides(req, qp);
           }
-        } else {
-          IdString id = path.remove(0);
-          try {
-            rsrc = rc.parse(rsrc, id);
-            globals.quotaChecker.enforce(rsrc, req);
-            if (path.isEmpty()) {
-              checkPreconditions(req);
-            }
-          } catch (ResourceNotFoundException e) {
-            if (!path.isEmpty()) {
-              throw e;
-            }
-            globals.quotaChecker.enforce(req);
+          checkUserSession(req);
 
-            if (isPost(req) || isPut(req)) {
-              RestView<RestResource> createView = rc.views().get(PluginName.GERRIT, "CREATE./");
-              if (createView != null) {
-                viewData = new ViewData(null, createView);
-                status = SC_CREATED;
-                path.add(id);
-              } else {
-                throw e;
-              }
-            } else if (isDelete(req)) {
-              RestView<RestResource> deleteView =
-                  rc.views().get(PluginName.GERRIT, "DELETE_MISSING./");
-              if (deleteView != null) {
-                viewData = new ViewData(null, deleteView);
-                status = SC_NO_CONTENT;
-                path.add(id);
-              } else {
-                throw e;
-              }
-            } else {
-              throw e;
-            }
-          }
-          if (viewData.view == null) {
-            viewData = view(rc, req.getMethod(), path);
-          }
-        }
-        checkRequiresCapability(viewData);
+          RestCollection<RestResource, RestResource> rc = members.get();
+          globals
+              .permissionBackend
+              .currentUser()
+              .checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
 
-        while (viewData.view instanceof RestCollection<?, ?>) {
-          @SuppressWarnings("unchecked")
-          RestCollection<RestResource, RestResource> c =
-              (RestCollection<RestResource, RestResource>) viewData.view;
+          viewData = new ViewData(null, null);
 
           if (path.isEmpty()) {
+            globals.quotaChecker.enforce(req);
+            if (rc instanceof NeedsParams) {
+              ((NeedsParams) rc).setParams(qp.params());
+            }
+
             if (isRead(req)) {
-              viewData = new ViewData(null, c.list());
+              viewData = new ViewData(null, rc.list());
             } else if (isPost(req)) {
-              // TODO: Here and on other collection methods: There is a bug that binds child views
-              // with pluginName="gerrit" instead of the real plugin name. This has never worked
-              // correctly and should be fixed where the binding gets created (DynamicMapProvider)
-              // and here.
               RestView<RestResource> restCollectionView =
-                  c.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
-              if (restCollectionView != null) {
-                viewData = new ViewData(null, restCollectionView);
-              } else {
-                throw methodNotAllowed(req);
-              }
-            } else if (isDelete(req)) {
-              RestView<RestResource> restCollectionView =
-                  c.views().get(PluginName.GERRIT, "DELETE_ON_COLLECTION./");
+                  rc.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
               if (restCollectionView != null) {
                 viewData = new ViewData(null, restCollectionView);
               } else {
                 throw methodNotAllowed(req);
               }
             } else {
+              // DELETE on root collections is not supported
               throw methodNotAllowed(req);
             }
-            break;
-          }
-          IdString id = path.remove(0);
-          try {
-            rsrc = c.parse(rsrc, id);
-            checkPreconditions(req);
-            viewData = new ViewData(null, null);
-          } catch (ResourceNotFoundException e) {
-            if (!path.isEmpty()) {
-              throw e;
-            }
+          } else {
+            IdString id = path.remove(0);
+            try {
+              rsrc = rc.parse(rsrc, id);
+              globals.quotaChecker.enforce(rsrc, req);
+              if (path.isEmpty()) {
+                checkPreconditions(req);
+              }
+            } catch (ResourceNotFoundException e) {
+              if (!path.isEmpty()) {
+                throw e;
+              }
+              globals.quotaChecker.enforce(req);
 
-            if (isPost(req) || isPut(req)) {
-              RestView<RestResource> createView = c.views().get(PluginName.GERRIT, "CREATE./");
-              if (createView != null) {
-                viewData = new ViewData(viewData.pluginName, createView);
-                status = SC_CREATED;
-                path.add(id);
+              if (isPost(req) || isPut(req)) {
+                RestView<RestResource> createView = rc.views().get(PluginName.GERRIT, "CREATE./");
+                if (createView != null) {
+                  viewData = new ViewData(null, createView);
+                  path.add(id);
+                } else {
+                  throw e;
+                }
+              } else if (isDelete(req)) {
+                RestView<RestResource> deleteView =
+                    rc.views().get(PluginName.GERRIT, "DELETE_MISSING./");
+                if (deleteView != null) {
+                  viewData = new ViewData(null, deleteView);
+                  path.add(id);
+                } else {
+                  throw e;
+                }
               } else {
                 throw e;
               }
-            } else if (isDelete(req)) {
-              RestView<RestResource> deleteView =
-                  c.views().get(PluginName.GERRIT, "DELETE_MISSING./");
-              if (deleteView != null) {
-                viewData = new ViewData(viewData.pluginName, deleteView);
-                status = SC_NO_CONTENT;
-                path.add(id);
-              } else {
-                throw e;
-              }
-            } else {
-              throw e;
             }
-          }
-          if (viewData.view == null) {
-            viewData = view(c, req.getMethod(), path);
+            if (viewData.view == null) {
+              viewData = view(rc, req.getMethod(), path);
+            }
           }
           checkRequiresCapability(viewData);
-        }
 
-        if (notModified(req, rsrc, viewData.view)) {
-          logger.atFinest().log("REST call succeeded: %d", SC_NOT_MODIFIED);
-          res.sendError(SC_NOT_MODIFIED);
-          return;
-        }
+          while (viewData.view instanceof RestCollection<?, ?>) {
+            @SuppressWarnings("unchecked")
+            RestCollection<RestResource, RestResource> c =
+                (RestCollection<RestResource, RestResource>) viewData.view;
 
-        if (!globals.paramParser.get().parse(viewData.view, qp.params(), req, res)) {
-          return;
-        }
-
-        if (viewData.view instanceof RestReadView<?> && isRead(req)) {
-          result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
-        } else if (viewData.view instanceof RestModifyView<?, ?>) {
-          @SuppressWarnings("unchecked")
-          RestModifyView<RestResource, Object> m =
-              (RestModifyView<RestResource, Object>) viewData.view;
-
-          Type type = inputType(m);
-          inputRequestBody = parseRequest(req, type);
-          result = m.apply(rsrc, inputRequestBody);
-          if (inputRequestBody instanceof RawInput) {
-            try (InputStream is = req.getInputStream()) {
-              ServletUtils.consumeRequestBody(is);
+            if (path.isEmpty()) {
+              if (isRead(req)) {
+                viewData = new ViewData(null, c.list());
+              } else if (isPost(req)) {
+                // TODO: Here and on other collection methods: There is a bug that binds child views
+                // with pluginName="gerrit" instead of the real plugin name. This has never worked
+                // correctly and should be fixed where the binding gets created (DynamicMapProvider)
+                // and here.
+                RestView<RestResource> restCollectionView =
+                    c.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
+                if (restCollectionView != null) {
+                  viewData = new ViewData(null, restCollectionView);
+                } else {
+                  throw methodNotAllowed(req);
+                }
+              } else if (isDelete(req)) {
+                RestView<RestResource> restCollectionView =
+                    c.views().get(PluginName.GERRIT, "DELETE_ON_COLLECTION./");
+                if (restCollectionView != null) {
+                  viewData = new ViewData(null, restCollectionView);
+                } else {
+                  throw methodNotAllowed(req);
+                }
+              } else {
+                throw methodNotAllowed(req);
+              }
+              break;
             }
-          }
-        } else if (viewData.view instanceof RestCollectionCreateView<?, ?, ?>) {
-          @SuppressWarnings("unchecked")
-          RestCollectionCreateView<RestResource, RestResource, Object> m =
-              (RestCollectionCreateView<RestResource, RestResource, Object>) viewData.view;
+            IdString id = path.remove(0);
+            try {
+              rsrc = c.parse(rsrc, id);
+              checkPreconditions(req);
+              viewData = new ViewData(null, null);
+            } catch (ResourceNotFoundException e) {
+              if (!path.isEmpty()) {
+                throw e;
+              }
 
-          Type type = inputType(m);
-          inputRequestBody = parseRequest(req, type);
-          result = m.apply(rsrc, path.get(0), inputRequestBody);
-          if (inputRequestBody instanceof RawInput) {
-            try (InputStream is = req.getInputStream()) {
-              ServletUtils.consumeRequestBody(is);
+              if (isPost(req) || isPut(req)) {
+                RestView<RestResource> createView = c.views().get(PluginName.GERRIT, "CREATE./");
+                if (createView != null) {
+                  viewData = new ViewData(viewData.pluginName, createView);
+                  path.add(id);
+                } else {
+                  throw e;
+                }
+              } else if (isDelete(req)) {
+                RestView<RestResource> deleteView =
+                    c.views().get(PluginName.GERRIT, "DELETE_MISSING./");
+                if (deleteView != null) {
+                  viewData = new ViewData(viewData.pluginName, deleteView);
+                  path.add(id);
+                } else {
+                  throw e;
+                }
+              } else {
+                throw e;
+              }
             }
-          }
-        } else if (viewData.view instanceof RestCollectionDeleteMissingView<?, ?, ?>) {
-          @SuppressWarnings("unchecked")
-          RestCollectionDeleteMissingView<RestResource, RestResource, Object> m =
-              (RestCollectionDeleteMissingView<RestResource, RestResource, Object>) viewData.view;
-
-          Type type = inputType(m);
-          inputRequestBody = parseRequest(req, type);
-          result = m.apply(rsrc, path.get(0), inputRequestBody);
-          if (inputRequestBody instanceof RawInput) {
-            try (InputStream is = req.getInputStream()) {
-              ServletUtils.consumeRequestBody(is);
+            if (viewData.view == null) {
+              viewData = view(c, req.getMethod(), path);
             }
+            checkRequiresCapability(viewData);
           }
-        } else if (viewData.view instanceof RestCollectionModifyView<?, ?, ?>) {
-          @SuppressWarnings("unchecked")
-          RestCollectionModifyView<RestResource, RestResource, Object> m =
-              (RestCollectionModifyView<RestResource, RestResource, Object>) viewData.view;
 
-          Type type = inputType(m);
-          inputRequestBody = parseRequest(req, type);
-          result = m.apply(rsrc, inputRequestBody);
-          if (inputRequestBody instanceof RawInput) {
-            try (InputStream is = req.getInputStream()) {
-              ServletUtils.consumeRequestBody(is);
+          if (notModified(req, rsrc, viewData.view)) {
+            logger.atFinest().log("REST call succeeded: %d", SC_NOT_MODIFIED);
+            res.sendError(SC_NOT_MODIFIED);
+            return;
+          }
+
+          if (!globals.paramParser.get().parse(viewData.view, qp.params(), req, res)) {
+            return;
+          }
+
+          if (viewData.view instanceof RestReadView<?> && isRead(req)) {
+            response = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
+          } else if (viewData.view instanceof RestModifyView<?, ?>) {
+            @SuppressWarnings("unchecked")
+            RestModifyView<RestResource, Object> m =
+                (RestModifyView<RestResource, Object>) viewData.view;
+
+            Type type = inputType(m);
+            inputRequestBody = parseRequest(req, type);
+            response = m.apply(rsrc, inputRequestBody);
+            if (inputRequestBody instanceof RawInput) {
+              try (InputStream is = req.getInputStream()) {
+                ServletUtils.consumeRequestBody(is);
+              }
             }
-          }
-        } else {
-          throw new ResourceNotFoundException();
-        }
+          } else if (viewData.view instanceof RestCollectionCreateView<?, ?, ?>) {
+            @SuppressWarnings("unchecked")
+            RestCollectionCreateView<RestResource, RestResource, Object> m =
+                (RestCollectionCreateView<RestResource, RestResource, Object>) viewData.view;
 
-        if (result instanceof Response) {
-          @SuppressWarnings("rawtypes")
-          Response<?> r = (Response) result;
-          status = r.statusCode();
-          configureCaching(req, res, rsrc, viewData.view, r.caching());
-        } else if (result instanceof Response.Redirect) {
-          CacheHeaders.setNotCacheable(res);
-          String location = ((Response.Redirect) result).location();
-          res.sendRedirect(location);
-          logger.atFinest().log("REST call redirected to: %s", location);
-          return;
-        } else if (result instanceof Response.Accepted) {
-          CacheHeaders.setNotCacheable(res);
-          res.setStatus(SC_ACCEPTED);
-          res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) result).location());
-          logger.atFinest().log("REST call succeeded: %d", SC_ACCEPTED);
-          return;
-        } else {
-          CacheHeaders.setNotCacheable(res);
-        }
-        res.setStatus(status);
-        logger.atFinest().log("REST call succeeded: %d", status);
+            Type type = inputType(m);
+            inputRequestBody = parseRequest(req, type);
+            response = m.apply(rsrc, path.get(0), inputRequestBody);
+            if (inputRequestBody instanceof RawInput) {
+              try (InputStream is = req.getInputStream()) {
+                ServletUtils.consumeRequestBody(is);
+              }
+            }
+          } else if (viewData.view instanceof RestCollectionDeleteMissingView<?, ?, ?>) {
+            @SuppressWarnings("unchecked")
+            RestCollectionDeleteMissingView<RestResource, RestResource, Object> m =
+                (RestCollectionDeleteMissingView<RestResource, RestResource, Object>) viewData.view;
 
-        if (result != Response.none()) {
-          result = Response.unwrap(result);
-          if (result instanceof BinaryResult) {
-            responseBytes = replyBinaryResult(req, res, (BinaryResult) result);
+            Type type = inputType(m);
+            inputRequestBody = parseRequest(req, type);
+            response = m.apply(rsrc, path.get(0), inputRequestBody);
+            if (inputRequestBody instanceof RawInput) {
+              try (InputStream is = req.getInputStream()) {
+                ServletUtils.consumeRequestBody(is);
+              }
+            }
+          } else if (viewData.view instanceof RestCollectionModifyView<?, ?, ?>) {
+            @SuppressWarnings("unchecked")
+            RestCollectionModifyView<RestResource, RestResource, Object> m =
+                (RestCollectionModifyView<RestResource, RestResource, Object>) viewData.view;
+
+            Type type = inputType(m);
+            inputRequestBody = parseRequest(req, type);
+            response = m.apply(rsrc, inputRequestBody);
+            if (inputRequestBody instanceof RawInput) {
+              try (InputStream is = req.getInputStream()) {
+                ServletUtils.consumeRequestBody(is);
+              }
+            }
           } else {
-            responseBytes = replyJson(req, res, false, qp.config(), result);
+            throw new ResourceNotFoundException();
+          }
+
+          traceId = response.traceId();
+          traceId.ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
+
+          if (response instanceof Response.Redirect) {
+            CacheHeaders.setNotCacheable(res);
+            String location = ((Response.Redirect) response).location();
+            res.sendRedirect(location);
+            logger.atFinest().log("REST call redirected to: %s", location);
+            return;
+          } else if (response instanceof Response.Accepted) {
+            CacheHeaders.setNotCacheable(res);
+            res.setStatus(response.statusCode());
+            res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) response).location());
+            logger.atFinest().log("REST call succeeded: %d", response.statusCode());
+            return;
+          } else if (response instanceof Response.InternalServerError) {
+            // Rethrow the exception to have exactly the same error handling as if the REST endpoint
+            // would have thrown the exception directly, instead of returning
+            // Response.InternalServerError.
+            Exception cause = ((Response.InternalServerError<?>) response).cause();
+            throw cause;
+          }
+
+          status = response.statusCode();
+          configureCaching(req, res, rsrc, viewData.view, response.caching());
+          res.setStatus(status);
+          logger.atFinest().log("REST call succeeded: %d", status);
+        }
+
+        if (response != Response.none()) {
+          Object value = Response.unwrap(response);
+          if (value instanceof BinaryResult) {
+            responseBytes = replyBinaryResult(req, res, (BinaryResult) value);
+          } else {
+            responseBytes = replyJson(req, res, false, qp.config(), value);
           }
         }
       } catch (MalformedJsonException | JsonParseException e) {
@@ -602,21 +636,27 @@
                 e.caching(),
                 e);
       } catch (NotImplementedException e) {
+        logger.atSevere().withCause(e).log("Error in %s %s", req.getMethod(), uriForLogging(req));
         responseBytes =
             replyError(req, res, status = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
       } catch (UpdateException e) {
         Throwable t = e.getCause();
         if (t instanceof LockFailureException) {
-          responseBytes =
-              replyError(
-                  req, res, status = SC_SERVICE_UNAVAILABLE, messageOr(t, "Lock failure"), e);
+          logger.atSevere().withCause(t).log("Error in %s %s", req.getMethod(), uriForLogging(req));
+          responseBytes = replyError(req, res, status = SC_SERVICE_UNAVAILABLE, "Lock failure", e);
         } else {
           status = SC_INTERNAL_SERVER_ERROR;
           responseBytes = handleException(e, req, res);
         }
       } catch (QuotaException e) {
         responseBytes =
-            replyError(req, res, status = 429, messageOr(e, "Quota limit reached"), e.caching(), e);
+            replyError(
+                req,
+                res,
+                status = SC_TOO_MANY_REQUESTS,
+                messageOr(e, "Quota limit reached"),
+                e.caching(),
+                e);
       } catch (Exception e) {
         status = SC_INTERNAL_SERVER_ERROR;
         responseBytes = handleException(e, req, res);
@@ -641,7 +681,7 @@
                 qp != null ? qp.params() : ImmutableListMultimap.of(),
                 inputRequestBody,
                 status,
-                result,
+                response,
                 rsrc,
                 viewData == null ? null : viewData.view));
       }
@@ -1029,7 +1069,7 @@
     TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE);
     buf.write(JSON_MAGIC);
     Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
-    Gson gson = newGson(config, req);
+    Gson gson = newGson(config);
     if (result instanceof JsonElement) {
       gson.toJson((JsonElement) result, w);
     } else {
@@ -1056,25 +1096,18 @@
         req, res, asBinaryResult(buf).setContentType(JSON_TYPE).setCharacterEncoding(UTF_8));
   }
 
-  private static Gson newGson(
-      ListMultimap<String, String> config, @Nullable HttpServletRequest req) {
+  private static Gson newGson(ListMultimap<String, String> config) {
     GsonBuilder gb = OutputFormat.JSON_COMPACT.newGsonBuilder();
 
-    enablePrettyPrint(gb, config, req);
+    enablePrettyPrint(gb, config);
     enablePartialGetFields(gb, config);
 
     return gb.create();
   }
 
-  private static void enablePrettyPrint(
-      GsonBuilder gb, ListMultimap<String, String> config, @Nullable HttpServletRequest req) {
-    String pp = Iterables.getFirst(config.get("pp"), null);
-    if (pp == null) {
-      pp = Iterables.getFirst(config.get("prettyPrint"), null);
-      if (pp == null && req != null) {
-        pp = acceptsJson(req) ? "0" : "1";
-      }
-    }
+  private static void enablePrettyPrint(GsonBuilder gb, ListMultimap<String, String> config) {
+    String pp =
+        Iterables.getFirst(config.get("pp"), Iterables.getFirst(config.get("prettyPrint"), "0"));
     if ("1".equals(pp) || "true".equals(pp)) {
       gb.setPrettyPrinting();
     }
@@ -1271,9 +1304,11 @@
 
     // Check if we want to delegate to a child collection. Child collections are bound with
     // GET.name so we have to check for this since we haven't found any other views.
-    core = views.get(PluginName.GERRIT, "GET." + p.get(0));
-    if (core != null) {
-      return new ViewData(PluginName.GERRIT, core);
+    if (method.equals("GET")) {
+      core = views.get(PluginName.GERRIT, "GET." + p.get(0));
+      if (core != null) {
+        return new ViewData(PluginName.GERRIT, core);
+      }
     }
 
     Map<String, RestView<RestResource>> r = new TreeMap<>();
@@ -1388,6 +1423,29 @@
     return traceContext;
   }
 
+  private RequestInfo createRequestInfo(
+      TraceContext traceContext, String requestUri, List<IdString> path) {
+    RequestInfo.Builder requestInfo =
+        RequestInfo.builder(RequestInfo.RequestType.REST, globals.currentUser.get(), traceContext)
+            .requestUri(requestUri);
+
+    if (path.size() < 1) {
+      return requestInfo.build();
+    }
+
+    RestCollection<?, ?> rootCollection = members.get();
+    String resourceId = path.get(0).get();
+    if (rootCollection instanceof ProjectsCollection) {
+      requestInfo.project(Project.nameKey(resourceId));
+    } else if (rootCollection instanceof ChangesCollection) {
+      ChangeNotes changeNotes = globals.changeFinder.findOne(resourceId);
+      if (changeNotes != null) {
+        requestInfo.project(changeNotes.getProjectName());
+      }
+    }
+    return requestInfo.build();
+  }
+
   private boolean isDelete(HttpServletRequest req) {
     return "DELETE".equals(req.getMethod());
   }
@@ -1430,18 +1488,23 @@
     }
   }
 
-  private static long handleException(
-      Throwable err, HttpServletRequest req, HttpServletResponse res) throws IOException {
+  private long handleException(Throwable err, HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uriForLogging(req));
+    if (!res.isCommitted()) {
+      res.reset();
+      traceId.ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
+      return replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err);
+    }
+    return 0;
+  }
+
+  private static String uriForLogging(HttpServletRequest req) {
     String uri = req.getRequestURI();
     if (!Strings.isNullOrEmpty(req.getQueryString())) {
       uri += "?" + LogRedactUtil.redactQueryString(req.getQueryString());
     }
-    logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uri);
-    if (!res.isCommitted()) {
-      res.reset();
-      return replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err);
-    }
-    return 0;
+    return uri;
   }
 
   public static long replyError(
@@ -1486,10 +1549,6 @@
   static long replyText(
       @Nullable HttpServletRequest req, HttpServletResponse res, boolean allowTracing, String text)
       throws IOException {
-    if ((req == null || isRead(req)) && isMaybeHTML(text)) {
-      return replyJson(
-          req, res, allowTracing, ImmutableListMultimap.of("pp", "0"), new JsonPrimitive(text));
-    }
     if (!text.endsWith("\n")) {
       text += "\n";
     }
@@ -1499,14 +1558,6 @@
     return replyBinaryResult(req, res, BinaryResult.create(text).setContentType(PLAIN_TEXT));
   }
 
-  private static boolean isMaybeHTML(String text) {
-    return CharMatcher.anyOf("<&").matchesAnyOf(text);
-  }
-
-  private static boolean acceptsJson(HttpServletRequest req) {
-    return req != null && isType(JSON_TYPE, req.getHeader(HttpHeaders.ACCEPT));
-  }
-
   private static boolean acceptsGzip(HttpServletRequest req) {
     if (req != null) {
       String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);
diff --git a/java/com/google/gerrit/index/BUILD b/java/com/google/gerrit/index/BUILD
index 55c7746..95b3581 100644
--- a/java/com/google/gerrit/index/BUILD
+++ b/java/com/google/gerrit/index/BUILD
@@ -22,17 +22,18 @@
         ":query_exception",
         "//antlr3:query_parser",
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/json",
         "//java/com/google/gerrit/metrics",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server/logging",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/antlr:java-runtime",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/flogger:api",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/index/IndexConfig.java b/java/com/google/gerrit/index/IndexConfig.java
index b5b36f1..29b8ea6 100644
--- a/java/com/google/gerrit/index/IndexConfig.java
+++ b/java/com/google/gerrit/index/IndexConfig.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.auto.value.AutoValue;
+import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 import org.eclipse.jgit.lib.Config;
 
@@ -39,6 +40,7 @@
     setIfPresent(cfg, "maxLimit", b::maxLimit);
     setIfPresent(cfg, "maxPages", b::maxPages);
     setIfPresent(cfg, "maxTerms", b::maxTerms);
+    setTypeOrDefault(cfg, b::type);
     return b;
   }
 
@@ -49,11 +51,17 @@
     }
   }
 
+  private static void setTypeOrDefault(Config cfg, Consumer<String> setter) {
+    String type = cfg != null ? cfg.getString("index", null, "type") : null;
+    setter.accept(new IndexType(type).toString());
+  }
+
   public static Builder builder() {
     return new AutoValue_IndexConfig.Builder()
         .maxLimit(Integer.MAX_VALUE)
         .maxPages(Integer.MAX_VALUE)
         .maxTerms(DEFAULT_MAX_TERMS)
+        .type(IndexType.getDefault())
         .separateChangeSubIndexes(false);
   }
 
@@ -71,6 +79,10 @@
 
     public abstract int maxTerms();
 
+    public abstract Builder type(String type);
+
+    public abstract String type();
+
     public abstract Builder separateChangeSubIndexes(boolean separate);
 
     abstract IndexConfig autoBuild();
@@ -105,6 +117,9 @@
    */
   public abstract int maxTerms();
 
+  /** @return index type. */
+  public abstract String type();
+
   /**
    * @return whether different subsets of changes may be stored in different physical sub-indexes.
    */
diff --git a/java/com/google/gerrit/index/IndexType.java b/java/com/google/gerrit/index/IndexType.java
new file mode 100644
index 0000000..ee44deb
--- /dev/null
+++ b/java/com/google/gerrit/index/IndexType.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2013 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.index;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
+
+/**
+ * Index types supported by the secondary index.
+ *
+ * <p>The explicitly known index types are Lucene (the default) and Elasticsearch.
+ *
+ * <p>The third supported index type is any other type String value, deemed as custom. This is for
+ * configuring index types that are internal or not to be disclosed. Supporting custom index types
+ * allows to not break that case upon core implementation changes.
+ */
+public class IndexType {
+  private static final String LUCENE = "lucene";
+  private static final String ELASTICSEARCH = "elasticsearch";
+
+  private final String type;
+
+  public IndexType(@Nullable String type) {
+    this.type = type == null ? getDefault() : type.toLowerCase();
+  }
+
+  public static String getDefault() {
+    return LUCENE;
+  }
+
+  public static ImmutableSet<String> getKnownTypes() {
+    return ImmutableSet.of(LUCENE, ELASTICSEARCH);
+  }
+
+  public boolean isLucene() {
+    return type.equals(LUCENE);
+  }
+
+  public boolean isElasticsearch() {
+    return type.equals(ELASTICSEARCH);
+  }
+
+  @Override
+  public String toString() {
+    return type;
+  }
+}
diff --git a/java/com/google/gerrit/index/RefState.java b/java/com/google/gerrit/index/RefState.java
index f0e465d..956dcab 100644
--- a/java/com/google/gerrit/index/RefState.java
+++ b/java/com/google/gerrit/index/RefState.java
@@ -23,10 +23,10 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.git.ObjectIds;
 import java.io.IOException;
 import java.util.List;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -42,7 +42,7 @@
       String s = new String(b, UTF_8);
       List<String> parts = Splitter.on(':').splitToList(s);
       RefState.check(parts.size() == 3 && !parts.get(0).isEmpty() && !parts.get(1).isEmpty(), s);
-      result.put(new Project.NameKey(parts.get(0)), RefState.create(parts.get(1), parts.get(2)));
+      result.put(Project.nameKey(parts.get(0)), RefState.create(parts.get(1), parts.get(2)));
     }
     return result;
   }
@@ -61,7 +61,7 @@
 
   public byte[] toByteArray(Project.NameKey project) {
     byte[] a = (project.toString() + ':' + ref() + ':').getBytes(UTF_8);
-    byte[] b = new byte[a.length + Constants.OBJECT_ID_STRING_LENGTH];
+    byte[] b = new byte[a.length + ObjectIds.STR_LEN];
     System.arraycopy(a, 0, b, 0, a.length);
     id().copyTo(b, a.length);
     return b;
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index f65d03e..bab2990 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -35,6 +35,7 @@
 
   public static class Builder<T> {
     private final List<FieldDef<T, ?>> fields = new ArrayList<>();
+    private boolean useLegacyNumericFields;
 
     public Builder<T> add(Schema<T> schema) {
       this.fields.addAll(schema.getFields().values());
@@ -53,8 +54,13 @@
       return this;
     }
 
+    public Builder<T> legacyNumericFields(boolean useLegacyNumericFields) {
+      this.useLegacyNumericFields = useLegacyNumericFields;
+      return this;
+    }
+
     public Schema<T> build() {
-      return new Schema<>(ImmutableList.copyOf(fields));
+      return new Schema<>(useLegacyNumericFields, ImmutableList.copyOf(fields));
     }
   }
 
@@ -83,14 +89,15 @@
 
   private final ImmutableMap<String, FieldDef<T, ?>> fields;
   private final ImmutableMap<String, FieldDef<T, ?>> storedFields;
+  private final boolean useLegacyNumericFields;
 
   private int version;
 
-  public Schema(Iterable<FieldDef<T, ?>> fields) {
-    this(0, fields);
+  public Schema(boolean useLegacyNumericFields, Iterable<FieldDef<T, ?>> fields) {
+    this(0, useLegacyNumericFields, fields);
   }
 
-  public Schema(int version, Iterable<FieldDef<T, ?>> fields) {
+  public Schema(int version, boolean useLegacyNumericFields, Iterable<FieldDef<T, ?>> fields) {
     this.version = version;
     ImmutableMap.Builder<String, FieldDef<T, ?>> b = ImmutableMap.builder();
     ImmutableMap.Builder<String, FieldDef<T, ?>> sb = ImmutableMap.builder();
@@ -102,12 +109,17 @@
     }
     this.fields = b.build();
     this.storedFields = sb.build();
+    this.useLegacyNumericFields = useLegacyNumericFields;
   }
 
   public final int getVersion() {
     return version;
   }
 
+  public final boolean useLegacyNumericFields() {
+    return useLegacyNumericFields;
+  }
+
   /**
    * Get all fields in this schema.
    *
diff --git a/java/com/google/gerrit/index/SchemaUtil.java b/java/com/google/gerrit/index/SchemaUtil.java
index c59f251..9599d6a 100644
--- a/java/com/google/gerrit/index/SchemaUtil.java
+++ b/java/com/google/gerrit/index/SchemaUtil.java
@@ -67,12 +67,19 @@
   }
 
   public static <V> Schema<V> schema(Collection<FieldDef<V, ?>> fields) {
-    return new Schema<>(ImmutableList.copyOf(fields));
+    return new Schema<>(true, ImmutableList.copyOf(fields));
+  }
+
+  public static <V> Schema<V> schema(Schema<V> schema, boolean useLegacyNumericFields) {
+    return new Schema<>(
+        useLegacyNumericFields,
+        new ImmutableList.Builder<FieldDef<V, ?>>().addAll(schema.getFields().values()).build());
   }
 
   @SafeVarargs
   public static <V> Schema<V> schema(Schema<V> schema, FieldDef<V, ?>... moreFields) {
     return new Schema<>(
+        true,
         new ImmutableList.Builder<FieldDef<V, ?>>()
             .addAll(schema.getFields().values())
             .addAll(ImmutableList.copyOf(moreFields))
@@ -81,7 +88,7 @@
 
   @SafeVarargs
   public static <V> Schema<V> schema(FieldDef<V, ?>... fields) {
-    return schema(ImmutableList.copyOf(fields));
+    return new Schema<>(true, ImmutableList.copyOf(fields));
   }
 
   public static Set<String> getPersonParts(PersonIdent person) {
diff --git a/java/com/google/gerrit/index/project/BUILD b/java/com/google/gerrit/index/project/BUILD
index 2c460fd..b423f84 100644
--- a/java/com/google/gerrit/index/project/BUILD
+++ b/java/com/google/gerrit/index/project/BUILD
@@ -5,9 +5,9 @@
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
-        "//java/com/google/gerrit/reviewdb:server",
         "//lib:guava",
         "//lib/guice",
     ],
diff --git a/java/com/google/gerrit/index/project/IndexedProjectQuery.java b/java/com/google/gerrit/index/project/IndexedProjectQuery.java
index 383ba1c..cdfeabd 100644
--- a/java/com/google/gerrit/index/project/IndexedProjectQuery.java
+++ b/java/com/google/gerrit/index/project/IndexedProjectQuery.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.index.project;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.IndexedQuery;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Project;
 
 public class IndexedProjectQuery extends IndexedQuery<Project.NameKey, ProjectData>
     implements DataSource<ProjectData> {
diff --git a/java/com/google/gerrit/index/project/ProjectData.java b/java/com/google/gerrit/index/project/ProjectData.java
index fb029ac..2bf9a4b5 100644
--- a/java/com/google/gerrit/index/project/ProjectData.java
+++ b/java/com/google/gerrit/index/project/ProjectData.java
@@ -18,7 +18,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
diff --git a/java/com/google/gerrit/index/project/ProjectField.java b/java/com/google/gerrit/index/project/ProjectField.java
index 119980c..c2c8986 100644
--- a/java/com/google/gerrit/index/project/ProjectField.java
+++ b/java/com/google/gerrit/index/project/ProjectField.java
@@ -20,11 +20,11 @@
 import static com.google.gerrit.index.FieldDef.prefix;
 import static com.google.gerrit.index.FieldDef.storedOnly;
 
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.RefState;
 import com.google.gerrit.index.SchemaUtil;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 
 /** Index schema for projects. */
 public class ProjectField {
diff --git a/java/com/google/gerrit/index/project/ProjectIndex.java b/java/com/google/gerrit/index/project/ProjectIndex.java
index db7302a..3e99d55 100644
--- a/java/com/google/gerrit/index/project/ProjectIndex.java
+++ b/java/com/google/gerrit/index/project/ProjectIndex.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.index.project;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Project;
 
 public interface ProjectIndex extends Index<Project.NameKey, ProjectData> {
 
diff --git a/java/com/google/gerrit/index/project/ProjectIndexCollection.java b/java/com/google/gerrit/index/project/ProjectIndexCollection.java
index 281f992..30227a3 100644
--- a/java/com/google/gerrit/index/project/ProjectIndexCollection.java
+++ b/java/com/google/gerrit/index/project/ProjectIndexCollection.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.index.project;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.IndexCollection;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Singleton;
 
 @Singleton
diff --git a/java/com/google/gerrit/index/project/ProjectIndexer.java b/java/com/google/gerrit/index/project/ProjectIndexer.java
index 1ca29f5..bd5efeb2 100644
--- a/java/com/google/gerrit/index/project/ProjectIndexer.java
+++ b/java/com/google/gerrit/index/project/ProjectIndexer.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.index.project;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 
 public interface ProjectIndexer {
 
diff --git a/java/com/google/gerrit/index/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index 7d817d2..538e11b 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -78,48 +78,55 @@
     if (source == null) {
       throw new StorageException("No DataSource: " + this);
     }
-    List<T> r = new ArrayList<>();
-    T last = null;
-    int nextStart = 0;
-    boolean skipped = false;
-    for (T data : buffer(source.read())) {
-      if (!isMatchable() || match(data)) {
-        r.add(data);
-      } else {
-        skipped = true;
-      }
-      last = data;
-      nextStart++;
-    }
 
-    if (skipped && last != null && source instanceof Paginated) {
-      // If our source is a paginated source and we skipped at
-      // least one of its results, we may not have filled the full
-      // limit the caller wants.  Restart the source and continue.
-      //
-      @SuppressWarnings("unchecked")
-      Paginated<T> p = (Paginated<T>) source;
-      while (skipped && r.size() < p.getOptions().limit() + start) {
-        skipped = false;
-        ResultSet<T> next = p.restart(nextStart);
-
-        for (T data : buffer(next)) {
-          if (match(data)) {
-            r.add(data);
-          } else {
-            skipped = true;
+    // ResultSets are lazy. Calling #read here first and then dealing with ResultSets only when
+    // requested allows the index to run asynchronous queries.
+    ResultSet<T> resultSet = source.read();
+    return new LazyResultSet<>(
+        () -> {
+          List<T> r = new ArrayList<>();
+          T last = null;
+          int nextStart = 0;
+          boolean skipped = false;
+          for (T data : buffer(resultSet)) {
+            if (!isMatchable() || match(data)) {
+              r.add(data);
+            } else {
+              skipped = true;
+            }
+            last = data;
+            nextStart++;
           }
-          nextStart++;
-        }
-      }
-    }
 
-    if (start >= r.size()) {
-      r = ImmutableList.of();
-    } else if (start > 0) {
-      r = ImmutableList.copyOf(r.subList(start, r.size()));
-    }
-    return new ListResultSet<>(r);
+          if (skipped && last != null && source instanceof Paginated) {
+            // If our source is a paginated source and we skipped at
+            // least one of its results, we may not have filled the full
+            // limit the caller wants.  Restart the source and continue.
+            //
+            @SuppressWarnings("unchecked")
+            Paginated<T> p = (Paginated<T>) source;
+            while (skipped && r.size() < p.getOptions().limit() + start) {
+              skipped = false;
+              ResultSet<T> next = p.restart(nextStart);
+
+              for (T data : buffer(next)) {
+                if (match(data)) {
+                  r.add(data);
+                } else {
+                  skipped = true;
+                }
+                nextStart++;
+              }
+            }
+          }
+
+          if (start >= r.size()) {
+            return ImmutableList.of();
+          } else if (start > 0) {
+            return ImmutableList.copyOf(r.subList(start, r.size()));
+          }
+          return ImmutableList.copyOf(r);
+        });
   }
 
   @Override
diff --git a/java/com/google/gerrit/index/query/LazyResultSet.java b/java/com/google/gerrit/index/query/LazyResultSet.java
new file mode 100644
index 0000000..f3fab5f
--- /dev/null
+++ b/java/com/google/gerrit/index/query/LazyResultSet.java
@@ -0,0 +1,56 @@
+// 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.index.query;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Iterator;
+import java.util.function.Supplier;
+
+/**
+ * Result set that allows for asynchronous execution of the actual query. Callers should dispatch
+ * the query and call the constructor of this class with a supplier that fetches the result and
+ * blocks on it if necessary.
+ *
+ * <p>If the execution is synchronous or the results are known a priori, consider using {@link
+ * ListResultSet}.
+ */
+public class LazyResultSet<T> implements ResultSet<T> {
+  private final Supplier<ImmutableList<T>> resultsCallback;
+
+  private boolean resultsReturned = false;
+
+  public LazyResultSet(Supplier<ImmutableList<T>> r) {
+    resultsCallback = requireNonNull(r, "results can't be null");
+  }
+
+  @Override
+  public Iterator<T> iterator() {
+    return toList().iterator();
+  }
+
+  @Override
+  public ImmutableList<T> toList() {
+    if (resultsReturned) {
+      throw new IllegalStateException("Results already obtained");
+    }
+    resultsReturned = true;
+    return resultsCallback.get();
+  }
+
+  @Override
+  public void close() {}
+}
diff --git a/java/com/google/gerrit/index/query/ListResultSet.java b/java/com/google/gerrit/index/query/ListResultSet.java
index 4cf48c8..9d7eadf 100644
--- a/java/com/google/gerrit/index/query/ListResultSet.java
+++ b/java/com/google/gerrit/index/query/ListResultSet.java
@@ -14,15 +14,25 @@
 
 package com.google.gerrit.index.query;
 
+import static java.util.Objects.requireNonNull;
+
 import com.google.common.collect.ImmutableList;
 import java.util.Iterator;
 import java.util.List;
 
+/**
+ * Result set for queries that run synchronously or for cases where the result is already known and
+ * we just need to pipe it back through our interfaces.
+ *
+ * <p>If your implementation benefits from asynchronous execution (i.e. dispatching a query and
+ * awaiting results only when {@link ResultSet#toList()} is called, consider using {@link
+ * LazyResultSet}.
+ */
 public class ListResultSet<T> implements ResultSet<T> {
-  private ImmutableList<T> items;
+  private ImmutableList<T> results;
 
   public ListResultSet(List<T> r) {
-    items = ImmutableList.copyOf(r);
+    results = ImmutableList.copyOf(requireNonNull(r, "results can't be null"));
   }
 
   @Override
@@ -32,16 +42,16 @@
 
   @Override
   public ImmutableList<T> toList() {
-    if (items == null) {
+    if (results == null) {
       throw new IllegalStateException("Results already obtained");
     }
-    ImmutableList<T> r = items;
-    items = null;
+    ImmutableList<T> r = results;
+    results = null;
     return r;
   }
 
   @Override
   public void close() {
-    items = null;
+    results = null;
   }
 }
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index 7077245..9501e52 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.server.logging.CallerFinder;
+import com.google.gerrit.server.logging.Metadata;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -60,14 +61,15 @@
     final Timer1<String> executionTime;
 
     Metrics(MetricMaker metricMaker) {
-      Field<String> index = Field.ofString("index", "index name");
       executionTime =
           metricMaker.newTimer(
               "query/query_latency",
               new Description("Successful query latency, accumulated over the life of the process")
                   .setCumulative()
                   .setUnit(Description.Units.MILLISECONDS),
-              index);
+              Field.ofString("index", Metadata.Builder::indexName)
+                  .description("index name")
+                  .build());
     }
   }
 
@@ -216,7 +218,7 @@
 
     logger.atFine().log(
         "Executing %d %s index queries for %s",
-        cnt, schemaDef.getName(), callerFinder.findCaller());
+        cnt, schemaDef.getName(), callerFinder.findCallerLazy());
     List<QueryResult<T>> out;
     try {
       // Parse and rewrite all queries.
diff --git a/java/com/google/gerrit/index/query/TooManyTermsInQueryException.java b/java/com/google/gerrit/index/query/TooManyTermsInQueryException.java
new file mode 100644
index 0000000..b0a394e
--- /dev/null
+++ b/java/com/google/gerrit/index/query/TooManyTermsInQueryException.java
@@ -0,0 +1,29 @@
+// 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.index.query;
+
+public class TooManyTermsInQueryException extends QueryParseException {
+  private static final long serialVersionUID = 1L;
+
+  private static final String MESSAGE = "too many terms in query";
+
+  public TooManyTermsInQueryException() {
+    super(MESSAGE);
+  }
+
+  public TooManyTermsInQueryException(Throwable why) {
+    super(MESSAGE, why);
+  }
+}
diff --git a/java/com/google/gerrit/index/query/testing/TreeSubject.java b/java/com/google/gerrit/index/query/testing/TreeSubject.java
index c60b363..7d2b868 100644
--- a/java/com/google/gerrit/index/query/testing/TreeSubject.java
+++ b/java/com/google/gerrit/index/query/testing/TreeSubject.java
@@ -23,43 +23,46 @@
 import com.google.gerrit.index.query.QueryParser;
 import org.antlr.runtime.tree.Tree;
 
-public class TreeSubject extends Subject<TreeSubject, Tree> {
+public class TreeSubject extends Subject {
   public static TreeSubject assertThat(Tree actual) {
     return assertAbout(TreeSubject::new).that(actual);
   }
 
+  private final Tree tree;
+
   private TreeSubject(FailureMetadata failureMetadata, Tree tree) {
     super(failureMetadata, tree);
+    this.tree = tree;
   }
 
   public void hasType(int expectedType) {
     isNotNull();
-    check("getType()").that(typeName(actual().getType())).isEqualTo(typeName(expectedType));
+    check("getType()").that(typeName(tree.getType())).isEqualTo(typeName(expectedType));
   }
 
   public void hasText(String expectedText) {
     requireNonNull(expectedText);
     isNotNull();
-    check("getText()").that(actual().getText()).isEqualTo(expectedText);
+    check("getText()").that(tree.getText()).isEqualTo(expectedText);
   }
 
   public void hasNoChildren() {
     isNotNull();
-    check("getChildCount()").that(actual().getChildCount()).isEqualTo(0);
+    check("getChildCount()").that(tree.getChildCount()).isEqualTo(0);
   }
 
   public void hasChildCount(int expectedChildCount) {
     checkArgument(
         expectedChildCount > 0, "expected child count must be positive: %s", expectedChildCount);
     isNotNull();
-    check("getChildCount()").that(actual().getChildCount()).isEqualTo(expectedChildCount);
+    check("getChildCount()").that(tree.getChildCount()).isEqualTo(expectedChildCount);
   }
 
   public TreeSubject child(int childIndex) {
     isNotNull();
     return check("getChild(%s)", childIndex)
         .about(TreeSubject::new)
-        .that(actual().getChild(childIndex));
+        .that(tree.getChild(childIndex));
   }
 
   private static String typeName(int type) {
diff --git a/java/com/google/gerrit/jgit/BUILD b/java/com/google/gerrit/jgit/BUILD
index e67ebfe..1041f1f 100644
--- a/java/com/google/gerrit/jgit/BUILD
+++ b/java/com/google/gerrit/jgit/BUILD
@@ -8,6 +8,6 @@
     visibility = ["//visibility:public"],
     deps = [
         "//lib:gson",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/json/BUILD b/java/com/google/gerrit/json/BUILD
index d9cec45..439f23f 100644
--- a/java/com/google/gerrit/json/BUILD
+++ b/java/com/google/gerrit/json/BUILD
@@ -6,5 +6,6 @@
     visibility = ["//visibility:public"],
     deps = [
         "//lib:gson",
+        "//lib/flogger:api",
     ],
 )
diff --git a/java/com/google/gerrit/json/EnumTypeAdapterFactory.java b/java/com/google/gerrit/json/EnumTypeAdapterFactory.java
new file mode 100644
index 0000000..21c4891
--- /dev/null
+++ b/java/com/google/gerrit/json/EnumTypeAdapterFactory.java
@@ -0,0 +1,78 @@
+// 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.json;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.internal.bind.TypeAdapters;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+
+/**
+ * A {@code TypeAdapterFactory} for enums.
+ *
+ * <p>This factory introduces a wrapper around Gson's own default enum handler to add the following
+ * special behavior: log when input which doesn't match any existing enum value is encountered.
+ */
+public class EnumTypeAdapterFactory implements TypeAdapterFactory {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @SuppressWarnings({"rawtypes", "unchecked"})
+  @Override
+  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+    TypeAdapter<T> defaultEnumAdapter = TypeAdapters.ENUM_FACTORY.create(gson, typeToken);
+    if (defaultEnumAdapter == null) {
+      // Not an enum. -> Enum type adapter doesn't apply.
+      return null;
+    }
+
+    return new EnumTypeAdapter(defaultEnumAdapter, typeToken);
+  }
+
+  private static class EnumTypeAdapter<T extends Enum<T>> extends TypeAdapter<T> {
+
+    private final TypeAdapter<T> defaultEnumAdapter;
+    private final TypeToken<T> typeToken;
+
+    public EnumTypeAdapter(TypeAdapter<T> defaultEnumAdapter, TypeToken<T> typeToken) {
+      this.defaultEnumAdapter = defaultEnumAdapter;
+      this.typeToken = typeToken;
+    }
+
+    @Override
+    public T read(JsonReader in) throws IOException {
+      // Still handle null values. -> Check them first.
+      if (in.peek() == JsonToken.NULL) {
+        in.nextNull();
+        return null;
+      }
+      T enumValue = defaultEnumAdapter.read(in);
+      if (enumValue == null) {
+        logger.atWarning().log("Expected an existing value for enum %s.", typeToken);
+      }
+      return enumValue;
+    }
+
+    @Override
+    public void write(JsonWriter out, T value) throws IOException {
+      defaultEnumAdapter.write(out, value);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/json/OutputFormat.java b/java/com/google/gerrit/json/OutputFormat.java
index a2d174f..3e7c319 100644
--- a/java/com/google/gerrit/json/OutputFormat.java
+++ b/java/com/google/gerrit/json/OutputFormat.java
@@ -55,7 +55,8 @@
     GsonBuilder gb =
         new GsonBuilder()
             .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
-            .registerTypeAdapter(Timestamp.class, new SqlTimestampDeserializer());
+            .registerTypeAdapter(Timestamp.class, new SqlTimestampDeserializer())
+            .registerTypeAdapterFactory(new EnumTypeAdapterFactory());
     if (this == OutputFormat.JSON) {
       gb.setPrettyPrinting();
     }
diff --git a/java/com/google/gerrit/json/SqlTimestampDeserializer.java b/java/com/google/gerrit/json/SqlTimestampDeserializer.java
index 0148226..e1cf382 100644
--- a/java/com/google/gerrit/json/SqlTimestampDeserializer.java
+++ b/java/com/google/gerrit/json/SqlTimestampDeserializer.java
@@ -44,7 +44,15 @@
       throw new JsonParseException("Expected string for timestamp type");
     }
 
-    return JavaSqlTimestampHelper.parseTimestamp(p.getAsString());
+    String input = p.getAsString();
+    if (input.trim().isEmpty()) {
+      // Magic timestamp to indicate no timestamp. (-> null object)
+      // Always create a new object as timestamps are mutable. Don't use TimeUtil.never() to not
+      // introduce an undesired dependency.
+      return new Timestamp(0);
+    }
+
+    return JavaSqlTimestampHelper.parseTimestamp(input);
   }
 
   @Override
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 93c0fad..a787d6f 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -64,8 +64,10 @@
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
 import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.document.IntPoint;
 import org.apache.lucene.document.LegacyIntField;
 import org.apache.lucene.document.LegacyLongField;
+import org.apache.lucene.document.LongPoint;
 import org.apache.lucene.document.StoredField;
 import org.apache.lucene.document.StringField;
 import org.apache.lucene.document.TextField;
@@ -340,15 +342,23 @@
 
     if (type == FieldType.INTEGER || type == FieldType.INTEGER_RANGE) {
       for (Object value : values.getValues()) {
-        doc.add(new LegacyIntField(name, (Integer) value, store));
+        Integer intValue = (Integer) value;
+        if (schema.useLegacyNumericFields()) {
+          doc.add(new LegacyIntField(name, intValue, store));
+        } else {
+          doc.add(new IntPoint(name, intValue));
+          if (store == Store.YES) {
+            doc.add(new StoredField(name, intValue));
+          }
+        }
       }
     } else if (type == FieldType.LONG) {
       for (Object value : values.getValues()) {
-        doc.add(new LegacyLongField(name, (Long) value, store));
+        addLongField(doc, name, store, (Long) value);
       }
     } else if (type == FieldType.TIMESTAMP) {
       for (Object value : values.getValues()) {
-        doc.add(new LegacyLongField(name, ((Timestamp) value).getTime(), store));
+        addLongField(doc, name, store, ((Timestamp) value).getTime());
       }
     } else if (type == FieldType.EXACT || type == FieldType.PREFIX) {
       for (Object value : values.getValues()) {
@@ -367,6 +377,17 @@
     }
   }
 
+  private void addLongField(Document doc, String name, Store store, Long longValue) {
+    if (schema.useLegacyNumericFields()) {
+      doc.add(new LegacyLongField(name, longValue, store));
+    } else {
+      doc.add(new LongPoint(name, longValue));
+      if (store == Store.YES) {
+        doc.add(new StoredField(name, longValue));
+      }
+    }
+  }
+
   protected FieldBundle toFieldBundle(Document doc) {
     Map<String, FieldDef<V, ?>> allFields = getSchema().getFields();
     ListMultimap<String, Object> rawFields = ArrayListMultimap.create();
diff --git a/java/com/google/gerrit/lucene/BUILD b/java/com/google/gerrit/lucene/BUILD
index 40e3157..879e706 100644
--- a/java/com/google/gerrit/lucene/BUILD
+++ b/java/com/google/gerrit/lucene/BUILD
@@ -24,23 +24,20 @@
     deps = [
         ":query_builder",
         "//java/com/google/gerrit/common:annotations",
-        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
-        "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
-        "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/proto",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/logging",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:protobuf",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/lucene:lucene-analyzers-common",
         "//lib/lucene:lucene-core-and-backward-codecs",
         "//lib/lucene:lucene-misc",
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index 8fcd0b1..7e6fba5 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.lucene;
 
 import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.gerrit.lucene.LuceneChangeIndex.ID2_SORT_FIELD;
 import static com.google.gerrit.lucene.LuceneChangeIndex.ID_SORT_FIELD;
 import static com.google.gerrit.lucene.LuceneChangeIndex.UPDATED_SORT_FIELD;
 import static com.google.gerrit.server.index.change.ChangeSchemaDefinitions.NAME;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
@@ -27,7 +29,6 @@
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.AutoFlush;
 import com.google.gerrit.server.index.change.ChangeField;
@@ -103,6 +104,9 @@
     if (f == ChangeField.LEGACY_ID) {
       int v = (Integer) getOnlyElement(values.getValues());
       doc.add(new NumericDocValuesField(ID_SORT_FIELD, v));
+    } else if (f == ChangeField.LEGACY_ID_STR) {
+      String v = (String) getOnlyElement(values.getValues());
+      doc.add(new NumericDocValuesField(ID2_SORT_FIELD, Integer.valueOf(v)));
     } else if (f == ChangeField.UPDATED) {
       long t = ((Timestamp) getOnlyElement(values.getValues())).getTime();
       doc.add(new NumericDocValuesField(UPDATED_SORT_FIELD, t));
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index 272c774..315752c 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -17,8 +17,10 @@
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.gerrit.server.index.account.AccountField.FULL_NAME;
 import static com.google.gerrit.server.index.account.AccountField.ID;
+import static com.google.gerrit.server.index.account.AccountField.ID_STR;
 import static com.google.gerrit.server.index.account.AccountField.PREFERRED_EMAIL_EXACT;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
@@ -27,7 +29,6 @@
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -61,13 +62,18 @@
   private static final String FULL_NAME_SORT_FIELD = sortFieldName(FULL_NAME);
   private static final String EMAIL_SORT_FIELD = sortFieldName(PREFERRED_EMAIL_EXACT);
   private static final String ID_SORT_FIELD = sortFieldName(ID);
+  private static final String ID2_SORT_FIELD = sortFieldName(ID_STR);
 
-  private static Term idTerm(AccountState as) {
-    return idTerm(as.getAccount().getId());
+  private static Term idTerm(boolean useLegacyNumericFields, AccountState as) {
+    return idTerm(useLegacyNumericFields, as.account().id());
   }
 
-  private static Term idTerm(Account.Id id) {
-    return QueryBuilder.intTerm(ID.getName(), id.get());
+  private static Term idTerm(boolean useLegacyNumericFields, Account.Id id) {
+    FieldDef<AccountState, ?> idField = useLegacyNumericFields ? ID : ID_STR;
+    if (useLegacyNumericFields) {
+      return QueryBuilder.intTerm(idField.getName(), id.get());
+    }
+    return QueryBuilder.stringTerm(idField.getName(), Integer.toString(id.get()));
   }
 
   private final GerritIndexWriterConfig indexWriterConfig;
@@ -113,6 +119,9 @@
     if (f == ID) {
       int v = (Integer) getOnlyElement(values.getValues());
       doc.add(new NumericDocValuesField(ID_SORT_FIELD, v));
+    } else if (f == ID_STR) {
+      String v = (String) getOnlyElement(values.getValues());
+      doc.add(new NumericDocValuesField(ID2_SORT_FIELD, Integer.valueOf(v)));
     } else if (f == FULL_NAME) {
       String value = (String) getOnlyElement(values.getValues());
       doc.add(new SortedDocValuesField(FULL_NAME_SORT_FIELD, new BytesRef(value)));
@@ -126,7 +135,7 @@
   @Override
   public void replace(AccountState as) {
     try {
-      replace(idTerm(as), toDocument(as)).get();
+      replace(idTerm(getSchema().useLegacyNumericFields(), as), toDocument(as)).get();
     } catch (ExecutionException | InterruptedException e) {
       throw new StorageException(e);
     }
@@ -135,7 +144,7 @@
   @Override
   public void delete(Account.Id key) {
     try {
-      delete(idTerm(key)).get();
+      delete(idTerm(getSchema().useLegacyNumericFields(), key)).get();
     } catch (ExecutionException | InterruptedException e) {
       throw new StorageException(e);
     }
@@ -144,20 +153,29 @@
   @Override
   public DataSource<AccountState> getSource(Predicate<AccountState> p, QueryOptions opts)
       throws QueryParseException {
+    queryBuilder.getSchema().useLegacyNumericFields();
     return new LuceneQuerySource(
-        opts.filterFields(IndexUtils::accountFields), queryBuilder.toQuery(p), getSort());
+        opts.filterFields(o -> IndexUtils.accountFields(o, getSchema().useLegacyNumericFields())),
+        queryBuilder.toQuery(p),
+        getSort());
   }
 
   private Sort getSort() {
+    String idSortField = getSchema().useLegacyNumericFields() ? ID_SORT_FIELD : ID2_SORT_FIELD;
     return new Sort(
         new SortField(FULL_NAME_SORT_FIELD, SortField.Type.STRING, false),
         new SortField(EMAIL_SORT_FIELD, SortField.Type.STRING, false),
-        new SortField(ID_SORT_FIELD, SortField.Type.LONG, false));
+        new SortField(idSortField, SortField.Type.LONG, false));
   }
 
   @Override
   protected AccountState fromDocument(Document doc) {
-    Account.Id id = new Account.Id(doc.getField(ID.getName()).numericValue().intValue());
+    FieldDef<AccountState, ?> idField = getSchema().useLegacyNumericFields() ? ID : ID_STR;
+    Account.Id id =
+        Account.id(
+            getSchema().useLegacyNumericFields()
+                ? doc.getField(idField.getName()).numericValue().intValue()
+                : Integer.valueOf(doc.getField(idField.getName()).stringValue()));
     // Use the AccountCache rather than depending on any stored fields in the document (of which
     // there shouldn't be any). The most expensive part to compute anyway is the effective group
     // IDs, and we don't have a good way to reindex when those change.
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index a9e431e..a1188da 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -18,13 +18,13 @@
 import static com.google.gerrit.lucene.AbstractLuceneIndex.sortFieldName;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
 import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
+import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID_STR;
 import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
 
-import com.google.common.base.Function;
 import com.google.common.base.Throwables;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.FluentIterable;
@@ -36,7 +36,16 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.converter.ChangeProtoConverter;
+import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
+import com.google.gerrit.entities.converter.PatchSetProtoConverter;
+import com.google.gerrit.entities.converter.ProtoConverter;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.FieldBundle;
@@ -44,14 +53,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.ResultSet;
 import com.google.gerrit.proto.Protos;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.converter.ChangeProtoConverter;
-import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
-import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
-import com.google.gerrit.reviewdb.converter.ProtoConverter;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -78,6 +79,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexableField;
@@ -107,6 +109,7 @@
 
   static final String UPDATED_SORT_FIELD = sortFieldName(ChangeField.UPDATED);
   static final String ID_SORT_FIELD = sortFieldName(ChangeField.LEGACY_ID);
+  static final String ID2_SORT_FIELD = sortFieldName(ChangeField.LEGACY_ID_STR);
 
   private static final String CHANGES = "changes";
   private static final String CHANGES_OPEN = "open";
@@ -135,12 +138,22 @@
   private static final String UNRESOLVED_COMMENT_COUNT_FIELD =
       ChangeField.UNRESOLVED_COMMENT_COUNT.getName();
 
-  static Term idTerm(ChangeData cd) {
-    return QueryBuilder.intTerm(LEGACY_ID.getName(), cd.getId().get());
+  @FunctionalInterface
+  static interface IdTerm {
+    Term get(String name, int id);
   }
 
-  static Term idTerm(Change.Id id) {
-    return QueryBuilder.intTerm(LEGACY_ID.getName(), id.get());
+  static Term idTerm(IdTerm idTerm, FieldDef<ChangeData, ?> idField, ChangeData cd) {
+    return idTerm(idTerm, idField, cd.getId());
+  }
+
+  static Term idTerm(IdTerm idTerm, FieldDef<ChangeData, ?> idField, Change.Id id) {
+    return idTerm.get(idField.getName(), id.get());
+  }
+
+  @FunctionalInterface
+  static interface ChangeIdExtractor {
+    Change.Id extract(IndexableField f);
   }
 
   private final ListeningExecutorService executor;
@@ -150,6 +163,12 @@
   private final ChangeSubIndex openIndex;
   private final ChangeSubIndex closedIndex;
 
+  // TODO(davido): Remove the below fields when support for legacy numeric fields is removed.
+  private final FieldDef<ChangeData, ?> idField;
+  private final String idSortFieldName;
+  private final IdTerm idTerm;
+  private final ChangeIdExtractor extractor;
+
   @Inject
   LuceneChangeIndex(
       @GerritServerConfig Config cfg,
@@ -202,6 +221,20 @@
               searcherFactory,
               autoFlush);
     }
+
+    idField = this.schema.useLegacyNumericFields() ? LEGACY_ID : LEGACY_ID_STR;
+    idSortFieldName = schema.useLegacyNumericFields() ? ID_SORT_FIELD : ID2_SORT_FIELD;
+    idTerm =
+        (name, id) ->
+            this.schema.useLegacyNumericFields()
+                ? QueryBuilder.intTerm(name, id)
+                : QueryBuilder.stringTerm(name, Integer.toString(id));
+    extractor =
+        (f) ->
+            Change.id(
+                this.schema.useLegacyNumericFields()
+                    ? f.numericValue().intValue()
+                    : Integer.valueOf(f.stringValue()));
   }
 
   @Override
@@ -220,7 +253,7 @@
 
   @Override
   public void replace(ChangeData cd) {
-    Term id = LuceneChangeIndex.idTerm(cd);
+    Term id = LuceneChangeIndex.idTerm(idTerm, idField, cd);
     // toDocument is essentially static and doesn't depend on the specific
     // sub-index, so just pick one.
     Document doc = openIndex.toDocument(cd);
@@ -236,10 +269,10 @@
   }
 
   @Override
-  public void delete(Change.Id id) {
-    Term idTerm = LuceneChangeIndex.idTerm(id);
+  public void delete(Change.Id changeId) {
+    Term id = LuceneChangeIndex.idTerm(idTerm, idField, changeId);
     try {
-      Futures.allAsList(openIndex.delete(idTerm), closedIndex.delete(idTerm)).get();
+      Futures.allAsList(openIndex.delete(id), closedIndex.delete(id)).get();
     } catch (ExecutionException | InterruptedException e) {
       throw new StorageException(e);
     }
@@ -275,11 +308,7 @@
   private Sort getSort() {
     return new Sort(
         new SortField(UPDATED_SORT_FIELD, SortField.Type.LONG, true),
-        new SortField(ID_SORT_FIELD, SortField.Type.LONG, true));
-  }
-
-  public ChangeSubIndex getClosedChangesIndex() {
-    return closedIndex;
+        new SortField(idSortFieldName, SortField.Type.LONG, true));
   }
 
   private class QuerySource implements ChangeDataSource {
@@ -327,7 +356,7 @@
         throw new StorageException("interrupted");
       }
 
-      final Set<String> fields = IndexUtils.changeFields(opts);
+      final Set<String> fields = IndexUtils.changeFields(opts, schema.useLegacyNumericFields());
       return new ChangeDataResults(
           executor.submit(
               new Callable<List<Document>>() {
@@ -348,7 +377,7 @@
     public ResultSet<FieldBundle> readRaw() {
       List<Document> documents;
       try {
-        documents = doRead(IndexUtils.changeFields(opts));
+        documents = doRead(IndexUtils.changeFields(opts, schema.useLegacyNumericFields()));
       } catch (IOException e) {
         throw new StorageException(e);
       }
@@ -426,9 +455,8 @@
         List<Document> docs = future.get();
         ImmutableList.Builder<ChangeData> result =
             ImmutableList.builderWithExpectedSize(docs.size());
-        String idFieldName = LEGACY_ID.getName();
         for (Document doc : docs) {
-          result.add(toChangeData(fields(doc, fields), fields, idFieldName));
+          result.add(toChangeData(fields(doc, fields), fields, idField.getName()));
         }
         return result.build();
       } catch (InterruptedException e) {
@@ -469,10 +497,10 @@
       cd = changeDataFactory.create(parseProtoFrom(proto, ChangeProtoConverter.INSTANCE));
     } else {
       IndexableField f = Iterables.getFirst(doc.get(idFieldName), null);
-      Change.Id id = new Change.Id(f.numericValue().intValue());
+
       // IndexUtils#changeFields ensures either CHANGE or PROJECT is always present.
       IndexableField project = doc.get(PROJECT.getName()).iterator().next();
-      cd = changeDataFactory.create(new Project.NameKey(project.stringValue()), id);
+      cd = changeDataFactory.create(Project.nameKey(project.stringValue()), extractor.extract(f));
     }
 
     // Any decoding that is done here must also be done in {@link ElasticChangeIndex}.
@@ -575,7 +603,7 @@
         if (reviewedBy.size() == 1 && id == ChangeField.NOT_REVIEWED) {
           break;
         }
-        accounts.add(new Account.Id(id));
+        accounts.add(Account.id(id));
       }
       cd.setReviewedBy(accounts);
     }
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index 47a5038..58323f0 100644
--- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.gerrit.server.index.group.GroupField.UUID;
 
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
@@ -25,7 +26,6 @@
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -142,7 +142,7 @@
 
   @Override
   protected InternalGroup fromDocument(Document doc) {
-    AccountGroup.UUID uuid = new AccountGroup.UUID(doc.getField(UUID.getName()).stringValue());
+    AccountGroup.UUID uuid = AccountGroup.uuid(doc.getField(UUID.getName()).stringValue());
     // Use the GroupCache rather than depending on any stored fields in the
     // document (of which there shouldn't be any).
     return groupCache.get().get(uuid).orElse(null);
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 601ed5f..837ac0f 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.gerrit.index.project.ProjectField.NAME;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
@@ -27,7 +28,6 @@
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.AutoFlush;
@@ -142,7 +142,7 @@
 
   @Override
   protected ProjectData fromDocument(Document doc) {
-    Project.NameKey nameKey = new Project.NameKey(doc.getField(NAME.getName()).stringValue());
+    Project.NameKey nameKey = Project.nameKey(doc.getField(NAME.getName()).stringValue());
     ProjectState projectState = projectCache.get().get(nameKey);
     return projectState == null ? null : projectState.toProjectData();
   }
diff --git a/java/com/google/gerrit/lucene/QueryBuilder.java b/java/com/google/gerrit/lucene/QueryBuilder.java
index 8dfc12e..7d82bf5 100644
--- a/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -36,6 +36,8 @@
 import java.util.Date;
 import java.util.List;
 import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.document.IntPoint;
+import org.apache.lucene.document.LongPoint;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.LegacyNumericRangeQuery;
@@ -49,6 +51,21 @@
 
 @SuppressWarnings("deprecation")
 public class QueryBuilder<V> {
+  @FunctionalInterface
+  static interface IntTermQuery {
+    Query get(String name, int value);
+  }
+
+  @FunctionalInterface
+  static interface IntRangeQuery {
+    Query get(String name, int min, int max);
+  }
+
+  @FunctionalInterface
+  static interface LongRangeQuery {
+    Query get(String name, long min, long max);
+  }
+
   static Term intTerm(String name, int value) {
     BytesRefBuilder builder = new BytesRefBuilder();
     LegacyNumericUtils.intToPrefixCoded(value, 0, builder);
@@ -61,12 +78,36 @@
     return new Term(name, builder.get());
   }
 
+  static Query intPoint(String name, int value) {
+    return IntPoint.newExactQuery(name, value);
+  }
+
   private final Schema<V> schema;
   private final org.apache.lucene.util.QueryBuilder queryBuilder;
 
+  // TODO(davido): Remove the below fields when support for legacy numeric fields is removed.
+  private final IntTermQuery intTermQuery;
+  private final IntRangeQuery intRangeTermQuery;
+  private final LongRangeQuery longRangeQuery;
+
   public QueryBuilder(Schema<V> schema, Analyzer analyzer) {
     this.schema = schema;
     queryBuilder = new org.apache.lucene.util.QueryBuilder(analyzer);
+    intTermQuery =
+        (name, value) ->
+            this.schema.useLegacyNumericFields()
+                ? new TermQuery(intTerm(name, value))
+                : intPoint(name, value);
+    intRangeTermQuery =
+        (name, min, max) ->
+            this.schema.useLegacyNumericFields()
+                ? LegacyNumericRangeQuery.newIntRange(name, min, max, true, true)
+                : IntPoint.newRangeQuery(name, min, max);
+    longRangeQuery =
+        (name, min, max) ->
+            this.schema.useLegacyNumericFields()
+                ? LegacyNumericRangeQuery.newLongRange(name, min, max, true, true)
+                : LongPoint.newRangeQuery(name, min, max);
   }
 
   public Query toQuery(Predicate<V> p) throws QueryParseException {
@@ -169,20 +210,20 @@
     } catch (NumberFormatException e) {
       throw new QueryParseException("not an integer: " + p.getValue(), e);
     }
-    return new TermQuery(intTerm(p.getField().getName(), value));
+    return intTermQuery.get(p.getField().getName(), value);
   }
 
   private Query intRangeQuery(IndexPredicate<V> p) throws QueryParseException {
     if (p instanceof IntegerRangePredicate) {
       IntegerRangePredicate<V> r = (IntegerRangePredicate<V>) p;
+      String name = r.getField().getName();
       int minimum = r.getMinimumValue();
       int maximum = r.getMaximumValue();
       if (minimum == maximum) {
         // Just fall back to a standard integer query.
-        return new TermQuery(intTerm(p.getField().getName(), minimum));
+        return intTermQuery.get(name, minimum);
       }
-      return LegacyNumericRangeQuery.newIntRange(
-          r.getField().getName(), minimum, maximum, true, true);
+      return intRangeTermQuery.get(name, minimum, maximum);
     }
     throw new QueryParseException("not an integer range: " + p);
   }
@@ -190,20 +231,16 @@
   private Query timestampQuery(IndexPredicate<V> p) throws QueryParseException {
     if (p instanceof TimestampRangePredicate) {
       TimestampRangePredicate<V> r = (TimestampRangePredicate<V>) p;
-      return LegacyNumericRangeQuery.newLongRange(
-          r.getField().getName(),
-          r.getMinTimestamp().getTime(),
-          r.getMaxTimestamp().getTime(),
-          true,
-          true);
+      return longRangeQuery.get(
+          r.getField().getName(), r.getMinTimestamp().getTime(), r.getMaxTimestamp().getTime());
     }
     throw new QueryParseException("not a timestamp: " + p);
   }
 
   private Query notTimestamp(TimestampRangePredicate<V> r) throws QueryParseException {
     if (r.getMinTimestamp().getTime() == 0) {
-      return LegacyNumericRangeQuery.newLongRange(
-          r.getField().getName(), r.getMaxTimestamp().getTime(), null, true, true);
+      return longRangeQuery.get(
+          r.getField().getName(), r.getMaxTimestamp().getTime(), Long.MAX_VALUE);
     }
     throw new QueryParseException("cannot negate: " + r);
   }
@@ -245,4 +282,8 @@
   public int toIndexTimeInMinutes(Date ts) {
     return (int) (ts.getTime() / 60000);
   }
+
+  public Schema<V> getSchema() {
+    return schema;
+  }
 }
diff --git a/java/com/google/gerrit/lucene/WrappableSearcherManager.java b/java/com/google/gerrit/lucene/WrappableSearcherManager.java
index ba8d7da..4044b90 100644
--- a/java/com/google/gerrit/lucene/WrappableSearcherManager.java
+++ b/java/com/google/gerrit/lucene/WrappableSearcherManager.java
@@ -177,7 +177,7 @@
    * Expert: creates a searcher from the provided {@link IndexReader} using the provided {@link
    * SearcherFactory}. NOTE: this decRefs incoming reader on throwing an exception.
    */
-  @SuppressWarnings("resource")
+  @SuppressWarnings({"resource", "ReferenceEquality"})
   public static IndexSearcher getSearcher(SearcherFactory searcherFactory, IndexReader reader)
       throws IOException {
     boolean success = false;
diff --git a/java/com/google/gerrit/mail/BUILD b/java/com/google/gerrit/mail/BUILD
index 6be5f0e..46bc77a 100644
--- a/java/com/google/gerrit/mail/BUILD
+++ b/java/com/google/gerrit/mail/BUILD
@@ -6,7 +6,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//lib:guava",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/java/com/google/gerrit/mail/HtmlParser.java b/java/com/google/gerrit/mail/HtmlParser.java
index 265c412..7905a0a 100644
--- a/java/com/google/gerrit/mail/HtmlParser.java
+++ b/java/com/google/gerrit/mail/HtmlParser.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.PeekingIterator;
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.entities.Comment;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
diff --git a/java/com/google/gerrit/mail/MailComment.java b/java/com/google/gerrit/mail/MailComment.java
index fd8198c..f024f17 100644
--- a/java/com/google/gerrit/mail/MailComment.java
+++ b/java/com/google/gerrit/mail/MailComment.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.mail;
 
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.entities.Comment;
 import java.util.Objects;
 
 /** A comment parsed from inbound email */
diff --git a/java/com/google/gerrit/mail/ParserUtil.java b/java/com/google/gerrit/mail/ParserUtil.java
index 6a27ac4..4b292f3 100644
--- a/java/com/google/gerrit/mail/ParserUtil.java
+++ b/java/com/google/gerrit/mail/ParserUtil.java
@@ -16,7 +16,7 @@
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.Iterables;
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.entities.Comment;
 import java.util.List;
 import java.util.StringJoiner;
 import java.util.regex.Pattern;
diff --git a/java/com/google/gerrit/mail/TextParser.java b/java/com/google/gerrit/mail/TextParser.java
index 1a63599..dac3deb 100644
--- a/java/com/google/gerrit/mail/TextParser.java
+++ b/java/com/google/gerrit/mail/TextParser.java
@@ -18,7 +18,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.PeekingIterator;
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.entities.Comment;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
diff --git a/java/com/google/gerrit/metrics/BUILD b/java/com/google/gerrit/metrics/BUILD
index 786ef68..3cc056b 100644
--- a/java/com/google/gerrit/metrics/BUILD
+++ b/java/com/google/gerrit/metrics/BUILD
@@ -8,9 +8,12 @@
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/server/logging",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
         "//lib/flogger:api",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/metrics/DisabledMetricMaker.java b/java/com/google/gerrit/metrics/DisabledMetricMaker.java
index adf6a66..1fb8c57 100644
--- a/java/com/google/gerrit/metrics/DisabledMetricMaker.java
+++ b/java/com/google/gerrit/metrics/DisabledMetricMaker.java
@@ -79,7 +79,7 @@
 
   @Override
   public <F1> Timer1<F1> newTimer(String name, Description desc, Field<F1> field1) {
-    return new Timer1<F1>(name) {
+    return new Timer1<F1>(name, field1) {
       @Override
       protected void doRecord(F1 field1, long value, TimeUnit unit) {}
 
@@ -91,7 +91,7 @@
   @Override
   public <F1, F2> Timer2<F1, F2> newTimer(
       String name, Description desc, Field<F1> field1, Field<F2> field2) {
-    return new Timer2<F1, F2>(name) {
+    return new Timer2<F1, F2>(name, field1, field2) {
       @Override
       protected void doRecord(F1 field1, F2 field2, long value, TimeUnit unit) {}
 
@@ -103,7 +103,7 @@
   @Override
   public <F1, F2, F3> Timer3<F1, F2, F3> newTimer(
       String name, Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
-    return new Timer3<F1, F2, F3>(name) {
+    return new Timer3<F1, F2, F3>(name, field1, field2, field3) {
       @Override
       protected void doRecord(F1 field1, F2 field2, F3 field3, long value, TimeUnit unit) {}
 
diff --git a/java/com/google/gerrit/metrics/Field.java b/java/com/google/gerrit/metrics/Field.java
index 95eb9cf..bdae854 100644
--- a/java/com/google/gerrit/metrics/Field.java
+++ b/java/com/google/gerrit/metrics/Field.java
@@ -16,34 +16,36 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
-import com.google.common.base.Function;
-import com.google.common.base.Functions;
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.server.logging.Metadata;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
 
 /**
  * Describes a bucketing field used by a metric.
  *
  * @param <T> type of field
  */
-public class Field<T> {
-  /**
-   * Break down metrics by boolean true/false.
-   *
-   * @param name field name
-   * @return boolean field
-   */
-  public static Field<Boolean> ofBoolean(String name) {
-    return ofBoolean(name, null);
+@AutoValue
+public abstract class Field<T> {
+  public static <T> BiConsumer<Metadata.Builder, T> ignoreMetadata() {
+    return (metadataBuilder, fieldValue) -> {};
   }
 
   /**
    * Break down metrics by boolean true/false.
    *
    * @param name field name
-   * @param description field description
-   * @return boolean field
+   * @return builder for the boolean field
    */
-  public static Field<Boolean> ofBoolean(String name, String description) {
-    return new Field<>(name, Boolean.class, description);
+  public static Field.Builder<Boolean> ofBoolean(
+      String name, BiConsumer<Metadata.Builder, Boolean> metadataMapper) {
+    return new AutoValue_Field.Builder<Boolean>()
+        .valueType(Boolean.class)
+        .formatter(Object::toString)
+        .name(name)
+        .metadataMapper(metadataMapper);
   }
 
   /**
@@ -51,50 +53,17 @@
    *
    * @param enumType type of enum
    * @param name field name
-   * @return enum field
+   * @return builder for the enum field
    */
-  public static <E extends Enum<E>> Field<E> ofEnum(Class<E> enumType, String name) {
-    return ofEnum(enumType, name, null);
-  }
-
-  /**
-   * Break down metrics by cases of an enum.
-   *
-   * @param enumType type of enum
-   * @param name field name
-   * @param description field description
-   * @return enum field
-   */
-  public static <E extends Enum<E>> Field<E> ofEnum(
-      Class<E> enumType, String name, String description) {
-    return new Field<>(name, enumType, description);
-  }
-
-  /**
-   * Break down metrics by string.
-   *
-   * <p>Each unique string will allocate a new submetric. <b>Do not use user content as a field
-   * value</b> as field values are never reclaimed.
-   *
-   * @param name field name
-   * @return string field
-   */
-  public static Field<String> ofString(String name) {
-    return ofString(name, null);
-  }
-
-  /**
-   * Break down metrics by string.
-   *
-   * <p>Each unique string will allocate a new submetric. <b>Do not use user content as a field
-   * value</b> as field values are never reclaimed.
-   *
-   * @param name field name
-   * @param description field description
-   * @return string field
-   */
-  public static Field<String> ofString(String name, String description) {
-    return new Field<>(name, String.class, description);
+  public static <E extends Enum<E>> Field.Builder<E> ofEnum(
+      Class<E> enumType, String name, BiConsumer<Metadata.Builder, String> metadataMapper) {
+    return new AutoValue_Field.Builder<E>()
+        .valueType(enumType)
+        .formatter(Enum::name)
+        .name(name)
+        .metadataMapper(
+            (metadataBuilder, fieldValue) ->
+                metadataMapper.accept(metadataBuilder, fieldValue.name()));
   }
 
   /**
@@ -104,67 +73,68 @@
    * value</b> as field values are never reclaimed.
    *
    * @param name field name
-   * @return integer field
+   * @return builder for the integer field
    */
-  public static Field<Integer> ofInteger(String name) {
-    return ofInteger(name, null);
+  public static Field.Builder<Integer> ofInteger(
+      String name, BiConsumer<Metadata.Builder, Integer> metadataMapper) {
+    return new AutoValue_Field.Builder<Integer>()
+        .valueType(Integer.class)
+        .formatter(Object::toString)
+        .name(name)
+        .metadataMapper(metadataMapper);
   }
 
   /**
-   * Break down metrics by integer.
+   * Break down metrics by string.
    *
-   * <p>Each unique integer will allocate a new submetric. <b>Do not use user content as a field
+   * <p>Each unique string will allocate a new submetric. <b>Do not use user content as a field
    * value</b> as field values are never reclaimed.
    *
    * @param name field name
-   * @param description field description
-   * @return integer field
+   * @return builder for the string field
    */
-  public static Field<Integer> ofInteger(String name, String description) {
-    return new Field<>(name, Integer.class, description);
-  }
-
-  private final String name;
-  private final Class<T> keyType;
-  private final Function<T, String> formatter;
-  private final String description;
-
-  private Field(String name, Class<T> keyType, String description) {
-    checkArgument(name.matches("^[a-z_]+$"), "name must match [a-z_]");
-    this.name = name;
-    this.keyType = keyType;
-    this.formatter = initFormatter(keyType);
-    this.description = description;
+  public static Field.Builder<String> ofString(
+      String name, BiConsumer<Metadata.Builder, String> metadataMapper) {
+    return new AutoValue_Field.Builder<String>()
+        .valueType(String.class)
+        .formatter(s -> s)
+        .name(name)
+        .metadataMapper(metadataMapper);
   }
 
   /** @return name of this field within the metric. */
-  public String getName() {
-    return name;
-  }
+  public abstract String name();
 
   /** @return type of value used within the field. */
-  public Class<T> getType() {
-    return keyType;
-  }
+  public abstract Class<T> valueType();
+
+  /** @return mapper that maps a field value to a field in the {@link Metadata} class. */
+  public abstract BiConsumer<Metadata.Builder, T> metadataMapper();
 
   /** @return description text for the field explaining its range of values. */
-  public String getDescription() {
-    return description;
-  }
+  public abstract Optional<String> description();
 
-  public Function<T, String> formatter() {
-    return formatter;
-  }
+  /** @return formatter to format field values. */
+  public abstract Function<T, String> formatter();
 
-  @SuppressWarnings("unchecked")
-  private static <T> Function<T, String> initFormatter(Class<T> keyType) {
-    if (keyType == String.class) {
-      return (Function<T, String>) Functions.<String>identity();
-    } else if (keyType == Integer.class || keyType == Boolean.class) {
-      return (Function<T, String>) Functions.toStringFunction();
-    } else if (Enum.class.isAssignableFrom(keyType)) {
-      return in -> ((Enum<?>) in).name();
+  @AutoValue.Builder
+  public abstract static class Builder<T> {
+    abstract Builder<T> name(String name);
+
+    abstract Builder<T> valueType(Class<T> type);
+
+    abstract Builder<T> formatter(Function<T, String> formatter);
+
+    abstract Builder<T> metadataMapper(BiConsumer<Metadata.Builder, T> metadataMapper);
+
+    public abstract Builder<T> description(String description);
+
+    abstract Field<T> autoBuild();
+
+    public Field<T> build() {
+      Field<T> field = autoBuild();
+      checkArgument(field.name().matches("^[a-z_]+$"), "name must match [a-z_]");
+      return field;
     }
-    throw new IllegalStateException("unsupported type " + keyType.getName());
   }
 }
diff --git a/java/com/google/gerrit/metrics/Timer0.java b/java/com/google/gerrit/metrics/Timer0.java
index 2134488..d0033a4 100644
--- a/java/com/google/gerrit/metrics/Timer0.java
+++ b/java/com/google/gerrit/metrics/Timer0.java
@@ -18,6 +18,8 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.PerformanceLogRecord;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -68,7 +70,10 @@
    * @param unit time unit of the value
    */
   public final void record(long value, TimeUnit unit) {
-    logger.atFinest().log("%s took %dms", name, unit.toMillis(value));
+    long durationMs = unit.toMillis(value);
+    LoggingContext.getInstance()
+        .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationMs));
+    logger.atFinest().log("%s took %dms", name, durationMs);
     doRecord(value, unit);
   }
 
diff --git a/java/com/google/gerrit/metrics/Timer1.java b/java/com/google/gerrit/metrics/Timer1.java
index 16c151e..a8fb1a2 100644
--- a/java/com/google/gerrit/metrics/Timer1.java
+++ b/java/com/google/gerrit/metrics/Timer1.java
@@ -18,6 +18,9 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.PerformanceLogRecord;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -35,56 +38,66 @@
 public abstract class Timer1<F1> implements RegistrationHandle {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  public static class Context extends TimerContext {
-    private final Timer1<Object> timer;
-    private final Object field1;
+  public static class Context<F1> extends TimerContext {
+    private final Timer1<F1> timer;
+    private final F1 fieldValue;
 
-    @SuppressWarnings("unchecked")
-    <F1> Context(Timer1<F1> timer, F1 field1) {
-      this.timer = (Timer1<Object>) timer;
-      this.field1 = field1;
+    Context(Timer1<F1> timer, F1 fieldValue) {
+      this.timer = timer;
+      this.fieldValue = fieldValue;
     }
 
     @Override
     public void record(long elapsed) {
-      timer.record(field1, elapsed, NANOSECONDS);
+      timer.record(fieldValue, elapsed, NANOSECONDS);
     }
   }
 
   protected final String name;
+  protected final Field<F1> field;
 
-  public Timer1(String name) {
+  public Timer1(String name, Field<F1> field) {
     this.name = name;
+    this.field = field;
   }
 
   /**
    * Begin a timer for the current block, value will be recorded when closed.
    *
-   * @param field1 bucket to record the timer
+   * @param fieldValue bucket to record the timer
    * @return timer context
    */
-  public Context start(F1 field1) {
-    return new Context(this, field1);
+  public Context<F1> start(F1 fieldValue) {
+    return new Context<>(this, fieldValue);
   }
 
   /**
    * Record a value in the distribution.
    *
-   * @param field1 bucket to record the timer
+   * @param fieldValue bucket to record the timer
    * @param value value to record
    * @param unit time unit of the value
    */
-  public final void record(F1 field1, long value, TimeUnit unit) {
-    logger.atFinest().log("%s (%s) took %dms", name, field1, unit.toMillis(value));
-    doRecord(field1, value, unit);
+  public final void record(F1 fieldValue, long value, TimeUnit unit) {
+    long durationMs = unit.toMillis(value);
+
+    Metadata.Builder metadataBuilder = Metadata.builder();
+    field.metadataMapper().accept(metadataBuilder, fieldValue);
+    Metadata metadata = metadataBuilder.build();
+
+    LoggingContext.getInstance()
+        .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationMs, metadata));
+
+    logger.atFinest().log("%s (%s = %s) took %dms", name, field.name(), fieldValue, durationMs);
+    doRecord(fieldValue, value, unit);
   }
 
   /**
    * Record a value in the distribution.
    *
-   * @param field1 bucket to record the timer
+   * @param fieldValue bucket to record the timer
    * @param value value to record
    * @param unit time unit of the value
    */
-  protected abstract void doRecord(F1 field1, long value, TimeUnit unit);
+  protected abstract void doRecord(F1 fieldValue, long value, TimeUnit unit);
 }
diff --git a/java/com/google/gerrit/metrics/Timer2.java b/java/com/google/gerrit/metrics/Timer2.java
index bf19448..8a4a793 100644
--- a/java/com/google/gerrit/metrics/Timer2.java
+++ b/java/com/google/gerrit/metrics/Timer2.java
@@ -18,6 +18,9 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.PerformanceLogRecord;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -36,61 +39,76 @@
 public abstract class Timer2<F1, F2> implements RegistrationHandle {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  public static class Context extends TimerContext {
-    private final Timer2<Object, Object> timer;
-    private final Object field1;
-    private final Object field2;
+  public static class Context<F1, F2> extends TimerContext {
+    private final Timer2<F1, F2> timer;
+    private final F1 fieldValue1;
+    private final F2 fieldValue2;
 
-    @SuppressWarnings("unchecked")
-    <F1, F2> Context(Timer2<F1, F2> timer, F1 field1, F2 field2) {
-      this.timer = (Timer2<Object, Object>) timer;
-      this.field1 = field1;
-      this.field2 = field2;
+    Context(Timer2<F1, F2> timer, F1 fieldValue1, F2 fieldValue2) {
+      this.timer = timer;
+      this.fieldValue1 = fieldValue1;
+      this.fieldValue2 = fieldValue2;
     }
 
     @Override
     public void record(long elapsed) {
-      timer.record(field1, field2, elapsed, NANOSECONDS);
+      timer.record(fieldValue1, fieldValue2, elapsed, NANOSECONDS);
     }
   }
 
   protected final String name;
+  protected final Field<F1> field1;
+  protected final Field<F2> field2;
 
-  public Timer2(String name) {
+  public Timer2(String name, Field<F1> field1, Field<F2> field2) {
     this.name = name;
+    this.field1 = field1;
+    this.field2 = field2;
   }
 
   /**
    * Begin a timer for the current block, value will be recorded when closed.
    *
-   * @param field1 bucket to record the timer
-   * @param field2 bucket to record the timer
+   * @param fieldValue1 bucket to record the timer
+   * @param fieldValue2 bucket to record the timer
    * @return timer context
    */
-  public Context start(F1 field1, F2 field2) {
-    return new Context(this, field1, field2);
+  public Context<F1, F2> start(F1 fieldValue1, F2 fieldValue2) {
+    return new Context<>(this, fieldValue1, fieldValue2);
   }
 
   /**
    * Record a value in the distribution.
    *
-   * @param field1 bucket to record the timer
-   * @param field2 bucket to record the timer
+   * @param fieldValue1 bucket to record the timer
+   * @param fieldValue2 bucket to record the timer
    * @param value value to record
    * @param unit time unit of the value
    */
-  public final void record(F1 field1, F2 field2, long value, TimeUnit unit) {
-    logger.atFinest().log("%s (%s, %s) took %dms", name, field1, field2, unit.toMillis(value));
-    doRecord(field1, field2, value, unit);
+  public final void record(F1 fieldValue1, F2 fieldValue2, long value, TimeUnit unit) {
+    long durationMs = unit.toMillis(value);
+
+    Metadata.Builder metadataBuilder = Metadata.builder();
+    field1.metadataMapper().accept(metadataBuilder, fieldValue1);
+    field2.metadataMapper().accept(metadataBuilder, fieldValue2);
+    Metadata metadata = metadataBuilder.build();
+
+    LoggingContext.getInstance()
+        .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationMs, metadata));
+
+    logger.atFinest().log(
+        "%s (%s = %s, %s = %s) took %dms",
+        name, field1.name(), fieldValue1, field2.name(), fieldValue2, durationMs);
+    doRecord(fieldValue1, fieldValue2, value, unit);
   }
 
   /**
    * Record a value in the distribution.
    *
-   * @param field1 bucket to record the timer
-   * @param field2 bucket to record the timer
+   * @param fieldValue1 bucket to record the timer
+   * @param fieldValue2 bucket to record the timer
    * @param value value to record
    * @param unit time unit of the value
    */
-  protected abstract void doRecord(F1 field1, F2 field2, long value, TimeUnit unit);
+  protected abstract void doRecord(F1 fieldValue1, F2 fieldValue2, long value, TimeUnit unit);
 }
diff --git a/java/com/google/gerrit/metrics/Timer3.java b/java/com/google/gerrit/metrics/Timer3.java
index c910eb0..2044da6 100644
--- a/java/com/google/gerrit/metrics/Timer3.java
+++ b/java/com/google/gerrit/metrics/Timer3.java
@@ -18,6 +18,9 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.PerformanceLogRecord;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -37,67 +40,93 @@
 public abstract class Timer3<F1, F2, F3> implements RegistrationHandle {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  public static class Context extends TimerContext {
-    private final Timer3<Object, Object, Object> timer;
-    private final Object field1;
-    private final Object field2;
-    private final Object field3;
+  public static class Context<F1, F2, F3> extends TimerContext {
+    private final Timer3<F1, F2, F3> timer;
+    private final F1 fieldValue1;
+    private final F2 fieldValue2;
+    private final F3 fieldValue3;
 
-    @SuppressWarnings("unchecked")
-    <F1, F2, F3> Context(Timer3<F1, F2, F3> timer, F1 f1, F2 f2, F3 f3) {
-      this.timer = (Timer3<Object, Object, Object>) timer;
-      this.field1 = f1;
-      this.field2 = f2;
-      this.field3 = f3;
+    Context(Timer3<F1, F2, F3> timer, F1 f1, F2 f2, F3 f3) {
+      this.timer = timer;
+      this.fieldValue1 = f1;
+      this.fieldValue2 = f2;
+      this.fieldValue3 = f3;
     }
 
     @Override
     public void record(long elapsed) {
-      timer.record(field1, field2, field3, elapsed, NANOSECONDS);
+      timer.record(fieldValue1, fieldValue2, fieldValue3, elapsed, NANOSECONDS);
     }
   }
 
   protected final String name;
+  protected final Field<F1> field1;
+  protected final Field<F2> field2;
+  protected final Field<F3> field3;
 
-  public Timer3(String name) {
+  public Timer3(String name, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
     this.name = name;
+    this.field1 = field1;
+    this.field2 = field2;
+    this.field3 = field3;
   }
 
   /**
    * Begin a timer for the current block, value will be recorded when closed.
    *
-   * @param field1 bucket to record the timer
-   * @param field2 bucket to record the timer
-   * @param field3 bucket to record the timer
+   * @param fieldValue1 bucket to record the timer
+   * @param fieldValue2 bucket to record the timer
+   * @param fieldValue3 bucket to record the timer
    * @return timer context
    */
-  public Context start(F1 field1, F2 field2, F3 field3) {
-    return new Context(this, field1, field2, field3);
+  public Context<F1, F2, F3> start(F1 fieldValue1, F2 fieldValue2, F3 fieldValue3) {
+    return new Context<>(this, fieldValue1, fieldValue2, fieldValue3);
   }
 
   /**
    * Record a value in the distribution.
    *
-   * @param field1 bucket to record the timer
-   * @param field2 bucket to record the timer
-   * @param field3 bucket to record the timer
+   * @param fieldValue1 bucket to record the timer
+   * @param fieldValue2 bucket to record the timer
+   * @param fieldValue3 bucket to record the timer
    * @param value value to record
    * @param unit time unit of the value
    */
-  public final void record(F1 field1, F2 field2, F3 field3, long value, TimeUnit unit) {
+  public final void record(
+      F1 fieldValue1, F2 fieldValue2, F3 fieldValue3, long value, TimeUnit unit) {
+    long durationMs = unit.toMillis(value);
+
+    Metadata.Builder metadataBuilder = Metadata.builder();
+    field1.metadataMapper().accept(metadataBuilder, fieldValue1);
+    field2.metadataMapper().accept(metadataBuilder, fieldValue2);
+    field3.metadataMapper().accept(metadataBuilder, fieldValue3);
+    Metadata metadata = metadataBuilder.build();
+
+    LoggingContext.getInstance()
+        .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationMs, metadata));
+
     logger.atFinest().log(
-        "%s (%s, %s, %s) took %dms", name, field1, field2, field3, unit.toMillis(value));
-    doRecord(field1, field2, field3, value, unit);
+        "%s (%s = %s, %s = %s, %s = %s) took %dms",
+        name,
+        field1.name(),
+        fieldValue1,
+        field2.name(),
+        fieldValue2,
+        field3.name(),
+        fieldValue3,
+        durationMs);
+    doRecord(fieldValue1, fieldValue2, fieldValue3, value, unit);
   }
 
   /**
    * Record a value in the distribution.
    *
-   * @param field1 bucket to record the timer
-   * @param field2 bucket to record the timer
-   * @param field3 bucket to record the timer
+   * @param fieldValue1 bucket to record the timer
+   * @param fieldValue2 bucket to record the timer
+   * @param fieldValue3 bucket to record the timer
    * @param value value to record
    * @param unit time unit of the value
    */
-  protected abstract void doRecord(F1 field1, F2 field2, F3 field3, long value, TimeUnit unit);
+  protected abstract void doRecord(
+      F1 fieldValue1, F2 fieldValue2, F3 fieldValue3, long value, TimeUnit unit);
 }
diff --git a/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl1.java b/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl1.java
index 6d1daf4..d718035 100644
--- a/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl1.java
+++ b/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl1.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.metrics.dropwizard;
 
 import com.codahale.metrics.MetricRegistry;
-import com.google.common.base.Function;
 import com.google.gerrit.metrics.CallbackMetric1;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
+import java.util.function.Function;
 
 /** Optimized version of {@link BucketedCallback} for single dimension. */
 class CallbackMetricImpl1<F1, V> extends BucketedCallback<V> {
diff --git a/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java b/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java
index 46434ce..0e554a8 100644
--- a/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java
+++ b/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.metrics.dropwizard;
 
-import com.google.common.base.Function;
 import com.google.gerrit.metrics.Counter1;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
+import java.util.function.Function;
 
 /** Optimized version of {@link BucketedCounter} for single dimension. */
 class CounterImpl1<F1> extends BucketedCounter {
diff --git a/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java b/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java
index 38c31a1..07afc2a 100644
--- a/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java
+++ b/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.metrics.dropwizard;
 
-import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.metrics.Counter2;
 import com.google.gerrit.metrics.Counter3;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
+import java.util.function.Function;
 
 /** Generalized implementation of N-dimensional counter metrics. */
 class CounterImplN extends BucketedCounter implements BucketedMetric {
diff --git a/java/com/google/gerrit/metrics/dropwizard/GetMetric.java b/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
index ae1e6ec..12dabfa 100644
--- a/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
+++ b/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.metrics.dropwizard;
 
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -36,10 +37,10 @@
   }
 
   @Override
-  public MetricJson apply(MetricResource resource)
+  public Response<MetricJson> apply(MetricResource resource)
       throws AuthException, PermissionBackendException {
     permissionBackend.currentUser().check(GlobalPermission.VIEW_CACHES);
-    return new MetricJson(
-        resource.getMetric(), metrics.getAnnotations(resource.getName()), dataOnly);
+    return Response.ok(
+        new MetricJson(resource.getMetric(), metrics.getAnnotations(resource.getName()), dataOnly));
   }
 }
diff --git a/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java b/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java
index 3eb12fa..4578db1 100644
--- a/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java
+++ b/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.metrics.dropwizard;
 
-import com.google.common.base.Function;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.Histogram1;
+import java.util.function.Function;
 
 /** Optimized version of {@link BucketedHistogram} for single dimension. */
 class HistogramImpl1<F1> extends BucketedHistogram implements BucketedMetric {
diff --git a/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java b/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java
index 3561c55a..446590c 100644
--- a/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java
+++ b/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.metrics.dropwizard;
 
-import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.Histogram2;
 import com.google.gerrit.metrics.Histogram3;
+import java.util.function.Function;
 
 /** Generalized implementation of N-dimensional Histogram metrics. */
 class HistogramImplN extends BucketedHistogram implements BucketedMetric {
diff --git a/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java b/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
index 0c69452..7e472c9 100644
--- a/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
+++ b/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
@@ -16,6 +16,7 @@
 
 import com.codahale.metrics.Metric;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -50,7 +51,7 @@
   }
 
   @Override
-  public Map<String, MetricJson> apply(ConfigResource resource)
+  public Response<Map<String, MetricJson>> apply(ConfigResource resource)
       throws AuthException, PermissionBackendException {
     permissionBackend.currentUser().check(GlobalPermission.VIEW_CACHES);
 
@@ -75,7 +76,7 @@
       }
     }
 
-    return out;
+    return Response.ok(out);
   }
 
   private MetricJson toJson(String q, Metric m) {
diff --git a/java/com/google/gerrit/metrics/dropwizard/MetricJson.java b/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
index 20f4fa3..d59a1d9 100644
--- a/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
+++ b/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
@@ -21,7 +21,6 @@
 import com.codahale.metrics.Metric;
 import com.codahale.metrics.Snapshot;
 import com.codahale.metrics.Timer;
-import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.metrics.Description;
@@ -30,6 +29,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.function.Function;
 
 class MetricJson {
   String description;
@@ -189,10 +189,10 @@
     String description;
 
     FieldJson(Field<?> field) {
-      this.name = field.getName();
-      this.description = field.getDescription();
+      this.name = field.name();
+      this.description = field.description().orElse(null);
       this.type =
-          Enum.class.isAssignableFrom(field.getType()) ? field.getType().getSimpleName() : null;
+          Enum.class.isAssignableFrom(field.valueType()) ? field.valueType().getSimpleName() : null;
     }
   }
 }
diff --git a/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java b/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
index d97e73a..b7d535b 100644
--- a/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
+++ b/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.metrics.dropwizard;
 
-import com.google.common.base.Function;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.Timer1;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 
 /** Optimized version of {@link BucketedTimer} for single dimension. */
 class TimerImpl1<F1> extends BucketedTimer implements BucketedMetric {
@@ -26,8 +26,9 @@
     super(metrics, name, desc, field1);
   }
 
+  @SuppressWarnings("unchecked")
   Timer1<F1> timer() {
-    return new Timer1<F1>(name) {
+    return new Timer1<F1>(name, (Field<F1>) fields[0]) {
       @Override
       protected void doRecord(F1 field1, long value, TimeUnit unit) {
         total.record(value, unit);
diff --git a/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java b/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
index be66009..dee800e 100644
--- a/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
+++ b/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.metrics.dropwizard;
 
-import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.metrics.Description;
@@ -22,6 +21,7 @@
 import com.google.gerrit.metrics.Timer2;
 import com.google.gerrit.metrics.Timer3;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 
 /** Generalized implementation of N-dimensional timer metrics. */
 class TimerImplN extends BucketedTimer implements BucketedMetric {
@@ -29,8 +29,9 @@
     super(metrics, name, desc, fields);
   }
 
+  @SuppressWarnings("unchecked")
   <F1, F2> Timer2<F1, F2> timer2() {
-    return new Timer2<F1, F2>(name) {
+    return new Timer2<F1, F2>(name, (Field<F1>) fields[0], (Field<F2>) fields[1]) {
       @Override
       protected void doRecord(F1 field1, F2 field2, long value, TimeUnit unit) {
         total.record(value, unit);
@@ -44,8 +45,10 @@
     };
   }
 
+  @SuppressWarnings("unchecked")
   <F1, F2, F3> Timer3<F1, F2, F3> timer3() {
-    return new Timer3<F1, F2, F3>(name) {
+    return new Timer3<F1, F2, F3>(
+        name, (Field<F1>) fields[0], (Field<F2>) fields[1], (Field<F3>) fields[2]) {
       @Override
       protected void doRecord(F1 field1, F2 field2, F3 field3, long value, TimeUnit unit) {
         total.record(value, unit);
diff --git a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
index a44f907..e8611b3 100644
--- a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
+++ b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.logging.Metadata;
 import java.util.Map;
 import org.eclipse.jgit.storage.file.WindowCacheStats;
 
@@ -183,7 +184,7 @@
                         + "having most data in the cache.")
                 .setGauge()
                 .setUnit("byte"),
-            Field.ofString("repository_name"));
+            Field.ofString("repository_name", Metadata.Builder::projectName).build());
     metrics.newTrigger(
         repoEnt,
         () -> {
diff --git a/java/com/google/gerrit/metrics/proc/ProcMetricModule.java b/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
index 2c29882..20ac8fa 100644
--- a/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
+++ b/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.logging.Metadata;
 import java.lang.management.GarbageCollectorMXBean;
 import java.lang.management.ManagementFactory;
 import java.lang.management.MemoryMXBean;
@@ -167,12 +168,17 @@
   }
 
   private void procJvmGc(MetricMaker metrics) {
+    Field<String> gcNameField =
+        Field.ofString("gc_name", Metadata.Builder::garbageCollectorName)
+            .description("The name of the garbage collector")
+            .build();
+
     CallbackMetric1<String, Long> gcCount =
         metrics.newCallbackMetric(
             "proc/jvm/gc/count",
             Long.class,
             new Description("Number of GCs").setCumulative(),
-            Field.ofString("gc_name", "The name of the garbage collector"));
+            gcNameField);
 
     CallbackMetric1<String, Long> gcTime =
         metrics.newCallbackMetric(
@@ -181,7 +187,7 @@
             new Description("Approximate accumulated GC elapsed time")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            Field.ofString("gc_name", "The name of the garbage collector"));
+            gcNameField);
 
     metrics.newTrigger(
         gcCount,
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index a8690b9..8b8f13c 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -1,17 +1,7 @@
 load("@rules_java//java:defs.bzl", "java_library")
 
-# TODO(davido): This indirection doesn't avoid unwanted depdencies
-# in acceptance-framework and should be removed. Instead, provided_deps
-# should be used, once https://github.com/bazelbuild/bazel/issues/1402
-# is fixed.
-alias(
-    name = "pgm",
-    actual = ":daemon",
-    visibility = ["//visibility:public"],
-)
-
 java_library(
-    name = "daemon",
+    name = "pgm",
     srcs = glob(["**/*.java"]),
     resource_strip_prefix = "resources",
     resources = ["//resources/com/google/gerrit/pgm"],
@@ -20,6 +10,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/elasticsearch",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/gpg",
@@ -37,7 +28,6 @@
         "//java/com/google/gerrit/pgm/init",
         "//java/com/google/gerrit/pgm/init/api",
         "//java/com/google/gerrit/pgm/util",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server:module",
         "//java/com/google/gerrit/server/api",
@@ -51,6 +41,7 @@
         "//java/com/google/gerrit/sshd",
         "//lib:args4j",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:protobuf",
         "//lib:servlet-api-without-neverlink",
         "//lib/auto:auto-value",
@@ -59,7 +50,6 @@
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/log:jsonevent-layout",
         "//lib/log:log4j",
         "//lib/prolog:cafeteria",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 6b4a065..2ca68c2 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.httpd.raw.StaticModule;
+import com.google.gerrit.index.IndexType;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
@@ -84,7 +85,6 @@
 import com.google.gerrit.server.group.PeriodicGroupIndexer;
 import com.google.gerrit.server.index.AutoFlush;
 import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.OnlineUpgrader;
 import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
@@ -150,8 +150,11 @@
     sshd = false;
   }
 
-  @Option(name = "--slave", usage = "Support fetch only")
-  private boolean slave;
+  @Option(
+      name = "--replica",
+      aliases = {"--slave"},
+      usage = "Support fetch only")
+  private boolean replica;
 
   @Option(name = "--console-log", usage = "Log to console (not $site_path/logs)")
   private boolean consoleLog;
@@ -218,15 +221,15 @@
     httpd = enable;
   }
 
+  public void setReplica(boolean replica) {
+    this.replica = replica;
+  }
+
   @VisibleForTesting
   public Injector getHttpdInjector() {
     return httpdInjector;
   }
 
-  public void setSlave(boolean slave) {
-    this.slave = slave;
-  }
-
   @Override
   public int run() throws Exception {
     if (stopOnly) {
@@ -249,7 +252,7 @@
     }
 
     if (httpd == null) {
-      httpd = !slave;
+      httpd = !replica;
     }
 
     if (!httpd && !sshd) {
@@ -338,7 +341,7 @@
     }
     cfgInjector = createCfgInjector();
     config = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-    initIndexType();
+    indexType = IndexModule.getIndexType(cfgInjector);
     sysInjector = createSysInjector();
     sysInjector.getInstance(PluginGuiceEnvironment.class).setDbCfgInjector(dbInjector, cfgInjector);
     manager.add(dbInjector, cfgInjector, sysInjector);
@@ -380,8 +383,8 @@
 
   private String myVersion() {
     List<String> versionParts = new ArrayList<>();
-    if (slave) {
-      versionParts.add("[slave]");
+    if (replica) {
+      versionParts.add("[replica]");
     }
     if (headless) {
       versionParts.add("[headless]");
@@ -417,7 +420,7 @@
     modules.add(new GerritApiModule());
     modules.add(new PluginApiModule());
 
-    modules.add(new SearchingChangeCacheImpl.Module(slave));
+    modules.add(new SearchingChangeCacheImpl.Module(replica));
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultPermissionBackendModule());
     modules.add(new DefaultMemoryCacheModule());
@@ -469,7 +472,8 @@
         new AbstractModule() {
           @Override
           protected void configure() {
-            bind(GerritOptions.class).toInstance(new GerritOptions(headless, slave, polyGerritDev));
+            bind(GerritOptions.class)
+                .toInstance(new GerritOptions(headless, replica, polyGerritDev));
             if (inMemoryTest) {
               bind(String.class)
                   .annotatedWith(SecureStoreClassName.class)
@@ -479,7 +483,7 @@
           }
         });
     modules.add(new GarbageCollectionModule());
-    if (slave) {
+    if (replica) {
       modules.add(new PeriodicGroupIndexer.Module());
     } else {
       modules.add(new AccountDeactivator.Module());
@@ -497,25 +501,13 @@
     if (luceneModule != null) {
       return luceneModule;
     }
-    switch (indexType) {
-      case LUCENE:
-        return LuceneIndexModule.latestVersion(slave, AutoFlush.ENABLED);
-      case ELASTICSEARCH:
-        return ElasticIndexModule.latestVersion(slave);
-      default:
-        throw new IllegalStateException("unsupported index.type = " + indexType);
+    if (indexType.isLucene()) {
+      return LuceneIndexModule.latestVersion(replica, AutoFlush.ENABLED);
     }
-  }
-
-  private void initIndexType() {
-    indexType = IndexModule.getIndexType(cfgInjector);
-    switch (indexType) {
-      case LUCENE:
-      case ELASTICSEARCH:
-        break;
-      default:
-        throw new IllegalStateException("unsupported index.type = " + indexType);
+    if (indexType.isElasticsearch()) {
+      return ElasticIndexModule.latestVersion(replica);
     }
+    throw new IllegalStateException("unsupported index.type = " + indexType);
   }
 
   private void initSshd() {
@@ -532,12 +524,12 @@
     }
     modules.add(
         new DefaultCommandModule(
-            slave,
+            replica,
             sysInjector.getInstance(DownloadConfig.class),
             sysInjector.getInstance(LfsPluginAuthCommand.Module.class)));
 
     modules.addAll(testSshModules);
-    if (!slave) {
+    if (!replica) {
       modules.add(new IndexCommandsModule(sysInjector));
     }
     return sysInjector.createChildInjector(modules);
diff --git a/java/com/google/gerrit/pgm/Init.java b/java/com/google/gerrit/pgm/Init.java
index 6d9d003..4e62a0f 100644
--- a/java/com/google/gerrit/pgm/Init.java
+++ b/java/com/google/gerrit/pgm/Init.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
 import com.google.gerrit.server.ioutil.HostPlatform;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
+import com.google.gerrit.server.util.ReplicaUtil;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
@@ -155,7 +156,7 @@
         });
     modules.add(new GerritServerConfigModule());
     Guice.createInjector(modules).injectMembers(this);
-    if (!run.flags.cfg.getBoolean("container", "slave", false)) {
+    if (!ReplicaUtil.isReplica(run.flags.cfg)) {
       for (SchemaDefinitions<?> schemaDef : schemaDefs) {
         if (!indexStatus.exists(schemaDef.getName())) {
           reindex(schemaDef);
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index 544c61d..0a8458b 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.index.IndexType;
 import com.google.gerrit.index.SiteIndexer;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lucene.LuceneIndexModule;
@@ -32,9 +33,9 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.AutoFlush;
 import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+import com.google.gerrit.server.util.ReplicaUtil;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Key;
@@ -147,21 +148,17 @@
     if (changesVersion != null) {
       versions.put(ChangeSchemaDefinitions.INSTANCE.getName(), changesVersion);
     }
-    boolean slave = globalConfig.getBoolean("container", "slave", false);
+    boolean replica = ReplicaUtil.isReplica(globalConfig);
     List<Module> modules = new ArrayList<>();
     Module indexModule;
-    switch (IndexModule.getIndexType(dbInjector)) {
-      case LUCENE:
-        indexModule =
-            LuceneIndexModule.singleVersionWithExplicitVersions(
-                versions, threads, slave, AutoFlush.DISABLED);
-        break;
-      case ELASTICSEARCH:
-        indexModule =
-            ElasticIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
-        break;
-      default:
-        throw new IllegalStateException("unsupported index.type");
+    IndexType indexType = IndexModule.getIndexType(dbInjector);
+    if (indexType.isLucene()) {
+      indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(versions, threads, replica, AutoFlush.DISABLED);
+    } else if (indexType.isElasticsearch()) {
+      indexModule =
+          ElasticIndexModule.singleVersionWithExplicitVersions(versions, threads, replica);
+    } else {
+      throw new IllegalStateException("unsupported index.type = " + indexType);
     }
     modules.add(indexModule);
     modules.add(new BatchProgramModule());
@@ -178,7 +175,7 @@
 
   private void overrideConfig() {
     // Disable auto-commit for speed; committing will happen at the end of the process.
-    if (IndexModule.getIndexType(dbInjector) == IndexType.LUCENE) {
+    if (IndexModule.getIndexType(dbInjector).isLucene()) {
       globalConfig.setLong("index", "changes_open", "commitWithin", -1);
       globalConfig.setLong("index", "changes_closed", "commitWithin", -1);
     }
diff --git a/java/com/google/gerrit/pgm/Rulec.java b/java/com/google/gerrit/pgm/Rulec.java
index aa72ae0..d695217 100644
--- a/java/com/google/gerrit/pgm/Rulec.java
+++ b/java/com/google/gerrit/pgm/Rulec.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.pgm;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.pgm.rules.PrologCompiler;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -73,7 +73,7 @@
 
     LinkedHashSet<Project.NameKey> names = new LinkedHashSet<>();
     for (String name : projectNames) {
-      names.add(new Project.NameKey(name));
+      names.add(Project.nameKey(name));
     }
     if (all) {
       names.addAll(gitManager.list());
diff --git a/java/com/google/gerrit/pgm/http/jetty/BUILD b/java/com/google/gerrit/pgm/http/jetty/BUILD
index 9d65ded..cd188f5 100644
--- a/java/com/google/gerrit/pgm/http/jetty/BUILD
+++ b/java/com/google/gerrit/pgm/http/jetty/BUILD
@@ -16,6 +16,7 @@
         "//java/com/google/gerrit/util/logging",
         "//lib:gson",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:servlet-api",
         "//lib/flogger:api",
         "//lib/guice",
@@ -24,7 +25,6 @@
         "//lib/jetty:jmx",
         "//lib/jetty:server",
         "//lib/jetty:servlet",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/log:log4j",
     ],
 )
diff --git a/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/java/com/google/gerrit/pgm/init/AccountsOnInit.java
index ff94905..536ddcd 100644
--- a/java/com/google/gerrit/pgm/init/AccountsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
 import com.google.gerrit.pgm.init.api.InitFlags;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.account.AccountProperties;
 import com.google.gerrit.server.account.Accounts;
@@ -60,63 +60,59 @@
     this.allUsers = allUsers.get();
   }
 
-  public void insert(Account account) throws IOException {
+  public Account insert(Account.Builder account) throws IOException {
     File path = getPath();
-    if (path != null) {
-      try (Repository repo = new FileRepository(path);
-          ObjectInserter oi = repo.newObjectInserter()) {
-        PersonIdent ident =
-            new PersonIdent(
-                new GerritPersonIdentProvider(flags.cfg).get(), account.getRegisteredOn());
+    try (Repository repo = new FileRepository(path);
+        ObjectInserter oi = repo.newObjectInserter()) {
+      PersonIdent ident =
+          new PersonIdent(new GerritPersonIdentProvider(flags.cfg).get(), account.registeredOn());
 
-        Config accountConfig = new Config();
-        AccountProperties.writeToAccountConfig(
-            InternalAccountUpdate.builder()
-                .setActive(account.isActive())
-                .setFullName(account.getFullName())
-                .setPreferredEmail(account.getPreferredEmail())
-                .setStatus(account.getStatus())
-                .build(),
-            accountConfig);
+      Config accountConfig = new Config();
+      AccountProperties.writeToAccountConfig(
+          InternalAccountUpdate.builder()
+              .setActive(!account.inactive())
+              .setFullName(account.fullName())
+              .setPreferredEmail(account.preferredEmail())
+              .setStatus(account.status())
+              .build(),
+          accountConfig);
 
-        DirCache newTree = DirCache.newInCore();
-        DirCacheEditor editor = newTree.editor();
-        final ObjectId blobId =
-            oi.insert(Constants.OBJ_BLOB, accountConfig.toText().getBytes(UTF_8));
-        editor.add(
-            new PathEdit(AccountProperties.ACCOUNT_CONFIG) {
-              @Override
-              public void apply(DirCacheEntry ent) {
-                ent.setFileMode(FileMode.REGULAR_FILE);
-                ent.setObjectId(blobId);
-              }
-            });
-        editor.finish();
+      DirCache newTree = DirCache.newInCore();
+      DirCacheEditor editor = newTree.editor();
+      final ObjectId blobId = oi.insert(Constants.OBJ_BLOB, accountConfig.toText().getBytes(UTF_8));
+      editor.add(
+          new PathEdit(AccountProperties.ACCOUNT_CONFIG) {
+            @Override
+            public void apply(DirCacheEntry ent) {
+              ent.setFileMode(FileMode.REGULAR_FILE);
+              ent.setObjectId(blobId);
+            }
+          });
+      editor.finish();
 
-        ObjectId treeId = newTree.writeTree(oi);
+      ObjectId treeId = newTree.writeTree(oi);
 
-        CommitBuilder cb = new CommitBuilder();
-        cb.setTreeId(treeId);
-        cb.setCommitter(ident);
-        cb.setAuthor(ident);
-        cb.setMessage("Create Account");
-        ObjectId id = oi.insert(cb);
-        oi.flush();
+      CommitBuilder cb = new CommitBuilder();
+      cb.setTreeId(treeId);
+      cb.setCommitter(ident);
+      cb.setAuthor(ident);
+      cb.setMessage("Create Account");
+      ObjectId id = oi.insert(cb);
+      oi.flush();
 
-        String refName = RefNames.refsUsers(account.getId());
-        RefUpdate ru = repo.updateRef(refName);
-        ru.setExpectedOldObjectId(ObjectId.zeroId());
-        ru.setNewObjectId(id);
-        ru.setRefLogIdent(ident);
-        ru.setRefLogMessage("Create Account", false);
-        Result result = ru.update();
-        if (result != Result.NEW) {
-          throw new IOException(
-              String.format("Failed to update ref %s: %s", refName, result.name()));
-        }
-        account.setMetaId(id.name());
+      String refName = RefNames.refsUsers(account.id());
+      RefUpdate ru = repo.updateRef(refName);
+      ru.setExpectedOldObjectId(ObjectId.zeroId());
+      ru.setNewObjectId(id);
+      ru.setRefLogIdent(ident);
+      ru.setRefLogMessage("Create Account", false);
+      Result result = ru.update();
+      if (result != Result.NEW) {
+        throw new IOException(String.format("Failed to update ref %s: %s", refName, result.name()));
       }
+      account.setMetaId(id.name()).build();
     }
+    return account.build();
   }
 
   public boolean hasAnyAccount() throws IOException {
@@ -132,7 +128,10 @@
 
   private File getPath() {
     Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
-    checkArgument(basePath != null, "gerrit.basePath must be configured");
-    return FileKey.resolve(basePath.resolve(allUsers).toFile(), FS.DETECTED);
+    requireNonNull(basePath, "gerrit.basePath must be configured");
+    File file = basePath.resolve(allUsers).toFile();
+    File resolvedFile = FileKey.resolve(file, FS.DETECTED);
+    requireNonNull(resolvedFile, () -> String.format("%s does not exist", file.getAbsolutePath()));
+    return resolvedFile;
   }
 }
diff --git a/java/com/google/gerrit/pgm/init/BUILD b/java/com/google/gerrit/pgm/init/BUILD
index eb0d49e..62c9526 100644
--- a/java/com/google/gerrit/pgm/init/BUILD
+++ b/java/com/google/gerrit/pgm/init/BUILD
@@ -10,6 +10,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/elasticsearch",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
@@ -18,17 +19,16 @@
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/pgm/init/api",
         "//java/com/google/gerrit/pgm/util",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/ioutil",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//lib:guava",
         "//lib:h2",
+        "//lib:jgit",
         "//lib/commons:validator",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index 0bc1860..62ff66a 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.Die;
 import com.google.gerrit.common.IoUtil;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.IndexType;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
@@ -412,15 +413,13 @@
           });
       Injector dbInjector = createDbInjector();
 
-      switch (IndexModule.getIndexType(dbInjector)) {
-        case LUCENE:
-          modules.add(new LuceneIndexModuleOnInit());
-          break;
-        case ELASTICSEARCH:
-          modules.add(new ElasticIndexModuleOnInit());
-          break;
-        default:
-          throw new IllegalStateException("unsupported index.type");
+      IndexType indexType = IndexModule.getIndexType(dbInjector);
+      if (indexType.isLucene()) {
+        modules.add(new LuceneIndexModuleOnInit());
+      } else if (indexType.isElasticsearch()) {
+        modules.add(new ElasticIndexModuleOnInit());
+      } else {
+        throw new IllegalStateException("unsupported index.type = " + indexType);
       }
       sysInjector = dbInjector.createChildInjector(modules);
     }
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 273ebfb..0333942 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -20,11 +20,11 @@
 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.exceptions.NoSuchGroupException;
 import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
 import com.google.gerrit.pgm.init.api.InitFlags;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerIdProvider;
@@ -155,7 +155,7 @@
 
   private static InternalGroupUpdate getMemberAdditionUpdate(Account account) {
     return InternalGroupUpdate.builder()
-        .setMemberModification(members -> Sets.union(members, ImmutableSet.of(account.getId())))
+        .setMemberModification(members -> Sets.union(members, ImmutableSet.of(account.id())))
         .build();
   }
 
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 27e6ce9..cf208ae 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -18,18 +18,16 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AuthType;
-import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.SequencesOnInit;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
@@ -49,7 +47,6 @@
 public class InitAdminUser implements InitStep {
   private final InitFlags flags;
   private final ConsoleUI ui;
-  private final AllUsersNameOnInitProvider allUsers;
   private final AccountsOnInit accounts;
   private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
   private final ExternalIdsOnInit externalIds;
@@ -62,7 +59,6 @@
   InitAdminUser(
       InitFlags flags,
       ConsoleUI ui,
-      AllUsersNameOnInitProvider allUsers,
       AccountsOnInit accounts,
       VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory,
       ExternalIdsOnInit externalIds,
@@ -70,7 +66,6 @@
       GroupsOnInit groupsOnInit) {
     this.flags = flags;
     this.ui = ui;
-    this.allUsers = allUsers;
     this.accounts = accounts;
     this.authorizedKeysFactory = authorizedKeysFactory;
     this.externalIds = externalIds;
@@ -101,7 +96,7 @@
     if (!accounts.hasAnyAccount()) {
       ui.header("Gerrit Administrator");
       if (ui.yesno(true, "Create administrator user")) {
-        Account.Id id = new Account.Id(sequencesOnInit.nextAccountId());
+        Account.Id id = Account.id(sequencesOnInit.nextAccountId());
         String username = ui.readString("admin", "username");
         String name = ui.readString("Administrator", "name");
         String httpPassword = ui.readString("secret", "HTTP password");
@@ -116,11 +111,9 @@
         }
         externalIds.insert("Add external IDs for initial admin user", extIds);
 
-        Account a = new Account(id, TimeUtil.nowTs());
-        a.setFullName(name);
-        a.setPreferredEmail(email);
-        accounts.insert(a);
-
+        Account persistedAccount =
+            accounts.insert(
+                Account.builder(id, TimeUtil.nowTs()).setFullName(name).setPreferredEmail(email));
         // Only two groups should exist at this point in time and hence iterating over all of them
         // is cheap.
         Optional<GroupReference> adminGroupReference =
@@ -132,7 +125,7 @@
           throw new NoSuchGroupException("Administrators");
         }
         GroupReference adminGroup = adminGroupReference.get();
-        groupsOnInit.addGroupMember(adminGroup.getUUID(), a);
+        groupsOnInit.addGroupMember(adminGroup.getUUID(), persistedAccount);
 
         if (sshKey != null) {
           VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
@@ -140,7 +133,7 @@
           authorizedKeys.save("Add SSH key for initial admin user\n");
         }
 
-        AccountState as = AccountState.forAccount(new AllUsersName(allUsers.get()), a, extIds);
+        AccountState as = AccountState.forAccount(persistedAccount, extIds);
         for (AccountIndex accountIndex : accountIndexCollection.getWriteIndexes()) {
           accountIndex.replace(as);
         }
diff --git a/java/com/google/gerrit/pgm/init/InitIndex.java b/java/com/google/gerrit/pgm/init/InitIndex.java
index 0de08f2..83d9261 100644
--- a/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.collect.Iterables;
+import com.google.gerrit.index.IndexType;
 import com.google.gerrit.index.SchemaDefinitions;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
@@ -22,7 +23,6 @@
 import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -53,27 +53,23 @@
 
   @Override
   public void run() throws IOException {
-    IndexType type = IndexType.LUCENE;
-    if (IndexType.values().length > 1) {
-      ui.header("Index");
-      type = index.select("Type", "type", type);
-    }
+    ui.header("Index");
+    IndexType type =
+        new IndexType(
+            index.select("Type", "type", IndexType.getDefault(), IndexType.getKnownTypes()));
 
-    if (type == IndexType.ELASTICSEARCH) {
+    if (type.isElasticsearch()) {
       Section elasticsearch = sections.get("elasticsearch", null);
       elasticsearch.string("Index Prefix", "prefix", "gerrit_");
       elasticsearch.string("Server", "server", "http://localhost:9200");
       index.string("Result window size", "maxLimit", "10000");
     }
 
-    if ((site.isNew || isEmptySite()) && type == IndexType.LUCENE) {
+    if ((site.isNew || isEmptySite()) && type.isLucene()) {
       for (SchemaDefinitions<?> def : IndexModule.ALL_SCHEMA_DEFS) {
         IndexUtils.setReady(site, def.getName(), def.getLatest().getVersion(), true);
       }
     } else {
-      if (IndexType.values().length <= 1) {
-        ui.header("Index");
-      }
       String message =
           String.format(
               "\nThe index must be %sbuilt before starting Gerrit:\n"
diff --git a/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java b/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
index a9c6cc8..acde91f 100644
--- a/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
+++ b/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
@@ -17,11 +17,11 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.VersionedMetaDataOnInit;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.account.AuthorizedKeys;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
diff --git a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index 20e7ba2..5ca239e 100644
--- a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -16,8 +16,8 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.project.GroupList;
@@ -43,7 +43,7 @@
     super(flags, site, allProjects.get(), RefNames.REFS_CONFIG);
     this.baseConfig =
         ProjectConfig.Factory.getBaseConfig(
-            site, new AllProjectsName(allProjects.get()), new Project.NameKey(allProjects.get()));
+            site, new AllProjectsName(allProjects.get()), Project.nameKey(allProjects.get()));
   }
 
   public Config getConfig() {
@@ -71,7 +71,7 @@
 
   private GroupList readGroupList() throws IOException {
     return GroupList.parse(
-        new Project.NameKey(project),
+        Project.nameKey(project),
         readUTF8(GroupList.FILE_NAME),
         error ->
             logger.atSevere().log(
diff --git a/java/com/google/gerrit/pgm/init/api/BUILD b/java/com/google/gerrit/pgm/init/api/BUILD
index 19203fc..693d319 100644
--- a/java/com/google/gerrit/pgm/init/api/BUILD
+++ b/java/com/google/gerrit/pgm/init/api/BUILD
@@ -7,13 +7,12 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/exceptions",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java b/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
index 2f94bdb..a937c4b 100644
--- a/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.pgm.init.api;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/pgm/init/api/Section.java b/java/com/google/gerrit/pgm/init/api/Section.java
index 87f7aeb..b5d35f4 100644
--- a/java/com/google/gerrit/pgm/init/api/Section.java
+++ b/java/com/google/gerrit/pgm/init/api/Section.java
@@ -127,8 +127,10 @@
 
   public <T extends Enum<?>, E extends EnumSet<? extends T>> T select(
       String title, String name, T defValue, boolean nullIfDefault) {
+    @SuppressWarnings("rawtypes")
+    Class<? extends Enum> declaringClass = defValue.getDeclaringClass();
     @SuppressWarnings("unchecked")
-    E allowedValues = (E) EnumSet.allOf(defValue.getClass());
+    E allowedValues = (E) EnumSet.allOf(declaringClass);
     return select(title, name, defValue, allowedValues, nullIfDefault);
   }
 
diff --git a/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java b/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
index d3d22cb..c11230c 100644
--- a/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.pgm.init.api;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.RepoSequence;
@@ -38,7 +38,7 @@
         new RepoSequence(
             repoManager,
             GitReferenceUpdated.DISABLED,
-            new Project.NameKey(allUsersName.get()),
+            Project.nameKey(allUsersName.get()),
             Sequences.NAME_ACCOUNTS,
             () -> Sequences.FIRST_ACCOUNT_ID,
             1);
diff --git a/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java b/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
index 738cafd..f779601 100644
--- a/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.pgm.init.api;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
@@ -58,7 +58,7 @@
     File path = getPath();
     if (path != null) {
       try (Repository repo = new FileRepository(path)) {
-        load(new Project.NameKey(project), repo);
+        load(Project.nameKey(project), repo);
       }
     }
     return this;
diff --git a/java/com/google/gerrit/pgm/rules/PrologCompiler.java b/java/com/google/gerrit/pgm/rules/PrologCompiler.java
index 2663f42..0a41db5 100644
--- a/java/com/google/gerrit/pgm/rules/PrologCompiler.java
+++ b/java/com/google/gerrit/pgm/rules/PrologCompiler.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.pgm.rules;
 
 import com.google.gerrit.common.Version;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.time.TimeUtil;
diff --git a/java/com/google/gerrit/pgm/util/BUILD b/java/com/google/gerrit/pgm/util/BUILD
index 60fb5e4..1b97971 100644
--- a/java/com/google/gerrit/pgm/util/BUILD
+++ b/java/com/google/gerrit/pgm/util/BUILD
@@ -6,11 +6,11 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/metrics/dropwizard",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/cache/mem",
@@ -19,9 +19,9 @@
         "//java/com/google/gerrit/util/cli",
         "//lib:args4j",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/flogger:api",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/log:jsonevent-layout",
         "//lib/log:log4j",
     ],
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 8e2a244..ce2b05d 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -19,13 +19,13 @@
 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.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCacheImpl;
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index eed307f..98558fb 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -18,6 +18,7 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.gerrit.common.Die;
+import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
@@ -28,6 +29,7 @@
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.GitRepositoryManagerModule;
+import com.google.gerrit.server.git.SystemReaderInstaller;
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.inject.AbstractModule;
@@ -106,6 +108,13 @@
           });
     }
 
+    modules.add(
+        new LifecycleModule() {
+          @Override
+          protected void configure() {
+            listener().to(SystemReaderInstaller.class);
+          }
+        });
     Module configModule = new GerritServerConfigModule();
     modules.add(configModule);
     modules.add(
diff --git a/java/com/google/gerrit/prettify/BUILD b/java/com/google/gerrit/prettify/BUILD
index 76afbe7..7c1241a 100644
--- a/java/com/google/gerrit/prettify/BUILD
+++ b/java/com/google/gerrit/prettify/BUILD
@@ -5,8 +5,7 @@
     srcs = glob(["common/**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
-        "//java/com/google/gerrit/reviewdb:server",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/proto/testing/BUILD b/java/com/google/gerrit/proto/testing/BUILD
index acfa8f0..0e5f887 100644
--- a/java/com/google/gerrit/proto/testing/BUILD
+++ b/java/com/google/gerrit/proto/testing/BUILD
@@ -7,7 +7,6 @@
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
     deps = [
-        "//java/com/google/gerrit/server/cache/serialize",
         "//lib:guava",
         "//lib/commons:lang3",
         "//lib/truth",
diff --git a/java/com/google/gerrit/proto/testing/SerializedClassSubject.java b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
index 546ff89..79affc6 100644
--- a/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
+++ b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
@@ -32,7 +32,7 @@
  * Subject about classes that are serialized into persistent caches or indices.
  *
  * <p>Hand-written {@link com.google.gerrit.server.cache.serialize.CacheSerializer CacheSerializer}
- * and {@link com.google.gerrit.reviewdb.converter.ProtoConverter ProtoConverter} implementations
+ * and {@link com.google.gerrit.entities.converter.ProtoConverter ProtoConverter} implementations
  * depend on the exact representation of the data stored in a class, so it is important to verify
  * any assumptions about the structure of the serialized classes. This class contains assertions
  * about serialized classes, and should be used for every class that has a custom serializer
@@ -48,7 +48,7 @@
  * the hand-written serializer. Usually, serializer implementations should be written in such a way
  * that new fields are considered optional, and won't require bumping the version.
  */
-public class SerializedClassSubject extends Subject<SerializedClassSubject, Class<?>> {
+public class SerializedClassSubject extends Subject {
   public static SerializedClassSubject assertThatSerializedClass(Class<?> actual) {
     // This formulation fails in Eclipse 4.7.3a with "The type
     // SerializedClassSubject does not define SerializedClassSubject() that is
@@ -60,20 +60,23 @@
     return assertAbout(factory).that(actual);
   }
 
-  private SerializedClassSubject(FailureMetadata metadata, Class<?> actual) {
-    super(metadata, actual);
+  private final Class<?> clazz;
+
+  private SerializedClassSubject(FailureMetadata metadata, Class<?> clazz) {
+    super(metadata, clazz);
+    this.clazz = clazz;
   }
 
   public void isAbstract() {
     isNotNull();
-    if (!Modifier.isAbstract(actual().getModifiers())) {
+    if (!Modifier.isAbstract(clazz.getModifiers())) {
       failWithActual(simpleFact("expected class to be abstract"));
     }
   }
 
   public void isConcrete() {
     isNotNull();
-    if (Modifier.isAbstract(actual().getModifiers())) {
+    if (Modifier.isAbstract(clazz.getModifiers())) {
       failWithActual(simpleFact("expected class to be concrete"));
     }
   }
@@ -82,7 +85,7 @@
     isConcrete();
     check("fields()")
         .that(
-            FieldUtils.getAllFieldsList(actual()).stream()
+            FieldUtils.getAllFieldsList(clazz).stream()
                 .filter(f -> !Modifier.isStatic(f.getModifiers()))
                 .collect(toImmutableMap(Field::getName, Field::getGenericType)))
         .containsExactlyEntriesIn(expectedFields);
@@ -91,9 +94,9 @@
   public void hasAutoValueMethods(Map<String, Type> expectedMethods) {
     // Would be nice if we could check clazz is an @AutoValue, but the retention is not RUNTIME.
     isAbstract();
-    check("noArgumentAbstractMethodsOn(%s)", actual().getName())
+    check("noArgumentAbstractMethods()")
         .that(
-            Arrays.stream(actual().getDeclaredMethods())
+            Arrays.stream(clazz.getDeclaredMethods())
                 .filter(m -> !Modifier.isStatic(m.getModifiers()))
                 .filter(m -> Modifier.isAbstract(m.getModifiers()))
                 .filter(m -> m.getParameters().length == 0)
@@ -103,8 +106,6 @@
 
   public void extendsClass(Type superclassType) {
     isNotNull();
-    check("superclass(%s)", actual().getName())
-        .that(actual().getGenericSuperclass())
-        .isEqualTo(superclassType);
+    check("getGenericSuperclass()").that(clazz.getGenericSuperclass()).isEqualTo(superclassType);
   }
 }
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupById.java b/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
deleted file mode 100644
index 578865c..0000000
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright (C) 2011 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.reviewdb.client;
-
-import com.google.gwtorm.client.CompoundKey;
-import java.util.Objects;
-
-/** Membership of an {@link AccountGroup} in an {@link AccountGroup}. */
-public final class AccountGroupById {
-  public static Key key(AccountGroup.Id groupId, AccountGroup.UUID includeUuid) {
-    return new Key(groupId, includeUuid);
-  }
-
-  public static class Key extends CompoundKey<AccountGroup.Id> {
-    private static final long serialVersionUID = 1L;
-
-    protected AccountGroup.Id groupId;
-
-    protected AccountGroup.UUID includeUUID;
-
-    protected Key() {
-      groupId = new AccountGroup.Id();
-      includeUUID = new AccountGroup.UUID();
-    }
-
-    public Key(AccountGroup.Id g, AccountGroup.UUID u) {
-      groupId = g;
-      includeUUID = u;
-    }
-
-    @Override
-    public AccountGroup.Id getParentKey() {
-      return groupId;
-    }
-
-    public AccountGroup.Id getGroupId() {
-      return groupId;
-    }
-
-    public AccountGroup.Id groupId() {
-      return getParentKey();
-    }
-
-    public AccountGroup.UUID getIncludeUUID() {
-      return includeUUID;
-    }
-
-    public AccountGroup.UUID includeUuid() {
-      return getIncludeUUID();
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {includeUUID};
-    }
-  }
-
-  protected Key key;
-
-  protected AccountGroupById() {}
-
-  public AccountGroupById(AccountGroupById.Key k) {
-    key = k;
-  }
-
-  public AccountGroupById.Key getKey() {
-    return key;
-  }
-
-  public AccountGroup.Id getGroupId() {
-    return key.groupId;
-  }
-
-  public AccountGroup.UUID getIncludeUUID() {
-    return key.includeUUID;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    return (o instanceof AccountGroupById) && Objects.equals(key, ((AccountGroupById) o).key);
-  }
-
-  @Override
-  public int hashCode() {
-    return key.hashCode();
-  }
-
-  @Override
-  public String toString() {
-    return getClass().getSimpleName() + "{key=" + key + "}";
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java b/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
deleted file mode 100644
index 308e1e1..0000000
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
+++ /dev/null
@@ -1,181 +0,0 @@
-// Copyright (C) 2011 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.reviewdb.client;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gwtorm.client.CompoundKey;
-import java.sql.Timestamp;
-import java.util.Objects;
-
-/** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
-public final class AccountGroupByIdAud {
-  public static Key key(AccountGroup.Id groupId, AccountGroup.UUID includeUuid, Timestamp addedOn) {
-    return new Key(groupId, includeUuid, addedOn);
-  }
-
-  public static class Key extends CompoundKey<AccountGroup.Id> {
-    private static final long serialVersionUID = 1L;
-
-    protected AccountGroup.Id groupId;
-
-    protected AccountGroup.UUID includeUUID;
-
-    protected Timestamp addedOn;
-
-    protected Key() {
-      groupId = new AccountGroup.Id();
-      includeUUID = new AccountGroup.UUID();
-    }
-
-    public Key(AccountGroup.Id g, AccountGroup.UUID u, Timestamp t) {
-      groupId = g;
-      includeUUID = u;
-      addedOn = t;
-    }
-
-    @Override
-    public AccountGroup.Id getParentKey() {
-      return groupId;
-    }
-
-    public AccountGroup.Id groupId() {
-      return getParentKey();
-    }
-
-    public AccountGroup.UUID getIncludeUUID() {
-      return includeUUID;
-    }
-
-    public AccountGroup.UUID includeUuid() {
-      return getIncludeUUID();
-    }
-
-    public Timestamp getAddedOn() {
-      return addedOn;
-    }
-
-    public Timestamp addedOn() {
-      return getAddedOn();
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {includeUUID};
-    }
-
-    @Override
-    public String toString() {
-      return "Key{"
-          + "groupId="
-          + groupId
-          + ", includeUUID="
-          + includeUUID
-          + ", addedOn="
-          + addedOn
-          + '}';
-    }
-  }
-
-  protected Key key;
-
-  protected Account.Id addedBy;
-
-  @Nullable protected Account.Id removedBy;
-
-  @Nullable protected Timestamp removedOn;
-
-  protected AccountGroupByIdAud() {}
-
-  public AccountGroupByIdAud(final AccountGroupById m, Account.Id adder, Timestamp when) {
-    final AccountGroup.Id group = m.getGroupId();
-    final AccountGroup.UUID include = m.getIncludeUUID();
-    key = new AccountGroupByIdAud.Key(group, include, when);
-    addedBy = adder;
-  }
-
-  public AccountGroupByIdAud(AccountGroupByIdAud.Key key, Account.Id adder) {
-    this.key = key;
-    addedBy = adder;
-  }
-
-  public AccountGroupByIdAud.Key getKey() {
-    return key;
-  }
-
-  public AccountGroup.Id getGroupId() {
-    return key.getParentKey();
-  }
-
-  public AccountGroup.UUID getIncludeUUID() {
-    return key.getIncludeUUID();
-  }
-
-  public boolean isActive() {
-    return removedOn == null;
-  }
-
-  public void removed(Account.Id deleter, Timestamp when) {
-    removedBy = deleter;
-    removedOn = when;
-  }
-
-  public Account.Id getAddedBy() {
-    return addedBy;
-  }
-
-  public Timestamp getAddedOn() {
-    return key.getAddedOn();
-  }
-
-  public Account.Id getRemovedBy() {
-    return removedBy;
-  }
-
-  public Timestamp getRemovedOn() {
-    return removedOn;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (!(o instanceof AccountGroupByIdAud)) {
-      return false;
-    }
-    AccountGroupByIdAud a = (AccountGroupByIdAud) o;
-    return Objects.equals(key, a.key)
-        && Objects.equals(addedBy, a.addedBy)
-        && Objects.equals(removedBy, a.removedBy)
-        && Objects.equals(removedOn, a.removedOn);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(key, addedBy, removedBy, removedOn);
-  }
-
-  @Override
-  public String toString() {
-    return getClass().getSimpleName()
-        + "{"
-        + "key="
-        + key
-        + ", addedBy="
-        + addedBy
-        + ", removedBy="
-        + removedBy
-        + ", removedOn="
-        + removedOn
-        + "}";
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java b/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
deleted file mode 100644
index dfa7d24..0000000
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gwtorm.client.CompoundKey;
-import java.util.Objects;
-
-/** Membership of an {@link Account} in an {@link AccountGroup}. */
-public final class AccountGroupMember {
-  public static Key key(Account.Id accountId, AccountGroup.Id groupId) {
-    return new Key(accountId, groupId);
-  }
-
-  public static class Key extends CompoundKey<Account.Id> {
-    private static final long serialVersionUID = 1L;
-
-    protected Account.Id accountId;
-
-    protected AccountGroup.Id groupId;
-
-    protected Key() {
-      accountId = new Account.Id();
-      groupId = new AccountGroup.Id();
-    }
-
-    public Key(Account.Id a, AccountGroup.Id g) {
-      accountId = a;
-      groupId = g;
-    }
-
-    @Override
-    public Account.Id getParentKey() {
-      return accountId;
-    }
-
-    public Account.Id accountId() {
-      return getParentKey();
-    }
-
-    public AccountGroup.Id getAccountGroupId() {
-      return groupId;
-    }
-
-    public AccountGroup.Id groupId() {
-      return getAccountGroupId();
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {groupId};
-    }
-  }
-
-  protected Key key;
-
-  protected AccountGroupMember() {}
-
-  public AccountGroupMember(AccountGroupMember.Key k) {
-    key = k;
-  }
-
-  public AccountGroupMember.Key getKey() {
-    return key;
-  }
-
-  public Account.Id getAccountId() {
-    return key.accountId;
-  }
-
-  public AccountGroup.Id getAccountGroupId() {
-    return key.groupId;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    return (o instanceof AccountGroupMember) && Objects.equals(key, ((AccountGroupMember) o).key);
-  }
-
-  @Override
-  public int hashCode() {
-    return key.hashCode();
-  }
-
-  @Override
-  public String toString() {
-    return getClass().getSimpleName() + "{key=" + key + "}";
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
deleted file mode 100644
index 5d43b4a..0000000
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
+++ /dev/null
@@ -1,186 +0,0 @@
-// Copyright (C) 2009 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.reviewdb.client;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gwtorm.client.CompoundKey;
-import java.sql.Timestamp;
-import java.util.Objects;
-
-/** Membership of an {@link Account} in an {@link AccountGroup}. */
-public final class AccountGroupMemberAudit {
-  public static Key key(Account.Id accountId, AccountGroup.Id groupId, Timestamp addedOn) {
-    return new Key(accountId, groupId, addedOn);
-  }
-
-  public static class Key extends CompoundKey<Account.Id> {
-    private static final long serialVersionUID = 1L;
-
-    protected Account.Id accountId;
-
-    protected AccountGroup.Id groupId;
-
-    protected Timestamp addedOn;
-
-    protected Key() {
-      accountId = new Account.Id();
-      groupId = new AccountGroup.Id();
-    }
-
-    public Key(Account.Id a, AccountGroup.Id g, Timestamp t) {
-      accountId = a;
-      groupId = g;
-      addedOn = t;
-    }
-
-    @Override
-    public Account.Id getParentKey() {
-      return accountId;
-    }
-
-    public Account.Id accountId() {
-      return getParentKey();
-    }
-
-    public AccountGroup.Id getGroupId() {
-      return groupId;
-    }
-
-    public AccountGroup.Id groupId() {
-      return getGroupId();
-    }
-
-    public Timestamp getAddedOn() {
-      return addedOn;
-    }
-
-    public Timestamp addedOn() {
-      return getAddedOn();
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {groupId};
-    }
-
-    @Override
-    public String toString() {
-      return "Key{"
-          + "groupId="
-          + groupId
-          + ", accountId="
-          + accountId
-          + ", addedOn="
-          + addedOn
-          + '}';
-    }
-  }
-
-  protected Key key;
-
-  protected Account.Id addedBy;
-
-  @Nullable protected Account.Id removedBy;
-
-  @Nullable protected Timestamp removedOn;
-
-  protected AccountGroupMemberAudit() {}
-
-  public AccountGroupMemberAudit(final AccountGroupMember m, Account.Id adder, Timestamp addedOn) {
-    final Account.Id who = m.getAccountId();
-    final AccountGroup.Id group = m.getAccountGroupId();
-    key = new AccountGroupMemberAudit.Key(who, group, addedOn);
-    addedBy = adder;
-  }
-
-  public AccountGroupMemberAudit(AccountGroupMemberAudit.Key key, Account.Id adder) {
-    this.key = key;
-    addedBy = adder;
-  }
-
-  public AccountGroupMemberAudit.Key getKey() {
-    return key;
-  }
-
-  public AccountGroup.Id getGroupId() {
-    return key.getGroupId();
-  }
-
-  public Account.Id getMemberId() {
-    return key.getParentKey();
-  }
-
-  public boolean isActive() {
-    return removedOn == null;
-  }
-
-  public void removed(Account.Id deleter, Timestamp when) {
-    removedBy = deleter;
-    removedOn = when;
-  }
-
-  public void removedLegacy() {
-    removedBy = addedBy;
-    removedOn = key.addedOn;
-  }
-
-  public Account.Id getAddedBy() {
-    return addedBy;
-  }
-
-  public Timestamp getAddedOn() {
-    return key.getAddedOn();
-  }
-
-  public Account.Id getRemovedBy() {
-    return removedBy;
-  }
-
-  public Timestamp getRemovedOn() {
-    return removedOn;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (!(o instanceof AccountGroupMemberAudit)) {
-      return false;
-    }
-    AccountGroupMemberAudit a = (AccountGroupMemberAudit) o;
-    return Objects.equals(key, a.key)
-        && Objects.equals(addedBy, a.addedBy)
-        && Objects.equals(removedBy, a.removedBy)
-        && Objects.equals(removedOn, a.removedOn);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(key, addedBy, removedBy, removedOn);
-  }
-
-  @Override
-  public String toString() {
-    return getClass().getSimpleName()
-        + "{"
-        + "key="
-        + key
-        + ", addedBy="
-        + addedBy
-        + ", removedBy="
-        + removedBy
-        + ", removedOn="
-        + removedOn
-        + "}";
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/Branch.java b/java/com/google/gerrit/reviewdb/client/Branch.java
deleted file mode 100644
index 4ea49b7..0000000
--- a/java/com/google/gerrit/reviewdb/client/Branch.java
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gwtorm.client.StringKey;
-
-/** Line of development within a {@link Project}. */
-public final class Branch {
-  public static NameKey nameKey(Project.NameKey projectName, String branchName) {
-    return new NameKey(projectName, RefNames.fullName(branchName));
-  }
-
-  public static NameKey nameKey(String projectName, String branchName) {
-    return nameKey(Project.nameKey(projectName), branchName);
-  }
-
-  /** Branch name key */
-  public static class NameKey extends StringKey<Project.NameKey> {
-    private static final long serialVersionUID = 1L;
-
-    protected Project.NameKey projectName;
-
-    protected String branchName;
-
-    protected NameKey() {
-      projectName = new Project.NameKey();
-    }
-
-    public NameKey(Project.NameKey proj, String branchName) {
-      projectName = proj;
-      set(branchName);
-    }
-
-    public NameKey(String proj, String branchName) {
-      this(new Project.NameKey(proj), branchName);
-    }
-
-    @Override
-    public String get() {
-      return branchName;
-    }
-
-    public String branch() {
-      return get();
-    }
-
-    @Override
-    protected void set(String newValue) {
-      branchName = RefNames.fullName(newValue);
-    }
-
-    @Override
-    public Project.NameKey getParentKey() {
-      return projectName;
-    }
-
-    public Project.NameKey project() {
-      return getParentKey();
-    }
-
-    public String getShortName() {
-      return RefNames.shortName(get());
-    }
-  }
-
-  protected NameKey name;
-  protected RevId revision;
-  protected boolean canDelete;
-
-  protected Branch() {}
-
-  public Branch(Branch.NameKey newName) {
-    name = newName;
-  }
-
-  public Branch.NameKey getNameKey() {
-    return name;
-  }
-
-  public String getName() {
-    return name.get();
-  }
-
-  public String getShortName() {
-    return name.getShortName();
-  }
-
-  public RevId getRevision() {
-    return revision;
-  }
-
-  public void setRevision(RevId id) {
-    revision = id;
-  }
-
-  public boolean getCanDelete() {
-    return canDelete;
-  }
-
-  public void setCanDelete(boolean canDelete) {
-    this.canDelete = canDelete;
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
deleted file mode 100644
index ce218c0..0000000
--- a/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ /dev/null
@@ -1,361 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.client.Comment.Range;
-import com.google.gwtorm.client.StringKey;
-import java.sql.Timestamp;
-import java.util.Objects;
-
-/**
- * A comment left by a user on a specific line of a {@link Patch}.
- *
- * <p>New APIs should not expose this class.
- *
- * @see Comment
- */
-public final class PatchLineComment {
-  public static class Key extends StringKey<Patch.Key> {
-    private static final long serialVersionUID = 1L;
-
-    public static Key from(Change.Id changeId, Comment.Key key) {
-      return new Key(
-          new Patch.Key(new PatchSet.Id(changeId, key.patchSetId), key.filename), key.uuid);
-    }
-
-    protected Patch.Key patchKey;
-
-    protected String uuid;
-
-    protected Key() {
-      patchKey = new Patch.Key();
-    }
-
-    public Key(Patch.Key p, String uuid) {
-      this.patchKey = p;
-      this.uuid = uuid;
-    }
-
-    @Override
-    public Patch.Key getParentKey() {
-      return patchKey;
-    }
-
-    @Override
-    public String get() {
-      return uuid;
-    }
-
-    @Override
-    public void set(String newValue) {
-      uuid = newValue;
-    }
-
-    public Comment.Key asCommentKey() {
-      return new Comment.Key(
-          get(), getParentKey().getFileName(), getParentKey().getParentKey().get());
-    }
-  }
-
-  public static final char STATUS_DRAFT = 'd';
-  public static final char STATUS_PUBLISHED = 'P';
-
-  public enum Status {
-    DRAFT(STATUS_DRAFT),
-
-    PUBLISHED(STATUS_PUBLISHED);
-
-    private final char code;
-
-    Status(char c) {
-      code = c;
-    }
-
-    public char getCode() {
-      return code;
-    }
-
-    public static Status forCode(char c) {
-      for (Status s : Status.values()) {
-        if (s.code == c) {
-          return s;
-        }
-      }
-      return null;
-    }
-  }
-
-  public static PatchLineComment from(
-      Change.Id changeId, PatchLineComment.Status status, Comment c) {
-    PatchLineComment.Key key =
-        new PatchLineComment.Key(
-            new Patch.Key(new PatchSet.Id(changeId, c.key.patchSetId), c.key.filename), c.key.uuid);
-
-    PatchLineComment plc =
-        new PatchLineComment(key, c.lineNbr, c.author.getId(), c.parentUuid, c.writtenOn);
-    plc.setSide(c.side);
-    plc.setMessage(c.message);
-    if (c.range != null) {
-      Comment.Range r = c.range;
-      plc.setRange(new CommentRange(r.startLine, r.startChar, r.endLine, r.endChar));
-    }
-    plc.setTag(c.tag);
-    plc.setRevId(new RevId(c.revId));
-    plc.setStatus(status);
-    plc.setRealAuthor(c.getRealAuthor().getId());
-    plc.setUnresolved(c.unresolved);
-    return plc;
-  }
-
-  protected Key key;
-
-  /** Line number this comment applies to; it should display after the line. */
-  protected int lineNbr;
-
-  /** Who wrote this comment. */
-  protected Account.Id author;
-
-  /** When this comment was drafted. */
-  protected Timestamp writtenOn;
-
-  /** Current publication state of the comment; see {@link Status}. */
-  protected char status;
-
-  /** Which file is this comment; 0 is ancestor, 1 is new version. */
-  protected short side;
-
-  /** The text left by the user. */
-  @Nullable protected String message;
-
-  /** The parent of this comment, or null if this is the first comment on this line */
-  @Nullable protected String parentUuid;
-
-  @Nullable protected CommentRange range;
-
-  @Nullable protected String tag;
-
-  /** Real user that added this comment on behalf of the user recorded in {@link #author}. */
-  @Nullable protected Account.Id realAuthor;
-
-  /** True if this comment requires further action. */
-  protected boolean unresolved;
-
-  /** The RevId for the commit to which this comment is referring. */
-  protected RevId revId;
-
-  protected PatchLineComment() {}
-
-  public PatchLineComment(
-      PatchLineComment.Key id, int line, Account.Id a, String parentUuid, Timestamp when) {
-    key = id;
-    lineNbr = line;
-    author = a;
-    setParentUuid(parentUuid);
-    setStatus(Status.DRAFT);
-    setWrittenOn(when);
-  }
-
-  public PatchLineComment(PatchLineComment o) {
-    key = o.key;
-    lineNbr = o.lineNbr;
-    author = o.author;
-    realAuthor = o.realAuthor;
-    writtenOn = o.writtenOn;
-    status = o.status;
-    side = o.side;
-    message = o.message;
-    parentUuid = o.parentUuid;
-    revId = o.revId;
-    if (o.range != null) {
-      range =
-          new CommentRange(
-              o.range.getStartLine(),
-              o.range.getStartCharacter(),
-              o.range.getEndLine(),
-              o.range.getEndCharacter());
-    }
-  }
-
-  public PatchLineComment.Key getKey() {
-    return key;
-  }
-
-  public PatchSet.Id getPatchSetId() {
-    return key.getParentKey().getParentKey();
-  }
-
-  public int getLine() {
-    return lineNbr;
-  }
-
-  public void setLine(int line) {
-    lineNbr = line;
-  }
-
-  public Account.Id getAuthor() {
-    return author;
-  }
-
-  public Account.Id getRealAuthor() {
-    return realAuthor != null ? realAuthor : getAuthor();
-  }
-
-  public void setRealAuthor(Account.Id id) {
-    // Use null for same real author, as before the column was added.
-    realAuthor = Objects.equals(getAuthor(), id) ? null : id;
-  }
-
-  public Timestamp getWrittenOn() {
-    return writtenOn;
-  }
-
-  public Status getStatus() {
-    return Status.forCode(status);
-  }
-
-  public void setStatus(Status s) {
-    status = s.getCode();
-  }
-
-  public short getSide() {
-    return side;
-  }
-
-  public void setSide(short s) {
-    side = s;
-  }
-
-  public String getMessage() {
-    return message;
-  }
-
-  public void setMessage(String s) {
-    message = s;
-  }
-
-  public void setWrittenOn(Timestamp ts) {
-    writtenOn = ts;
-  }
-
-  public String getParentUuid() {
-    return parentUuid;
-  }
-
-  public void setParentUuid(String inReplyTo) {
-    parentUuid = inReplyTo;
-  }
-
-  public void setRange(Range r) {
-    if (r != null) {
-      range =
-          new CommentRange(
-              r.startLine, r.startCharacter,
-              r.endLine, r.endCharacter);
-    } else {
-      range = null;
-    }
-  }
-
-  public void setRange(CommentRange r) {
-    range = r;
-  }
-
-  public CommentRange getRange() {
-    return range;
-  }
-
-  public void setRevId(RevId rev) {
-    revId = rev;
-  }
-
-  public RevId getRevId() {
-    return revId;
-  }
-
-  public void setTag(String tag) {
-    this.tag = tag;
-  }
-
-  public String getTag() {
-    return tag;
-  }
-
-  public void setUnresolved(Boolean unresolved) {
-    this.unresolved = unresolved;
-  }
-
-  public Boolean getUnresolved() {
-    return unresolved;
-  }
-
-  public Comment asComment(String serverId) {
-    Comment c =
-        new Comment(key.asCommentKey(), author, writtenOn, side, message, serverId, unresolved);
-    c.setRevId(revId);
-    c.setRange(range);
-    c.lineNbr = lineNbr;
-    c.parentUuid = parentUuid;
-    c.tag = tag;
-    c.setRealAuthor(getRealAuthor());
-    return c;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o instanceof PatchLineComment) {
-      PatchLineComment c = (PatchLineComment) o;
-      return Objects.equals(key, c.getKey())
-          && Objects.equals(lineNbr, c.getLine())
-          && Objects.equals(author, c.getAuthor())
-          && Objects.equals(writtenOn, c.getWrittenOn())
-          && Objects.equals(status, c.getStatus().getCode())
-          && Objects.equals(side, c.getSide())
-          && Objects.equals(message, c.getMessage())
-          && Objects.equals(parentUuid, c.getParentUuid())
-          && Objects.equals(range, c.getRange())
-          && Objects.equals(revId, c.getRevId())
-          && Objects.equals(tag, c.getTag())
-          && Objects.equals(unresolved, c.getUnresolved());
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    return key.hashCode();
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder builder = new StringBuilder();
-    builder.append("PatchLineComment{");
-    builder.append("key=").append(key).append(',');
-    builder.append("lineNbr=").append(lineNbr).append(',');
-    builder.append("author=").append(author.get()).append(',');
-    builder.append("realAuthor=").append(realAuthor != null ? realAuthor.get() : "").append(',');
-    builder.append("writtenOn=").append(writtenOn.toString()).append(',');
-    builder.append("status=").append(status).append(',');
-    builder.append("side=").append(side).append(',');
-    builder.append("message=").append(Objects.toString(message, "")).append(',');
-    builder.append("parentUuid=").append(Objects.toString(parentUuid, "")).append(',');
-    builder.append("range=").append(Objects.toString(range, "")).append(',');
-    builder.append("revId=").append(revId != null ? revId.get() : "").append(',');
-    builder.append("tag=").append(Objects.toString(tag, "")).append(',');
-    builder.append("unresolved=").append(unresolved);
-    builder.append('}');
-    return builder.toString();
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/PatchSet.java b/java/com/google/gerrit/reviewdb/client/PatchSet.java
deleted file mode 100644
index 684f092..0000000
--- a/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ /dev/null
@@ -1,304 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gwtorm.client.IntKey;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-
-/** A single revision of a {@link Change}. */
-public final class PatchSet {
-  /** Is the reference name a change reference? */
-  public static boolean isChangeRef(String name) {
-    return Id.fromRef(name) != null;
-  }
-
-  /**
-   * Is the reference name a change reference?
-   *
-   * @deprecated use isChangeRef instead.
-   */
-  @Deprecated
-  public static boolean isRef(String name) {
-    return isChangeRef(name);
-  }
-
-  public static String joinGroups(List<String> groups) {
-    if (groups == null) {
-      throw new IllegalArgumentException("groups may not be null");
-    }
-    StringBuilder sb = new StringBuilder();
-    boolean first = true;
-    for (String g : groups) {
-      if (!first) {
-        sb.append(',');
-      } else {
-        first = false;
-      }
-      sb.append(g);
-    }
-    return sb.toString();
-  }
-
-  public static List<String> splitGroups(String joinedGroups) {
-    if (joinedGroups == null) {
-      throw new IllegalArgumentException("groups may not be null");
-    }
-    List<String> groups = new ArrayList<>();
-    int i = 0;
-    while (true) {
-      int idx = joinedGroups.indexOf(',', i);
-      if (idx < 0) {
-        groups.add(joinedGroups.substring(i));
-        break;
-      }
-      groups.add(joinedGroups.substring(i, idx));
-      i = idx + 1;
-    }
-    return groups;
-  }
-
-  public static Id id(Change.Id changeId, int id) {
-    return new Id(changeId, id);
-  }
-
-  public static class Id extends IntKey<Change.Id> {
-    private static final long serialVersionUID = 1L;
-
-    public Change.Id changeId;
-
-    public int patchSetId;
-
-    public Id() {
-      changeId = new Change.Id();
-    }
-
-    public Id(Change.Id change, int id) {
-      this.changeId = change;
-      this.patchSetId = id;
-    }
-
-    @Override
-    public Change.Id getParentKey() {
-      return changeId;
-    }
-
-    public Change.Id changeId() {
-      return getParentKey();
-    }
-
-    @Override
-    public int get() {
-      return patchSetId;
-    }
-
-    @Override
-    protected void set(int newValue) {
-      patchSetId = newValue;
-    }
-
-    public String toRefName() {
-      return changeId.refPrefixBuilder().append(patchSetId).toString();
-    }
-
-    /** Parse a PatchSet.Id out of a string representation. */
-    public static Id parse(String str) {
-      final Id r = new Id();
-      r.fromString(str);
-      return r;
-    }
-
-    /** Parse a PatchSet.Id from a {@link PatchSet#getRefName()} result. */
-    public static Id fromRef(String ref) {
-      int cs = Change.Id.startIndex(ref);
-      if (cs < 0) {
-        return null;
-      }
-      int ce = Change.Id.nextNonDigit(ref, cs);
-      int patchSetId = fromRef(ref, ce);
-      if (patchSetId < 0) {
-        return null;
-      }
-      int changeId = Integer.parseInt(ref.substring(cs, ce));
-      return new PatchSet.Id(new Change.Id(changeId), patchSetId);
-    }
-
-    static int fromRef(String ref, int changeIdEnd) {
-      // Patch set ID.
-      int ps = changeIdEnd + 1;
-      if (ps >= ref.length() || ref.charAt(ps) == '0') {
-        return -1;
-      }
-      for (int i = ps; i < ref.length(); i++) {
-        if (ref.charAt(i) < '0' || ref.charAt(i) > '9') {
-          return -1;
-        }
-      }
-      return Integer.parseInt(ref.substring(ps));
-    }
-
-    public String getId() {
-      return toId(patchSetId);
-    }
-
-    public static String toId(int number) {
-      return number == 0 ? "edit" : String.valueOf(number);
-    }
-  }
-
-  protected Id id;
-
-  @Nullable protected RevId revision;
-
-  protected Account.Id uploader;
-
-  /** When this patch set was first introduced onto the change. */
-  protected Timestamp createdOn;
-
-  /**
-   * Opaque group identifier, usually assigned during creation.
-   *
-   * <p>This field is actually a comma-separated list of values, as in rare cases involving merge
-   * commits a patch set may belong to multiple groups.
-   *
-   * <p>Changes on the same branch having patch sets with intersecting groups are considered
-   * related, as in the "Related Changes" tab.
-   */
-  @Nullable protected String groups;
-
-  // DELETED id = 7 (pushCertficate)
-
-  /** Certificate sent with a push that created this patch set. */
-  @Nullable protected String pushCertificate;
-
-  /**
-   * Optional user-supplied description for this patch set.
-   *
-   * <p>When this field is null, the description was never set on the patch set. When this field is
-   * an empty string, the description was set and later cleared.
-   */
-  @Nullable protected String description;
-
-  protected PatchSet() {}
-
-  public PatchSet(PatchSet.Id k) {
-    id = k;
-  }
-
-  public PatchSet(PatchSet src) {
-    this.id = src.id;
-    this.revision = src.revision;
-    this.uploader = src.uploader;
-    this.createdOn = src.createdOn;
-    this.groups = src.groups;
-    this.pushCertificate = src.pushCertificate;
-    this.description = src.description;
-  }
-
-  public PatchSet.Id getId() {
-    return id;
-  }
-
-  public int getPatchSetId() {
-    return id.get();
-  }
-
-  public RevId getRevision() {
-    return revision;
-  }
-
-  public void setRevision(RevId i) {
-    revision = i;
-  }
-
-  public Account.Id getUploader() {
-    return uploader;
-  }
-
-  public void setUploader(Account.Id who) {
-    uploader = who;
-  }
-
-  public Timestamp getCreatedOn() {
-    return createdOn;
-  }
-
-  public void setCreatedOn(Timestamp ts) {
-    createdOn = ts;
-  }
-
-  public List<String> getGroups() {
-    if (groups == null) {
-      return Collections.emptyList();
-    }
-    return splitGroups(groups);
-  }
-
-  public void setGroups(List<String> groups) {
-    if (groups == null) {
-      groups = Collections.emptyList();
-    }
-    this.groups = joinGroups(groups);
-  }
-
-  public String getRefName() {
-    return id.toRefName();
-  }
-
-  public String getPushCertificate() {
-    return pushCertificate;
-  }
-
-  public void setPushCertificate(String cert) {
-    pushCertificate = cert;
-  }
-
-  public String getDescription() {
-    return description;
-  }
-
-  public void setDescription(String description) {
-    this.description = description;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (!(o instanceof PatchSet)) {
-      return false;
-    }
-    PatchSet p = (PatchSet) o;
-    return Objects.equals(id, p.id)
-        && Objects.equals(revision, p.revision)
-        && Objects.equals(uploader, p.uploader)
-        && Objects.equals(createdOn, p.createdOn)
-        && Objects.equals(groups, p.groups)
-        && Objects.equals(pushCertificate, p.pushCertificate)
-        && Objects.equals(description, p.description);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(id, revision, uploader, createdOn, groups, pushCertificate, description);
-  }
-
-  @Override
-  public String toString() {
-    return "[PatchSet " + getId().toString() + "]";
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
deleted file mode 100644
index e1c4ea9..0000000
--- a/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ /dev/null
@@ -1,238 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gwtorm.client.CompoundKey;
-import java.sql.Timestamp;
-import java.util.Date;
-import java.util.Objects;
-
-/** An approval (or negative approval) on a patch set. */
-public final class PatchSetApproval {
-  public static Key key(PatchSet.Id patchSetId, Account.Id accountId, LabelId labelId) {
-    return new Key(patchSetId, accountId, labelId);
-  }
-
-  public static class Key extends CompoundKey<PatchSet.Id> {
-    private static final long serialVersionUID = 1L;
-
-    protected PatchSet.Id patchSetId;
-
-    protected Account.Id accountId;
-
-    protected LabelId categoryId;
-
-    protected Key() {
-      patchSetId = new PatchSet.Id();
-      accountId = new Account.Id();
-      categoryId = new LabelId();
-    }
-
-    public Key(PatchSet.Id ps, Account.Id a, LabelId c) {
-      this.patchSetId = ps;
-      this.accountId = a;
-      this.categoryId = c;
-    }
-
-    @Override
-    public PatchSet.Id getParentKey() {
-      return patchSetId;
-    }
-
-    public PatchSet.Id patchSetId() {
-      return getParentKey();
-    }
-
-    public Account.Id getAccountId() {
-      return accountId;
-    }
-
-    public Account.Id accountId() {
-      return getAccountId();
-    }
-
-    public LabelId getLabelId() {
-      return categoryId;
-    }
-
-    public LabelId labelId() {
-      return getLabelId();
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {accountId, categoryId};
-    }
-  }
-
-  protected Key key;
-
-  /**
-   * Value assigned by the user.
-   *
-   * <p>The precise meaning of "value" is up to each category.
-   *
-   * <p>In general:
-   *
-   * <ul>
-   *   <li><b>&lt; 0:</b> The approval is rejected/revoked.
-   *   <li><b>= 0:</b> No indication either way is provided.
-   *   <li><b>&gt; 0:</b> The approval is approved/positive.
-   * </ul>
-   *
-   * and in the negative and positive direction a magnitude can be assumed.The further from 0 the
-   * more assertive the approval.
-   */
-  protected short value;
-
-  protected Timestamp granted;
-
-  @Nullable protected String tag;
-
-  /** Real user that made this approval on behalf of the user recorded in {@link Key#accountId}. */
-  @Nullable protected Account.Id realAccountId;
-
-  protected boolean postSubmit;
-
-  // DELETED: id = 4 (changeOpen)
-  // DELETED: id = 5 (changeSortKey)
-
-  protected PatchSetApproval() {}
-
-  public PatchSetApproval(PatchSetApproval.Key k, short v, Date ts) {
-    key = k;
-    setValue(v);
-    setGranted(ts);
-  }
-
-  public PatchSetApproval(PatchSet.Id psId, PatchSetApproval src) {
-    key = new PatchSetApproval.Key(psId, src.getAccountId(), src.getLabelId());
-    value = src.getValue();
-    granted = src.granted;
-    realAccountId = src.realAccountId;
-    tag = src.tag;
-    postSubmit = src.postSubmit;
-  }
-
-  public PatchSetApproval(PatchSetApproval src) {
-    this(src.getPatchSetId(), src);
-  }
-
-  public PatchSetApproval.Key getKey() {
-    return key;
-  }
-
-  public PatchSet.Id getPatchSetId() {
-    return key.patchSetId;
-  }
-
-  public Account.Id getAccountId() {
-    return key.accountId;
-  }
-
-  public Account.Id getRealAccountId() {
-    return realAccountId != null ? realAccountId : getAccountId();
-  }
-
-  public void setRealAccountId(Account.Id id) {
-    // Use null for same real author, as before the column was added.
-    realAccountId = Objects.equals(getAccountId(), id) ? null : id;
-  }
-
-  public LabelId getLabelId() {
-    return key.categoryId;
-  }
-
-  public short getValue() {
-    return value;
-  }
-
-  public void setValue(short v) {
-    value = v;
-  }
-
-  public Timestamp getGranted() {
-    return granted;
-  }
-
-  public void setGranted(Date when) {
-    if (when instanceof Timestamp) {
-      granted = (Timestamp) when;
-    } else {
-      granted = new Timestamp(when.getTime());
-    }
-  }
-
-  public void setTag(String t) {
-    tag = t;
-  }
-
-  public String getLabel() {
-    return getLabelId().get();
-  }
-
-  public boolean isLegacySubmit() {
-    return LabelId.LEGACY_SUBMIT_NAME.equals(getLabel());
-  }
-
-  public String getTag() {
-    return tag;
-  }
-
-  public void setPostSubmit(boolean postSubmit) {
-    this.postSubmit = postSubmit;
-  }
-
-  public boolean isPostSubmit() {
-    return postSubmit;
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder sb =
-        new StringBuilder("[")
-            .append(key)
-            .append(": ")
-            .append(value)
-            .append(",tag:")
-            .append(tag)
-            .append(",realAccountId:")
-            .append(realAccountId);
-    if (postSubmit) {
-      sb.append(",postSubmit");
-    }
-    return sb.append(']').toString();
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o instanceof PatchSetApproval) {
-      PatchSetApproval p = (PatchSetApproval) o;
-      return Objects.equals(key, p.key)
-          && Objects.equals(value, p.value)
-          && Objects.equals(granted, p.granted)
-          && Objects.equals(tag, p.tag)
-          && Objects.equals(realAccountId, p.realAccountId)
-          && postSubmit == p.postSubmit;
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(key, value, granted, tag, realAccountId, postSubmit);
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/RevId.java b/java/com/google/gerrit/reviewdb/client/RevId.java
deleted file mode 100644
index 99b6c2c..0000000
--- a/java/com/google/gerrit/reviewdb/client/RevId.java
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-/** A revision identifier for a file or a change. */
-public final class RevId {
-  public static final int ABBREV_LEN = 7;
-  public static final int LEN = 40;
-
-  protected String id;
-
-  protected RevId() {}
-
-  public RevId(String str) {
-    id = str;
-  }
-
-  /** @return the value of this revision id. */
-  public String get() {
-    return id;
-  }
-
-  /** @return true if this revision id has all required digits. */
-  public boolean isComplete() {
-    return get().length() == LEN;
-  }
-
-  /**
-   * @return if {@link #isComplete()}, {@code this}; otherwise a new RevId with 'z' appended on the
-   *     end.
-   */
-  public RevId max() {
-    if (isComplete()) {
-      return this;
-    }
-
-    final StringBuilder revEnd = new StringBuilder(get().length() + 1);
-    revEnd.append(get());
-    revEnd.append('z');
-    return new RevId(revEnd.toString());
-  }
-
-  @Override
-  public int hashCode() {
-    return id.hashCode();
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    return (o instanceof RevId) && id.equals(((RevId) o).id);
-  }
-
-  @Override
-  public String toString() {
-    return getClass().getSimpleName() + "{" + id + "}";
-  }
-
-  public boolean matches(String str) {
-    return id.startsWith(str.toLowerCase());
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/RobotComment.java b/java/com/google/gerrit/reviewdb/client/RobotComment.java
deleted file mode 100644
index eceb0bf..0000000
--- a/java/com/google/gerrit/reviewdb/client/RobotComment.java
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import java.sql.Timestamp;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-public class RobotComment extends Comment {
-  public String robotId;
-  public String robotRunId;
-  public String url;
-  public Map<String, String> properties;
-  public List<FixSuggestion> fixSuggestions;
-
-  public RobotComment(
-      Key key,
-      Account.Id author,
-      Timestamp writtenOn,
-      short side,
-      String message,
-      String serverId,
-      String robotId,
-      String robotRunId) {
-    super(key, author, writtenOn, side, message, serverId, false);
-    this.robotId = robotId;
-    this.robotRunId = robotRunId;
-  }
-
-  @Override
-  public String toString() {
-    return new StringBuilder()
-        .append("RobotComment{")
-        .append("key=")
-        .append(key)
-        .append(',')
-        .append("robotId=")
-        .append(robotId)
-        .append(',')
-        .append("robotRunId=")
-        .append(robotRunId)
-        .append(',')
-        .append("lineNbr=")
-        .append(lineNbr)
-        .append(',')
-        .append("author=")
-        .append(author.getId().get())
-        .append(',')
-        .append("realAuthor=")
-        .append(realAuthor != null ? realAuthor.getId().get() : "")
-        .append(',')
-        .append("writtenOn=")
-        .append(writtenOn.toString())
-        .append(',')
-        .append("side=")
-        .append(side)
-        .append(',')
-        .append("message=")
-        .append(Objects.toString(message, ""))
-        .append(',')
-        .append("parentUuid=")
-        .append(Objects.toString(parentUuid, ""))
-        .append(',')
-        .append("range=")
-        .append(Objects.toString(range, ""))
-        .append(',')
-        .append("revId=")
-        .append(revId != null ? revId : "")
-        .append(',')
-        .append("tag=")
-        .append(Objects.toString(tag, ""))
-        .append(',')
-        .append("unresolved=")
-        .append(unresolved)
-        .append(',')
-        .append("url=")
-        .append(url)
-        .append(',')
-        .append("properties=")
-        .append(properties != null ? properties : "")
-        .append("fixSuggestions=")
-        .append(fixSuggestions != null ? fixSuggestions : "")
-        .append('}')
-        .toString();
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/client/SubmoduleSubscription.java b/java/com/google/gerrit/reviewdb/client/SubmoduleSubscription.java
deleted file mode 100644
index b297dfb..0000000
--- a/java/com/google/gerrit/reviewdb/client/SubmoduleSubscription.java
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright (C) 2011 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.reviewdb.client;
-
-import com.google.gwtorm.client.StringKey;
-
-/**
- * Defining a project/branch subscription to a project/branch project.
- *
- * <p>This means a class instance represents a repo/branch subscription to a project/branch (the
- * subscriber).
- *
- * <p>A subscriber operates a submodule in defined path.
- */
-public final class SubmoduleSubscription {
-  /** Subscription key */
-  public static class Key extends StringKey<Branch.NameKey> {
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * Indicates the super project, aka subscriber: the project owner of the gitlinks to the
-     * submodules.
-     */
-    protected Branch.NameKey superProject;
-
-    protected String submodulePath;
-
-    protected Key() {
-      superProject = new Branch.NameKey();
-    }
-
-    protected Key(Branch.NameKey superProject, String path) {
-      this.superProject = superProject;
-      this.submodulePath = path;
-    }
-
-    @Override
-    public Branch.NameKey getParentKey() {
-      return superProject;
-    }
-
-    @Override
-    public String get() {
-      return submodulePath;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      this.submodulePath = newValue;
-    }
-  }
-
-  protected Key key;
-
-  protected Branch.NameKey submodule;
-
-  protected SubmoduleSubscription() {}
-
-  public SubmoduleSubscription(Branch.NameKey superProject, Branch.NameKey submodule, String path) {
-    this.key = new Key(superProject, path);
-    this.submodule = submodule;
-  }
-
-  public Key getKey() {
-    return key;
-  }
-
-  public Branch.NameKey getSuperProject() {
-    return key.superProject;
-  }
-
-  public String getPath() {
-    return key.get();
-  }
-
-  public Branch.NameKey getSubmodule() {
-    return submodule;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o instanceof SubmoduleSubscription) {
-      return key.equals(((SubmoduleSubscription) o).key)
-          && submodule.equals(((SubmoduleSubscription) o).submodule);
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    return key.hashCode();
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder sb = new StringBuilder();
-    sb.append(getSuperProject()).append(':').append(getPath());
-    sb.append(" follows ");
-    sb.append(getSubmodule());
-    return sb.toString();
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java
deleted file mode 100644
index 75ee800..0000000
--- a/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// 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.
-
-package com.google.gerrit.reviewdb.converter;
-
-import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.protobuf.Parser;
-import java.sql.Timestamp;
-import java.util.List;
-
-public enum PatchSetProtoConverter implements ProtoConverter<Entities.PatchSet, PatchSet> {
-  INSTANCE;
-
-  private final ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
-      PatchSetIdProtoConverter.INSTANCE;
-  private final ProtoConverter<Entities.RevId, RevId> revIdConverter = RevIdProtoConverter.INSTANCE;
-  private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
-      AccountIdProtoConverter.INSTANCE;
-
-  @Override
-  public Entities.PatchSet toProto(PatchSet patchSet) {
-    Entities.PatchSet.Builder builder =
-        Entities.PatchSet.newBuilder().setId(patchSetIdConverter.toProto(patchSet.getId()));
-    RevId revision = patchSet.getRevision();
-    if (revision != null) {
-      builder.setRevision(revIdConverter.toProto(revision));
-    }
-    Account.Id uploader = patchSet.getUploader();
-    if (uploader != null) {
-      builder.setUploaderAccountId(accountIdConverter.toProto(uploader));
-    }
-    Timestamp createdOn = patchSet.getCreatedOn();
-    if (createdOn != null) {
-      builder.setCreatedOn(createdOn.getTime());
-    }
-    List<String> groups = patchSet.getGroups();
-    if (!groups.isEmpty()) {
-      builder.setGroups(PatchSet.joinGroups(groups));
-    }
-    String pushCertificate = patchSet.getPushCertificate();
-    if (pushCertificate != null) {
-      builder.setPushCertificate(pushCertificate);
-    }
-    String description = patchSet.getDescription();
-    if (description != null) {
-      builder.setDescription(description);
-    }
-    return builder.build();
-  }
-
-  @Override
-  public PatchSet fromProto(Entities.PatchSet proto) {
-    PatchSet patchSet = new PatchSet(patchSetIdConverter.fromProto(proto.getId()));
-    if (proto.hasRevision()) {
-      patchSet.setRevision(revIdConverter.fromProto(proto.getRevision()));
-    }
-    if (proto.hasUploaderAccountId()) {
-      patchSet.setUploader(accountIdConverter.fromProto(proto.getUploaderAccountId()));
-    }
-    if (proto.hasCreatedOn()) {
-      patchSet.setCreatedOn(new Timestamp(proto.getCreatedOn()));
-    }
-    if (proto.hasGroups()) {
-      patchSet.setGroups(PatchSet.splitGroups(proto.getGroups()));
-    }
-    if (proto.hasPushCertificate()) {
-      patchSet.setPushCertificate(proto.getPushCertificate());
-    }
-    if (proto.hasDescription()) {
-      patchSet.setDescription(proto.getDescription());
-    }
-    return patchSet;
-  }
-
-  @Override
-  public Parser<Entities.PatchSet> getParser() {
-    return Entities.PatchSet.parser();
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/converter/RevIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/RevIdProtoConverter.java
deleted file mode 100644
index b3c998b..0000000
--- a/java/com/google/gerrit/reviewdb/converter/RevIdProtoConverter.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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.
-
-package com.google.gerrit.reviewdb.converter;
-
-import com.google.gerrit.proto.Entities;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.protobuf.Parser;
-
-public enum RevIdProtoConverter implements ProtoConverter<Entities.RevId, RevId> {
-  INSTANCE;
-
-  @Override
-  public Entities.RevId toProto(RevId revId) {
-    return Entities.RevId.newBuilder().setId(revId.get()).build();
-  }
-
-  @Override
-  public RevId fromProto(Entities.RevId proto) {
-    return new RevId(proto.getId());
-  }
-
-  @Override
-  public Parser<Entities.RevId> getParser() {
-    return Entities.RevId.parser();
-  }
-}
diff --git a/java/com/google/gerrit/server/ApprovalCopier.java b/java/com/google/gerrit/server/ApprovalCopier.java
deleted file mode 100644
index 09508bb..0000000
--- a/java/com/google/gerrit/server/ApprovalCopier.java
+++ /dev/null
@@ -1,336 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.collect.HashBasedTable;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Table;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.client.ChangeKind;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.server.change.ChangeKindCache;
-import com.google.gerrit.server.change.LabelNormalizer;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.TreeMap;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-/**
- * Copies approvals between patch sets.
- *
- * <p>The result of a copy may either be stored, as when stamping approvals in the database at
- * submit time, or refreshed on demand, as when reading approvals from the NoteDb.
- */
-@Singleton
-public class ApprovalCopier {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private final ProjectCache projectCache;
-  private final ChangeKindCache changeKindCache;
-  private final LabelNormalizer labelNormalizer;
-  private final ChangeData.Factory changeDataFactory;
-  private final PatchSetUtil psUtil;
-
-  @Inject
-  ApprovalCopier(
-      ProjectCache projectCache,
-      ChangeKindCache changeKindCache,
-      LabelNormalizer labelNormalizer,
-      ChangeData.Factory changeDataFactory,
-      PatchSetUtil psUtil) {
-    this.projectCache = projectCache;
-    this.changeKindCache = changeKindCache;
-    this.labelNormalizer = labelNormalizer;
-    this.changeDataFactory = changeDataFactory;
-    this.psUtil = psUtil;
-  }
-
-  Iterable<PatchSetApproval> getForPatchSet(
-      ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
-    return getForPatchSet(notes, psId, rw, repoConfig, Collections.emptyList());
-  }
-
-  Iterable<PatchSetApproval> getForPatchSet(
-      ChangeNotes notes,
-      PatchSet.Id psId,
-      @Nullable RevWalk rw,
-      @Nullable Config repoConfig,
-      Iterable<PatchSetApproval> dontCopy) {
-    PatchSet ps = psUtil.get(notes, psId);
-    if (ps == null) {
-      return Collections.emptyList();
-    }
-    return getForPatchSet(notes, ps, rw, repoConfig, dontCopy);
-  }
-
-  private Iterable<PatchSetApproval> getForPatchSet(
-      ChangeNotes notes,
-      PatchSet ps,
-      @Nullable RevWalk rw,
-      @Nullable Config repoConfig,
-      Iterable<PatchSetApproval> dontCopy) {
-    requireNonNull(ps, "ps should not be null");
-    ChangeData cd = changeDataFactory.create(notes);
-    try {
-      ProjectState project = projectCache.checkedGet(cd.change().getDest().getParentKey());
-      ListMultimap<PatchSet.Id, PatchSetApproval> all = cd.approvals();
-      requireNonNull(all, "all should not be null");
-
-      Table<String, Account.Id, PatchSetApproval> wontCopy = HashBasedTable.create();
-      for (PatchSetApproval psa : dontCopy) {
-        wontCopy.put(psa.getLabel(), psa.getAccountId(), psa);
-      }
-
-      Table<String, Account.Id, PatchSetApproval> byUser = HashBasedTable.create();
-      for (PatchSetApproval psa : all.get(ps.getId())) {
-        if (!wontCopy.contains(psa.getLabel(), psa.getAccountId())) {
-          byUser.put(psa.getLabel(), psa.getAccountId(), psa);
-        }
-      }
-
-      TreeMap<Integer, PatchSet> patchSets = getPatchSets(cd);
-
-      // Walk patch sets strictly less than current in descending order.
-      Collection<PatchSet> allPrior =
-          patchSets.descendingMap().tailMap(ps.getId().get(), false).values();
-      for (PatchSet priorPs : allPrior) {
-        List<PatchSetApproval> priorApprovals = all.get(priorPs.getId());
-        if (priorApprovals.isEmpty()) {
-          continue;
-        }
-
-        ChangeKind kind =
-            changeKindCache.getChangeKind(
-                project.getNameKey(),
-                rw,
-                repoConfig,
-                ObjectId.fromString(priorPs.getRevision().get()),
-                ObjectId.fromString(ps.getRevision().get()));
-
-        for (PatchSetApproval psa : priorApprovals) {
-          if (wontCopy.contains(psa.getLabel(), psa.getAccountId())) {
-            continue;
-          }
-          if (byUser.contains(psa.getLabel(), psa.getAccountId())) {
-            continue;
-          }
-          if (!canCopy(project, psa, ps.getId(), kind)) {
-            wontCopy.put(psa.getLabel(), psa.getAccountId(), psa);
-            continue;
-          }
-          byUser.put(psa.getLabel(), psa.getAccountId(), copy(psa, ps.getId()));
-        }
-      }
-      return labelNormalizer.normalize(notes, byUser.values()).getNormalized();
-    } catch (IOException e) {
-      throw new StorageException(e);
-    }
-  }
-
-  private static TreeMap<Integer, PatchSet> getPatchSets(ChangeData cd) {
-    Collection<PatchSet> patchSets = cd.patchSets();
-    TreeMap<Integer, PatchSet> result = new TreeMap<>();
-    for (PatchSet ps : patchSets) {
-      result.put(ps.getId().get(), ps);
-    }
-    return result;
-  }
-
-  private static boolean canCopy(
-      ProjectState project, PatchSetApproval psa, PatchSet.Id psId, ChangeKind kind) {
-    int n = psa.getKey().getParentKey().get();
-    checkArgument(n != psId.get());
-    LabelType type = project.getLabelTypes().byLabel(psa.getLabelId());
-    if (type == null) {
-      logger.atFine().log(
-          "approval %d on label %s of patch set %d of change %d cannot be copied"
-              + " to patch set %d because the label no longer exists on project %s",
-          psa.getValue(),
-          psa.getLabel(),
-          n,
-          psa.getKey().getParentKey().changeId.get(),
-          psId.get(),
-          project.getName());
-      return false;
-    } else if (type.isCopyMinScore() && type.isMaxNegative(psa)) {
-      logger.atFine().log(
-          "veto approval %s on label %s of patch set %d of change %d can be copied"
-              + " to patch set %d because the label has set copyMinScore = true on project %s",
-          psa.getValue(),
-          psa.getLabel(),
-          n,
-          psa.getKey().getParentKey().changeId.get(),
-          psId.get(),
-          project.getName());
-      return true;
-    } else if (type.isCopyMaxScore() && type.isMaxPositive(psa)) {
-      logger.atFine().log(
-          "max approval %s on label %s of patch set %d of change %d can be copied"
-              + " to patch set %d because the label has set copyMaxScore = true on project %s",
-          psa.getValue(),
-          psa.getLabel(),
-          n,
-          psa.getKey().getParentKey().changeId.get(),
-          psId.get(),
-          project.getName());
-      return true;
-    }
-    switch (kind) {
-      case MERGE_FIRST_PARENT_UPDATE:
-        if (type.isCopyAllScoresOnMergeFirstParentUpdate()) {
-          logger.atFine().log(
-              "approval %d on label %s of patch set %d of change %d can be copied"
-                  + " to patch set %d because change kind is %s and the label has set"
-                  + " copyAllScoresOnMergeFirstParentUpdate = true on project %s",
-              psa.getValue(),
-              psa.getLabel(),
-              n,
-              psa.getKey().getParentKey().changeId.get(),
-              psId.get(),
-              kind,
-              project.getName());
-          return true;
-        }
-        return false;
-      case NO_CODE_CHANGE:
-        if (type.isCopyAllScoresIfNoCodeChange()) {
-          logger.atFine().log(
-              "approval %d on label %s of patch set %d of change %d can be copied"
-                  + " to patch set %d because change kind is %s and the label has set"
-                  + " copyAllScoresIfNoCodeChange = true on project %s",
-              psa.getValue(),
-              psa.getLabel(),
-              n,
-              psa.getKey().getParentKey().changeId.get(),
-              psId.get(),
-              kind,
-              project.getName());
-          return true;
-        }
-        return false;
-      case TRIVIAL_REBASE:
-        if (type.isCopyAllScoresOnTrivialRebase()) {
-          logger.atFine().log(
-              "approval %d on label %s of patch set %d of change %d can be copied"
-                  + " to patch set %d because change kind is %s and the label has set"
-                  + " copyAllScoresOnTrivialRebase = true on project %s",
-              psa.getValue(),
-              psa.getLabel(),
-              n,
-              psa.getKey().getParentKey().changeId.get(),
-              psId.get(),
-              kind,
-              project.getName());
-          return true;
-        }
-        return false;
-      case NO_CHANGE:
-        if (type.isCopyAllScoresIfNoChange()) {
-          logger.atFine().log(
-              "approval %d on label %s of patch set %d of change %d can be copied"
-                  + " to patch set %d because change kind is %s and the label has set"
-                  + " copyAllScoresIfNoCodeChange = true on project %s",
-              psa.getValue(),
-              psa.getLabel(),
-              n,
-              psa.getKey().getParentKey().changeId.get(),
-              psId.get(),
-              kind,
-              project.getName());
-          return true;
-        }
-        if (type.isCopyAllScoresOnTrivialRebase()) {
-          logger.atFine().log(
-              "approval %d on label %s of patch set %d of change %d can be copied"
-                  + " to patch set %d because change kind is %s and the label has set"
-                  + " copyAllScoresOnTrivialRebase = true on project %s",
-              psa.getValue(),
-              psa.getLabel(),
-              n,
-              psa.getKey().getParentKey().changeId.get(),
-              psId.get(),
-              kind,
-              project.getName());
-          return true;
-        }
-        if (type.isCopyAllScoresOnMergeFirstParentUpdate()) {
-          logger.atFine().log(
-              "approval %d on label %s of patch set %d of change %d can be copied"
-                  + " to patch set %d because change kind is %s and the label has set"
-                  + " copyAllScoresOnMergeFirstParentUpdate = true on project %s",
-              psa.getValue(),
-              psa.getLabel(),
-              n,
-              psa.getKey().getParentKey().changeId.get(),
-              psId.get(),
-              kind,
-              project.getName());
-          return true;
-        }
-        if (type.isCopyAllScoresIfNoCodeChange()) {
-          logger.atFine().log(
-              "approval %d on label %s of patch set %d of change %d can be copied"
-                  + " to patch set %d because change kind is %s and the label has set"
-                  + " copyAllScoresIfNoCodeChange = true on project %s",
-              psa.getValue(),
-              psa.getLabel(),
-              n,
-              psa.getKey().getParentKey().changeId.get(),
-              psId.get(),
-              kind,
-              project.getName());
-          return true;
-        }
-        return false;
-      case REWORK:
-      default:
-        logger.atFine().log(
-            "approval %d on label %s of patch set %d of change %d cannot be copied"
-                + " to patch set %d because change kind is %s",
-            psa.getValue(),
-            psa.getLabel(),
-            n,
-            psa.getKey().getParentKey().changeId.get(),
-            psId.get(),
-            kind);
-        return false;
-    }
-  }
-
-  private static PatchSetApproval copy(PatchSetApproval src, PatchSet.Id psId) {
-    if (src.getKey().getParentKey().equals(psId)) {
-      return src;
-    }
-    return new PatchSetApproval(psId, src);
-  }
-}
diff --git a/java/com/google/gerrit/server/ApprovalInference.java b/java/com/google/gerrit/server/ApprovalInference.java
new file mode 100644
index 0000000..566a32b
--- /dev/null
+++ b/java/com/google/gerrit/server/ApprovalInference.java
@@ -0,0 +1,333 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Table;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.change.LabelNormalizer;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * Computes approvals for a given patch set by looking at approvals applied to the given patch set
+ * and by additionally inferring approvals from the patch set's parents. The latter is done by
+ * asserting a change's kind and checking the project config for allowed forward-inference.
+ *
+ * <p>The result of a copy may either be stored, as when stamping approvals in the database at
+ * submit time, or refreshed on demand, as when reading approvals from the NoteDb.
+ */
+@Singleton
+public class ApprovalInference {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final ProjectCache projectCache;
+  private final ChangeKindCache changeKindCache;
+  private final LabelNormalizer labelNormalizer;
+
+  @Inject
+  ApprovalInference(
+      ProjectCache projectCache, ChangeKindCache changeKindCache, LabelNormalizer labelNormalizer) {
+    this.projectCache = projectCache;
+    this.changeKindCache = changeKindCache;
+    this.labelNormalizer = labelNormalizer;
+  }
+
+  /**
+   * Returns all approvals that apply to the given patch set. Honors direct and indirect (approval
+   * on parents) approvals.
+   */
+  Iterable<PatchSetApproval> forPatchSet(
+      ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
+    ProjectState project;
+    try (TraceTimer traceTimer =
+        TraceContext.newTimer(
+            "Computing labels for patch set",
+            Metadata.builder()
+                .changeId(notes.load().getChangeId().get())
+                .patchSetId(psId.get())
+                .build())) {
+      project = projectCache.checkedGet(notes.getProjectName());
+      Collection<PatchSetApproval> approvals =
+          getForPatchSetWithoutNormalization(notes, project, psId, rw, repoConfig);
+      return labelNormalizer.normalize(notes, approvals).getNormalized();
+    } catch (IOException e) {
+      throw new StorageException(e);
+    }
+  }
+
+  private static boolean canCopy(
+      ProjectState project, PatchSetApproval psa, PatchSet.Id psId, ChangeKind kind) {
+    int n = psa.key().patchSetId().get();
+    checkArgument(n != psId.get());
+    LabelType type = project.getLabelTypes().byLabel(psa.labelId());
+    if (type == null) {
+      logger.atFine().log(
+          "approval %d on label %s of patch set %d of change %d cannot be copied"
+              + " to patch set %d because the label no longer exists on project %s",
+          psa.value(),
+          psa.label(),
+          n,
+          psa.key().patchSetId().changeId().get(),
+          psId.get(),
+          project.getName());
+      return false;
+    } else if (type.isCopyMinScore() && type.isMaxNegative(psa)) {
+      logger.atFine().log(
+          "veto approval %s on label %s of patch set %d of change %d can be copied"
+              + " to patch set %d because the label has set copyMinScore = true on project %s",
+          psa.value(),
+          psa.label(),
+          n,
+          psa.key().patchSetId().changeId().get(),
+          psId.get(),
+          project.getName());
+      return true;
+    } else if (type.isCopyMaxScore() && type.isMaxPositive(psa)) {
+      logger.atFine().log(
+          "max approval %s on label %s of patch set %d of change %d can be copied"
+              + " to patch set %d because the label has set copyMaxScore = true on project %s",
+          psa.value(),
+          psa.label(),
+          n,
+          psa.key().patchSetId().changeId().get(),
+          psId.get(),
+          project.getName());
+      return true;
+    } else if (type.isCopyAnyScore()) {
+      logger.atFine().log(
+          "approval %d on label %s of patch set %d of change %d can be copied"
+              + " to patch set %d because the label has set copyAnyScore = true on project %s",
+          psa.value(),
+          psa.label(),
+          n,
+          psa.key().patchSetId().changeId().get(),
+          psId.get(),
+          project.getName());
+      return true;
+    }
+    switch (kind) {
+      case MERGE_FIRST_PARENT_UPDATE:
+        if (type.isCopyAllScoresOnMergeFirstParentUpdate()) {
+          logger.atFine().log(
+              "approval %d on label %s of patch set %d of change %d can be copied"
+                  + " to patch set %d because change kind is %s and the label has set"
+                  + " copyAllScoresOnMergeFirstParentUpdate = true on project %s",
+              psa.value(),
+              psa.label(),
+              n,
+              psa.key().patchSetId().changeId().get(),
+              psId.get(),
+              kind,
+              project.getName());
+          return true;
+        }
+        return false;
+      case NO_CODE_CHANGE:
+        if (type.isCopyAllScoresIfNoCodeChange()) {
+          logger.atFine().log(
+              "approval %d on label %s of patch set %d of change %d can be copied"
+                  + " to patch set %d because change kind is %s and the label has set"
+                  + " copyAllScoresIfNoCodeChange = true on project %s",
+              psa.value(),
+              psa.label(),
+              n,
+              psa.key().patchSetId().changeId().get(),
+              psId.get(),
+              kind,
+              project.getName());
+          return true;
+        }
+        return false;
+      case TRIVIAL_REBASE:
+        if (type.isCopyAllScoresOnTrivialRebase()) {
+          logger.atFine().log(
+              "approval %d on label %s of patch set %d of change %d can be copied"
+                  + " to patch set %d because change kind is %s and the label has set"
+                  + " copyAllScoresOnTrivialRebase = true on project %s",
+              psa.value(),
+              psa.label(),
+              n,
+              psa.key().patchSetId().changeId().get(),
+              psId.get(),
+              kind,
+              project.getName());
+          return true;
+        }
+        return false;
+      case NO_CHANGE:
+        if (type.isCopyAllScoresIfNoChange()) {
+          logger.atFine().log(
+              "approval %d on label %s of patch set %d of change %d can be copied"
+                  + " to patch set %d because change kind is %s and the label has set"
+                  + " copyAllScoresIfNoCodeChange = true on project %s",
+              psa.value(),
+              psa.label(),
+              n,
+              psa.key().patchSetId().changeId().get(),
+              psId.get(),
+              kind,
+              project.getName());
+          return true;
+        }
+        if (type.isCopyAllScoresOnTrivialRebase()) {
+          logger.atFine().log(
+              "approval %d on label %s of patch set %d of change %d can be copied"
+                  + " to patch set %d because change kind is %s and the label has set"
+                  + " copyAllScoresOnTrivialRebase = true on project %s",
+              psa.value(),
+              psa.label(),
+              n,
+              psa.key().patchSetId().changeId().get(),
+              psId.get(),
+              kind,
+              project.getName());
+          return true;
+        }
+        if (type.isCopyAllScoresOnMergeFirstParentUpdate()) {
+          logger.atFine().log(
+              "approval %d on label %s of patch set %d of change %d can be copied"
+                  + " to patch set %d because change kind is %s and the label has set"
+                  + " copyAllScoresOnMergeFirstParentUpdate = true on project %s",
+              psa.value(),
+              psa.label(),
+              n,
+              psa.key().patchSetId().changeId().get(),
+              psId.get(),
+              kind,
+              project.getName());
+          return true;
+        }
+        if (type.isCopyAllScoresIfNoCodeChange()) {
+          logger.atFine().log(
+              "approval %d on label %s of patch set %d of change %d can be copied"
+                  + " to patch set %d because change kind is %s and the label has set"
+                  + " copyAllScoresIfNoCodeChange = true on project %s",
+              psa.value(),
+              psa.label(),
+              n,
+              psa.key().patchSetId().changeId().get(),
+              psId.get(),
+              kind,
+              project.getName());
+          return true;
+        }
+        return false;
+      case REWORK:
+      default:
+        logger.atFine().log(
+            "approval %d on label %s of patch set %d of change %d cannot be copied"
+                + " to patch set %d because change kind is %s",
+            psa.value(), psa.label(), n, psa.key().patchSetId().changeId().get(), psId.get(), kind);
+        return false;
+    }
+  }
+
+  private Collection<PatchSetApproval> getForPatchSetWithoutNormalization(
+      ChangeNotes notes,
+      ProjectState project,
+      PatchSet.Id psId,
+      @Nullable RevWalk rw,
+      @Nullable Config repoConfig) {
+    checkState(
+        project.getNameKey().equals(notes.getProjectName()),
+        "project must match %s, %s",
+        project.getNameKey(),
+        notes.getProjectName());
+
+    PatchSet ps = notes.load().getPatchSets().get(psId);
+    if (ps == null) {
+      return Collections.emptyList();
+    }
+
+    // Add approvals on the given patch set to the result
+    Table<String, Account.Id, PatchSetApproval> resultByUser = HashBasedTable.create();
+    ImmutableList<PatchSetApproval> approvalsForGivenPatchSet =
+        notes.load().getApprovals().get(ps.id());
+    approvalsForGivenPatchSet.forEach(psa -> resultByUser.put(psa.label(), psa.accountId(), psa));
+
+    // Bail out immediately if this is the first patch set. Return only approvals granted on the
+    // given patch set.
+    if (psId.get() == 1) {
+      return resultByUser.values();
+    }
+
+    // Call this algorithm recursively to check if the prior patch set had approvals. This has the
+    // advantage that all caches - most importantly ChangeKindCache - have values cached for what we
+    // need for this computation.
+    // The way this algorithm is written is that any approval will be copied forward by one patch
+    // set at a time if configs and change kind allow so. Once an approval is held back - for
+    // example because the patch set is a REWORK - it will not be picked up again in a future
+    // patch set.
+    Map.Entry<PatchSet.Id, PatchSet> priorPatchSet = notes.load().getPatchSets().lowerEntry(psId);
+    if (priorPatchSet == null) {
+      return resultByUser.values();
+    }
+
+    Iterable<PatchSetApproval> priorApprovals =
+        getForPatchSetWithoutNormalization(
+            notes, project, priorPatchSet.getValue().id(), rw, repoConfig);
+    if (!priorApprovals.iterator().hasNext()) {
+      return resultByUser.values();
+    }
+
+    // Add labels from the previous patch set to the result in case the label isn't already there
+    // and settings as well as change kind allow copying.
+    ChangeKind kind =
+        changeKindCache.getChangeKind(
+            project.getNameKey(),
+            rw,
+            repoConfig,
+            priorPatchSet.getValue().commitId(),
+            ps.commitId());
+    logger.atFine().log(
+        "change kind for patch set %d of change %d against prior patch set %s is %s",
+        ps.id().get(), ps.id().changeId().get(), priorPatchSet.getValue().id().changeId(), kind);
+    for (PatchSetApproval psa : priorApprovals) {
+      if (resultByUser.contains(psa.label(), psa.accountId())) {
+        continue;
+      }
+      if (!canCopy(project, psa, ps.id(), kind)) {
+        continue;
+      }
+      resultByUser.put(psa.label(), psa.accountId(), psa.copyWithPatchSet(ps.id()));
+    }
+    return resultByUser.values();
+  }
+}
diff --git a/java/com/google/gerrit/server/ApprovalsUtil.java b/java/com/google/gerrit/server/ApprovalsUtil.java
index 135276e..58b601f 100644
--- a/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -25,20 +25,19 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
-import com.google.common.primitives.Shorts;
 import com.google.gerrit.common.Nullable;
 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.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.PatchSetInfo;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -71,38 +70,38 @@
  * for each reviewer, even if the reviewer hasn't actually given a score to the change. To mark the
  * "no score" case, a dummy approval, which may live in any of the available categories, with a
  * score of 0 is used.
- *
- * <p>The methods in this class only modify the gwtorm database.
  */
 @Singleton
 public class ApprovalsUtil {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  public static PatchSetApproval newApproval(
+  public static PatchSetApproval.Builder newApproval(
       PatchSet.Id psId, CurrentUser user, LabelId labelId, int value, Date when) {
-    PatchSetApproval psa =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(psId, user.getAccountId(), labelId),
-            Shorts.checkedCast(value),
-            when);
-    user.updateRealAccountId(psa::setRealAccountId);
-    return psa;
+    PatchSetApproval.Builder b =
+        PatchSetApproval.builder()
+            .key(PatchSetApproval.key(psId, user.getAccountId(), labelId))
+            .value(value)
+            .granted(when);
+    user.updateRealAccountId(b::realAccountId);
+    return b;
   }
 
   private static Iterable<PatchSetApproval> filterApprovals(
       Iterable<PatchSetApproval> psas, Account.Id accountId) {
-    return Iterables.filter(psas, a -> Objects.equals(a.getAccountId(), accountId));
+    return Iterables.filter(psas, a -> Objects.equals(a.accountId(), accountId));
   }
 
-  private final ApprovalCopier copier;
+  private final ApprovalInference approvalInference;
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
 
   @VisibleForTesting
   @Inject
   public ApprovalsUtil(
-      ApprovalCopier copier, PermissionBackend permissionBackend, ProjectCache projectCache) {
-    this.copier = copier;
+      ApprovalInference approvalInference,
+      PermissionBackend permissionBackend,
+      ProjectCache projectCache) {
+    this.approvalInference = approvalInference;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
   }
@@ -149,7 +148,7 @@
         update,
         labelTypes,
         change,
-        ps.getId(),
+        ps.id(),
         info.getAuthor().getAccount(),
         info.getCommitter().getAccount(),
         wantReviewers,
@@ -209,8 +208,11 @@
     LabelId labelId = Iterables.getLast(allTypes).getLabelId();
     for (Account.Id account : need) {
       cells.add(
-          new PatchSetApproval(
-              new PatchSetApproval.Key(psId, account, labelId), (short) 0, update.getWhen()));
+          PatchSetApproval.builder()
+              .key(PatchSetApproval.key(psId, account, labelId))
+              .value(0)
+              .granted(update.getWhen())
+              .build());
       update.putReviewer(account, REVIEWER);
     }
     return Collections.unmodifiableList(cells);
@@ -239,17 +241,28 @@
    * @param notes change notes.
    * @param update change update.
    * @param wantCCs accounts to CC.
+   * @param keepExistingReviewers whether provided accounts that are already reviewer should be kept
+   *     as reviewer or be downgraded to CC
    * @return whether a change was made.
    */
   public Collection<Account.Id> addCcs(
-      ChangeNotes notes, ChangeUpdate update, Collection<Account.Id> wantCCs) {
-    return addCcs(update, wantCCs, notes.load().getReviewers());
+      ChangeNotes notes,
+      ChangeUpdate update,
+      Collection<Account.Id> wantCCs,
+      boolean keepExistingReviewers) {
+    return addCcs(update, wantCCs, notes.load().getReviewers(), keepExistingReviewers);
   }
 
   private Collection<Account.Id> addCcs(
-      ChangeUpdate update, Collection<Account.Id> wantCCs, ReviewerSet existingReviewers) {
+      ChangeUpdate update,
+      Collection<Account.Id> wantCCs,
+      ReviewerSet existingReviewers,
+      boolean keepExistingReviewers) {
     Set<Account.Id> need = new LinkedHashSet<>(wantCCs);
-    need.removeAll(existingReviewers.all());
+    need.removeAll(existingReviewers.byState(CC));
+    if (keepExistingReviewers) {
+      need.removeAll(existingReviewers.byState(REVIEWER));
+    }
     need.removeAll(update.getReviewers().keySet());
     for (Account.Id account : need) {
       update.putReviewer(account, CC);
@@ -276,10 +289,10 @@
       throws RestApiException, PermissionBackendException {
     Account.Id accountId = user.getAccountId();
     checkArgument(
-        accountId.equals(ps.getUploader()),
+        accountId.equals(ps.uploader()),
         "expected user %s to match patch set uploader %s",
         accountId,
-        ps.getUploader());
+        ps.uploader());
     if (approvals.isEmpty()) {
       return ImmutableList.of();
     }
@@ -288,10 +301,10 @@
     Date ts = update.getWhen();
     for (Map.Entry<String, Short> vote : approvals.entrySet()) {
       LabelType lt = labelTypes.byLabel(vote.getKey());
-      cells.add(newApproval(ps.getId(), user, lt.getLabelId(), vote.getValue(), ts));
+      cells.add(newApproval(ps.id(), user, lt.getLabelId(), vote.getValue(), ts).build());
     }
     for (PatchSetApproval psa : cells) {
-      update.putApproval(psa.getLabel(), psa.getValue());
+      update.putApproval(psa.label(), psa.value());
     }
     return cells;
   }
@@ -329,7 +342,7 @@
 
   public Iterable<PatchSetApproval> byPatchSet(
       ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
-    return copier.getForPatchSet(notes, psId, rw, repoConfig);
+    return approvalInference.forPatchSet(notes, psId, rw, repoConfig);
   }
 
   public Iterable<PatchSetApproval> byPatchSetUser(
@@ -359,8 +372,8 @@
     }
     PatchSetApproval submitter = null;
     for (PatchSetApproval a : approvals) {
-      if (a.getPatchSetId().equals(c) && a.getValue() > 0 && a.isLegacySubmit()) {
-        if (submitter == null || a.getGranted().compareTo(submitter.getGranted()) > 0) {
+      if (a.patchSetId().equals(c) && a.value() > 0 && a.isLegacySubmit()) {
+        if (submitter == null || a.granted().compareTo(submitter.granted()) > 0) {
           submitter = a;
         }
       }
@@ -374,7 +387,7 @@
     if (!n.isEmpty()) {
       boolean first = true;
       for (Map.Entry<String, Short> e : n.entrySet()) {
-        if (c.containsKey(e.getKey()) && c.get(e.getKey()).getValue() == e.getValue()) {
+        if (c.containsKey(e.getKey()) && c.get(e.getKey()).value() == e.getValue()) {
           continue;
         }
         if (first) {
diff --git a/java/com/google/gerrit/server/AssigneeStatusUpdate.java b/java/com/google/gerrit/server/AssigneeStatusUpdate.java
new file mode 100644
index 0000000..3d6242b
--- /dev/null
+++ b/java/com/google/gerrit/server/AssigneeStatusUpdate.java
@@ -0,0 +1,35 @@
+// 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;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Account;
+import java.sql.Timestamp;
+import java.util.Optional;
+
+/** Change to an assignee's status. */
+@AutoValue
+public abstract class AssigneeStatusUpdate {
+  public static AssigneeStatusUpdate create(
+      Timestamp ts, Account.Id updatedBy, Optional<Account.Id> currentAssignee) {
+    return new AutoValue_AssigneeStatusUpdate(ts, updatedBy, currentAssignee);
+  }
+
+  public abstract Timestamp date();
+
+  public abstract Account.Id updatedBy();
+
+  public abstract Optional<Account.Id> currentAssignee();
+}
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 9e7f3dd..6675595 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -9,6 +9,11 @@
     "config/GerritGlobalModule.java",
 ]
 
+TESTING_SRC = [
+    "account/externalids/testing/ExternalIdInserter.java",
+    "account/externalids/testing/ExternalIdTestUtil.java",
+]
+
 java_library(
     name = "constants",
     srcs = CONSTANTS_SRC,
@@ -25,7 +30,7 @@
     name = "server",
     srcs = glob(
         ["**/*.java"],
-        exclude = CONSTANTS_SRC + GERRIT_GLOBAL_MODULE_SRC,
+        exclude = CONSTANTS_SRC + GERRIT_GLOBAL_MODULE_SRC + TESTING_SRC,
     ),
     resource_strip_prefix = "resources",
     resources = ["//resources/com/google/gerrit/server"],
@@ -34,6 +39,7 @@
         ":constants",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
@@ -47,7 +53,6 @@
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/prettify:server",
         "//java/com/google/gerrit/proto",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server/cache/serialize",
         "//java/com/google/gerrit/server/ioutil",
         "//java/com/google/gerrit/server/logging",
@@ -55,7 +60,6 @@
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/ssl",
-        "//java/com/google/gwtorm",
         "//java/org/apache/commons/net",
         "//lib:args4j",
         "//lib:autolink",
@@ -89,6 +93,8 @@
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
+        "//lib:jgit",
+        "//lib:jgit-archive",
         "//lib:jsch",
         "//lib:juniversalchardet",
         "//lib:mime-util",
@@ -110,8 +116,6 @@
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jsoup",
         "//lib/log:jsonevent-layout",
         "//lib/log:log4j",
@@ -138,12 +142,13 @@
         ":server",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/server/git/receive",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/restapi",
         "//lib:blame-cache",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:soy",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
 
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 97ba8f0..5f00b69 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -18,10 +18,10 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -72,10 +72,7 @@
     Account.Id accountId = user.isInternalUser() ? null : user.getAccountId();
     ChangeMessage m =
         new ChangeMessage(
-            new ChangeMessage.Key(psId.getParentKey(), ChangeUtil.messageUuid()),
-            accountId,
-            when,
-            psId);
+            ChangeMessage.key(psId.changeId(), ChangeUtil.messageUuid()), accountId, when, psId);
     m.setMessage(body);
     m.setTag(tag);
     user.updateRealAccountId(m::setRealAuthor);
@@ -127,7 +124,7 @@
       ChangeMessage message, AccountLoader accountLoader) {
     PatchSet.Id patchNum = message.getPatchSetId();
     ChangeMessageInfo cmi = new ChangeMessageInfo();
-    cmi.id = message.getKey().get();
+    cmi.id = message.getKey().uuid();
     cmi.author = accountLoader.get(message.getAuthor());
     cmi.date = message.getWrittenOn();
     cmi.message = message.getMessage();
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java
index b39b544..7a41fd1 100644
--- a/java/com/google/gerrit/server/ChangeUtil.java
+++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -20,10 +20,10 @@
 import com.google.common.collect.Ordering;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -48,7 +48,7 @@
   private static final BaseEncoding UUID_ENCODING = BaseEncoding.base16().lowerCase();
 
   public static final Ordering<PatchSet> PS_ID_ORDER =
-      Ordering.from(comparingInt(PatchSet::getPatchSetId));
+      Ordering.from(comparingInt(PatchSet::number));
 
   /** @return a new unique identifier for change message entities. */
   public static String messageUuid() {
@@ -91,7 +91,7 @@
     Set<PatchSet.Id> existing =
         changeRefNames
             .map(PatchSet.Id::fromRef)
-            .filter(psId -> psId != null && psId.getParentKey().equals(id.getParentKey()))
+            .filter(psId -> psId != null && psId.changeId().equals(id.changeId()))
             .collect(toSet());
     PatchSet.Id next = nextPatchSetId(id);
     while (existing.contains(next)) {
@@ -111,7 +111,7 @@
    * @return next patch set ID for the same change, incrementing by 1.
    */
   public static PatchSet.Id nextPatchSetId(PatchSet.Id id) {
-    return new PatchSet.Id(id.getParentKey(), id.get() + 1);
+    return PatchSet.id(id.changeId(), id.get() + 1);
   }
 
   /**
@@ -124,7 +124,7 @@
    */
   public static PatchSet.Id nextPatchSetId(Repository git, PatchSet.Id id) throws IOException {
     return nextPatchSetIdFromChangeRefs(
-        git.getRefDatabase().getRefsByPrefix(id.getParentKey().toRefPrefix()).stream()
+        git.getRefDatabase().getRefsByPrefix(id.changeId().toRefPrefix()).stream()
             .map(Ref::getName),
         id);
   }
diff --git a/java/com/google/gerrit/server/CmdLineParserModule.java b/java/com/google/gerrit/server/CmdLineParserModule.java
index d7f6e30..d943889 100644
--- a/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.args4j.AccountGroupIdHandler;
 import com.google.gerrit.server.args4j.AccountGroupUUIDHandler;
 import com.google.gerrit.server.args4j.AccountIdHandler;
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index a5332eb..ae4ba4b 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -19,22 +19,20 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ComparisonChain;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -50,7 +48,6 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 
@@ -92,7 +89,7 @@
       };
 
   public static PatchSet.Id getCommentPsId(Change.Id changeId, Comment comment) {
-    return new PatchSet.Id(changeId, comment.key.patchSetId);
+    return PatchSet.id(changeId, comment.key.patchSetId);
   }
 
   public static String extractMessageId(@Nullable String tag) {
@@ -131,7 +128,7 @@
         unresolved = false;
       } else {
         // Inherit unresolved value from inReplyTo comment if not specified.
-        Comment.Key key = new Comment.Key(parentUuid, path, psId.patchSetId);
+        Comment.Key key = new Comment.Key(parentUuid, path, psId.get());
         Optional<Comment> parent = getPublished(ctx.getNotes(), key);
         if (!parent.isPresent()) {
           throw new UnprocessableEntityException("Invalid parentUuid supplied for comment");
@@ -260,8 +257,7 @@
     return sort(comments);
   }
 
-  public void putComments(
-      ChangeUpdate update, PatchLineComment.Status status, Iterable<Comment> comments) {
+  public void putComments(ChangeUpdate update, Comment.Status status, Iterable<Comment> comments) {
     for (Comment c : comments) {
       update.putComment(status, c);
     }
@@ -306,22 +302,22 @@
     return sort(result);
   }
 
-  public static void setCommentRevId(Comment c, PatchListCache cache, Change change, PatchSet ps)
+  public static void setCommentCommitId(Comment c, PatchListCache cache, Change change, PatchSet ps)
       throws PatchListNotAvailableException {
     checkArgument(
-        c.key.patchSetId == ps.getId().get(),
-        "cannot set RevId for patch set %s on comment %s",
-        ps.getId(),
+        c.key.patchSetId == ps.id().get(),
+        "cannot set commit ID for patch set %s on comment %s",
+        ps.id(),
         c);
-    if (c.revId == null) {
+    if (c.getCommitId() == null) {
       if (Side.fromShort(c.side) == Side.PARENT) {
         if (c.side < 0) {
-          c.revId = ObjectId.toString(cache.getOldId(change, ps, -c.side));
+          c.setCommitId(cache.getOldId(change, ps, -c.side));
         } else {
-          c.revId = ObjectId.toString(cache.getOldId(change, ps, null));
+          c.setCommitId(cache.getOldId(change, ps, null));
         }
       } else {
-        c.revId = ps.getRevision().get();
+        c.setCommitId(ps.commitId());
       }
     }
   }
@@ -354,15 +350,4 @@
     comments.sort(COMMENT_ORDER);
     return comments;
   }
-
-  public static Iterable<PatchLineComment> toPatchLineComments(
-      Change.Id changeId, PatchLineComment.Status status, Iterable<Comment> comments) {
-    return FluentIterable.from(comments).transform(c -> PatchLineComment.from(changeId, status, c));
-  }
-
-  public static List<Comment> toComments(
-      final String serverId, Iterable<PatchLineComment> comments) {
-    return COMMENT_ORDER.sortedCopy(
-        FluentIterable.from(comments).transform(plc -> plc.asComment(serverId)));
-  }
 }
diff --git a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
index 27fb9d7..996257c 100644
--- a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
+++ b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
@@ -22,8 +22,8 @@
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.events.ChangeMergedListener;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
diff --git a/java/com/google/gerrit/server/CurrentUser.java b/java/com/google/gerrit/server/CurrentUser.java
index 03b9f54..75afc04 100644
--- a/java/com/google/gerrit/server/CurrentUser.java
+++ b/java/com/google/gerrit/server/CurrentUser.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.inject.servlet.RequestScoped;
diff --git a/java/com/google/gerrit/server/ExceptionHook.java b/java/com/google/gerrit/server/ExceptionHook.java
new file mode 100644
index 0000000..a0d98d2
--- /dev/null
+++ b/java/com/google/gerrit/server/ExceptionHook.java
@@ -0,0 +1,42 @@
+// 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;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Allows implementors to control how certain exceptions should be handled.
+ *
+ * <p>This interface is intended to be implemented for cluster setups with multiple primary nodes to
+ * control the behavior for handling exceptions that are thrown by a lower layer that handles the
+ * consensus and synchronization between different server nodes. E.g. if an operation fails because
+ * consensus for a Git update could not be achieved (e.g. due to slow responding server nodes) this
+ * interface can be used to retry the request instead of failing it immediately.
+ */
+@ExtensionPoint
+public interface ExceptionHook {
+  /**
+   * Whether an operation should be retried if it failed with the given throwable.
+   *
+   * <p>Only affects operations that are executed with {@link
+   * com.google.gerrit.server.update.RetryHelper}.
+   *
+   * @param throwable throwable that was thrown while executing the operation
+   * @return whether the operation should be retried
+   */
+  default boolean shouldRetry(Throwable throwable) {
+    return false;
+  }
+}
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index e65f562..7cafdc0 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -22,7 +22,7 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.GroupBackend;
@@ -234,7 +234,7 @@
         groupBackend,
         enableReverseDnsLookup,
         remotePeerProvider,
-        state.getAccount().getId(),
+        state.account().id(),
         realUser);
     this.state = state;
   }
@@ -323,15 +323,14 @@
    */
   @Override
   public Optional<String> getUserName() {
-    return state().getUserName();
+    return state().userName();
   }
 
   /** @return unique name of the user for logging, never {@code null} */
   @Override
   public String getLoggableName() {
     return getUserName()
-        .orElseGet(
-            () -> firstNonNull(getAccount().getPreferredEmail(), "a/" + getAccountId().get()));
+        .orElseGet(() -> firstNonNull(getAccount().preferredEmail(), "a/" + getAccountId().get()));
   }
 
   /**
@@ -340,7 +339,7 @@
    * @return the account of the identified user, an empty account if the account is missing
    */
   public Account getAccount() {
-    return state().getAccount();
+    return state().account();
   }
 
   public boolean hasEmailAddress(String email) {
@@ -377,7 +376,7 @@
   @Override
   public GroupMembership getEffectiveGroups() {
     if (effectiveGroups == null) {
-      if (authConfig.isIdentityTrustable(state().getExternalIds())) {
+      if (authConfig.isIdentityTrustable(state().externalIds())) {
         effectiveGroups = groupBackend.membershipsOf(this);
         logger.atFinest().log(
             "Known groups of %s: %s", getLoggableName(), lazy(effectiveGroups::getKnownGroups));
@@ -403,29 +402,29 @@
   public PersonIdent newRefLogIdent(Date when, TimeZone tz) {
     final Account ua = getAccount();
 
-    String name = ua.getFullName();
+    String name = ua.fullName();
     if (name == null || name.isEmpty()) {
-      name = ua.getPreferredEmail();
+      name = ua.preferredEmail();
     }
     if (name == null || name.isEmpty()) {
       name = anonymousCowardName;
     }
 
-    String user = getUserName().orElse("") + "|account-" + ua.getId().toString();
+    String user = getUserName().orElse("") + "|account-" + ua.id().toString();
     return new PersonIdent(name, user + "@" + guessHost(), when, tz);
   }
 
   public PersonIdent newCommitterIdent(Date when, TimeZone tz) {
     final Account ua = getAccount();
-    String name = ua.getFullName();
-    String email = ua.getPreferredEmail();
+    String name = ua.fullName();
+    String email = ua.preferredEmail();
 
     if (email == null || email.isEmpty()) {
       // No preferred email is configured. Use a generic identity so we
       // don't leak an address the user may have given us, but doesn't
       // necessarily want to publish through Git records.
       //
-      String user = getUserName().orElseGet(() -> "account-" + ua.getId().toString());
+      String user = getUserName().orElseGet(() -> "account-" + ua.id().toString());
 
       String host;
       if (canonicalUrl.get() != null) {
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index 2a78eb6..b53e666 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -20,14 +20,14 @@
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -39,6 +39,7 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -89,8 +90,8 @@
       PatchSet.Id psId,
       ObjectId commit,
       List<String> groups,
-      String pushCertificate,
-      String description)
+      @Nullable String pushCertificate,
+      @Nullable String description)
       throws IOException {
     requireNonNull(groups, "groups may not be null");
     ensurePatchSetMatches(psId, update);
@@ -99,20 +100,21 @@
     update.setPsDescription(description);
     update.setGroups(groups);
 
-    PatchSet ps = new PatchSet(psId);
-    ps.setRevision(new RevId(commit.name()));
-    ps.setUploader(update.getAccountId());
-    ps.setCreatedOn(new Timestamp(update.getWhen().getTime()));
-    ps.setGroups(groups);
-    ps.setPushCertificate(pushCertificate);
-    ps.setDescription(description);
-    return ps;
+    return PatchSet.builder()
+        .id(psId)
+        .commitId(commit)
+        .uploader(update.getAccountId())
+        .createdOn(new Timestamp(update.getWhen().getTime()))
+        .groups(groups)
+        .pushCertificate(Optional.ofNullable(pushCertificate))
+        .description(Optional.ofNullable(description))
+        .build();
   }
 
   private static void ensurePatchSetMatches(PatchSet.Id psId, ChangeUpdate update) {
-    Change.Id changeId = update.getChange().getId();
+    Change.Id changeId = update.getId();
     checkArgument(
-        psId.getParentKey().equals(changeId),
+        psId.changeId().equals(changeId),
         "cannot modify patch set %s on update for change %s",
         psId,
         changeId);
@@ -127,11 +129,6 @@
     }
   }
 
-  public void setGroups(ChangeUpdate update, PatchSet ps, List<String> groups) {
-    ps.setGroups(groups);
-    update.setGroups(groups);
-  }
-
   /** Check if the current patch set of the change is locked. */
   public void checkPatchSetNotLocked(ChangeNotes notes)
       throws IOException, ResourceConflictException {
@@ -155,10 +152,8 @@
     ApprovalsUtil approvalsUtil = approvalsUtilProvider.get();
     for (PatchSetApproval ap :
         approvalsUtil.byPatchSet(notes, change.currentPatchSetId(), null, null)) {
-      LabelType type = projectState.getLabelTypes(notes).byLabel(ap.getLabel());
-      if (type != null
-          && ap.getValue() == 1
-          && type.getFunction() == LabelFunction.PATCH_SET_LOCK) {
+      LabelType type = projectState.getLabelTypes(notes).byLabel(ap.label());
+      if (type != null && ap.value() == 1 && type.getFunction() == LabelFunction.PATCH_SET_LOCK) {
         return true;
       }
     }
@@ -169,7 +164,7 @@
   public RevCommit getRevCommit(Project.NameKey project, PatchSet patchSet) throws IOException {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
-      RevCommit src = rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
+      RevCommit src = rw.parseCommit(patchSet.commitId());
       rw.parseBody(src);
       return src;
     }
diff --git a/java/com/google/gerrit/server/ProjectUtil.java b/java/com/google/gerrit/server/ProjectUtil.java
index 1db4aa3..fa056b3 100644
--- a/java/com/google/gerrit/server/ProjectUtil.java
+++ b/java/com/google/gerrit/server/ProjectUtil.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -33,13 +33,14 @@
    * @throws RepositoryNotFoundException the repository of the branch's project does not exist.
    * @throws IOException error while retrieving the branch from the repository.
    */
-  public static boolean branchExists(final GitRepositoryManager repoManager, Branch.NameKey branch)
+  public static boolean branchExists(final GitRepositoryManager repoManager, BranchNameKey branch)
       throws RepositoryNotFoundException, IOException {
-    try (Repository repo = repoManager.openRepository(branch.getParentKey())) {
-      boolean exists = repo.getRefDatabase().exactRef(branch.get()) != null;
+    try (Repository repo = repoManager.openRepository(branch.project())) {
+      boolean exists = repo.getRefDatabase().exactRef(branch.branch()) != null;
       if (!exists) {
         exists =
-            repo.getFullBranch().equals(branch.get()) || RefNames.REFS_CONFIG.equals(branch.get());
+            repo.getFullBranch().equals(branch.branch())
+                || RefNames.REFS_CONFIG.equals(branch.branch());
       }
       return exists;
     }
diff --git a/java/com/google/gerrit/server/PublishCommentUtil.java b/java/com/google/gerrit/server/PublishCommentUtil.java
index ad93ef0..26539c5 100644
--- a/java/com/google/gerrit/server/PublishCommentUtil.java
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -15,16 +15,21 @@
 package com.google.gerrit.server;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED;
+import static com.google.gerrit.entities.Comment.Status;
 import static java.util.stream.Collectors.toSet;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentValidationFailure;
+import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -46,35 +51,55 @@
   }
 
   public void publish(
-      ChangeContext ctx, PatchSet.Id psId, Collection<Comment> drafts, @Nullable String tag) {
+      ChangeContext ctx,
+      PatchSet.Id psId,
+      Collection<Comment> draftComments,
+      @Nullable String tag) {
     ChangeNotes notes = ctx.getNotes();
     checkArgument(notes != null);
-    if (drafts.isEmpty()) {
+    if (draftComments.isEmpty()) {
       return;
     }
 
     Map<PatchSet.Id, PatchSet> patchSets =
-        psUtil.getAsMap(notes, drafts.stream().map(d -> psId(notes, d)).collect(toSet()));
-    for (Comment d : drafts) {
-      PatchSet ps = patchSets.get(psId(notes, d));
+        psUtil.getAsMap(notes, draftComments.stream().map(d -> psId(notes, d)).collect(toSet()));
+    for (Comment draftComment : draftComments) {
+      PatchSet.Id psIdOfDraftComment = psId(notes, draftComment);
+      PatchSet ps = patchSets.get(psIdOfDraftComment);
       if (ps == null) {
-        throw new StorageException("patch set " + ps + " not found");
+        throw new StorageException("patch set " + psIdOfDraftComment + " not found");
       }
-      d.writtenOn = ctx.getWhen();
-      d.tag = tag;
+      draftComment.writtenOn = ctx.getWhen();
+      draftComment.tag = tag;
       // Draft may have been created by a different real user; copy the current real user. (Only
       // applies to X-Gerrit-RunAs, since modifying drafts via on_behalf_of is not allowed.)
-      ctx.getUser().updateRealAccountId(d::setRealAuthor);
+      ctx.getUser().updateRealAccountId(draftComment::setRealAuthor);
       try {
-        CommentsUtil.setCommentRevId(d, patchListCache, notes.getChange(), ps);
+        CommentsUtil.setCommentCommitId(draftComment, patchListCache, notes.getChange(), ps);
       } catch (PatchListNotAvailableException e) {
         throw new StorageException(e);
       }
     }
-    commentsUtil.putComments(ctx.getUpdate(psId), PUBLISHED, drafts);
+    commentsUtil.putComments(ctx.getUpdate(psId), Status.PUBLISHED, draftComments);
   }
 
   private static PatchSet.Id psId(ChangeNotes notes, Comment c) {
-    return new PatchSet.Id(notes.getChangeId(), c.key.patchSetId);
+    return PatchSet.id(notes.getChangeId(), c.key.patchSetId);
+  }
+
+  /**
+   * Helper to run the specified set of {@link CommentValidator}-s on the specified comments.
+   *
+   * @return See {@link CommentValidator#validateComments(ImmutableList)}.
+   */
+  public static ImmutableList<CommentValidationFailure> findInvalidComments(
+      PluginSetContext<CommentValidator> commentValidators,
+      ImmutableList<CommentForValidation> commentsForValidation) {
+    ImmutableList.Builder<CommentValidationFailure> commentValidationFailures =
+        new ImmutableList.Builder<>();
+    commentValidators.runEach(
+        listener ->
+            commentValidationFailures.addAll(listener.validateComments(commentsForValidation)));
+    return commentValidationFailures.build();
   }
 }
diff --git a/java/com/google/gerrit/server/RequestInfo.java b/java/com/google/gerrit/server/RequestInfo.java
new file mode 100644
index 0000000..f369239
--- /dev/null
+++ b/java/com/google/gerrit/server/RequestInfo.java
@@ -0,0 +1,96 @@
+// 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;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.logging.TraceContext;
+import java.util.Optional;
+
+/** Information about a request that was received from a user. */
+@AutoValue
+public abstract class RequestInfo {
+  /** Channel through which a user request was received. */
+  public enum RequestType {
+    /** request type for git push */
+    GIT_RECEIVE,
+
+    /** request type for git fetch */
+    GIT_UPLOAD,
+
+    /** request type for call to REST API */
+    REST,
+
+    /** request type for call to SSH API */
+    SSH
+  }
+
+  /**
+   * Type of the request, telling through which channel the request was coming in.
+   *
+   * <p>See {@link RequestType} for the types that are used by Gerrit core. Other request types are
+   * possible, e.g. if a plugin supports receiving requests through another channel.
+   */
+  public abstract String requestType();
+
+  /**
+   * Request URI.
+   *
+   * <p>Only set if request type is {@link RequestType#REST}.
+   *
+   * <p>Never includes the "/a" prefix.
+   */
+  public abstract Optional<String> requestUri();
+
+  /** The user that has sent the request. */
+  public abstract CurrentUser callingUser();
+
+  /** The trace context of the request. */
+  public abstract TraceContext traceContext();
+
+  /**
+   * The name of the project for which the request is being done. Only available if the request is
+   * tied to a project or change. If a project is available it's not guaranteed that it actually
+   * exists (e.g. if a user made a request for a project that doesn't exist).
+   */
+  public abstract Optional<Project.NameKey> project();
+
+  public static RequestInfo.Builder builder(
+      RequestType requestType, CurrentUser callingUser, TraceContext traceContext) {
+    return new AutoValue_RequestInfo.Builder()
+        .requestType(requestType)
+        .callingUser(callingUser)
+        .traceContext(traceContext);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder requestType(String requestType);
+
+    public Builder requestType(RequestType requestType) {
+      return requestType(requestType.name());
+    }
+
+    public abstract Builder requestUri(String requestUri);
+
+    public abstract Builder callingUser(CurrentUser callingUser);
+
+    public abstract Builder traceContext(TraceContext traceContext);
+
+    public abstract Builder project(Project.NameKey projectName);
+
+    public abstract RequestInfo build();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/client/CodedEnum.java b/java/com/google/gerrit/server/RequestListener.java
similarity index 68%
copy from java/com/google/gerrit/reviewdb/client/CodedEnum.java
copy to java/com/google/gerrit/server/RequestListener.java
index 11e7efa..461b91a 100644
--- a/java/com/google/gerrit/reviewdb/client/CodedEnum.java
+++ b/java/com/google/gerrit/server/RequestListener.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// 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.
@@ -12,9 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.server;
 
-/** Extension of Enum which provides distinct character code values. */
-public interface CodedEnum {
-  char getCode();
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public interface RequestListener {
+  void onRequest(RequestInfo requestInfo);
 }
diff --git a/java/com/google/gerrit/server/ReviewerSet.java b/java/com/google/gerrit/server/ReviewerSet.java
index f36e3ab..0f6bf29 100644
--- a/java/com/google/gerrit/server/ReviewerSet.java
+++ b/java/com/google/gerrit/server/ReviewerSet.java
@@ -22,8 +22,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableTable;
 import com.google.common.collect.Table;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import java.sql.Timestamp;
 
@@ -44,18 +44,14 @@
         first = psa;
       } else {
         checkArgument(
-            first
-                .getKey()
-                .getParentKey()
-                .getParentKey()
-                .equals(psa.getKey().getParentKey().getParentKey()),
+            first.key().patchSetId().changeId().equals(psa.key().patchSetId().changeId()),
             "multiple change IDs: %s, %s",
-            first.getKey(),
-            psa.getKey());
+            first.key(),
+            psa.key());
       }
-      Account.Id id = psa.getAccountId();
-      reviewers.put(REVIEWER, id, psa.getGranted());
-      if (psa.getValue() != 0) {
+      Account.Id id = psa.accountId();
+      reviewers.put(REVIEWER, id, psa.granted());
+      if (psa.value() != 0) {
         reviewers.remove(CC, id);
       }
     }
diff --git a/java/com/google/gerrit/server/ReviewerStatusUpdate.java b/java/com/google/gerrit/server/ReviewerStatusUpdate.java
index c4f3e2a..938d985 100644
--- a/java/com/google/gerrit/server/ReviewerStatusUpdate.java
+++ b/java/com/google/gerrit/server/ReviewerStatusUpdate.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server;
 
 import com.google.auto.value.AutoValue;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import java.sql.Timestamp;
 
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 9020025..ad5f0aa 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -31,18 +31,20 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.git.GitUpdateFailureException;
 import com.google.gerrit.git.LockFailureException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -89,7 +91,7 @@
         if (id == null) {
           return null;
         }
-        Account.Id accountId = new Account.Id(id);
+        Account.Id accountId = Account.id(id);
         String label = s.substring(p + 1);
         return create(accountId, label);
       }
@@ -234,7 +236,16 @@
     }
   }
 
-  public void unstarAll(Project.NameKey project, Change.Id changeId) {
+  /**
+   * Unstar the given change for all users.
+   *
+   * <p>Intended for use only when we're about to delete a change. For that reason, the change is
+   * not reindexed.
+   *
+   * @param changeId change ID.
+   * @throws IOException if an error occurred.
+   */
+  public void unstarAllForChangeDeletion(Change.Id changeId) throws IOException {
     try (Repository repo = repoManager.openRepository(allUsers);
         RevWalk rw = new RevWalk(repo)) {
       BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
@@ -244,7 +255,9 @@
       for (Account.Id accountId : byChangeFromIndex(changeId).keySet()) {
         String refName = RefNames.refsStarredChanges(changeId, accountId);
         Ref ref = repo.getRefDatabase().exactRef(refName);
-        batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), refName));
+        if (ref != null) {
+          batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), refName));
+        }
       }
       batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
       for (ReceiveCommand command : batchUpdate.getCommands()) {
@@ -256,12 +269,9 @@
           if (command.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
             throw new LockFailureException(message, batchUpdate);
           }
-          throw new IOException(message);
+          throw new GitUpdateFailureException(message, batchUpdate);
         }
       }
-      indexer.index(project, changeId);
-    } catch (IOException e) {
-      throw new StorageException(String.format("Unstar change %d failed", changeId.get()), e);
     }
   }
 
@@ -273,7 +283,7 @@
         if (id == null) {
           continue;
         }
-        Account.Id accountId = new Account.Id(id);
+        Account.Id accountId = Account.id(id);
         builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
       }
       return builder.build();
@@ -375,7 +385,9 @@
   }
 
   public static StarRef readLabels(Repository repo, String refName) throws IOException {
-    try (TraceTimer traceTimer = TraceContext.newTimer("Read star labels from %s", refName)) {
+    try (TraceTimer traceTimer =
+        TraceContext.newTimer(
+            "Read star labels", Metadata.builder().noteDbRefName(refName).build())) {
       Ref ref = repo.exactRef(refName);
       return readLabels(repo, ref);
     }
@@ -386,7 +398,8 @@
       return StarRef.MISSING;
     }
     try (TraceTimer traceTimer =
-            TraceContext.newTimer("Read star labels from %s (without ref lookup)", ref.getName());
+            TraceContext.newTimer(
+                String.format("Read star labels from %s (without ref lookup)", ref.getName()));
         ObjectReader reader = repo.newObjectReader()) {
       ObjectLoader obj = reader.open(ref.getObjectId(), Constants.OBJ_BLOB);
       return StarRef.create(
@@ -454,7 +467,9 @@
       Repository repo, String refName, ObjectId oldObjectId, Collection<String> labels)
       throws IOException, InvalidLabelsException {
     try (TraceTimer traceTimer =
-            TraceContext.newTimer("Update star labels in %s (labels=%s)", refName, labels);
+            TraceContext.newTimer(
+                "Update star labels",
+                Metadata.builder().noteDbRefName(refName).resourceCount(labels.size()).build());
         RevWalk rw = new RevWalk(repo)) {
       RefUpdate u = repo.updateRef(refName);
       u.setExpectedOldObjectId(oldObjectId);
@@ -491,7 +506,9 @@
       return;
     }
 
-    try (TraceTimer traceTimer = TraceContext.newTimer("Delete star labels in %s", refName)) {
+    try (TraceTimer traceTimer =
+        TraceContext.newTimer(
+            "Delete star labels", Metadata.builder().noteDbRefName(refName).build())) {
       RefUpdate u = repo.updateRef(refName);
       u.setForceUpdate(true);
       u.setExpectedOldObjectId(oldObjectId);
diff --git a/java/com/google/gerrit/server/TraceRequestListener.java b/java/com/google/gerrit/server/TraceRequestListener.java
new file mode 100644
index 0000000..20c9f57
--- /dev/null
+++ b/java/com/google/gerrit/server/TraceRequestListener.java
@@ -0,0 +1,228 @@
+// 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;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.RequestId;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Request listener that sets additional logging tags and enables tracing automatically if the
+ * request matches any tracing configuration in gerrit.config (see description of
+ * 'tracing.<trace-id>' subsection in config-gerrit.txt).
+ */
+@Singleton
+public class TraceRequestListener implements RequestListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final Config cfg;
+  private final ImmutableList<TraceConfig> traceConfigs;
+
+  @Inject
+  TraceRequestListener(@GerritServerConfig Config cfg) {
+    this.cfg = cfg;
+    this.traceConfigs = parseTraceConfigs();
+  }
+
+  @Override
+  public void onRequest(RequestInfo requestInfo) {
+    requestInfo.project().ifPresent(p -> requestInfo.traceContext().addTag("project", p));
+    traceConfigs.stream()
+        .filter(traceConfig -> traceConfig.matches(requestInfo))
+        .forEach(
+            traceConfig ->
+                requestInfo
+                    .traceContext()
+                    .forceLogging()
+                    .addTag(RequestId.Type.TRACE_ID, traceConfig.traceId()));
+  }
+
+  private ImmutableList<TraceConfig> parseTraceConfigs() {
+    ImmutableList.Builder<TraceConfig> traceConfigs = ImmutableList.builder();
+
+    for (String traceId : cfg.getSubsections("tracing")) {
+      try {
+        TraceConfig.Builder traceConfig = TraceConfig.builder();
+        traceConfig.traceId(traceId);
+        traceConfig.requestTypes(parseRequestTypes(traceId));
+        traceConfig.requestUriPatterns(parseRequestUriPatterns(traceId));
+        traceConfig.accountIds(parseAccounts(traceId));
+        traceConfig.projectPatterns(parseProjectPatterns(traceId));
+        traceConfigs.add(traceConfig.build());
+      } catch (ConfigInvalidException e) {
+        logger.atWarning().log("Ignoring invalid tracing configuration:\n %s", e.getMessage());
+      }
+    }
+
+    return traceConfigs.build();
+  }
+
+  private ImmutableSet<String> parseRequestTypes(String traceId) {
+    return ImmutableSet.copyOf(cfg.getStringList("tracing", traceId, "requestType"));
+  }
+
+  private ImmutableSet<Pattern> parseRequestUriPatterns(String traceId)
+      throws ConfigInvalidException {
+    return parsePatterns(traceId, "requestUriPattern");
+  }
+
+  private ImmutableSet<Account.Id> parseAccounts(String traceId) throws ConfigInvalidException {
+    ImmutableSet.Builder<Account.Id> accountIds = ImmutableSet.builder();
+    String[] accounts = cfg.getStringList("tracing", traceId, "account");
+    for (String account : accounts) {
+      Optional<Account.Id> accountId = Account.Id.tryParse(account);
+      if (!accountId.isPresent()) {
+        throw new ConfigInvalidException(
+            String.format(
+                "Invalid tracing config ('tracing.%s.account = %s'): invalid account ID",
+                traceId, account));
+      }
+      accountIds.add(accountId.get());
+    }
+    return accountIds.build();
+  }
+
+  private ImmutableSet<Pattern> parseProjectPatterns(String traceId) throws ConfigInvalidException {
+    return parsePatterns(traceId, "projectPattern");
+  }
+
+  private ImmutableSet<Pattern> parsePatterns(String traceId, String name)
+      throws ConfigInvalidException {
+    ImmutableSet.Builder<Pattern> patterns = ImmutableSet.builder();
+    String[] patternRegExs = cfg.getStringList("tracing", traceId, name);
+    for (String patternRegEx : patternRegExs) {
+      try {
+        patterns.add(Pattern.compile(patternRegEx));
+      } catch (PatternSyntaxException e) {
+        throw new ConfigInvalidException(
+            String.format(
+                "Invalid tracing config ('tracing.%s.%s = %s'): %s",
+                traceId, name, patternRegEx, e.getMessage()));
+      }
+    }
+    return patterns.build();
+  }
+
+  @AutoValue
+  abstract static class TraceConfig {
+    /** ID for the trace */
+    abstract String traceId();
+
+    /** request types that should be traced */
+    abstract ImmutableSet<String> requestTypes();
+
+    /** pattern matching request URIs */
+    abstract ImmutableSet<Pattern> requestUriPatterns();
+
+    /** accounts IDs matching calling user */
+    abstract ImmutableSet<Account.Id> accountIds();
+
+    /** pattern matching projects names */
+    abstract ImmutableSet<Pattern> projectPatterns();
+
+    static Builder builder() {
+      return new AutoValue_TraceRequestListener_TraceConfig.Builder();
+    }
+
+    /**
+     * Whether this trace config matches a given request.
+     *
+     * @param requestInfo request info
+     * @return whether this trace config matches
+     */
+    boolean matches(RequestInfo requestInfo) {
+      // If in the trace config request types are set and none of them matches, then the request is
+      // not matched.
+      if (!requestTypes().isEmpty()
+          && requestTypes().stream()
+              .noneMatch(type -> type.equalsIgnoreCase(requestInfo.requestType()))) {
+        return false;
+      }
+
+      // If in the trace config request URI patterns are set and none of them matches, then the
+      // request is not matched.
+      if (!requestUriPatterns().isEmpty()) {
+        if (!requestInfo.requestUri().isPresent()) {
+          // The request has no request URI, hence it cannot match a request URI pattern.
+          return false;
+        }
+
+        if (requestUriPatterns().stream()
+            .noneMatch(p -> p.matcher(requestInfo.requestUri().get()).matches())) {
+          return false;
+        }
+      }
+
+      // If in the trace config accounts are set and none of them matches, then the request is not
+      // matched.
+      if (!accountIds().isEmpty()) {
+        try {
+          if (accountIds().stream()
+              .noneMatch(id -> id.equals(requestInfo.callingUser().getAccountId()))) {
+            return false;
+          }
+        } catch (UnsupportedOperationException e) {
+          // The calling user is not logged in, hence it cannot match an account.
+          return false;
+        }
+      }
+
+      // If in the trace config project patterns are set and none of them matches, then the request
+      // is not matched.
+      if (!projectPatterns().isEmpty()) {
+        if (!requestInfo.project().isPresent()) {
+          // The request is not for a project, hence it cannot match a project pattern.
+          return false;
+        }
+
+        if (projectPatterns().stream()
+            .noneMatch(p -> p.matcher(requestInfo.project().get().get()).matches())) {
+          return false;
+        }
+      }
+
+      // For any match criteria (request type, request URI pattern, account, project pattern) that
+      // was specified in the trace config, at least one of the configured value matched the
+      // request.
+      return true;
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+      abstract Builder traceId(String traceId);
+
+      abstract Builder requestTypes(ImmutableSet<String> requestTypes);
+
+      abstract Builder requestUriPatterns(ImmutableSet<Pattern> requestUriPatterns);
+
+      abstract Builder accountIds(ImmutableSet<Account.Id> accountIds);
+
+      abstract Builder projectPatterns(ImmutableSet<Pattern> projectPatterns);
+
+      abstract TraceConfig build();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index 589344c..88b0b21 100644
--- a/java/com/google/gerrit/server/WebLinks.java
+++ b/java/com/google/gerrit/server/WebLinks.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server;
 
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.common.base.Strings;
-import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.DiffWebLinkInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -31,12 +34,10 @@
 import com.google.gerrit.extensions.webui.ProjectWebLink;
 import com.google.gerrit.extensions.webui.TagWebLink;
 import com.google.gerrit.extensions.webui.WebLink;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.util.Collections;
-import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
 
 @Singleton
 public class WebLinks {
@@ -87,7 +88,7 @@
    * @param commit SHA1 of commit.
    * @return Links for patch sets.
    */
-  public List<WebLinkInfo> getPatchSetLinks(Project.NameKey project, String commit) {
+  public ImmutableList<WebLinkInfo> getPatchSetLinks(Project.NameKey project, String commit) {
     return filterLinks(patchSetLinks, webLink -> webLink.getPatchSetWebLink(project.get(), commit));
   }
 
@@ -96,7 +97,7 @@
    * @param revision SHA1 of the parent revision.
    * @return Links for patch sets.
    */
-  public List<WebLinkInfo> getParentLinks(Project.NameKey project, String revision) {
+  public ImmutableList<WebLinkInfo> getParentLinks(Project.NameKey project, String revision) {
     return filterLinks(parentLinks, webLink -> webLink.getParentWebLink(project.get(), revision));
   }
 
@@ -106,9 +107,9 @@
    * @param file File name.
    * @return Links for files.
    */
-  public List<WebLinkInfo> getFileLinks(String project, String revision, String file) {
+  public ImmutableList<WebLinkInfo> getFileLinks(String project, String revision, String file) {
     return Patch.isMagic(file)
-        ? Collections.emptyList()
+        ? ImmutableList.of()
         : filterLinks(fileLinks, webLink -> webLink.getFileWebLink(project, revision, file));
   }
 
@@ -118,14 +119,15 @@
    * @param file File name.
    * @return Links for file history
    */
-  public List<WebLinkInfo> getFileHistoryLinks(String project, String revision, String file) {
+  public ImmutableList<WebLinkInfo> getFileHistoryLinks(
+      String project, String revision, String file) {
     if (Patch.isMagic(file)) {
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
-    return FluentIterable.from(fileHistoryLinks)
-        .transform(webLink -> webLink.getFileHistoryWebLink(project, revision, file))
+    return Streams.stream(fileHistoryLinks)
+        .map(webLink -> webLink.getFileHistoryWebLink(project, revision, file))
         .filter(INVALID_WEBLINK)
-        .toList();
+        .collect(toImmutableList());
   }
 
   /**
@@ -138,20 +140,20 @@
    * @param fileB File name of side B.
    * @return Links for file diffs.
    */
-  public List<DiffWebLinkInfo> getDiffLinks(
-      final String project,
-      final int changeId,
-      final Integer patchSetIdA,
-      final String revisionA,
-      final String fileA,
-      final int patchSetIdB,
-      final String revisionB,
-      final String fileB) {
+  public ImmutableList<DiffWebLinkInfo> getDiffLinks(
+      String project,
+      int changeId,
+      Integer patchSetIdA,
+      String revisionA,
+      String fileA,
+      int patchSetIdB,
+      String revisionB,
+      String fileB) {
     if (Patch.isMagic(fileA) || Patch.isMagic(fileB)) {
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
-    return FluentIterable.from(diffLinks)
-        .transform(
+    return Streams.stream(diffLinks)
+        .map(
             webLink ->
                 webLink.getDiffLink(
                     project,
@@ -163,14 +165,14 @@
                     revisionB,
                     fileB))
         .filter(INVALID_WEBLINK)
-        .toList();
+        .collect(toImmutableList());
   }
 
   /**
    * @param project Project name.
    * @return Links for projects.
    */
-  public List<WebLinkInfo> getProjectLinks(String project) {
+  public ImmutableList<WebLinkInfo> getProjectLinks(String project) {
     return filterLinks(projectLinks, webLink -> webLink.getProjectWeblink(project));
   }
 
@@ -179,7 +181,7 @@
    * @param branch Branch name
    * @return Links for branches.
    */
-  public List<WebLinkInfo> getBranchLinks(String project, String branch) {
+  public ImmutableList<WebLinkInfo> getBranchLinks(String project, String branch) {
     return filterLinks(branchLinks, webLink -> webLink.getBranchWebLink(project, branch));
   }
 
@@ -188,12 +190,15 @@
    * @param tag Tag name
    * @return Links for tags.
    */
-  public List<WebLinkInfo> getTagLinks(String project, String tag) {
+  public ImmutableList<WebLinkInfo> getTagLinks(String project, String tag) {
     return filterLinks(tagLinks, webLink -> webLink.getTagWebLink(project, tag));
   }
 
-  private <T extends WebLink> List<WebLinkInfo> filterLinks(
+  private <T extends WebLink> ImmutableList<WebLinkInfo> filterLinks(
       DynamicSet<T> links, Function<T, WebLinkInfo> transformer) {
-    return FluentIterable.from(links).transform(transformer).filter(INVALID_WEBLINK).toList();
+    return Streams.stream(links)
+        .map(transformer)
+        .filter(INVALID_WEBLINK)
+        .collect(toImmutableList());
   }
 }
diff --git a/java/com/google/gerrit/server/account/AbstractGroupBackend.java b/java/com/google/gerrit/server/account/AbstractGroupBackend.java
index b50b003..93241d6 100644
--- a/java/com/google/gerrit/server/account/AbstractGroupBackend.java
+++ b/java/com/google/gerrit/server/account/AbstractGroupBackend.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 
 public abstract class AbstractGroupBackend implements GroupBackend {
   @Override
diff --git a/java/com/google/gerrit/server/account/AbstractRealm.java b/java/com/google/gerrit/server/account/AbstractRealm.java
index e61736d..380001d 100644
--- a/java/com/google/gerrit/server/account/AbstractRealm.java
+++ b/java/com/google/gerrit/server/account/AbstractRealm.java
@@ -53,7 +53,7 @@
 
   @Override
   public boolean hasEmailAddress(IdentifiedUser user, String email) {
-    for (ExternalId ext : user.state().getExternalIds()) {
+    for (ExternalId ext : user.state().externalIds()) {
       if (email != null && email.equalsIgnoreCase(ext.email())) {
         return true;
       }
@@ -63,7 +63,7 @@
 
   @Override
   public Set<String> getEmailAddresses(IdentifiedUser user) {
-    Collection<ExternalId> ids = user.state().getExternalIds();
+    Collection<ExternalId> ids = user.state().externalIds();
     Set<String> emails = Sets.newHashSetWithExpectedSize(ids.size());
     for (ExternalId ext : ids) {
       if (!Strings.isNullOrEmpty(ext.email())) {
diff --git a/java/com/google/gerrit/server/account/AccountCache.java b/java/com/google/gerrit/server/account/AccountCache.java
index 17493bf..47cf25b 100644
--- a/java/com/google/gerrit/server/account/AccountCache.java
+++ b/java/com/google/gerrit/server/account/AccountCache.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index acd7bd3..ef4e1c0 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -21,12 +21,12 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -59,7 +59,7 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        cache(BYID_NAME, Account.Id.class, new TypeLiteral<Optional<AccountState>>() {})
+        cache(BYID_NAME, Account.Id.class, new TypeLiteral<AccountState>() {})
             .loader(ByIdLoader.class);
 
         bind(AccountCacheImpl.class);
@@ -68,18 +68,15 @@
     };
   }
 
-  private final AllUsersName allUsersName;
   private final ExternalIds externalIds;
-  private final LoadingCache<Account.Id, Optional<AccountState>> byId;
+  private final LoadingCache<Account.Id, AccountState> byId;
   private final ExecutorService executor;
 
   @Inject
   AccountCacheImpl(
-      AllUsersName allUsersName,
       ExternalIds externalIds,
-      @Named(BYID_NAME) LoadingCache<Account.Id, Optional<AccountState>> byId,
+      @Named(BYID_NAME) LoadingCache<Account.Id, AccountState> byId,
       @FanOutExecutor ExecutorService executor) {
-    this.allUsersName = allUsersName;
     this.externalIds = externalIds;
     this.byId = byId;
     this.executor = executor;
@@ -88,9 +85,11 @@
   @Override
   public AccountState getEvenIfMissing(Account.Id accountId) {
     try {
-      return byId.get(accountId).orElse(missing(accountId));
+      return byId.get(accountId);
     } catch (ExecutionException e) {
-      logger.atWarning().withCause(e).log("Cannot load AccountState for %s", accountId);
+      if (!(e.getCause() instanceof AccountNotFoundException)) {
+        logger.atWarning().withCause(e).log("Cannot load AccountState for %s", accountId);
+      }
       return missing(accountId);
     }
   }
@@ -98,9 +97,11 @@
   @Override
   public Optional<AccountState> get(Account.Id accountId) {
     try {
-      return byId.get(accountId);
+      return Optional.ofNullable(byId.get(accountId));
     } catch (ExecutionException e) {
-      logger.atWarning().withCause(e).log("Cannot load AccountState for ID %s", accountId);
+      if (!(e.getCause() instanceof AccountNotFoundException)) {
+        logger.atWarning().withCause(e).log("Cannot load AccountState for %s", accountId);
+      }
       return Optional.empty();
     }
   }
@@ -110,10 +111,10 @@
     Map<Account.Id, AccountState> accountStates = new HashMap<>(accountIds.size());
     List<Callable<Optional<AccountState>>> callables = new ArrayList<>();
     for (Account.Id accountId : accountIds) {
-      Optional<AccountState> state = byId.getIfPresent(accountId);
+      AccountState state = byId.getIfPresent(accountId);
       if (state != null) {
         // The value is in-memory, so we just get the state
-        state.ifPresent(s -> accountStates.put(accountId, s));
+        accountStates.put(accountId, state);
       } else {
         // Queue up a callable so that we can load accounts in parallel
         callables.add(() -> get(accountId));
@@ -132,7 +133,7 @@
     }
     for (Future<Optional<AccountState>> f : futures) {
       try {
-        f.get().ifPresent(s -> accountStates.put(s.getAccount().getId(), s));
+        f.get().ifPresent(s -> accountStates.put(s.account().id(), s));
       } catch (InterruptedException | ExecutionException e) {
         logger.atSevere().withCause(e).log("Cannot load AccountState");
       }
@@ -168,12 +169,12 @@
   }
 
   private AccountState missing(Account.Id accountId) {
-    Account account = new Account(accountId, TimeUtil.nowTs());
+    Account.Builder account = Account.builder(accountId, TimeUtil.nowTs());
     account.setActive(false);
-    return AccountState.forAccount(allUsersName, account);
+    return AccountState.forAccount(account.build());
   }
 
-  static class ByIdLoader extends CacheLoader<Account.Id, Optional<AccountState>> {
+  static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
     private final Accounts accounts;
 
     @Inject
@@ -182,10 +183,23 @@
     }
 
     @Override
-    public Optional<AccountState> load(Account.Id who) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading account %s", who)) {
-        return accounts.get(who);
+    public AccountState load(Account.Id who) throws Exception {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading account", Metadata.builder().accountId(who.get()).build())) {
+        return accounts
+            .get(who)
+            .orElseThrow(() -> new AccountNotFoundException(who + " not found"));
       }
     }
   }
+
+  /** Signals that the account was not found in the primary storage. */
+  private static class AccountNotFoundException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public AccountNotFoundException(String message) {
+      super(message);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index 06f7a08..5a1bb8a 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -21,12 +21,12 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
 import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -68,7 +68,7 @@
  *   <li>'account.config': Contains the account properties. Parsing and writing it is delegated to
  *       {@link AccountProperties}.
  *   <li>'preferences.config': Contains the preferences. Parsing and writing it is delegated to
- *       {@link Preferences}.
+ *       {@link StoredPreferences}.
  *   <li>'account.config': Contains the project watches. Parsing and writing it is delegated to
  *       {@link ProjectWatches}.
  * </ul>
@@ -85,7 +85,7 @@
   private Optional<AccountProperties> loadedAccountProperties;
   private Optional<ObjectId> externalIdsRev;
   private ProjectWatches projectWatches;
-  private Preferences preferences;
+  private StoredPreferences preferences;
   private Optional<InternalAccountUpdate> accountUpdate = Optional.empty();
   private List<ValidationError> validationErrors;
 
@@ -122,7 +122,7 @@
    * Returns the revision of the {@code refs/meta/external-ids} branch.
    *
    * <p>This revision can be used to load the external IDs of the loaded account lazily via {@link
-   * ExternalIds#byAccount(com.google.gerrit.reviewdb.client.Account.Id, ObjectId)}.
+   * ExternalIds#byAccount(com.google.gerrit.entities.Account.Id, ObjectId)}.
    *
    * @return revision of the {@code refs/meta/external-ids} branch, {@link Optional#empty()} if no
    *     {@code refs/meta/external-ids} branch exists
@@ -184,14 +184,14 @@
     checkLoaded();
     this.loadedAccountProperties =
         Optional.of(
-            new AccountProperties(account.getId(), account.getRegisteredOn(), new Config(), null));
+            new AccountProperties(account.id(), account.registeredOn(), new Config(), null));
     this.accountUpdate =
         Optional.of(
             InternalAccountUpdate.builder()
                 .setActive(account.isActive())
-                .setFullName(account.getFullName())
-                .setPreferredEmail(account.getPreferredEmail())
-                .setStatus(account.getStatus())
+                .setFullName(account.fullName())
+                .setPreferredEmail(account.preferredEmail())
+                .setStatus(account.status())
                 .build());
     return this;
   }
@@ -242,10 +242,10 @@
       projectWatches = new ProjectWatches(accountId, readConfig(ProjectWatches.WATCH_CONFIG), this);
 
       preferences =
-          new Preferences(
+          new StoredPreferences(
               accountId,
-              readConfig(Preferences.PREFERENCES_CONFIG),
-              Preferences.readDefaultConfig(allUsersName, repo),
+              readConfig(StoredPreferences.PREFERENCES_CONFIG),
+              StoredPreferences.readDefaultConfig(allUsersName, repo),
               this);
 
       projectWatches.parse();
@@ -256,8 +256,11 @@
       projectWatches = new ProjectWatches(accountId, new Config(), this);
 
       preferences =
-          new Preferences(
-              accountId, new Config(), Preferences.readDefaultConfig(allUsersName, repo), this);
+          new StoredPreferences(
+              accountId,
+              new Config(),
+              StoredPreferences.readDefaultConfig(allUsersName, repo),
+              this);
     }
 
     Ref externalIdsRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
@@ -331,7 +334,7 @@
     }
 
     saveConfig(
-        Preferences.PREFERENCES_CONFIG,
+        StoredPreferences.PREFERENCES_CONFIG,
         preferences.saveGeneralPreferences(
             accountUpdate.get().getGeneralPreferences(),
             accountUpdate.get().getDiffPreferences(),
diff --git a/java/com/google/gerrit/server/account/AccountControl.java b/java/com/google/gerrit/server/account/AccountControl.java
index 4b8be81..f8a5c5c 100644
--- a/java/com/google/gerrit/server/account/AccountControl.java
+++ b/java/com/google/gerrit/server/account/AccountControl.java
@@ -17,11 +17,11 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.group.SystemGroupBackend;
@@ -133,7 +133,7 @@
         new OtherUser() {
           @Override
           Account.Id getId() {
-            return otherUser.getAccount().getId();
+            return otherUser.account().id();
           }
 
           @Override
diff --git a/java/com/google/gerrit/server/account/AccountDeactivator.java b/java/com/google/gerrit/server/account/AccountDeactivator.java
index 3c33c98..ea327f8 100644
--- a/java/com/google/gerrit/server/account/AccountDeactivator.java
+++ b/java/com/google/gerrit/server/account/AccountDeactivator.java
@@ -104,15 +104,15 @@
   }
 
   private boolean processAccount(AccountState accountState) {
-    if (!accountState.getUserName().isPresent()) {
+    if (!accountState.userName().isPresent()) {
       return false;
     }
 
-    String userName = accountState.getUserName().get();
+    String userName = accountState.userName().get();
     logger.atFine().log("processing account %s", userName);
     try {
-      if (realm.accountBelongsToRealm(accountState.getExternalIds()) && !realm.isActive(userName)) {
-        sif.deactivate(accountState.getAccount().getId());
+      if (realm.accountBelongsToRealm(accountState.externalIds()) && !realm.isActive(userName)) {
+        sif.deactivate(accountState.account().id());
         logger.atInfo().log("deactivated account %s", userName);
         return true;
       }
@@ -121,7 +121,7 @@
     } catch (Exception e) {
       logger.atSevere().withCause(e).log(
           "Error deactivating account: %s (%s) %s",
-          userName, accountState.getAccount().getId(), e.getMessage());
+          userName, accountState.account().id(), e.getMessage());
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/account/AccountDirectory.java b/java/com/google/gerrit/server/account/AccountDirectory.java
index ee9265f..60c1678 100644
--- a/java/com/google/gerrit/server/account/AccountDirectory.java
+++ b/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -45,7 +45,10 @@
     ID,
 
     /** The user-settable status of this account (e.g. busy, OOO, available) */
-    STATUS
+    STATUS,
+
+    /** The state of the account (e.g. active or inactive) */
+    STATE
   }
 
   public abstract void fillAccountInfo(Iterable<? extends AccountInfo> in, Set<FillOptions> options)
diff --git a/java/com/google/gerrit/server/account/AccountExternalIdCreator.java b/java/com/google/gerrit/server/account/AccountExternalIdCreator.java
index 8cf4ee0..dedd916 100644
--- a/java/com/google/gerrit/server/account/AccountExternalIdCreator.java
+++ b/java/com/google/gerrit/server/account/AccountExternalIdCreator.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import java.util.List;
 
diff --git a/java/com/google/gerrit/server/account/AccountLoader.java b/java/com/google/gerrit/server/account/AccountLoader.java
index 4398d9e..a8e4194 100644
--- a/java/com/google/gerrit/server/account/AccountLoader.java
+++ b/java/com/google/gerrit/server/account/AccountLoader.java
@@ -17,8 +17,9 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.assistedinject.Assisted;
@@ -41,6 +42,7 @@
               FillOptions.EMAIL,
               FillOptions.USERNAME,
               FillOptions.STATUS,
+              FillOptions.STATE,
               FillOptions.AVATARS));
 
   public interface Factory {
@@ -67,7 +69,8 @@
     provided = new ArrayList<>();
   }
 
-  public synchronized AccountInfo get(Account.Id id) {
+  @Nullable
+  public synchronized AccountInfo get(@Nullable Account.Id id) {
     if (id == null) {
       return null;
     }
@@ -95,7 +98,8 @@
     fill();
   }
 
-  public AccountInfo fillOne(Account.Id id) throws PermissionBackendException {
+  @Nullable
+  public AccountInfo fillOne(@Nullable Account.Id id) throws PermissionBackendException {
     AccountInfo info = get(id);
     fill();
     return info;
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index c52b8bc..7a5b1aa 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -26,11 +27,11 @@
 import com.google.gerrit.common.data.AccessSection;
 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.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate.AccountUpdater;
@@ -154,7 +155,7 @@
       }
 
       // Account exists
-      Optional<Account> act = updateAccountActiveStatus(who, accountState.get().getAccount());
+      Optional<Account> act = updateAccountActiveStatus(who, accountState.get().account());
       if (!act.isPresent()) {
         // The account was deleted since we checked for it last time. This should never happen
         // since we don't support deletion of accounts.
@@ -199,18 +200,18 @@
 
     if (authRequest.isActive()) {
       try {
-        setInactiveFlag.activate(account.getId());
+        setInactiveFlag.activate(account.id());
       } catch (Exception e) {
-        throw new AccountException("Unable to activate account " + account.getId(), e);
+        throw new AccountException("Unable to activate account " + account.id(), e);
       }
     } else {
       try {
-        setInactiveFlag.deactivate(account.getId());
+        setInactiveFlag.deactivate(account.id());
       } catch (Exception e) {
-        throw new AccountException("Unable to deactivate account " + account.getId(), e);
+        throw new AccountException("Unable to deactivate account " + account.id(), e);
       }
     }
-    return byIdCache.get(account.getId()).map(AccountState::getAccount);
+    return byIdCache.get(account.id()).map(AccountState::account);
   }
 
   private boolean shouldUpdateActiveStatus(AuthRequest authRequest) {
@@ -233,13 +234,13 @@
       checkEmailNotUsed(extId.accountId(), extIdWithNewEmail);
       accountUpdates.add(u -> u.replaceExternalId(extId, extIdWithNewEmail));
 
-      if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
+      if (oldEmail != null && oldEmail.equals(user.getAccount().preferredEmail())) {
         accountUpdates.add(u -> u.setPreferredEmail(newEmail));
       }
     }
 
     if (!Strings.isNullOrEmpty(who.getDisplayName())
-        && !Objects.equals(user.getAccount().getFullName(), who.getDisplayName())) {
+        && !Objects.equals(user.getAccount().fullName(), who.getDisplayName())) {
       accountUpdates.add(a -> a.setFullName(who.getDisplayName()));
     }
 
@@ -269,7 +270,7 @@
 
   private AuthResult create(AuthRequest who)
       throws AccountException, IOException, ConfigInvalidException {
-    Account.Id newId = new Account.Id(sequences.nextAccountId());
+    Account.Id newId = Account.id(sequences.nextAccountId());
     logger.atFine().log("Assigning new Id %s to account", newId);
 
     ExternalId extId =
@@ -333,7 +334,7 @@
       addGroupMember(adminGroupUuid, user);
     }
 
-    realm.onCreateAccount(who, accountState.getAccount());
+    realm.onCreateAccount(who, accountState.account());
     return new AuthResult(newId, extId.key(), true);
   }
 
@@ -422,7 +423,7 @@
               to,
               (a, u) -> {
                 u.addExternalId(newExtId);
-                if (who.getEmailAddress() != null && a.getAccount().getPreferredEmail() == null) {
+                if (who.getEmailAddress() != null && a.account().preferredEmail() == null) {
                   u.setPreferredEmail(who.getEmailAddress());
                 }
               });
@@ -450,8 +451,10 @@
             "Delete External IDs on Update Link",
             to,
             (a, u) -> {
-              Collection<ExternalId> filteredExtIdsByScheme =
-                  a.getExternalIds(who.getExternalIdKey().scheme());
+              Set<ExternalId> filteredExtIdsByScheme =
+                  a.externalIds().stream()
+                      .filter(e -> e.key().isScheme(who.getExternalIdKey().scheme()))
+                      .collect(toImmutableSet());
               if (filteredExtIdsByScheme.isEmpty()) {
                 return;
               }
@@ -513,9 +516,9 @@
             from,
             (a, u) -> {
               u.deleteExternalIds(extIds);
-              if (a.getAccount().getPreferredEmail() != null
+              if (a.account().preferredEmail() != null
                   && extIds.stream()
-                      .anyMatch(e -> a.getAccount().getPreferredEmail().equals(e.email()))) {
+                      .anyMatch(e -> a.account().preferredEmail().equals(e.email()))) {
                 u.setPreferredEmail(null);
               }
             });
diff --git a/java/com/google/gerrit/server/account/AccountProperties.java b/java/com/google/gerrit/server/account/AccountProperties.java
index 6fcf56d..4f29b25 100644
--- a/java/com/google/gerrit/server/account/AccountProperties.java
+++ b/java/com/google/gerrit/server/account/AccountProperties.java
@@ -16,7 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import java.sql.Timestamp;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -88,15 +88,16 @@
   }
 
   private void parse() {
-    account = new Account(accountId, registeredOn);
-    account.setActive(accountConfig.getBoolean(ACCOUNT, null, KEY_ACTIVE, true));
-    account.setFullName(get(accountConfig, KEY_FULL_NAME));
+    Account.Builder accountBuilder = Account.builder(accountId, registeredOn);
+    accountBuilder.setActive(accountConfig.getBoolean(ACCOUNT, null, KEY_ACTIVE, true));
+    accountBuilder.setFullName(get(accountConfig, KEY_FULL_NAME));
 
     String preferredEmail = get(accountConfig, KEY_PREFERRED_EMAIL);
-    account.setPreferredEmail(preferredEmail);
+    accountBuilder.setPreferredEmail(preferredEmail);
 
-    account.setStatus(get(accountConfig, KEY_STATUS));
-    account.setMetaId(metaId != null ? metaId.name() : null);
+    accountBuilder.setStatus(get(accountConfig, KEY_STATUS));
+    accountBuilder.setMetaId(metaId != null ? metaId.name() : null);
+    account = accountBuilder.build();
   }
 
   Config save(InternalAccountUpdate accountUpdate) {
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index f9945b5..2324465 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -26,9 +26,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.index.Schema;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -109,13 +109,16 @@
 
     return result.asList().stream()
         .map(a -> formatForException(result, a))
-        .collect(joining("\n", "Account '" + result.input() + "' is ambiguous:\n", ""));
+        .limit(3)
+        .collect(
+            joining(
+                "\n", "Account '" + result.input() + "' is ambiguous (at most 3 shown):\n", ""));
   }
 
   private static String formatForException(Result result, AccountState state) {
-    return state.getAccount().getId()
+    return state.account().id()
         + ": "
-        + state.getAccount().getNameEmail(result.accountResolver().anonymousCowardName);
+        + state.account().getNameEmail(result.accountResolver().anonymousCowardName);
   }
 
   public static boolean isSelf(String input) {
@@ -135,7 +138,7 @@
     }
 
     private ImmutableList<AccountState> canonicalize(List<AccountState> list) {
-      TreeSet<AccountState> set = new TreeSet<>(comparing(a -> a.getAccount().getId().get()));
+      TreeSet<AccountState> set = new TreeSet<>(comparing(a -> a.account().id().get()));
       set.addAll(requireNonNull(list));
       return ImmutableList.copyOf(set);
     }
@@ -160,7 +163,7 @@
     }
 
     public ImmutableSet<Account.Id> asIdSet() {
-      return list.stream().map(a -> a.getAccount().getId()).collect(toImmutableSet());
+      return list.stream().map(a -> a.account().id()).collect(toImmutableSet());
     }
 
     public AccountState asUnique() throws UnresolvableAccountException {
@@ -192,7 +195,7 @@
         return self.get().asIdentifiedUser();
       }
       return userFactory.runAs(
-          null, list.get(0).getAccount().getId(), requireNonNull(caller).getRealUser());
+          null, list.get(0).account().id(), requireNonNull(caller).getRealUser());
     }
 
     @VisibleForTesting
@@ -349,7 +352,7 @@
       String name = nameOrEmail.substring(0, lt - 1);
       ImmutableList<AccountState> nameMatches =
           allMatches.stream()
-              .filter(a -> name.equals(a.getAccount().getFullName()))
+              .filter(a -> name.equals(a.account().fullName()))
               .collect(toImmutableList());
       return !nameMatches.isEmpty() ? nameMatches.stream() : allMatches.stream();
     }
@@ -558,7 +561,7 @@
   }
 
   private Predicate<AccountState> accountActivityPredicate() {
-    return (AccountState accountState) -> accountState.getAccount().isActive();
+    return (AccountState accountState) -> accountState.account().isActive();
   }
 
   @VisibleForTesting
diff --git a/java/com/google/gerrit/server/account/AccountResource.java b/java/com/google/gerrit/server/account/AccountResource.java
index d09dff5..4fb69bd 100644
--- a/java/com/google/gerrit/server/account/AccountResource.java
+++ b/java/com/google/gerrit/server/account/AccountResource.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.inject.TypeLiteral;
diff --git a/java/com/google/gerrit/server/account/AccountSshKey.java b/java/com/google/gerrit/server/account/AccountSshKey.java
index f132585..d2f8775 100644
--- a/java/com/google/gerrit/server/account/AccountSshKey.java
+++ b/java/com/google/gerrit/server/account/AccountSshKey.java
@@ -16,7 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Splitter;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import java.util.List;
 
 /** An SSH key approved for use by an {@link Account}. */
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index 1854dc1..a270a76 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -14,34 +14,23 @@
 
 package com.google.gerrit.server.account;
 
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
-
-import com.google.common.base.Function;
+import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
-import com.google.common.base.Strings;
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.CurrentUser.PropertyKey;
-import com.google.gerrit.server.IdentifiedUser;
 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;
 import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gerrit.server.config.AllUsersName;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Optional;
-import org.apache.commons.codec.DecoderException;
 import org.eclipse.jgit.lib.ObjectId;
 
 /**
@@ -51,25 +40,19 @@
  * <p>Most callers should not construct AccountStates directly but rather lookup accounts via the
  * account cache (see {@link AccountCache#get(Account.Id)}).
  */
-public class AccountState {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public static final Function<AccountState, Account.Id> ACCOUNT_ID_FUNCTION =
-      a -> a.getAccount().getId();
-
+@AutoValue
+public abstract class AccountState {
   /**
    * Creates an AccountState from the given account config.
    *
-   * @param allUsersName the name of the All-Users repository
    * @param externalIds class to access external IDs
    * @param accountConfig the account config, must already be loaded
    * @return the account state, {@link Optional#empty()} if the account doesn't exist
    * @throws IOException if accessing the external IDs fails
    */
   public static Optional<AccountState> fromAccountConfig(
-      AllUsersName allUsersName, ExternalIds externalIds, AccountConfig accountConfig)
-      throws IOException {
-    return fromAccountConfig(allUsersName, externalIds, accountConfig, null);
+      ExternalIds externalIds, AccountConfig accountConfig) throws IOException {
+    return fromAccountConfig(externalIds, accountConfig, null);
   }
 
   /**
@@ -82,7 +65,6 @@
    * updated the revision of the external IDs branch in account config is outdated. Hence after
    * updating external IDs the external ID notes must be provided.
    *
-   * @param allUsersName the name of the All-Users repository
    * @param externalIds class to access external IDs
    * @param accountConfig the account config, must already be loaded
    * @param extIdNotes external ID notes, must already be loaded, may be {@code null}
@@ -90,10 +72,7 @@
    * @throws IOException if accessing the external IDs fails
    */
   public static Optional<AccountState> fromAccountConfig(
-      AllUsersName allUsersName,
-      ExternalIds externalIds,
-      AccountConfig accountConfig,
-      @Nullable ExternalIdNotes extIdNotes)
+      ExternalIds externalIds, AccountConfig accountConfig, @Nullable ExternalIdNotes extIdNotes)
       throws IOException {
     if (!accountConfig.getLoadedAccount().isPresent()) {
       return Optional.empty();
@@ -106,22 +85,25 @@
             : accountConfig.getExternalIdsRev();
     ImmutableSet<ExternalId> extIds =
         extIdsRev.isPresent()
-            ? ImmutableSet.copyOf(externalIds.byAccount(account.getId(), extIdsRev.get()))
+            ? ImmutableSet.copyOf(externalIds.byAccount(account.id(), extIdsRev.get()))
             : ImmutableSet.of();
 
     // Don't leak references to AccountConfig into the AccountState, since it holds a reference to
     // an open Repository instance.
     ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches =
         accountConfig.getProjectWatches();
-    GeneralPreferencesInfo generalPreferences = accountConfig.getGeneralPreferences();
-    DiffPreferencesInfo diffPreferences = accountConfig.getDiffPreferences();
-    EditPreferencesInfo editPreferences = accountConfig.getEditPreferences();
+    Preferences.General generalPreferences =
+        Preferences.General.fromInfo(accountConfig.getGeneralPreferences());
+    Preferences.Diff diffPreferences =
+        Preferences.Diff.fromInfo(accountConfig.getDiffPreferences());
+    Preferences.Edit editPreferences =
+        Preferences.Edit.fromInfo(accountConfig.getEditPreferences());
 
     return Optional.of(
-        new AccountState(
-            allUsersName,
+        new AutoValue_AccountState(
             account,
             extIds,
+            ExternalId.getUserName(extIds),
             projectWatches,
             generalPreferences,
             diffPreferences,
@@ -132,71 +114,35 @@
    * Creates an AccountState for a given account with no external IDs, no project watches and
    * default preferences.
    *
-   * @param allUsersName the name of the All-Users repository
    * @param account the account
    * @return the account state
    */
-  public static AccountState forAccount(AllUsersName allUsersName, Account account) {
-    return forAccount(allUsersName, account, ImmutableSet.of());
+  public static AccountState forAccount(Account account) {
+    return forAccount(account, ImmutableSet.of());
   }
 
   /**
    * Creates an AccountState for a given account with no project watches and default preferences.
    *
-   * @param allUsersName the name of the All-Users repository
    * @param account the account
    * @param extIds the external IDs
    * @return the account state
    */
-  public static AccountState forAccount(
-      AllUsersName allUsersName, Account account, Collection<ExternalId> extIds) {
-    return new AccountState(
-        allUsersName,
+  public static AccountState forAccount(Account account, Collection<ExternalId> extIds) {
+    return new AutoValue_AccountState(
         account,
         ImmutableSet.copyOf(extIds),
+        ExternalId.getUserName(extIds),
         ImmutableMap.of(),
-        GeneralPreferencesInfo.defaults(),
-        DiffPreferencesInfo.defaults(),
-        EditPreferencesInfo.defaults());
-  }
-
-  private final AllUsersName allUsersName;
-  private final Account account;
-  private final ImmutableSet<ExternalId> externalIds;
-  private final Optional<String> userName;
-  private final ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches;
-  private final GeneralPreferencesInfo generalPreferences;
-  private final DiffPreferencesInfo diffPreferences;
-  private final EditPreferencesInfo editPreferences;
-  private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
-
-  private AccountState(
-      AllUsersName allUsersName,
-      Account account,
-      ImmutableSet<ExternalId> externalIds,
-      ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches,
-      GeneralPreferencesInfo generalPreferences,
-      DiffPreferencesInfo diffPreferences,
-      EditPreferencesInfo editPreferences) {
-    this.allUsersName = allUsersName;
-    this.account = account;
-    this.externalIds = externalIds;
-    this.userName = ExternalId.getUserName(externalIds);
-    this.projectWatches = projectWatches;
-    this.generalPreferences = generalPreferences;
-    this.diffPreferences = diffPreferences;
-    this.editPreferences = editPreferences;
-  }
-
-  public AllUsersName getAllUsersNameForIndexing() {
-    return allUsersName;
+        Preferences.General.fromInfo(GeneralPreferencesInfo.defaults()),
+        Preferences.Diff.fromInfo(DiffPreferencesInfo.defaults()),
+        Preferences.Edit.fromInfo(EditPreferencesInfo.defaults()));
   }
 
   /** Get the cached account metadata. */
-  public Account getAccount() {
-    return account;
-  }
-
+  public abstract Account account();
+  /** The external identities that identify the account holder. */
+  public abstract ImmutableSet<ExternalId> externalIds();
   /**
    * Get the username, if one has been declared for this user.
    *
@@ -205,122 +151,36 @@
    * @return the username, {@link Optional#empty()} if the user has no username, or if the username
    *     is empty
    */
-  public Optional<String> getUserName() {
-    return userName;
-  }
-
-  public boolean checkPassword(@Nullable String password, String username) {
-    if (password == null) {
-      return false;
-    }
-    for (ExternalId id : getExternalIds()) {
-      // Only process the "username:$USER" entry, which is unique.
-      if (!id.isScheme(SCHEME_USERNAME) || !username.equals(id.key().id())) {
-        continue;
-      }
-
-      String hashedStr = id.password();
-      if (!Strings.isNullOrEmpty(hashedStr)) {
-        try {
-          return HashedPassword.decode(hashedStr).checkPassword(password);
-        } catch (DecoderException e) {
-          logger.atSevere().log("DecoderException for user %s: %s ", username, e.getMessage());
-          return false;
-        }
-      }
-    }
-    return false;
-  }
-
-  /** The external identities that identify the account holder. */
-  public ImmutableSet<ExternalId> getExternalIds() {
-    return externalIds;
-  }
-
-  /** The external identities that identify the account holder that match the given scheme. */
-  public ImmutableSet<ExternalId> getExternalIds(String scheme) {
-    return externalIds.stream().filter(e -> e.key().isScheme(scheme)).collect(toImmutableSet());
-  }
-
+  public abstract Optional<String> userName();
   /** The project watches of the account. */
-  public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
-    return projectWatches;
-  }
+  public abstract ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches();
+  /** The general preferences of the account. */
 
   /** The general preferences of the account. */
-  public GeneralPreferencesInfo getGeneralPreferences() {
-    return generalPreferences;
+  public GeneralPreferencesInfo generalPreferences() {
+    return immutableGeneralPreferences().toInfo();
   }
 
   /** The diff preferences of the account. */
-  public DiffPreferencesInfo getDiffPreferences() {
-    return diffPreferences;
+  public DiffPreferencesInfo diffPreferences() {
+    return immutableDiffPreferences().toInfo();
   }
 
   /** The edit preferences of the account. */
-  public EditPreferencesInfo getEditPreferences() {
-    return editPreferences;
-  }
-
-  /**
-   * Lookup a previously stored property.
-   *
-   * <p>All properties are automatically cleared when the account cache invalidates the {@code
-   * AccountState}. This method is thread-safe.
-   *
-   * @param key unique property key.
-   * @return previously stored value, or {@code null}.
-   */
-  @Nullable
-  public <T> T get(PropertyKey<T> key) {
-    Cache<PropertyKey<Object>, Object> p = properties(false);
-    if (p != null) {
-      @SuppressWarnings("unchecked")
-      T value = (T) p.getIfPresent(key);
-      return value;
-    }
-    return null;
-  }
-
-  /**
-   * Store a property for later retrieval.
-   *
-   * <p>This method is thread-safe.
-   *
-   * @param key unique property key.
-   * @param value value to store; or {@code null} to clear the value.
-   */
-  public <T> void put(PropertyKey<T> key, @Nullable T value) {
-    Cache<PropertyKey<Object>, Object> p = properties(value != null);
-    if (p != null) {
-      @SuppressWarnings("unchecked")
-      PropertyKey<Object> k = (PropertyKey<Object>) key;
-      if (value != null) {
-        p.put(k, value);
-      } else {
-        p.invalidate(k);
-      }
-    }
-  }
-
-  private synchronized Cache<PropertyKey<Object>, Object> properties(boolean allocate) {
-    if (properties == null && allocate) {
-      properties =
-          CacheBuilder.newBuilder()
-              .concurrencyLevel(1)
-              .initialCapacity(16)
-              // Use weakKeys to ensure plugins that garbage collect will also
-              // eventually release data held in any still live AccountState.
-              .weakKeys()
-              .build();
-    }
-    return properties;
+  public EditPreferencesInfo editPreferences() {
+    return immutableEditPreferences().toInfo();
   }
 
   @Override
-  public String toString() {
+  public final String toString() {
     MoreObjects.ToStringHelper h = MoreObjects.toStringHelper(this);
-    h.addValue(getAccount().getId());
+    h.addValue(account().id());
     return h.toString();
   }
+
+  protected abstract Preferences.General immutableGeneralPreferences();
+
+  protected abstract Preferences.Diff immutableDiffPreferences();
+
+  protected abstract Preferences.Edit immutableEditPreferences();
 }
diff --git a/java/com/google/gerrit/server/account/Accounts.java b/java/com/google/gerrit/server/account/Accounts.java
index f3758bf..8136631 100644
--- a/java/com/google/gerrit/server/account/Accounts.java
+++ b/java/com/google/gerrit/server/account/Accounts.java
@@ -19,8 +19,8 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -134,9 +134,7 @@
   private Optional<AccountState> read(Repository allUsersRepository, Account.Id accountId)
       throws IOException, ConfigInvalidException {
     return AccountState.fromAccountConfig(
-        allUsersName,
-        externalIds,
-        new AccountConfig(accountId, allUsersName, allUsersRepository).load());
+        externalIds, new AccountConfig(accountId, allUsersName, allUsersRepository).load());
   }
 
   public static Stream<Account.Id> readUserRefs(Repository repo) throws IOException {
diff --git a/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java b/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java
index b43d86c..db350c6 100644
--- a/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -35,14 +35,14 @@
     List<ConsistencyProblemInfo> problems = new ArrayList<>();
 
     for (AccountState accountState : accounts.all()) {
-      Account account = accountState.getAccount();
-      if (account.getPreferredEmail() != null) {
-        if (accountState.getExternalIds().stream()
-            .noneMatch(e -> account.getPreferredEmail().equals(e.email()))) {
+      Account account = accountState.account();
+      if (account.preferredEmail() != null) {
+        if (accountState.externalIds().stream()
+            .noneMatch(e -> account.preferredEmail().equals(e.email()))) {
           addError(
               String.format(
                   "Account '%s' has no external ID for its preferred email '%s'",
-                  account.getId().get(), account.getPreferredEmail()),
+                  account.id().get(), account.preferredEmail()),
               problems);
         }
       }
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 20a1c97..1caee58 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -24,11 +24,11 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.Runnables;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
@@ -88,9 +88,9 @@
  * The timestamp of the first commit on a user branch denotes the registration date. The initial
  * commit on the user branch may be empty (since having an 'account.config' is optional). See {@link
  * AccountConfig} for details of the 'account.config' file format. In addition the user branch can
- * contain a 'preferences.config' config file to store preferences (see {@link Preferences}) and a
- * 'watch.config' config file to store project watches (see {@link ProjectWatches}). External IDs
- * are stored separately in the {@code refs/meta/external-ids} notes branch (see {@link
+ * contain a 'preferences.config' config file to store preferences (see {@link StoredPreferences})
+ * and a 'watch.config' config file to store project watches (see {@link ProjectWatches}). External
+ * IDs are stored separately in the {@code refs/meta/external-ids} notes branch (see {@link
  * ExternalIdNotes}).
  *
  * <p>On updating an account the account is evicted from the account cache and reindexed. The
@@ -321,7 +321,7 @@
               AccountConfig accountConfig = read(r, accountId);
               Account account =
                   accountConfig.getNewAccount(new Timestamp(committerIdent.getWhen().getTime()));
-              AccountState accountState = AccountState.forAccount(allUsersName, account);
+              AccountState accountState = AccountState.forAccount(account);
               InternalAccountUpdate.Builder updateBuilder = InternalAccountUpdate.builder();
               updater.update(accountState, updateBuilder);
 
@@ -330,7 +330,7 @@
               ExternalIdNotes extIdNotes =
                   createExternalIdNotes(r, accountConfig.getExternalIdsRev(), accountId, update);
               UpdatedAccount updatedAccounts =
-                  new UpdatedAccount(allUsersName, externalIds, message, accountConfig, extIdNotes);
+                  new UpdatedAccount(externalIds, message, accountConfig, extIdNotes);
               updatedAccounts.setCreated(true);
               return updatedAccounts;
             })
@@ -377,7 +377,7 @@
         r -> {
           AccountConfig accountConfig = read(r, accountId);
           Optional<AccountState> account =
-              AccountState.fromAccountConfig(allUsersName, externalIds, accountConfig);
+              AccountState.fromAccountConfig(externalIds, accountConfig);
           if (!account.isPresent()) {
             return null;
           }
@@ -390,7 +390,7 @@
           ExternalIdNotes extIdNotes =
               createExternalIdNotes(r, accountConfig.getExternalIdsRev(), accountId, update);
           UpdatedAccount updatedAccounts =
-              new UpdatedAccount(allUsersName, externalIds, message, accountConfig, extIdNotes);
+              new UpdatedAccount(externalIds, message, accountConfig, extIdNotes);
           return updatedAccounts;
         });
   }
@@ -561,7 +561,6 @@
   }
 
   private static class UpdatedAccount {
-    private final AllUsersName allUsersName;
     private final ExternalIds externalIds;
     private final String message;
     private final AccountConfig accountConfig;
@@ -570,13 +569,11 @@
     private boolean created;
 
     private UpdatedAccount(
-        AllUsersName allUsersName,
         ExternalIds externalIds,
         String message,
         AccountConfig accountConfig,
         ExternalIdNotes extIdNotes) {
       checkState(!Strings.isNullOrEmpty(message), "message for account update must be set");
-      this.allUsersName = requireNonNull(allUsersName);
       this.externalIds = requireNonNull(externalIds);
       this.message = requireNonNull(message);
       this.accountConfig = requireNonNull(accountConfig);
@@ -592,8 +589,7 @@
     }
 
     public AccountState getAccount() throws IOException {
-      return AccountState.fromAccountConfig(allUsersName, externalIds, accountConfig, extIdNotes)
-          .get();
+      return AccountState.fromAccountConfig(externalIds, accountConfig, extIdNotes).get();
     }
 
     public ExternalIdNotes getExternalIdNotes() {
diff --git a/java/com/google/gerrit/server/account/AuthResult.java b/java/com/google/gerrit/server/account/AuthResult.java
index 2b1bc96..1f89827 100644
--- a/java/com/google/gerrit/server/account/AuthResult.java
+++ b/java/com/google/gerrit/server/account/AuthResult.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
 
 /** Result from {@link AccountManager#authenticate(AuthRequest)}. */
diff --git a/java/com/google/gerrit/server/account/AuthorizedKeys.java b/java/com/google/gerrit/server/account/AuthorizedKeys.java
index b392c18..203ac5c 100644
--- a/java/com/google/gerrit/server/account/AuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/AuthorizedKeys.java
@@ -16,7 +16,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
diff --git a/java/com/google/gerrit/server/account/CreateGroupArgs.java b/java/com/google/gerrit/server/account/CreateGroupArgs.java
index 5bcb84b..2a764cc 100644
--- a/java/com/google/gerrit/server/account/CreateGroupArgs.java
+++ b/java/com/google/gerrit/server/account/CreateGroupArgs.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import java.util.Collection;
 
 public class CreateGroupArgs {
   private AccountGroup.NameKey groupName;
+  public AccountGroup.UUID uuid;
   public String groupDescription;
   public boolean visibleToAll;
   public AccountGroup.UUID ownerGroupUuid;
@@ -34,7 +35,7 @@
   }
 
   public void setGroupName(String n) {
-    groupName = n != null ? new AccountGroup.NameKey(n) : null;
+    groupName = n != null ? AccountGroup.nameKey(n) : null;
   }
 
   public void setGroupName(AccountGroup.NameKey n) {
diff --git a/java/com/google/gerrit/server/account/DefaultRealm.java b/java/com/google/gerrit/server/account/DefaultRealm.java
index 33de2d2..329825f 100644
--- a/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -16,10 +16,10 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.client.AuthType;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
diff --git a/java/com/google/gerrit/server/account/DestinationList.java b/java/com/google/gerrit/server/account/DestinationList.java
index 04e710a..15c1e25 100644
--- a/java/com/google/gerrit/server/account/DestinationList.java
+++ b/java/com/google/gerrit/server/account/DestinationList.java
@@ -18,8 +18,8 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.TabFile;
 import java.io.IOException;
@@ -28,10 +28,10 @@
 
 public class DestinationList extends TabFile {
   public static final String DIR_NAME = "destinations";
-  private SetMultimap<String, Branch.NameKey> destinations =
+  private SetMultimap<String, BranchNameKey> destinations =
       MultimapBuilder.hashKeys().hashSetValues().build();
 
-  public Set<Branch.NameKey> getDestinations(String label) {
+  public Set<BranchNameKey> getDestinations(String label) {
     return destinations.get(label);
   }
 
@@ -40,21 +40,21 @@
   }
 
   String asText(String label) {
-    Set<Branch.NameKey> dests = destinations.get(label);
+    Set<BranchNameKey> dests = destinations.get(label);
     if (dests == null) {
       return null;
     }
     List<Row> rows = Lists.newArrayListWithCapacity(dests.size());
-    for (Branch.NameKey dest : sort(dests)) {
-      rows.add(new Row(dest.get(), dest.getParentKey().get()));
+    for (BranchNameKey dest : sort(dests)) {
+      rows.add(new Row(dest.branch(), dest.project().get()));
     }
     return asText("Ref", "Project", rows);
   }
 
-  private static Set<Branch.NameKey> toSet(List<Row> destRows) {
-    Set<Branch.NameKey> dests = Sets.newHashSetWithExpectedSize(destRows.size());
+  private static Set<BranchNameKey> toSet(List<Row> destRows) {
+    Set<BranchNameKey> dests = Sets.newHashSetWithExpectedSize(destRows.size());
     for (Row row : destRows) {
-      dests.add(new Branch.NameKey(new Project.NameKey(row.right), row.left));
+      dests.add(BranchNameKey.create(Project.nameKey(row.right), row.left));
     }
     return dests;
   }
diff --git a/java/com/google/gerrit/server/account/Emails.java b/java/com/google/gerrit/server/account/Emails.java
index 426d6ea..76c22cf 100644
--- a/java/com/google/gerrit/server/account/Emails.java
+++ b/java/com/google/gerrit/server/account/Emails.java
@@ -14,14 +14,17 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
-import com.google.common.collect.Streams;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.UserIdentity;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -32,6 +35,11 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.lib.PersonIdent;
 
 /** Class to access accounts by email. */
 @Singleton
@@ -65,15 +73,20 @@
    * have no external ID for the preferred email. Having accounts with a preferred email that does
    * not exist as external ID is an inconsistency, but existing functionality relies on still
    * getting those accounts, which is why they are included. Accounts by preferred email are fetched
-   * from the account index.
+   * from the account index as a fallback for email addresses that could not be resolved using
+   * {@link ExternalIds}.
    *
    * @see #getAccountsFor(String...)
    */
   public ImmutableSet<Account.Id> getAccountFor(String email) throws IOException {
-    return Streams.concat(
-            externalIds.byEmail(email).stream().map(ExternalId::accountId),
-            executeIndexQuery(() -> queryProvider.get().byPreferredEmail(email).stream())
-                .map(a -> a.getAccount().getId()))
+    ImmutableSet<Account.Id> accounts =
+        externalIds.byEmail(email).stream().map(ExternalId::accountId).collect(toImmutableSet());
+    if (!accounts.isEmpty()) {
+      return accounts;
+    }
+
+    return executeIndexQuery(() -> queryProvider.get().byPreferredEmail(email).stream())
+        .map(a -> a.account().id())
         .collect(toImmutableSet());
   }
 
@@ -84,12 +97,18 @@
    */
   public ImmutableSetMultimap<String, Account.Id> getAccountsFor(String... emails)
       throws IOException {
-    ImmutableSetMultimap.Builder<String, Account.Id> builder = ImmutableSetMultimap.builder();
+    SetMultimap<String, Account.Id> result =
+        MultimapBuilder.hashKeys(emails.length).hashSetValues(1).build();
     externalIds.byEmails(emails).entries().stream()
-        .forEach(e -> builder.put(e.getKey(), e.getValue().accountId()));
-    executeIndexQuery(() -> queryProvider.get().byPreferredEmail(emails).entries().stream())
-        .forEach(e -> builder.put(e.getKey(), e.getValue().getAccount().getId()));
-    return builder.build();
+        .forEach(e -> result.put(e.getKey(), e.getValue().accountId()));
+    List<String> emailsToBackfill =
+        Arrays.stream(emails).filter(e -> !result.containsKey(e)).collect(toImmutableList());
+    if (!emailsToBackfill.isEmpty()) {
+      executeIndexQuery(
+              () -> queryProvider.get().byPreferredEmail(emailsToBackfill).entries().stream())
+          .forEach(e -> result.put(e.getKey(), e.getValue().account().id()));
+    }
+    return ImmutableSetMultimap.copyOf(result);
   }
 
   /**
@@ -102,6 +121,24 @@
     return externalIds.byEmail(email).stream().map(ExternalId::accountId).collect(toImmutableSet());
   }
 
+  public UserIdentity toUserIdentity(PersonIdent who) throws IOException {
+    UserIdentity u = new UserIdentity();
+    u.setName(who.getName());
+    u.setEmail(who.getEmailAddress());
+    u.setDate(new Timestamp(who.getWhen().getTime()));
+    u.setTimeZone(who.getTimeZoneOffset());
+
+    // If only one account has access to this email address, select it
+    // as the identity of the user.
+    //
+    Set<Account.Id> a = getAccountFor(u.getEmail());
+    if (a.size() == 1) {
+      u.setAccount(a.iterator().next());
+    }
+
+    return u;
+  }
+
   private <T> T executeIndexQuery(Action<T> action) {
     try {
       return retryHelper.execute(
diff --git a/java/com/google/gerrit/server/account/FakeRealm.java b/java/com/google/gerrit/server/account/FakeRealm.java
index a53f64e..30274e0 100644
--- a/java/com/google/gerrit/server/account/FakeRealm.java
+++ b/java/com/google/gerrit/server/account/FakeRealm.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gerrit.reviewdb.client.Account;
 
 /** Fake implementation of {@link Realm} that does not communicate. */
 public class FakeRealm extends AbstractRealm {
diff --git a/java/com/google/gerrit/server/account/GroupBackend.java b/java/com/google/gerrit/server/account/GroupBackend.java
index 2d46260..3a874bb 100644
--- a/java/com/google/gerrit/server/account/GroupBackend.java
+++ b/java/com/google/gerrit/server/account/GroupBackend.java
@@ -17,8 +17,8 @@
 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.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ProjectState;
 import java.util.Collection;
diff --git a/java/com/google/gerrit/server/account/GroupCache.java b/java/com/google/gerrit/server/account/GroupCache.java
index 8133d9c..90d3aa9 100644
--- a/java/com/google/gerrit/server/account/GroupCache.java
+++ b/java/com/google/gerrit/server/account/GroupCache.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import java.util.Optional;
 
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index c85e2df..fe22028 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -17,10 +17,11 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.query.group.InternalGroupQuery;
@@ -149,7 +150,9 @@
 
     @Override
     public Optional<InternalGroup> load(AccountGroup.Id key) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading group %s by ID", key)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading group by ID", Metadata.builder().groupId(key.get()).build())) {
         return groupQueryProvider.get().byId(key);
       }
     }
@@ -165,8 +168,10 @@
 
     @Override
     public Optional<InternalGroup> load(String name) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading group '%s' by name", name)) {
-        return groupQueryProvider.get().byName(new AccountGroup.NameKey(name));
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading group by name", Metadata.builder().groupName(name).build())) {
+        return groupQueryProvider.get().byName(AccountGroup.nameKey(name));
       }
     }
   }
@@ -181,8 +186,10 @@
 
     @Override
     public Optional<InternalGroup> load(String uuid) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading group %s by UUID", uuid)) {
-        return groups.getGroup(new AccountGroup.UUID(uuid));
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading group by UUID", Metadata.builder().groupUuid(uuid).build())) {
+        return groups.getGroup(AccountGroup.uuid(uuid));
       }
     }
   }
diff --git a/java/com/google/gerrit/server/account/GroupControl.java b/java/com/google/gerrit/server/account/GroupControl.java
index b3e6739..2228525 100644
--- a/java/com/google/gerrit/server/account/GroupControl.java
+++ b/java/com/google/gerrit/server/account/GroupControl.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCache.java b/java/com/google/gerrit/server/account/GroupIncludeCache.java
index 612730b..6547619 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCache.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCache.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import java.util.Collection;
 
 /** Tracks group inclusions in memory for efficient access. */
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index c27d6c3..7883b11 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -22,11 +22,12 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.query.group.InternalGroupQuery;
@@ -152,7 +153,9 @@
 
     @Override
     public ImmutableSet<AccountGroup.UUID> load(Account.Id memberId) {
-      try (TraceTimer timer = TraceContext.newTimer("Loading groups with member %s", memberId)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading groups with member", Metadata.builder().accountId(memberId.get()).build())) {
         return groupQueryProvider.get().byMember(memberId).stream()
             .map(InternalGroup::getGroupUUID)
             .collect(toImmutableSet());
@@ -171,7 +174,9 @@
 
     @Override
     public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key) {
-      try (TraceTimer timer = TraceContext.newTimer("Loading parent groups of %s", key)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading parent groups", Metadata.builder().groupUuid(key.get()).build())) {
         return groupQueryProvider.get().bySubgroup(key).stream()
             .map(InternalGroup::getGroupUUID)
             .collect(toImmutableList());
diff --git a/java/com/google/gerrit/server/account/GroupMembers.java b/java/com/google/gerrit/server/account/GroupMembers.java
index d7e97ba..c2b935b 100644
--- a/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/java/com/google/gerrit/server/account/GroupMembers.java
@@ -19,9 +19,9 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.group.SystemGroupBackend;
@@ -131,7 +131,7 @@
             .filter(groupControl::canSeeMember)
             .map(accountCache::get)
             .flatMap(Streams::stream)
-            .map(AccountState::getAccount)
+            .map(AccountState::account)
             .collect(toImmutableSet());
 
     Set<Account> indirectMembers = new HashSet<>();
diff --git a/java/com/google/gerrit/server/account/GroupMembership.java b/java/com/google/gerrit/server/account/GroupMembership.java
index b59b989..e051794 100644
--- a/java/com/google/gerrit/server/account/GroupMembership.java
+++ b/java/com/google/gerrit/server/account/GroupMembership.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import java.util.Collections;
 import java.util.Set;
 
diff --git a/java/com/google/gerrit/server/account/GroupUUID.java b/java/com/google/gerrit/server/account/GroupUUID.java
index a7b32a1..ac83482 100644
--- a/java/com/google/gerrit/server/account/GroupUUID.java
+++ b/java/com/google/gerrit/server/account/GroupUUID.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import java.security.MessageDigest;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -26,7 +26,7 @@
     md.update(Constants.encode("group " + groupName + "\n"));
     md.update(Constants.encode("creator " + creator.toExternalString() + "\n"));
     md.update(Constants.encode(String.valueOf(Math.random())));
-    return new AccountGroup.UUID(ObjectId.fromRaw(md.digest()).name());
+    return AccountGroup.uuid(ObjectId.fromRaw(md.digest()).name());
   }
 
   private GroupUUID() {}
diff --git a/java/com/google/gerrit/server/account/HashedPassword.java b/java/com/google/gerrit/server/account/HashedPassword.java
index bffa3ce..64a4495 100644
--- a/java/com/google/gerrit/server/account/HashedPassword.java
+++ b/java/com/google/gerrit/server/account/HashedPassword.java
@@ -22,7 +22,6 @@
 import java.nio.charset.StandardCharsets;
 import java.security.SecureRandom;
 import java.util.List;
-import org.apache.commons.codec.DecoderException;
 import org.bouncycastle.crypto.generators.BCrypt;
 import org.bouncycastle.util.Arrays;
 
@@ -39,6 +38,14 @@
   // for a high cost.
   private static final int DEFAULT_COST = 4;
 
+  public static class DecoderException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public DecoderException(String message) {
+      super(message);
+    }
+  }
+
   /**
    * decodes a hashed password encoded with {@link #encode}.
    *
diff --git a/java/com/google/gerrit/server/account/IncludingGroupMembership.java b/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index b6969ac..6dc7976 100644
--- a/java/com/google/gerrit/server/account/IncludingGroupMembership.java
+++ b/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -17,7 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index ce97ff9..e27b77c 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -20,11 +20,11 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.AvatarInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -98,15 +98,14 @@
 
     Set<FillOptions> fillOptionsWithoutSecondaryEmails =
         Sets.difference(options, EnumSet.of(FillOptions.SECONDARY_EMAILS));
-    Set<Account.Id> ids =
-        Streams.stream(in).map(a -> new Account.Id(a._accountId)).collect(toSet());
+    Set<Account.Id> ids = Streams.stream(in).map(a -> Account.id(a._accountId)).collect(toSet());
     Map<Account.Id, AccountState> accountStates = accountCache.get(ids);
     for (AccountInfo info : in) {
-      Account.Id id = new Account.Id(info._accountId);
+      Account.Id id = Account.id(info._accountId);
       AccountState state = accountStates.get(id);
       if (state != null) {
         if (!options.contains(FillOptions.SECONDARY_EMAILS)
-            || Objects.equals(currentUserId, state.getAccount().getId())
+            || Objects.equals(currentUserId, state.account().id())
             || canModifyAccount) {
           fill(info, accountStates.get(id), options);
         } else {
@@ -121,50 +120,53 @@
   }
 
   private void fill(AccountInfo info, AccountState accountState, Set<FillOptions> options) {
-    Account account = accountState.getAccount();
+    Account account = accountState.account();
     if (options.contains(FillOptions.ID)) {
-      info._accountId = account.getId().get();
+      info._accountId = account.id().get();
     } else {
       // Was previously set to look up account for filling.
       info._accountId = null;
     }
     if (options.contains(FillOptions.NAME)) {
-      info.name = Strings.emptyToNull(account.getFullName());
+      info.name = Strings.emptyToNull(account.fullName());
       if (info.name == null) {
-        info.name = accountState.getUserName().orElse(null);
+        info.name = accountState.userName().orElse(null);
       }
     }
     if (options.contains(FillOptions.EMAIL)) {
-      info.email = account.getPreferredEmail();
+      info.email = account.preferredEmail();
     }
     if (options.contains(FillOptions.SECONDARY_EMAILS)) {
-      info.secondaryEmails = getSecondaryEmails(account, accountState.getExternalIds());
+      info.secondaryEmails = getSecondaryEmails(account, accountState.externalIds());
     }
     if (options.contains(FillOptions.USERNAME)) {
-      info.username = accountState.getUserName().orElse(null);
+      info.username = accountState.userName().orElse(null);
     }
 
     if (options.contains(FillOptions.STATUS)) {
-      info.status = account.getStatus();
+      info.status = account.status();
+    }
+
+    if (options.contains(FillOptions.STATE)) {
+      info.inactive = account.inactive() ? true : null;
     }
 
     if (options.contains(FillOptions.AVATARS)) {
       AvatarProvider ap = avatar.get();
       if (ap != null) {
-        info.avatars = new ArrayList<>(3);
-        IdentifiedUser user = userFactory.create(account.getId());
+        info.avatars = new ArrayList<>();
+        IdentifiedUser user = userFactory.create(account.id());
 
-        // GWT UI uses DEFAULT_SIZE (26px).
+        // PolyGerrit UI uses the following sizes for avatars:
+        // - 32px for avatars next to names e.g. on the dashboard. This is also Gerrit's default.
+        // - 56px for the user's own avatar in the menu
+        // - 100ox for other user's avatars on dashboards
+        // - 120px for the user's own profile settings page
         addAvatar(ap, info, user, AvatarInfo.DEFAULT_SIZE);
-
-        // PolyGerrit UI prefers 32px and 100px.
         if (!info.avatars.isEmpty()) {
-          if (32 != AvatarInfo.DEFAULT_SIZE) {
-            addAvatar(ap, info, user, 32);
-          }
-          if (100 != AvatarInfo.DEFAULT_SIZE) {
-            addAvatar(ap, info, user, 100);
-          }
+          addAvatar(ap, info, user, 56);
+          addAvatar(ap, info, user, 100);
+          addAvatar(ap, info, user, 120);
         }
       }
     }
@@ -172,7 +174,7 @@
 
   public List<String> getSecondaryEmails(Account account, Collection<ExternalId> externalIds) {
     return ExternalId.getEmails(externalIds)
-        .filter(e -> !e.equals(account.getPreferredEmail()))
+        .filter(e -> !e.equals(account.preferredEmail()))
         .sorted()
         .collect(toList());
   }
diff --git a/java/com/google/gerrit/server/account/InternalAccountUpdate.java b/java/com/google/gerrit/server/account/InternalAccountUpdate.java
index c778fca..cf77a75 100644
--- a/java/com/google/gerrit/server/account/InternalAccountUpdate.java
+++ b/java/com/google/gerrit/server/account/InternalAccountUpdate.java
@@ -18,10 +18,10 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
 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.reviewdb.client.Account;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
diff --git a/java/com/google/gerrit/server/account/InternalGroupBackend.java b/java/com/google/gerrit/server/account/InternalGroupBackend.java
index ea6eb87..ddd3da2 100644
--- a/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -19,7 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 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/ListGroupMembership.java b/java/com/google/gerrit/server/account/ListGroupMembership.java
index 60e7345..0f4fb78 100644
--- a/java/com/google/gerrit/server/account/ListGroupMembership.java
+++ b/java/com/google/gerrit/server/account/ListGroupMembership.java
@@ -16,7 +16,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import java.util.Set;
 
 /** GroupMembership over an explicit list. */
diff --git a/java/com/google/gerrit/server/account/Preferences.java b/java/com/google/gerrit/server/account/Preferences.java
index cd849b8..ece610b 100644
--- a/java/com/google/gerrit/server/account/Preferences.java
+++ b/java/com/google/gerrit/server/account/Preferences.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2018 The Android Open Source Project
+// 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.
@@ -11,566 +11,419 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-
 package com.google.gerrit.server.account;
 
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
-import static com.google.gerrit.server.config.ConfigUtil.skipField;
-import static com.google.gerrit.server.config.ConfigUtil.storeSection;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.flogger.FluentLogger;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DateFormat;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DefaultBase;
+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.TimeFormat;
 import com.google.gerrit.extensions.client.MenuItem;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.UserConfigSections;
-import com.google.gerrit.server.git.ValidationError;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.git.meta.VersionedMetaData;
-import java.io.IOException;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Optional;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Repository;
 
-/**
- * Parses/writes preferences from/to a {@link Config} file.
- *
- * <p>This is a low-level API. Read/write of preferences in a user branch should be done through
- * {@link AccountsUpdate} or {@link AccountConfig}.
- *
- * <p>The config file has separate sections for general, diff and edit preferences:
- *
- * <pre>
- *   [general]
- *     showSiteHeader = false
- *   [diff]
- *     hideTopMenu = true
- *   [edit]
- *     lineLength = 80
- * </pre>
- *
- * <p>The parameter names match the names that are used in the preferences REST API.
- *
- * <p>If the preference is omitted in the config file, then the default value for the preference is
- * used.
- *
- * <p>Defaults for preferences that apply for all accounts can be configured in the {@code
- * refs/users/default} branch in the {@code All-Users} repository. The config for the default
- * preferences must be provided to this class so that it can read default values from it.
- *
- * <p>The preferences are lazily parsed.
- */
-public class Preferences {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+@AutoValue
+public abstract class Preferences {
+  @AutoValue
+  public abstract static class General {
+    public abstract Optional<Integer> changesPerPage();
 
-  public static final String PREFERENCES_CONFIG = "preferences.config";
+    public abstract Optional<String> downloadScheme();
 
-  private final Account.Id accountId;
-  private final Config cfg;
-  private final Config defaultCfg;
-  private final ValidationError.Sink validationErrorSink;
+    public abstract Optional<DateFormat> dateFormat();
 
-  private GeneralPreferencesInfo generalPreferences;
-  private DiffPreferencesInfo diffPreferences;
-  private EditPreferencesInfo editPreferences;
+    public abstract Optional<TimeFormat> timeFormat();
 
-  Preferences(
-      Account.Id accountId,
-      Config cfg,
-      Config defaultCfg,
-      ValidationError.Sink validationErrorSink) {
-    this.accountId = requireNonNull(accountId, "accountId");
-    this.cfg = requireNonNull(cfg, "cfg");
-    this.defaultCfg = requireNonNull(defaultCfg, "defaultCfg");
-    this.validationErrorSink = requireNonNull(validationErrorSink, "validationErrorSink");
-  }
+    public abstract Optional<Boolean> expandInlineDiffs();
 
-  public GeneralPreferencesInfo getGeneralPreferences() {
-    if (generalPreferences == null) {
-      parse();
-    }
-    return generalPreferences;
-  }
+    public abstract Optional<Boolean> highlightAssigneeInChangeTable();
 
-  public DiffPreferencesInfo getDiffPreferences() {
-    if (diffPreferences == null) {
-      parse();
-    }
-    return diffPreferences;
-  }
+    public abstract Optional<Boolean> relativeDateInChangeTable();
 
-  public EditPreferencesInfo getEditPreferences() {
-    if (editPreferences == null) {
-      parse();
-    }
-    return editPreferences;
-  }
+    public abstract Optional<DiffView> diffView();
 
-  public void parse() {
-    generalPreferences = parseGeneralPreferences(null);
-    diffPreferences = parseDiffPreferences(null);
-    editPreferences = parseEditPreferences(null);
-  }
+    public abstract Optional<Boolean> sizeBarInChangeTable();
 
-  public Config saveGeneralPreferences(
-      Optional<GeneralPreferencesInfo> generalPreferencesInput,
-      Optional<DiffPreferencesInfo> diffPreferencesInput,
-      Optional<EditPreferencesInfo> editPreferencesInput)
-      throws ConfigInvalidException {
-    if (generalPreferencesInput.isPresent()) {
-      GeneralPreferencesInfo mergedGeneralPreferencesInput =
-          parseGeneralPreferences(generalPreferencesInput.get());
+    public abstract Optional<Boolean> legacycidInChangeTable();
 
-      storeSection(
-          cfg,
-          UserConfigSections.GENERAL,
-          null,
-          mergedGeneralPreferencesInput,
-          parseDefaultGeneralPreferences(defaultCfg, null));
-      setChangeTable(cfg, mergedGeneralPreferencesInput.changeTable);
-      setMy(cfg, mergedGeneralPreferencesInput.my);
+    public abstract Optional<Boolean> muteCommonPathPrefixes();
 
-      // evict the cached general preferences
-      this.generalPreferences = null;
+    public abstract Optional<Boolean> signedOffBy();
+
+    public abstract Optional<EmailStrategy> emailStrategy();
+
+    public abstract Optional<EmailFormat> emailFormat();
+
+    public abstract Optional<DefaultBase> defaultBaseForMerges();
+
+    public abstract Optional<Boolean> publishCommentsOnPush();
+
+    public abstract Optional<Boolean> workInProgressByDefault();
+
+    public abstract Optional<ImmutableList<MenuItem>> my();
+
+    public abstract Optional<ImmutableList<String>> changeTable();
+
+    @AutoValue.Builder
+    public abstract static class Builder {
+      abstract Builder changesPerPage(@Nullable Integer val);
+
+      abstract Builder downloadScheme(@Nullable String val);
+
+      abstract Builder dateFormat(@Nullable DateFormat val);
+
+      abstract Builder timeFormat(@Nullable TimeFormat val);
+
+      abstract Builder expandInlineDiffs(@Nullable Boolean val);
+
+      abstract Builder highlightAssigneeInChangeTable(@Nullable Boolean val);
+
+      abstract Builder relativeDateInChangeTable(@Nullable Boolean val);
+
+      abstract Builder diffView(@Nullable DiffView val);
+
+      abstract Builder sizeBarInChangeTable(@Nullable Boolean val);
+
+      abstract Builder legacycidInChangeTable(@Nullable Boolean val);
+
+      abstract Builder muteCommonPathPrefixes(@Nullable Boolean val);
+
+      abstract Builder signedOffBy(@Nullable Boolean val);
+
+      abstract Builder emailStrategy(@Nullable EmailStrategy val);
+
+      abstract Builder emailFormat(@Nullable EmailFormat val);
+
+      abstract Builder defaultBaseForMerges(@Nullable DefaultBase val);
+
+      abstract Builder publishCommentsOnPush(@Nullable Boolean val);
+
+      abstract Builder workInProgressByDefault(@Nullable Boolean val);
+
+      abstract Builder my(@Nullable ImmutableList<MenuItem> val);
+
+      abstract Builder changeTable(@Nullable ImmutableList<String> val);
+
+      abstract General build();
     }
 
-    if (diffPreferencesInput.isPresent()) {
-      DiffPreferencesInfo mergedDiffPreferencesInput =
-          parseDiffPreferences(diffPreferencesInput.get());
-
-      storeSection(
-          cfg,
-          UserConfigSections.DIFF,
-          null,
-          mergedDiffPreferencesInput,
-          parseDefaultDiffPreferences(defaultCfg, null));
-
-      // evict the cached diff preferences
-      this.diffPreferences = null;
+    public static General fromInfo(GeneralPreferencesInfo info) {
+      return (new AutoValue_Preferences_General.Builder())
+          .changesPerPage(info.changesPerPage)
+          .downloadScheme(info.downloadScheme)
+          .dateFormat(info.dateFormat)
+          .timeFormat(info.timeFormat)
+          .expandInlineDiffs(info.expandInlineDiffs)
+          .highlightAssigneeInChangeTable(info.highlightAssigneeInChangeTable)
+          .relativeDateInChangeTable(info.relativeDateInChangeTable)
+          .diffView(info.diffView)
+          .sizeBarInChangeTable(info.sizeBarInChangeTable)
+          .legacycidInChangeTable(info.legacycidInChangeTable)
+          .muteCommonPathPrefixes(info.muteCommonPathPrefixes)
+          .signedOffBy(info.signedOffBy)
+          .emailStrategy(info.emailStrategy)
+          .emailFormat(info.emailFormat)
+          .defaultBaseForMerges(info.defaultBaseForMerges)
+          .publishCommentsOnPush(info.publishCommentsOnPush)
+          .workInProgressByDefault(info.workInProgressByDefault)
+          .my(info.my == null ? null : ImmutableList.copyOf(info.my))
+          .changeTable(info.changeTable == null ? null : ImmutableList.copyOf(info.changeTable))
+          .build();
     }
 
-    if (editPreferencesInput.isPresent()) {
-      EditPreferencesInfo mergedEditPreferencesInput =
-          parseEditPreferences(editPreferencesInput.get());
-
-      storeSection(
-          cfg,
-          UserConfigSections.EDIT,
-          null,
-          mergedEditPreferencesInput,
-          parseDefaultEditPreferences(defaultCfg, null));
-
-      // evict the cached edit preferences
-      this.editPreferences = null;
-    }
-
-    return cfg;
-  }
-
-  private GeneralPreferencesInfo parseGeneralPreferences(@Nullable GeneralPreferencesInfo input) {
-    try {
-      return parseGeneralPreferences(cfg, defaultCfg, input);
-    } catch (ConfigInvalidException e) {
-      validationErrorSink.error(
-          new ValidationError(
-              PREFERENCES_CONFIG,
-              String.format(
-                  "Invalid general preferences for account %d: %s",
-                  accountId.get(), e.getMessage())));
-      return new GeneralPreferencesInfo();
+    public GeneralPreferencesInfo toInfo() {
+      GeneralPreferencesInfo info = new GeneralPreferencesInfo();
+      info.changesPerPage = changesPerPage().orElse(null);
+      info.downloadScheme = downloadScheme().orElse(null);
+      info.dateFormat = dateFormat().orElse(null);
+      info.timeFormat = timeFormat().orElse(null);
+      info.expandInlineDiffs = expandInlineDiffs().orElse(null);
+      info.highlightAssigneeInChangeTable = highlightAssigneeInChangeTable().orElse(null);
+      info.relativeDateInChangeTable = relativeDateInChangeTable().orElse(null);
+      info.diffView = diffView().orElse(null);
+      info.sizeBarInChangeTable = sizeBarInChangeTable().orElse(null);
+      info.legacycidInChangeTable = legacycidInChangeTable().orElse(null);
+      info.muteCommonPathPrefixes = muteCommonPathPrefixes().orElse(null);
+      info.signedOffBy = signedOffBy().orElse(null);
+      info.emailStrategy = emailStrategy().orElse(null);
+      info.emailFormat = emailFormat().orElse(null);
+      info.defaultBaseForMerges = defaultBaseForMerges().orElse(null);
+      info.publishCommentsOnPush = publishCommentsOnPush().orElse(null);
+      info.workInProgressByDefault = workInProgressByDefault().orElse(null);
+      info.my = my().orElse(null);
+      info.changeTable = changeTable().orElse(null);
+      return info;
     }
   }
 
-  private DiffPreferencesInfo parseDiffPreferences(@Nullable DiffPreferencesInfo input) {
-    try {
-      return parseDiffPreferences(cfg, defaultCfg, input);
-    } catch (ConfigInvalidException e) {
-      validationErrorSink.error(
-          new ValidationError(
-              PREFERENCES_CONFIG,
-              String.format(
-                  "Invalid diff preferences for account %d: %s", accountId.get(), e.getMessage())));
-      return new DiffPreferencesInfo();
+  @AutoValue
+  public abstract static class Edit {
+    public abstract Optional<Integer> tabSize();
+
+    public abstract Optional<Integer> lineLength();
+
+    public abstract Optional<Integer> indentUnit();
+
+    public abstract Optional<Integer> cursorBlinkRate();
+
+    public abstract Optional<Boolean> hideTopMenu();
+
+    public abstract Optional<Boolean> showTabs();
+
+    public abstract Optional<Boolean> showWhitespaceErrors();
+
+    public abstract Optional<Boolean> syntaxHighlighting();
+
+    public abstract Optional<Boolean> hideLineNumbers();
+
+    public abstract Optional<Boolean> matchBrackets();
+
+    public abstract Optional<Boolean> lineWrapping();
+
+    public abstract Optional<Boolean> indentWithTabs();
+
+    public abstract Optional<Boolean> autoCloseBrackets();
+
+    public abstract Optional<Boolean> showBase();
+
+    @AutoValue.Builder
+    public abstract static class Builder {
+      abstract Builder tabSize(@Nullable Integer val);
+
+      abstract Builder lineLength(@Nullable Integer val);
+
+      abstract Builder indentUnit(@Nullable Integer val);
+
+      abstract Builder cursorBlinkRate(@Nullable Integer val);
+
+      abstract Builder hideTopMenu(@Nullable Boolean val);
+
+      abstract Builder showTabs(@Nullable Boolean val);
+
+      abstract Builder showWhitespaceErrors(@Nullable Boolean val);
+
+      abstract Builder syntaxHighlighting(@Nullable Boolean val);
+
+      abstract Builder hideLineNumbers(@Nullable Boolean val);
+
+      abstract Builder matchBrackets(@Nullable Boolean val);
+
+      abstract Builder lineWrapping(@Nullable Boolean val);
+
+      abstract Builder indentWithTabs(@Nullable Boolean val);
+
+      abstract Builder autoCloseBrackets(@Nullable Boolean val);
+
+      abstract Builder showBase(@Nullable Boolean val);
+
+      abstract Edit build();
+    }
+
+    public static Edit fromInfo(EditPreferencesInfo info) {
+      return (new AutoValue_Preferences_Edit.Builder())
+          .tabSize(info.tabSize)
+          .lineLength(info.lineLength)
+          .indentUnit(info.indentUnit)
+          .cursorBlinkRate(info.cursorBlinkRate)
+          .hideTopMenu(info.hideTopMenu)
+          .showTabs(info.showTabs)
+          .showWhitespaceErrors(info.showWhitespaceErrors)
+          .syntaxHighlighting(info.syntaxHighlighting)
+          .hideLineNumbers(info.hideLineNumbers)
+          .matchBrackets(info.matchBrackets)
+          .lineWrapping(info.lineWrapping)
+          .indentWithTabs(info.indentWithTabs)
+          .autoCloseBrackets(info.autoCloseBrackets)
+          .showBase(info.showBase)
+          .build();
+    }
+
+    public EditPreferencesInfo toInfo() {
+      EditPreferencesInfo info = new EditPreferencesInfo();
+      info.tabSize = tabSize().orElse(null);
+      info.lineLength = lineLength().orElse(null);
+      info.indentUnit = indentUnit().orElse(null);
+      info.cursorBlinkRate = cursorBlinkRate().orElse(null);
+      info.hideTopMenu = hideTopMenu().orElse(null);
+      info.showTabs = showTabs().orElse(null);
+      info.showWhitespaceErrors = showWhitespaceErrors().orElse(null);
+      info.syntaxHighlighting = syntaxHighlighting().orElse(null);
+      info.hideLineNumbers = hideLineNumbers().orElse(null);
+      info.matchBrackets = matchBrackets().orElse(null);
+      info.lineWrapping = lineWrapping().orElse(null);
+      info.indentWithTabs = indentWithTabs().orElse(null);
+      info.autoCloseBrackets = autoCloseBrackets().orElse(null);
+      info.showBase = showBase().orElse(null);
+      return info;
     }
   }
 
-  private EditPreferencesInfo parseEditPreferences(@Nullable EditPreferencesInfo input) {
-    try {
-      return parseEditPreferences(cfg, defaultCfg, input);
-    } catch (ConfigInvalidException e) {
-      validationErrorSink.error(
-          new ValidationError(
-              PREFERENCES_CONFIG,
-              String.format(
-                  "Invalid edit preferences for account %d: %s", accountId.get(), e.getMessage())));
-      return new EditPreferencesInfo();
-    }
-  }
+  @AutoValue
+  public abstract static class Diff {
+    public abstract Optional<Integer> context();
 
-  private static GeneralPreferencesInfo parseGeneralPreferences(
-      Config cfg, @Nullable Config defaultCfg, @Nullable GeneralPreferencesInfo input)
-      throws ConfigInvalidException {
-    GeneralPreferencesInfo r =
-        loadSection(
-            cfg,
-            UserConfigSections.GENERAL,
-            null,
-            new GeneralPreferencesInfo(),
-            defaultCfg != null
-                ? parseDefaultGeneralPreferences(defaultCfg, input)
-                : GeneralPreferencesInfo.defaults(),
-            input);
-    if (input != null) {
-      r.changeTable = input.changeTable;
-      r.my = input.my;
-    } else {
-      r.changeTable = parseChangeTableColumns(cfg, defaultCfg);
-      r.my = parseMyMenus(cfg, defaultCfg);
-    }
-    return r;
-  }
+    public abstract Optional<Integer> tabSize();
 
-  private static DiffPreferencesInfo parseDiffPreferences(
-      Config cfg, @Nullable Config defaultCfg, @Nullable DiffPreferencesInfo input)
-      throws ConfigInvalidException {
-    return loadSection(
-        cfg,
-        UserConfigSections.DIFF,
-        null,
-        new DiffPreferencesInfo(),
-        defaultCfg != null
-            ? parseDefaultDiffPreferences(defaultCfg, input)
-            : DiffPreferencesInfo.defaults(),
-        input);
-  }
+    public abstract Optional<Integer> fontSize();
 
-  private static EditPreferencesInfo parseEditPreferences(
-      Config cfg, @Nullable Config defaultCfg, @Nullable EditPreferencesInfo input)
-      throws ConfigInvalidException {
-    return loadSection(
-        cfg,
-        UserConfigSections.EDIT,
-        null,
-        new EditPreferencesInfo(),
-        defaultCfg != null
-            ? parseDefaultEditPreferences(defaultCfg, input)
-            : EditPreferencesInfo.defaults(),
-        input);
-  }
+    public abstract Optional<Integer> lineLength();
 
-  private static GeneralPreferencesInfo parseDefaultGeneralPreferences(
-      Config defaultCfg, GeneralPreferencesInfo input) throws ConfigInvalidException {
-    GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
-    loadSection(
-        defaultCfg,
-        UserConfigSections.GENERAL,
-        null,
-        allUserPrefs,
-        GeneralPreferencesInfo.defaults(),
-        input);
-    return updateGeneralPreferencesDefaults(allUserPrefs);
-  }
+    public abstract Optional<Integer> cursorBlinkRate();
 
-  private static DiffPreferencesInfo parseDefaultDiffPreferences(
-      Config defaultCfg, DiffPreferencesInfo input) throws ConfigInvalidException {
-    DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
-    loadSection(
-        defaultCfg,
-        UserConfigSections.DIFF,
-        null,
-        allUserPrefs,
-        DiffPreferencesInfo.defaults(),
-        input);
-    return updateDiffPreferencesDefaults(allUserPrefs);
-  }
+    public abstract Optional<Boolean> expandAllComments();
 
-  private static EditPreferencesInfo parseDefaultEditPreferences(
-      Config defaultCfg, EditPreferencesInfo input) throws ConfigInvalidException {
-    EditPreferencesInfo allUserPrefs = new EditPreferencesInfo();
-    loadSection(
-        defaultCfg,
-        UserConfigSections.EDIT,
-        null,
-        allUserPrefs,
-        EditPreferencesInfo.defaults(),
-        input);
-    return updateEditPreferencesDefaults(allUserPrefs);
-  }
+    public abstract Optional<Boolean> intralineDifference();
 
-  private static GeneralPreferencesInfo updateGeneralPreferencesDefaults(
-      GeneralPreferencesInfo input) {
-    GeneralPreferencesInfo result = GeneralPreferencesInfo.defaults();
-    try {
-      for (Field field : input.getClass().getDeclaredFields()) {
-        if (skipField(field)) {
-          continue;
-        }
-        Object newVal = field.get(input);
-        if (newVal != null) {
-          field.set(result, newVal);
-        }
-      }
-    } catch (IllegalAccessException e) {
-      logger.atSevere().withCause(e).log("Failed to apply default general preferences");
-      return GeneralPreferencesInfo.defaults();
-    }
-    return result;
-  }
+    public abstract Optional<Boolean> manualReview();
 
-  private static DiffPreferencesInfo updateDiffPreferencesDefaults(DiffPreferencesInfo input) {
-    DiffPreferencesInfo result = DiffPreferencesInfo.defaults();
-    try {
-      for (Field field : input.getClass().getDeclaredFields()) {
-        if (skipField(field)) {
-          continue;
-        }
-        Object newVal = field.get(input);
-        if (newVal != null) {
-          field.set(result, newVal);
-        }
-      }
-    } catch (IllegalAccessException e) {
-      logger.atSevere().withCause(e).log("Failed to apply default diff preferences");
-      return DiffPreferencesInfo.defaults();
-    }
-    return result;
-  }
+    public abstract Optional<Boolean> showLineEndings();
 
-  private static EditPreferencesInfo updateEditPreferencesDefaults(EditPreferencesInfo input) {
-    EditPreferencesInfo result = EditPreferencesInfo.defaults();
-    try {
-      for (Field field : input.getClass().getDeclaredFields()) {
-        if (skipField(field)) {
-          continue;
-        }
-        Object newVal = field.get(input);
-        if (newVal != null) {
-          field.set(result, newVal);
-        }
-      }
-    } catch (IllegalAccessException e) {
-      logger.atSevere().withCause(e).log("Failed to apply default edit preferences");
-      return EditPreferencesInfo.defaults();
-    }
-    return result;
-  }
+    public abstract Optional<Boolean> showTabs();
 
-  private static List<String> parseChangeTableColumns(Config cfg, @Nullable Config defaultCfg) {
-    List<String> changeTable = changeTable(cfg);
-    if (changeTable == null && defaultCfg != null) {
-      changeTable = changeTable(defaultCfg);
-    }
-    return changeTable;
-  }
+    public abstract Optional<Boolean> showWhitespaceErrors();
 
-  private static List<MenuItem> parseMyMenus(Config cfg, @Nullable Config defaultCfg) {
-    List<MenuItem> my = my(cfg);
-    if (my.isEmpty() && defaultCfg != null) {
-      my = my(defaultCfg);
-    }
-    if (my.isEmpty()) {
-      my.add(new MenuItem("Changes", "#/dashboard/self", null));
-      my.add(new MenuItem("Draft Comments", "#/q/has:draft", null));
-      my.add(new MenuItem("Edits", "#/q/has:edit", null));
-      my.add(new MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
-      my.add(new MenuItem("Starred Changes", "#/q/is:starred", null));
-      my.add(new MenuItem("Groups", "#/groups/self", null));
-    }
-    return my;
-  }
+    public abstract Optional<Boolean> syntaxHighlighting();
 
-  public static GeneralPreferencesInfo readDefaultGeneralPreferences(
-      AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    return parseGeneralPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
-  }
+    public abstract Optional<Boolean> hideTopMenu();
 
-  public static DiffPreferencesInfo readDefaultDiffPreferences(
-      AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    return parseDiffPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
-  }
+    public abstract Optional<Boolean> autoHideDiffTableHeader();
 
-  public static EditPreferencesInfo readDefaultEditPreferences(
-      AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    return parseEditPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
-  }
+    public abstract Optional<Boolean> hideLineNumbers();
 
-  static Config readDefaultConfig(AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
-    defaultPrefs.load(allUsersName, allUsersRepo);
-    return defaultPrefs.getConfig();
-  }
+    public abstract Optional<Boolean> renderEntireFile();
 
-  public static GeneralPreferencesInfo updateDefaultGeneralPreferences(
-      MetaDataUpdate md, GeneralPreferencesInfo input) throws IOException, ConfigInvalidException {
-    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
-    defaultPrefs.load(md);
-    storeSection(
-        defaultPrefs.getConfig(),
-        UserConfigSections.GENERAL,
-        null,
-        input,
-        GeneralPreferencesInfo.defaults());
-    setMy(defaultPrefs.getConfig(), input.my);
-    setChangeTable(defaultPrefs.getConfig(), input.changeTable);
-    defaultPrefs.commit(md);
+    public abstract Optional<Boolean> hideEmptyPane();
 
-    return parseGeneralPreferences(defaultPrefs.getConfig(), null, null);
-  }
+    public abstract Optional<Boolean> matchBrackets();
 
-  public static DiffPreferencesInfo updateDefaultDiffPreferences(
-      MetaDataUpdate md, DiffPreferencesInfo input) throws IOException, ConfigInvalidException {
-    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
-    defaultPrefs.load(md);
-    storeSection(
-        defaultPrefs.getConfig(),
-        UserConfigSections.DIFF,
-        null,
-        input,
-        DiffPreferencesInfo.defaults());
-    defaultPrefs.commit(md);
+    public abstract Optional<Boolean> lineWrapping();
 
-    return parseDiffPreferences(defaultPrefs.getConfig(), null, null);
-  }
+    public abstract Optional<Whitespace> ignoreWhitespace();
 
-  public static EditPreferencesInfo updateDefaultEditPreferences(
-      MetaDataUpdate md, EditPreferencesInfo input) throws IOException, ConfigInvalidException {
-    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
-    defaultPrefs.load(md);
-    storeSection(
-        defaultPrefs.getConfig(),
-        UserConfigSections.EDIT,
-        null,
-        input,
-        EditPreferencesInfo.defaults());
-    defaultPrefs.commit(md);
+    public abstract Optional<Boolean> retainHeader();
 
-    return parseEditPreferences(defaultPrefs.getConfig(), null, null);
-  }
+    public abstract Optional<Boolean> skipDeleted();
 
-  private static List<String> changeTable(Config cfg) {
-    return Lists.newArrayList(cfg.getStringList(CHANGE_TABLE, null, CHANGE_TABLE_COLUMN));
-  }
+    public abstract Optional<Boolean> skipUnchanged();
 
-  private static void setChangeTable(Config cfg, List<String> changeTable) {
-    if (changeTable != null) {
-      unsetSection(cfg, UserConfigSections.CHANGE_TABLE);
-      cfg.setStringList(UserConfigSections.CHANGE_TABLE, null, CHANGE_TABLE_COLUMN, changeTable);
-    }
-  }
+    public abstract Optional<Boolean> skipUncommented();
 
-  private static List<MenuItem> my(Config cfg) {
-    List<MenuItem> my = new ArrayList<>();
-    for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
-      String url = my(cfg, subsection, KEY_URL, "#/");
-      String target = my(cfg, subsection, KEY_TARGET, url.startsWith("#") ? null : "_blank");
-      my.add(new MenuItem(subsection, url, target, my(cfg, subsection, KEY_ID, null)));
-    }
-    return my;
-  }
+    @AutoValue.Builder
+    public abstract static class Builder {
+      abstract Builder context(@Nullable Integer val);
 
-  private static String my(Config cfg, String subsection, String key, String defaultValue) {
-    String val = cfg.getString(UserConfigSections.MY, subsection, key);
-    return !Strings.isNullOrEmpty(val) ? val : defaultValue;
-  }
+      abstract Builder tabSize(@Nullable Integer val);
 
-  private static void setMy(Config cfg, List<MenuItem> my) {
-    if (my != null) {
-      unsetSection(cfg, UserConfigSections.MY);
-      for (MenuItem item : my) {
-        checkState(!isNullOrEmpty(item.name), "MenuItem.name must not be null or empty");
-        checkState(!isNullOrEmpty(item.url), "MenuItem.url must not be null or empty");
+      abstract Builder fontSize(@Nullable Integer val);
 
-        setMy(cfg, item.name, KEY_URL, item.url);
-        setMy(cfg, item.name, KEY_TARGET, item.target);
-        setMy(cfg, item.name, KEY_ID, item.id);
-      }
-    }
-  }
+      abstract Builder lineLength(@Nullable Integer val);
 
-  public static void validateMy(List<MenuItem> my) throws BadRequestException {
-    if (my == null) {
-      return;
-    }
-    for (MenuItem item : my) {
-      checkRequiredMenuItemField(item.name, "name");
-      checkRequiredMenuItemField(item.url, "URL");
-    }
-  }
+      abstract Builder cursorBlinkRate(@Nullable Integer val);
 
-  private static void checkRequiredMenuItemField(String value, String name)
-      throws BadRequestException {
-    if (isNullOrEmpty(value)) {
-      throw new BadRequestException(name + " for menu item is required");
-    }
-  }
+      abstract Builder expandAllComments(@Nullable Boolean val);
 
-  private static boolean isNullOrEmpty(String value) {
-    return value == null || value.trim().isEmpty();
-  }
+      abstract Builder intralineDifference(@Nullable Boolean val);
 
-  private static void setMy(Config cfg, String section, String key, @Nullable String val) {
-    if (val == null || val.trim().isEmpty()) {
-      cfg.unset(UserConfigSections.MY, section.trim(), key);
-    } else {
-      cfg.setString(UserConfigSections.MY, section.trim(), key, val.trim());
-    }
-  }
+      abstract Builder manualReview(@Nullable Boolean val);
 
-  private static void unsetSection(Config cfg, String section) {
-    cfg.unsetSection(section, null);
-    for (String subsection : cfg.getSubsections(section)) {
-      cfg.unsetSection(section, subsection);
-    }
-  }
+      abstract Builder showLineEndings(@Nullable Boolean val);
 
-  private static class VersionedDefaultPreferences extends VersionedMetaData {
-    private Config cfg;
+      abstract Builder showTabs(@Nullable Boolean val);
 
-    @Override
-    protected String getRefName() {
-      return RefNames.REFS_USERS_DEFAULT;
+      abstract Builder showWhitespaceErrors(@Nullable Boolean val);
+
+      abstract Builder syntaxHighlighting(@Nullable Boolean val);
+
+      abstract Builder hideTopMenu(@Nullable Boolean val);
+
+      abstract Builder autoHideDiffTableHeader(@Nullable Boolean val);
+
+      abstract Builder hideLineNumbers(@Nullable Boolean val);
+
+      abstract Builder renderEntireFile(@Nullable Boolean val);
+
+      abstract Builder hideEmptyPane(@Nullable Boolean val);
+
+      abstract Builder matchBrackets(@Nullable Boolean val);
+
+      abstract Builder lineWrapping(@Nullable Boolean val);
+
+      abstract Builder ignoreWhitespace(@Nullable Whitespace val);
+
+      abstract Builder retainHeader(@Nullable Boolean val);
+
+      abstract Builder skipDeleted(@Nullable Boolean val);
+
+      abstract Builder skipUnchanged(@Nullable Boolean val);
+
+      abstract Builder skipUncommented(@Nullable Boolean val);
+
+      abstract Diff build();
     }
 
-    private Config getConfig() {
-      checkState(cfg != null, "Default preferences not loaded yet.");
-      return cfg;
+    public static Diff fromInfo(DiffPreferencesInfo info) {
+      return (new AutoValue_Preferences_Diff.Builder())
+          .context(info.context)
+          .tabSize(info.tabSize)
+          .fontSize(info.fontSize)
+          .lineLength(info.lineLength)
+          .cursorBlinkRate(info.cursorBlinkRate)
+          .expandAllComments(info.expandAllComments)
+          .intralineDifference(info.intralineDifference)
+          .manualReview(info.manualReview)
+          .showLineEndings(info.showLineEndings)
+          .showTabs(info.showTabs)
+          .showWhitespaceErrors(info.showWhitespaceErrors)
+          .syntaxHighlighting(info.syntaxHighlighting)
+          .hideTopMenu(info.hideTopMenu)
+          .autoHideDiffTableHeader(info.autoHideDiffTableHeader)
+          .hideLineNumbers(info.hideLineNumbers)
+          .renderEntireFile(info.renderEntireFile)
+          .hideEmptyPane(info.hideEmptyPane)
+          .matchBrackets(info.matchBrackets)
+          .lineWrapping(info.lineWrapping)
+          .ignoreWhitespace(info.ignoreWhitespace)
+          .retainHeader(info.retainHeader)
+          .skipDeleted(info.skipDeleted)
+          .skipUnchanged(info.skipUnchanged)
+          .skipUncommented(info.skipUncommented)
+          .build();
     }
 
-    @Override
-    protected void onLoad() throws IOException, ConfigInvalidException {
-      cfg = readConfig(PREFERENCES_CONFIG);
-    }
-
-    @Override
-    protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
-      if (Strings.isNullOrEmpty(commit.getMessage())) {
-        commit.setMessage("Update default preferences\n");
-      }
-      saveConfig(PREFERENCES_CONFIG, cfg);
-      return true;
+    public DiffPreferencesInfo toInfo() {
+      DiffPreferencesInfo info = new DiffPreferencesInfo();
+      info.context = context().orElse(null);
+      info.tabSize = tabSize().orElse(null);
+      info.fontSize = fontSize().orElse(null);
+      info.lineLength = lineLength().orElse(null);
+      info.cursorBlinkRate = cursorBlinkRate().orElse(null);
+      info.expandAllComments = expandAllComments().orElse(null);
+      info.intralineDifference = intralineDifference().orElse(null);
+      info.manualReview = manualReview().orElse(null);
+      info.showLineEndings = showLineEndings().orElse(null);
+      info.showTabs = showTabs().orElse(null);
+      info.showWhitespaceErrors = showWhitespaceErrors().orElse(null);
+      info.syntaxHighlighting = syntaxHighlighting().orElse(null);
+      info.hideTopMenu = hideTopMenu().orElse(null);
+      info.autoHideDiffTableHeader = autoHideDiffTableHeader().orElse(null);
+      info.hideLineNumbers = hideLineNumbers().orElse(null);
+      info.renderEntireFile = renderEntireFile().orElse(null);
+      info.hideEmptyPane = hideEmptyPane().orElse(null);
+      info.matchBrackets = matchBrackets().orElse(null);
+      info.lineWrapping = lineWrapping().orElse(null);
+      info.ignoreWhitespace = ignoreWhitespace().orElse(null);
+      info.retainHeader = retainHeader().orElse(null);
+      info.skipDeleted = skipDeleted().orElse(null);
+      info.skipUnchanged = skipUnchanged().orElse(null);
+      info.skipUncommented = skipUncommented().orElse(null);
+      return info;
     }
   }
 }
diff --git a/java/com/google/gerrit/server/account/ProjectWatches.java b/java/com/google/gerrit/server/account/ProjectWatches.java
index a750ba5..b153b78 100644
--- a/java/com/google/gerrit/server/account/ProjectWatches.java
+++ b/java/com/google/gerrit/server/account/ProjectWatches.java
@@ -30,8 +30,8 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.ValidationError;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -168,7 +168,7 @@
         }
 
         ProjectWatchKey key =
-            ProjectWatchKey.create(new Project.NameKey(projectName), notifyValue.filter());
+            ProjectWatchKey.create(Project.nameKey(projectName), notifyValue.filter());
         if (!projectWatches.containsKey(key)) {
           projectWatches.put(key, EnumSet.noneOf(NotifyType.class));
         }
diff --git a/java/com/google/gerrit/server/account/Realm.java b/java/com/google/gerrit/server/account/Realm.java
index 4e8cf09..798b4e8 100644
--- a/java/com/google/gerrit/server/account/Realm.java
+++ b/java/com/google/gerrit/server/account/Realm.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/account/SetInactiveFlag.java b/java/com/google/gerrit/server/account/SetInactiveFlag.java
index da2d640..40cc185 100644
--- a/java/com/google/gerrit/server/account/SetInactiveFlag.java
+++ b/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.events.AccountActivationListener;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.validators.AccountActivationValidationListener;
@@ -37,13 +38,16 @@
   private final PluginSetContext<AccountActivationValidationListener>
       accountActivationValidationListeners;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
+  private final PluginSetContext<AccountActivationListener> accountActivationListeners;
 
   @Inject
   SetInactiveFlag(
       PluginSetContext<AccountActivationValidationListener> accountActivationValidationListeners,
-      @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
+      @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
+      PluginSetContext<AccountActivationListener> accountActivationListeners) {
     this.accountActivationValidationListeners = accountActivationValidationListeners;
     this.accountsUpdateProvider = accountsUpdateProvider;
+    this.accountActivationListeners = accountActivationListeners;
   }
 
   public Response<?> deactivate(Account.Id accountId)
@@ -56,7 +60,7 @@
             "Deactivate Account via API",
             accountId,
             (a, u) -> {
-              if (!a.getAccount().isActive()) {
+              if (!a.account().isActive()) {
                 alreadyInactive.set(true);
               } else {
                 try {
@@ -76,6 +80,12 @@
     if (alreadyInactive.get()) {
       throw new ResourceConflictException("account not active");
     }
+
+    // At this point the account got set inactive and no errors occurred
+
+    int id = accountId.get();
+    accountActivationListeners.runEach(l -> l.onAccountDeactivated(id));
+
     return Response.none();
   }
 
@@ -89,7 +99,7 @@
             "Activate Account via API",
             accountId,
             (a, u) -> {
-              if (a.getAccount().isActive()) {
+              if (a.account().isActive()) {
                 alreadyActive.set(true);
               } else {
                 try {
@@ -106,6 +116,16 @@
     if (exception.get().isPresent()) {
       throw exception.get().get();
     }
-    return alreadyActive.get() ? Response.ok("") : Response.created("");
+
+    Response<String> res;
+    if (alreadyActive.get()) {
+      res = Response.ok("");
+    } else {
+      res = Response.created("");
+
+      int id = accountId.get();
+      accountActivationListeners.runEach(l -> l.onAccountActivated(id));
+    }
+    return res;
   }
 }
diff --git a/java/com/google/gerrit/server/account/StoredPreferences.java b/java/com/google/gerrit/server/account/StoredPreferences.java
new file mode 100644
index 0000000..0e8eb04
--- /dev/null
+++ b/java/com/google/gerrit/server/account/StoredPreferences.java
@@ -0,0 +1,574 @@
+// 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.
+
+package com.google.gerrit.server.account;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+import static com.google.gerrit.server.config.ConfigUtil.skipField;
+import static com.google.gerrit.server.config.ConfigUtil.storeSection;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.MenuItem;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.git.meta.VersionedMetaData;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Parses/writes preferences from/to a {@link Config} file.
+ *
+ * <p>This is a low-level API. Read/write of preferences in a user branch should be done through
+ * {@link AccountsUpdate} or {@link AccountConfig}.
+ *
+ * <p>The config file has separate sections for general, diff and edit preferences:
+ *
+ * <pre>
+ *   [diff]
+ *     hideTopMenu = true
+ *   [edit]
+ *     lineLength = 80
+ * </pre>
+ *
+ * <p>The parameter names match the names that are used in the preferences REST API.
+ *
+ * <p>If the preference is omitted in the config file, then the default value for the preference is
+ * used.
+ *
+ * <p>Defaults for preferences that apply for all accounts can be configured in the {@code
+ * refs/users/default} branch in the {@code All-Users} repository. The config for the default
+ * preferences must be provided to this class so that it can read default values from it.
+ *
+ * <p>The preferences are lazily parsed.
+ */
+public class StoredPreferences {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public static final String PREFERENCES_CONFIG = "preferences.config";
+
+  private final Account.Id accountId;
+  private final Config cfg;
+  private final Config defaultCfg;
+  private final ValidationError.Sink validationErrorSink;
+
+  private GeneralPreferencesInfo generalPreferences;
+  private DiffPreferencesInfo diffPreferences;
+  private EditPreferencesInfo editPreferences;
+
+  StoredPreferences(
+      Account.Id accountId,
+      Config cfg,
+      Config defaultCfg,
+      ValidationError.Sink validationErrorSink) {
+    this.accountId = requireNonNull(accountId, "accountId");
+    this.cfg = requireNonNull(cfg, "cfg");
+    this.defaultCfg = requireNonNull(defaultCfg, "defaultCfg");
+    this.validationErrorSink = requireNonNull(validationErrorSink, "validationErrorSink");
+  }
+
+  public GeneralPreferencesInfo getGeneralPreferences() {
+    if (generalPreferences == null) {
+      parse();
+    }
+    return generalPreferences;
+  }
+
+  public DiffPreferencesInfo getDiffPreferences() {
+    if (diffPreferences == null) {
+      parse();
+    }
+    return diffPreferences;
+  }
+
+  public EditPreferencesInfo getEditPreferences() {
+    if (editPreferences == null) {
+      parse();
+    }
+    return editPreferences;
+  }
+
+  public void parse() {
+    generalPreferences = parseGeneralPreferences(null);
+    diffPreferences = parseDiffPreferences(null);
+    editPreferences = parseEditPreferences(null);
+  }
+
+  public Config saveGeneralPreferences(
+      Optional<GeneralPreferencesInfo> generalPreferencesInput,
+      Optional<DiffPreferencesInfo> diffPreferencesInput,
+      Optional<EditPreferencesInfo> editPreferencesInput)
+      throws ConfigInvalidException {
+    if (generalPreferencesInput.isPresent()) {
+      GeneralPreferencesInfo mergedGeneralPreferencesInput =
+          parseGeneralPreferences(generalPreferencesInput.get());
+
+      storeSection(
+          cfg,
+          UserConfigSections.GENERAL,
+          null,
+          mergedGeneralPreferencesInput,
+          parseDefaultGeneralPreferences(defaultCfg, null));
+      setChangeTable(cfg, mergedGeneralPreferencesInput.changeTable);
+      setMy(cfg, mergedGeneralPreferencesInput.my);
+
+      // evict the cached general preferences
+      this.generalPreferences = null;
+    }
+
+    if (diffPreferencesInput.isPresent()) {
+      DiffPreferencesInfo mergedDiffPreferencesInput =
+          parseDiffPreferences(diffPreferencesInput.get());
+
+      storeSection(
+          cfg,
+          UserConfigSections.DIFF,
+          null,
+          mergedDiffPreferencesInput,
+          parseDefaultDiffPreferences(defaultCfg, null));
+
+      // evict the cached diff preferences
+      this.diffPreferences = null;
+    }
+
+    if (editPreferencesInput.isPresent()) {
+      EditPreferencesInfo mergedEditPreferencesInput =
+          parseEditPreferences(editPreferencesInput.get());
+
+      storeSection(
+          cfg,
+          UserConfigSections.EDIT,
+          null,
+          mergedEditPreferencesInput,
+          parseDefaultEditPreferences(defaultCfg, null));
+
+      // evict the cached edit preferences
+      this.editPreferences = null;
+    }
+
+    return cfg;
+  }
+
+  private GeneralPreferencesInfo parseGeneralPreferences(@Nullable GeneralPreferencesInfo input) {
+    try {
+      return parseGeneralPreferences(cfg, defaultCfg, input);
+    } catch (ConfigInvalidException e) {
+      validationErrorSink.error(
+          new ValidationError(
+              PREFERENCES_CONFIG,
+              String.format(
+                  "Invalid general preferences for account %d: %s",
+                  accountId.get(), e.getMessage())));
+      return new GeneralPreferencesInfo();
+    }
+  }
+
+  private DiffPreferencesInfo parseDiffPreferences(@Nullable DiffPreferencesInfo input) {
+    try {
+      return parseDiffPreferences(cfg, defaultCfg, input);
+    } catch (ConfigInvalidException e) {
+      validationErrorSink.error(
+          new ValidationError(
+              PREFERENCES_CONFIG,
+              String.format(
+                  "Invalid diff preferences for account %d: %s", accountId.get(), e.getMessage())));
+      return new DiffPreferencesInfo();
+    }
+  }
+
+  private EditPreferencesInfo parseEditPreferences(@Nullable EditPreferencesInfo input) {
+    try {
+      return parseEditPreferences(cfg, defaultCfg, input);
+    } catch (ConfigInvalidException e) {
+      validationErrorSink.error(
+          new ValidationError(
+              PREFERENCES_CONFIG,
+              String.format(
+                  "Invalid edit preferences for account %d: %s", accountId.get(), e.getMessage())));
+      return new EditPreferencesInfo();
+    }
+  }
+
+  private static GeneralPreferencesInfo parseGeneralPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable GeneralPreferencesInfo input)
+      throws ConfigInvalidException {
+    GeneralPreferencesInfo r =
+        loadSection(
+            cfg,
+            UserConfigSections.GENERAL,
+            null,
+            new GeneralPreferencesInfo(),
+            defaultCfg != null
+                ? parseDefaultGeneralPreferences(defaultCfg, input)
+                : GeneralPreferencesInfo.defaults(),
+            input);
+    if (input != null) {
+      r.changeTable = input.changeTable;
+      r.my = input.my;
+    } else {
+      r.changeTable = parseChangeTableColumns(cfg, defaultCfg);
+      r.my = parseMyMenus(cfg, defaultCfg);
+    }
+    return r;
+  }
+
+  private static DiffPreferencesInfo parseDiffPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable DiffPreferencesInfo input)
+      throws ConfigInvalidException {
+    return loadSection(
+        cfg,
+        UserConfigSections.DIFF,
+        null,
+        new DiffPreferencesInfo(),
+        defaultCfg != null
+            ? parseDefaultDiffPreferences(defaultCfg, input)
+            : DiffPreferencesInfo.defaults(),
+        input);
+  }
+
+  private static EditPreferencesInfo parseEditPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable EditPreferencesInfo input)
+      throws ConfigInvalidException {
+    return loadSection(
+        cfg,
+        UserConfigSections.EDIT,
+        null,
+        new EditPreferencesInfo(),
+        defaultCfg != null
+            ? parseDefaultEditPreferences(defaultCfg, input)
+            : EditPreferencesInfo.defaults(),
+        input);
+  }
+
+  private static GeneralPreferencesInfo parseDefaultGeneralPreferences(
+      Config defaultCfg, GeneralPreferencesInfo input) throws ConfigInvalidException {
+    GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.GENERAL,
+        null,
+        allUserPrefs,
+        GeneralPreferencesInfo.defaults(),
+        input);
+    return updateGeneralPreferencesDefaults(allUserPrefs);
+  }
+
+  private static DiffPreferencesInfo parseDefaultDiffPreferences(
+      Config defaultCfg, DiffPreferencesInfo input) throws ConfigInvalidException {
+    DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.DIFF,
+        null,
+        allUserPrefs,
+        DiffPreferencesInfo.defaults(),
+        input);
+    return updateDiffPreferencesDefaults(allUserPrefs);
+  }
+
+  private static EditPreferencesInfo parseDefaultEditPreferences(
+      Config defaultCfg, EditPreferencesInfo input) throws ConfigInvalidException {
+    EditPreferencesInfo allUserPrefs = new EditPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.EDIT,
+        null,
+        allUserPrefs,
+        EditPreferencesInfo.defaults(),
+        input);
+    return updateEditPreferencesDefaults(allUserPrefs);
+  }
+
+  private static GeneralPreferencesInfo updateGeneralPreferencesDefaults(
+      GeneralPreferencesInfo input) {
+    GeneralPreferencesInfo result = GeneralPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      logger.atSevere().withCause(e).log("Failed to apply default general preferences");
+      return GeneralPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static DiffPreferencesInfo updateDiffPreferencesDefaults(DiffPreferencesInfo input) {
+    DiffPreferencesInfo result = DiffPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      logger.atSevere().withCause(e).log("Failed to apply default diff preferences");
+      return DiffPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static EditPreferencesInfo updateEditPreferencesDefaults(EditPreferencesInfo input) {
+    EditPreferencesInfo result = EditPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      logger.atSevere().withCause(e).log("Failed to apply default edit preferences");
+      return EditPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static List<String> parseChangeTableColumns(Config cfg, @Nullable Config defaultCfg) {
+    List<String> changeTable = changeTable(cfg);
+    if (changeTable == null && defaultCfg != null) {
+      changeTable = changeTable(defaultCfg);
+    }
+    return changeTable;
+  }
+
+  private static List<MenuItem> parseMyMenus(Config cfg, @Nullable Config defaultCfg) {
+    List<MenuItem> my = my(cfg);
+    if (my.isEmpty() && defaultCfg != null) {
+      my = my(defaultCfg);
+    }
+    if (my.isEmpty()) {
+      my.add(new MenuItem("Changes", "#/dashboard/self", null));
+      my.add(new MenuItem("Draft Comments", "#/q/has:draft", null));
+      my.add(new MenuItem("Edits", "#/q/has:edit", null));
+      my.add(new MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
+      my.add(new MenuItem("Starred Changes", "#/q/is:starred", null));
+      my.add(new MenuItem("Groups", "#/settings/#Groups", null));
+    }
+    return my;
+  }
+
+  public static GeneralPreferencesInfo readDefaultGeneralPreferences(
+      AllUsersName allUsersName, Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    return parseGeneralPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
+  }
+
+  public static DiffPreferencesInfo readDefaultDiffPreferences(
+      AllUsersName allUsersName, Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    return parseDiffPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
+  }
+
+  public static EditPreferencesInfo readDefaultEditPreferences(
+      AllUsersName allUsersName, Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    return parseEditPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
+  }
+
+  static Config readDefaultConfig(AllUsersName allUsersName, Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
+    defaultPrefs.load(allUsersName, allUsersRepo);
+    return defaultPrefs.getConfig();
+  }
+
+  public static GeneralPreferencesInfo updateDefaultGeneralPreferences(
+      MetaDataUpdate md, GeneralPreferencesInfo input) throws IOException, ConfigInvalidException {
+    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
+    defaultPrefs.load(md);
+    storeSection(
+        defaultPrefs.getConfig(),
+        UserConfigSections.GENERAL,
+        null,
+        input,
+        GeneralPreferencesInfo.defaults());
+    setMy(defaultPrefs.getConfig(), input.my);
+    setChangeTable(defaultPrefs.getConfig(), input.changeTable);
+    defaultPrefs.commit(md);
+
+    return parseGeneralPreferences(defaultPrefs.getConfig(), null, null);
+  }
+
+  public static DiffPreferencesInfo updateDefaultDiffPreferences(
+      MetaDataUpdate md, DiffPreferencesInfo input) throws IOException, ConfigInvalidException {
+    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
+    defaultPrefs.load(md);
+    storeSection(
+        defaultPrefs.getConfig(),
+        UserConfigSections.DIFF,
+        null,
+        input,
+        DiffPreferencesInfo.defaults());
+    defaultPrefs.commit(md);
+
+    return parseDiffPreferences(defaultPrefs.getConfig(), null, null);
+  }
+
+  public static EditPreferencesInfo updateDefaultEditPreferences(
+      MetaDataUpdate md, EditPreferencesInfo input) throws IOException, ConfigInvalidException {
+    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
+    defaultPrefs.load(md);
+    storeSection(
+        defaultPrefs.getConfig(),
+        UserConfigSections.EDIT,
+        null,
+        input,
+        EditPreferencesInfo.defaults());
+    defaultPrefs.commit(md);
+
+    return parseEditPreferences(defaultPrefs.getConfig(), null, null);
+  }
+
+  private static List<String> changeTable(Config cfg) {
+    return Lists.newArrayList(cfg.getStringList(CHANGE_TABLE, null, CHANGE_TABLE_COLUMN));
+  }
+
+  private static void setChangeTable(Config cfg, List<String> changeTable) {
+    if (changeTable != null) {
+      unsetSection(cfg, UserConfigSections.CHANGE_TABLE);
+      cfg.setStringList(UserConfigSections.CHANGE_TABLE, null, CHANGE_TABLE_COLUMN, changeTable);
+    }
+  }
+
+  private static List<MenuItem> my(Config cfg) {
+    List<MenuItem> my = new ArrayList<>();
+    for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
+      String url = my(cfg, subsection, KEY_URL, "#/");
+      String target = my(cfg, subsection, KEY_TARGET, url.startsWith("#") ? null : "_blank");
+      my.add(new MenuItem(subsection, url, target, my(cfg, subsection, KEY_ID, null)));
+    }
+    return my;
+  }
+
+  private static String my(Config cfg, String subsection, String key, String defaultValue) {
+    String val = cfg.getString(UserConfigSections.MY, subsection, key);
+    return !Strings.isNullOrEmpty(val) ? val : defaultValue;
+  }
+
+  private static void setMy(Config cfg, List<MenuItem> my) {
+    if (my != null) {
+      unsetSection(cfg, UserConfigSections.MY);
+      for (MenuItem item : my) {
+        checkState(!isNullOrEmpty(item.name), "MenuItem.name must not be null or empty");
+        checkState(!isNullOrEmpty(item.url), "MenuItem.url must not be null or empty");
+
+        setMy(cfg, item.name, KEY_URL, item.url);
+        setMy(cfg, item.name, KEY_TARGET, item.target);
+        setMy(cfg, item.name, KEY_ID, item.id);
+      }
+    }
+  }
+
+  public static void validateMy(List<MenuItem> my) throws BadRequestException {
+    if (my == null) {
+      return;
+    }
+    for (MenuItem item : my) {
+      checkRequiredMenuItemField(item.name, "name");
+      checkRequiredMenuItemField(item.url, "URL");
+    }
+  }
+
+  private static void checkRequiredMenuItemField(String value, String name)
+      throws BadRequestException {
+    if (isNullOrEmpty(value)) {
+      throw new BadRequestException(name + " for menu item is required");
+    }
+  }
+
+  private static boolean isNullOrEmpty(String value) {
+    return value == null || value.trim().isEmpty();
+  }
+
+  private static void setMy(Config cfg, String section, String key, @Nullable String val) {
+    if (val == null || val.trim().isEmpty()) {
+      cfg.unset(UserConfigSections.MY, section.trim(), key);
+    } else {
+      cfg.setString(UserConfigSections.MY, section.trim(), key, val.trim());
+    }
+  }
+
+  private static void unsetSection(Config cfg, String section) {
+    cfg.unsetSection(section, null);
+    for (String subsection : cfg.getSubsections(section)) {
+      cfg.unsetSection(section, subsection);
+    }
+  }
+
+  private static class VersionedDefaultPreferences extends VersionedMetaData {
+    private Config cfg;
+
+    @Override
+    protected String getRefName() {
+      return RefNames.REFS_USERS_DEFAULT;
+    }
+
+    private Config getConfig() {
+      checkState(cfg != null, "Default preferences not loaded yet.");
+      return cfg;
+    }
+
+    @Override
+    protected void onLoad() throws IOException, ConfigInvalidException {
+      cfg = readConfig(PREFERENCES_CONFIG);
+    }
+
+    @Override
+    protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
+      if (Strings.isNullOrEmpty(commit.getMessage())) {
+        commit.setMessage("Update default preferences\n");
+      }
+      saveConfig(PREFERENCES_CONFIG, cfg);
+      return true;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index ecd7468..fddbd2b 100644
--- a/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -26,7 +26,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.StartupCheck;
 import com.google.gerrit.server.StartupException;
@@ -220,7 +220,7 @@
           cfg.getSubsections("groups").stream()
               .filter(
                   sub -> {
-                    AccountGroup.UUID uuid = new AccountGroup.UUID(sub);
+                    AccountGroup.UUID uuid = AccountGroup.uuid(sub);
                     GroupBackend groupBackend = universalGroupBackend.backend(uuid);
                     return groupBackend == null || groupBackend.get(uuid) == null;
                   })
diff --git a/java/com/google/gerrit/server/account/VersionedAccountDestinations.java b/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
index e2f1bc2..f1cf9fe 100644
--- a/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
+++ b/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
diff --git a/java/com/google/gerrit/server/account/VersionedAccountQueries.java b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
index daf7100..e2ffe9b 100644
--- a/java/com/google/gerrit/server/account/VersionedAccountQueries.java
+++ b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
@@ -14,11 +14,17 @@
 
 package com.google.gerrit.server.account;
 
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 
@@ -46,6 +52,16 @@
     return queryList;
   }
 
+  public void setQueryList(String text) throws IOException, ConfigInvalidException {
+    List<ValidationError> errors = new ArrayList<>();
+    QueryList newQueryList = QueryList.parse(text, error -> errors.add(error));
+    if (!errors.isEmpty()) {
+      String messages = errors.stream().map(ValidationError::getMessage).collect(joining(", "));
+      throw new ConfigInvalidException("Invalid named queries: " + messages);
+    }
+    queryList = newQueryList;
+  }
+
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
     queryList =
@@ -58,6 +74,10 @@
 
   @Override
   protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
-    throw new UnsupportedOperationException("Cannot yet save named queries");
+    if (Strings.isNullOrEmpty(commit.getMessage())) {
+      commit.setMessage("Updated named queries\n");
+    }
+    saveUTF8(QueryList.FILE_NAME, queryList.asText());
+    return true;
   }
 }
diff --git a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index c7808de..235537c 100644
--- a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -20,9 +20,9 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Ordering;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
diff --git a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
index 5d12ae1..4da2a9e 100644
--- a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
@@ -21,8 +21,8 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.SetMultimap;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.proto.Protos;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto;
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto.ExternalIdProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
@@ -96,7 +96,7 @@
     private static ExternalId toExternalId(ObjectIdConverter idConverter, ExternalIdProto proto) {
       return ExternalId.create(
           ExternalId.Key.parse(proto.getKey()),
-          new Account.Id(proto.getAccountId()),
+          Account.id(proto.getAccountId()),
           // ExternalId treats null and empty strings the same, so no need to distinguish here.
           proto.getEmail(),
           proto.getPassword(),
diff --git a/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java b/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
index 5894051..e1e9c70 100644
--- a/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
+++ b/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
@@ -16,7 +16,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index ab82714..2d501ad 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -27,8 +27,9 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.AuthType;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.server.account.HashedPassword;
 import java.io.Serializable;
 import java.util.Collection;
@@ -39,7 +40,6 @@
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 
 @AutoValue
@@ -364,7 +364,7 @@
 
     return create(
         externalIdKey,
-        new Account.Id(accountId),
+        Account.id(accountId),
         Strings.emptyToNull(email),
         Strings.emptyToNull(password),
         blobId);
@@ -432,10 +432,10 @@
 
   public byte[] toByteArray() {
     checkState(blobId() != null, "Missing blobId in external ID %s", key().get());
-    byte[] b = new byte[2 * Constants.OBJECT_ID_STRING_LENGTH + 1];
+    byte[] b = new byte[2 * ObjectIds.STR_LEN + 1];
     key().sha1().copyTo(b, 0);
-    b[Constants.OBJECT_ID_STRING_LENGTH] = ':';
-    blobId().copyTo(b, Constants.OBJECT_ID_STRING_LENGTH + 1);
+    b[ObjectIds.STR_LEN] = ':';
+    blobId().copyTo(b, ObjectIds.STR_LEN + 1);
     return b;
   }
 
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
index 1ac737e..0edf154 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.account.externalids;
 
 import com.google.common.collect.SetMultimap;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Set;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
index 5aa19d8..84b25c0 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -14,16 +14,12 @@
 
 package com.google.gerrit.server.account.externalids;
 
-import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.entities.Account;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
@@ -146,23 +142,4 @@
       lock.unlock();
     }
   }
-
-  static class Loader extends CacheLoader<ObjectId, AllExternalIds> {
-    private final ExternalIdReader externalIdReader;
-
-    @Inject
-    Loader(ExternalIdReader externalIdReader) {
-      this.externalIdReader = externalIdReader;
-    }
-
-    @Override
-    public AllExternalIds load(ObjectId notesRev) throws Exception {
-      try (TraceTimer timer =
-          TraceContext.newTimer("Loading external IDs (revision=%s)", notesRev)) {
-        ImmutableSet<ExternalId> externalIds = externalIdReader.all(notesRev);
-        externalIds.forEach(ExternalId::checkThatBlobIdIsSet);
-        return AllExternalIds.create(externalIds);
-      }
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
new file mode 100644
index 0000000..8887e06
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
@@ -0,0 +1,277 @@
+// 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.account.externalids;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+
+/** Loads cache values for the external ID cache using either a full or a partial reload. */
+@Singleton
+public class ExternalIdCacheLoader extends CacheLoader<ObjectId, AllExternalIds> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  // Maximum number of prior states we inspect to find a base for differential. If no cached state
+  // is found within this number of parents, we fall back to reading everything from scratch.
+  private static final int MAX_HISTORY_LOOKBACK = 10;
+
+  private final ExternalIdReader externalIdReader;
+  private final Provider<Cache<ObjectId, AllExternalIds>> externalIdCache;
+  private final GitRepositoryManager gitRepositoryManager;
+  private final AllUsersName allUsersName;
+  private final Counter1<Boolean> reloadCounter;
+  private final Timer0 reloadDifferential;
+  private final boolean enablePartialReloads;
+  private final boolean isPersistentCache;
+
+  @Inject
+  ExternalIdCacheLoader(
+      GitRepositoryManager gitRepositoryManager,
+      AllUsersName allUsersName,
+      ExternalIdReader externalIdReader,
+      @Named(ExternalIdCacheImpl.CACHE_NAME)
+          Provider<Cache<ObjectId, AllExternalIds>> externalIdCache,
+      MetricMaker metricMaker,
+      @GerritServerConfig Config config) {
+    this.externalIdReader = externalIdReader;
+    this.externalIdCache = externalIdCache;
+    this.gitRepositoryManager = gitRepositoryManager;
+    this.allUsersName = allUsersName;
+    this.reloadCounter =
+        metricMaker.newCounter(
+            "notedb/external_id_cache_load_count",
+            new Description("Total number of external ID cache reloads from Git.")
+                .setRate()
+                .setUnit("updates"),
+            Field.ofBoolean("partial", Metadata.Builder::partial).build());
+    this.reloadDifferential =
+        metricMaker.newTimer(
+            "notedb/external_id_partial_read_latency",
+            new Description(
+                    "Latency for generating a new external ID cache state from a prior state.")
+                .setCumulative()
+                .setUnit(Units.MILLISECONDS));
+    this.enablePartialReloads =
+        config.getBoolean("cache", ExternalIdCacheImpl.CACHE_NAME, "enablePartialReloads", true);
+    this.isPersistentCache =
+        config.getInt("cache", ExternalIdCacheImpl.CACHE_NAME, "diskLimit", 0) > 0;
+  }
+
+  @Override
+  public AllExternalIds load(ObjectId notesRev) throws IOException, ConfigInvalidException {
+    if (!enablePartialReloads) {
+      logger.atInfo().log(
+          "Partial reloads of "
+              + ExternalIdCacheImpl.CACHE_NAME
+              + " disabled. Falling back to full reload.");
+      return reloadAllExternalIds(notesRev);
+    }
+
+    // The requested value was not in the cache (hence, this loader was invoked). Therefore, try to
+    // create this entry from a past value using the minimal amount of Git operations possible to
+    // reduce latency.
+    //
+    // First, try to find the most recent state we have in the cache. Most of the time, this will be
+    // the state before the last update happened, but it can also date further back. We try a best
+    // effort approach and check the last 10 states. If nothing is found, we default to loading the
+    // value from scratch.
+    //
+    // If a prior state was found, we use Git to diff the trees and find modifications. This is
+    // faster than just loading the complete current tree and working off of that because of how the
+    // data is structured: NotesMaps use nested trees, so, for example, a NotesMap with 200k entries
+    // has two layers of nesting: 12/34/1234..99. TreeWalk is smart in skipping the traversal of
+    // identical subtrees.
+    //
+    // Once we know what files changed, we apply additions and removals to the previously cached
+    // state.
+
+    try (Repository repo = gitRepositoryManager.openRepository(allUsersName);
+        RevWalk rw = new RevWalk(repo)) {
+      long start = System.nanoTime();
+      Ref extIdRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
+      if (extIdRef == null) {
+        logger.atInfo().log(
+            RefNames.REFS_EXTERNAL_IDS + " not initialized, falling back to full reload.");
+        return reloadAllExternalIds(notesRev);
+      }
+
+      RevCommit currentCommit = rw.parseCommit(extIdRef.getObjectId());
+      rw.markStart(currentCommit);
+      RevCommit parentWithCacheValue;
+      AllExternalIds oldExternalIds = null;
+      int i = 0;
+      while ((parentWithCacheValue = rw.next()) != null
+          && i++ < MAX_HISTORY_LOOKBACK
+          && parentWithCacheValue.getParentCount() < 2) {
+        oldExternalIds = externalIdCache.get().getIfPresent(parentWithCacheValue.getId());
+        if (oldExternalIds != null) {
+          // We found a previously cached state.
+          break;
+        }
+      }
+      if (oldExternalIds == null) {
+        if (isPersistentCache) {
+          // If there is no persistence, this is normal. Don't upset admins reading the logs.
+          logger.atWarning().log(
+              "Unable to find an old ExternalId cache state, falling back to full reload");
+        }
+        return reloadAllExternalIds(notesRev);
+      }
+
+      // Diff trees to recognize modifications
+      Set<ObjectId> removals = new HashSet<>(); // Set<Blob-Object-Id>
+      Map<ObjectId, ObjectId> additions = new HashMap<>(); // Map<Name-ObjectId, Blob-Object-Id>
+      try (TreeWalk treeWalk = new TreeWalk(repo)) {
+        treeWalk.setFilter(TreeFilter.ANY_DIFF);
+        treeWalk.setRecursive(true);
+        treeWalk.reset(parentWithCacheValue.getTree(), currentCommit.getTree());
+        while (treeWalk.next()) {
+          String path = treeWalk.getPathString();
+          ObjectId oldBlob = treeWalk.getObjectId(0);
+          ObjectId newBlob = treeWalk.getObjectId(1);
+          if (ObjectId.zeroId().equals(newBlob)) {
+            // Deletion
+            removals.add(oldBlob);
+          } else if (ObjectId.zeroId().equals(oldBlob)) {
+            // Addition
+            additions.put(fileNameToObjectId(path), newBlob);
+          } else {
+            // Modification
+            removals.add(oldBlob);
+            additions.put(fileNameToObjectId(path), newBlob);
+          }
+        }
+      }
+
+      AllExternalIds allExternalIds =
+          buildAllExternalIds(repo, oldExternalIds, additions, removals);
+      reloadCounter.increment(true);
+      reloadDifferential.record(System.nanoTime() - start, TimeUnit.NANOSECONDS);
+      return allExternalIds;
+    }
+  }
+
+  private static ObjectId fileNameToObjectId(String path) {
+    return ObjectId.fromString(CharMatcher.is('/').removeFrom(path));
+  }
+
+  /**
+   * Build a new {@link AllExternalIds} from an old state by applying additions and removals that
+   * were performed since then.
+   *
+   * <p>Removals are applied before additions.
+   *
+   * @param repo open repository
+   * @param oldExternalIds prior state that is used as base
+   * @param additions map of name to blob ID for each external ID that should be added
+   * @param removals set of name {@link ObjectId}s that should be removed
+   */
+  private static AllExternalIds buildAllExternalIds(
+      Repository repo,
+      AllExternalIds oldExternalIds,
+      Map<ObjectId, ObjectId> additions,
+      Set<ObjectId> removals)
+      throws IOException {
+    ImmutableSetMultimap.Builder<Account.Id, ExternalId> byAccount = ImmutableSetMultimap.builder();
+    ImmutableSetMultimap.Builder<String, ExternalId> byEmail = ImmutableSetMultimap.builder();
+
+    // Copy over old ExternalIds but exclude deleted ones
+    for (ExternalId externalId : oldExternalIds.byAccount().values()) {
+      if (removals.contains(externalId.blobId())) {
+        continue;
+      }
+
+      byAccount.put(externalId.accountId(), externalId);
+      if (externalId.email() != null) {
+        byEmail.put(externalId.email(), externalId);
+      }
+    }
+
+    // Add newly discovered ExternalIds
+    try (ObjectReader reader = repo.newObjectReader()) {
+      for (Map.Entry<ObjectId, ObjectId> nameToBlob : additions.entrySet()) {
+        ExternalId parsedExternalId;
+        try {
+          parsedExternalId =
+              ExternalId.parse(
+                  nameToBlob.getKey().name(),
+                  reader.open(nameToBlob.getValue()).getCachedBytes(),
+                  nameToBlob.getValue());
+        } catch (ConfigInvalidException | RuntimeException e) {
+          logger.atSevere().withCause(e).log(
+              "Ignoring invalid external ID note %s", nameToBlob.getKey().name());
+          continue;
+        }
+
+        byAccount.put(parsedExternalId.accountId(), parsedExternalId);
+        if (parsedExternalId.email() != null) {
+          byEmail.put(parsedExternalId.email(), parsedExternalId);
+        }
+      }
+    }
+    return new AutoValue_AllExternalIds(byAccount.build(), byEmail.build());
+  }
+
+  private AllExternalIds reloadAllExternalIds(ObjectId notesRev)
+      throws IOException, ConfigInvalidException {
+    try (TraceTimer ignored =
+        TraceContext.newTimer(
+            "Loading external IDs from scratch",
+            Metadata.builder().revision(notesRev.name()).build())) {
+      ImmutableSet<ExternalId> externalIds = externalIdReader.all(notesRev);
+      externalIds.forEach(ExternalId::checkThatBlobIdIsSet);
+      AllExternalIds allExternalIds = AllExternalIds.create(externalIds);
+      reloadCounter.increment(false);
+      return allExternalIds;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
index fc311e7..3e5d7b8 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account.externalids;
 
-import com.google.gerrit.server.account.externalids.ExternalIdCacheImpl.Loader;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
 import com.google.inject.TypeLiteral;
@@ -31,9 +30,12 @@
         // from the cache. Extend the cache size by 1 to cover this case, but expire the extra
         // object after a short period of time, since it may be a potentially large amount of
         // memory.
+        // When loading a new value because the primary data advanced, we want to leverage the old
+        // cache state to recompute only what changed. This doesn't affect cache size though as
+        // Guava calls the loader first and evicts later on.
         .maximumWeight(2)
         .expireFromMemoryAfterAccess(Duration.ofMinutes(1))
-        .loader(Loader.class)
+        .loader(ExternalIdCacheLoader.class)
         .diskLimit(-1)
         .version(1)
         .keySerializer(ObjectIdCacheSerializer.INSTANCE)
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 9c35232a..ec4f5534 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -28,17 +28,21 @@
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.index.account.AccountIndexer;
+import com.google.gerrit.server.logging.CallerFinder;
+import com.google.gerrit.server.update.RetryHelper;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -264,6 +268,7 @@
   private final AllUsersName allUsersName;
   private final Counter0 updateCount;
   private final Repository repo;
+  private final CallerFinder callerFinder;
 
   private NoteMap noteMap;
   private ObjectId oldRev;
@@ -293,6 +298,19 @@
             new Description("Total number of external ID updates.").setRate().setUnit("updates"));
     this.allUsersName = requireNonNull(allUsersName, "allUsersRepo");
     this.repo = requireNonNull(allUsersRepo, "allUsersRepo");
+    this.callerFinder =
+        CallerFinder.builder()
+            // 1. callers that come through ExternalIds
+            .addTarget(ExternalIds.class)
+
+            // 2. callers that come through AccountsUpdate
+            .addTarget(AccountsUpdate.class)
+            .addIgnoredPackage("com.github.rholder.retry")
+            .addIgnoredClass(RetryHelper.class)
+
+            // 3. direct callers
+            .addTarget(ExternalIdNotes.class)
+            .build();
   }
 
   public ExternalIdNotes setAfterReadRevision(Runnable afterReadRevision) {
@@ -657,7 +675,8 @@
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
     if (revision != null) {
-      logger.atFine().log("Reading external ID note map");
+      logger.atFine().log(
+          "Reading external ID note map (caller: %s)", callerFinder.findCallerLazy());
       noteMap = NoteMap.read(reader, revision);
     } else {
       noteMap = NoteMap.newEmptyMap();
@@ -670,7 +689,7 @@
 
   @Override
   public RevCommit commit(MetaDataUpdate update) throws IOException {
-    oldRev = revision != null ? revision.copy() : ObjectId.zeroId();
+    oldRev = ObjectIds.copyOrZero(revision);
     RevCommit commit = super.commit(update);
     updateCount.increment();
     return commit;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java b/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
index cf5500e..6334265 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
@@ -17,11 +17,11 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
index 9098630..28d3af2 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -18,7 +18,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.SetMultimap;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java b/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
index 4897487..815f7d0 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
@@ -30,7 +30,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
-import org.apache.commons.codec.DecoderException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -134,7 +133,7 @@
     if (extId.password() != null && extId.isScheme(SCHEME_USERNAME)) {
       try {
         HashedPassword.decode(extId.password());
-      } catch (DecoderException e) {
+      } catch (HashedPassword.DecoderException e) {
         addError(
             String.format(
                 "External ID '%s' has an invalid password: %s", extId.key().get(), e.getMessage()),
diff --git a/java/com/google/gerrit/server/account/externalids/PasswordVerifier.java b/java/com/google/gerrit/server/account/externalids/PasswordVerifier.java
new file mode 100644
index 0000000..3f2f774
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/PasswordVerifier.java
@@ -0,0 +1,53 @@
+// 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.account.externalids;
+
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+
+import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.account.HashedPassword;
+import java.util.Collection;
+
+/** Checks if a given username and password match a user's external IDs. */
+public class PasswordVerifier {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  /** Returns {@code true} if there is an external ID matching both the username and password. */
+  public static boolean checkPassword(
+      Collection<ExternalId> externalIds, String username, @Nullable String password) {
+    if (password == null) {
+      return false;
+    }
+    for (ExternalId id : externalIds) {
+      // Only process the "username:$USER" entry, which is unique.
+      if (!id.isScheme(SCHEME_USERNAME) || !username.equals(id.key().id())) {
+        continue;
+      }
+
+      String hashedStr = id.password();
+      if (!Strings.isNullOrEmpty(hashedStr)) {
+        try {
+          return HashedPassword.decode(hashedStr).checkPassword(password);
+        } catch (HashedPassword.DecoderException e) {
+          logger.atSevere().log("DecoderException for user %s: %s ", username, e.getMessage());
+          return false;
+        }
+      }
+    }
+    return false;
+  }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/testing/BUILD b/java/com/google/gerrit/server/account/externalids/testing/BUILD
new file mode 100644
index 0000000..0e469e3
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/testing/BUILD
@@ -0,0 +1,13 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+    name = "testing",
+    testonly = 1,
+    srcs = glob(["**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/server",
+        "//lib:jgit",
+    ],
+)
diff --git a/java/com/google/gerrit/server/account/externalids/testing/ExternalIdInserter.java b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdInserter.java
new file mode 100644
index 0000000..a93192a
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdInserter.java
@@ -0,0 +1,25 @@
+// 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.account.externalids.testing;
+
+import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.notes.NoteMap;
+
+@FunctionalInterface
+public interface ExternalIdInserter {
+  public ObjectId addNote(ObjectInserter ins, NoteMap noteMap) throws IOException;
+}
diff --git a/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
new file mode 100644
index 0000000..b8040f7
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
@@ -0,0 +1,163 @@
+// 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.account.externalids.testing;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdReader;
+import java.io.IOException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Common methods for dealing with external IDs in tests. */
+public class ExternalIdTestUtil {
+
+  public static String insertExternalIdWithoutAccountId(
+      Repository repo, RevWalk rw, PersonIdent ident, Account.Id accountId, String externalId)
+      throws IOException {
+    return insertExternalId(
+        repo,
+        rw,
+        ident,
+        (ins, noteMap) -> {
+          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), accountId);
+          ObjectId noteId = extId.key().sha1();
+          Config c = new Config();
+          extId.writeToConfig(c);
+          c.unset("externalId", extId.key().get(), "accountId");
+          byte[] raw = c.toText().getBytes(UTF_8);
+          ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
+          noteMap.set(noteId, dataBlob);
+          return noteId;
+        });
+  }
+
+  public static String insertExternalIdWithKeyThatDoesntMatchNoteId(
+      Repository repo, RevWalk rw, PersonIdent ident, Account.Id accountId, String externalId)
+      throws IOException {
+    return insertExternalId(
+        repo,
+        rw,
+        ident,
+        (ins, noteMap) -> {
+          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), accountId);
+          ObjectId noteId = ExternalId.Key.parse(externalId + "x").sha1();
+          Config c = new Config();
+          extId.writeToConfig(c);
+          byte[] raw = c.toText().getBytes(UTF_8);
+          ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
+          noteMap.set(noteId, dataBlob);
+          return noteId;
+        });
+  }
+
+  public static String insertExternalIdWithInvalidConfig(
+      Repository repo, RevWalk rw, PersonIdent ident, String externalId) throws IOException {
+    return insertExternalId(
+        repo,
+        rw,
+        ident,
+        (ins, noteMap) -> {
+          ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
+          byte[] raw = "bad-config".getBytes(UTF_8);
+          ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
+          noteMap.set(noteId, dataBlob);
+          return noteId;
+        });
+  }
+
+  public static String insertExternalIdWithEmptyNote(
+      Repository repo, RevWalk rw, PersonIdent ident, String externalId) throws IOException {
+    return insertExternalId(
+        repo,
+        rw,
+        ident,
+        (ins, noteMap) -> {
+          ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
+          byte[] raw = "".getBytes(UTF_8);
+          ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
+          noteMap.set(noteId, dataBlob);
+          return noteId;
+        });
+  }
+
+  private static String insertExternalId(
+      Repository repo, RevWalk rw, PersonIdent ident, ExternalIdInserter extIdInserter)
+      throws IOException {
+    ObjectId rev = ExternalIdReader.readRevision(repo);
+    NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
+
+    try (ObjectInserter ins = repo.newObjectInserter()) {
+      ObjectId noteId = extIdInserter.addNote(ins, noteMap);
+
+      CommitBuilder cb = new CommitBuilder();
+      cb.setMessage("Update external IDs");
+      cb.setTreeId(noteMap.writeTree(ins));
+      cb.setAuthor(ident);
+      cb.setCommitter(ident);
+      if (!rev.equals(ObjectId.zeroId())) {
+        cb.setParentId(rev);
+      } else {
+        cb.setParentIds(); // Ref is currently nonexistent, commit has no parents.
+      }
+      if (cb.getTreeId() == null) {
+        if (rev.equals(ObjectId.zeroId())) {
+          cb.setTreeId(ins.insert(OBJ_TREE, new byte[] {})); // No parent, assume empty tree.
+        } else {
+          RevCommit p = rw.parseCommit(rev);
+          cb.setTreeId(p.getTree()); // Copy tree from parent.
+        }
+      }
+      ObjectId commitId = ins.insert(cb);
+      ins.flush();
+
+      RefUpdate u = repo.updateRef(RefNames.REFS_EXTERNAL_IDS);
+      u.setExpectedOldObjectId(rev);
+      u.setNewObjectId(commitId);
+      RefUpdate.Result res = u.update();
+      switch (res) {
+        case NEW:
+        case FAST_FORWARD:
+        case NO_CHANGE:
+        case RENAMED:
+        case FORCED:
+          break;
+        case LOCK_FAILURE:
+        case IO_FAILURE:
+        case NOT_ATTEMPTED:
+        case REJECTED:
+        case REJECTED_CURRENT_BRANCH:
+        case REJECTED_MISSING_OBJECT:
+        case REJECTED_OTHER_REASON:
+        default:
+          throw new IOException("Updating external IDs failed with " + res);
+      }
+      return noteId.getName();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/api/BUILD b/java/com/google/gerrit/server/api/BUILD
index 37e04bb..0275c79 100644
--- a/java/com/google/gerrit/server/api/BUILD
+++ b/java/com/google/gerrit/server/api/BUILD
@@ -9,17 +9,17 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/util/cli",
         "//lib:args4j",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:servlet-api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 673f5ae..7fa9767 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -240,7 +240,7 @@
   @Override
   public AccountDetailInfo detail() throws RestApiException {
     try {
-      return getDetail.apply(account);
+      return getDetail.apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get detail", e);
     }
@@ -248,8 +248,12 @@
 
   @Override
   public boolean getActive() throws RestApiException {
-    Response<String> result = getActive.apply(account);
-    return result.statusCode() == SC_OK && result.value().equals("ok");
+    try {
+      Response<String> result = getActive.apply(account);
+      return result.statusCode() == SC_OK && result.value().equals("ok");
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get active", e);
+    }
   }
 
   @Override
@@ -274,7 +278,7 @@
   @Override
   public GeneralPreferencesInfo getPreferences() throws RestApiException {
     try {
-      return getPreferences.apply(account);
+      return getPreferences.apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get preferences", e);
     }
@@ -283,7 +287,7 @@
   @Override
   public GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in) throws RestApiException {
     try {
-      return setPreferences.apply(account, in);
+      return setPreferences.apply(account, in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot set preferences", e);
     }
@@ -292,7 +296,7 @@
   @Override
   public DiffPreferencesInfo getDiffPreferences() throws RestApiException {
     try {
-      return getDiffPreferences.apply(account);
+      return getDiffPreferences.apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot query diff preferences", e);
     }
@@ -301,7 +305,7 @@
   @Override
   public DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) throws RestApiException {
     try {
-      return setDiffPreferences.apply(account, in);
+      return setDiffPreferences.apply(account, in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot set diff preferences", e);
     }
@@ -310,7 +314,7 @@
   @Override
   public EditPreferencesInfo getEditPreferences() throws RestApiException {
     try {
-      return getEditPreferences.apply(account);
+      return getEditPreferences.apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot query edit preferences", e);
     }
@@ -319,7 +323,7 @@
   @Override
   public EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) throws RestApiException {
     try {
-      return setEditPreferences.apply(account, in);
+      return setEditPreferences.apply(account, in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot set edit preferences", e);
     }
@@ -328,7 +332,7 @@
   @Override
   public List<ProjectWatchInfo> getWatchedProjects() throws RestApiException {
     try {
-      return getWatchedProjects.apply(account);
+      return getWatchedProjects.apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get watched projects", e);
     }
@@ -338,7 +342,7 @@
   public List<ProjectWatchInfo> setWatchedProjects(List<ProjectWatchInfo> in)
       throws RestApiException {
     try {
-      return postWatchedProjects.apply(account, in);
+      return postWatchedProjects.apply(account, in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot update watched projects", e);
     }
@@ -389,7 +393,7 @@
   public SortedSet<String> getStars(String changeId) throws RestApiException {
     try {
       AccountResource.Star rsrc = stars.parse(account, IdString.fromUrl(changeId));
-      return starsGet.apply(rsrc);
+      return starsGet.apply(rsrc).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get stars", e);
     }
@@ -398,7 +402,7 @@
   @Override
   public List<ChangeInfo> getStarredChanges() throws RestApiException {
     try {
-      return stars.list().apply(account);
+      return stars.list().apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get starred changes", e);
     }
@@ -407,7 +411,7 @@
   @Override
   public List<GroupInfo> getGroups() throws RestApiException {
     try {
-      return getGroups.apply(account);
+      return getGroups.apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get groups", e);
     }
@@ -416,7 +420,7 @@
   @Override
   public List<EmailInfo> getEmails() throws RestApiException {
     try {
-      return getEmails.apply(account);
+      return getEmails.apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get emails", e);
     }
@@ -475,7 +479,7 @@
   @Override
   public List<SshKeyInfo> listSshKeys() throws RestApiException {
     try {
-      return getSshKeys.apply(account);
+      return getSshKeys.apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list SSH keys", e);
     }
@@ -534,7 +538,7 @@
   @Override
   public List<AgreementInfo> listAgreements() throws RestApiException {
     try {
-      return getAgreements.apply(account);
+      return getAgreements.apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get agreements", e);
     }
@@ -563,7 +567,7 @@
   @Override
   public List<AccountExternalIdInfo> getExternalIds() throws RestApiException {
     try {
-      return getExternalIds.apply(account);
+      return getExternalIds.apply(account).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get external IDs", e);
     }
@@ -582,7 +586,7 @@
   public List<DeletedDraftCommentInfo> deleteDraftComments(DeleteDraftCommentsInput input)
       throws RestApiException {
     try {
-      return deleteDraftComments.apply(account, input);
+      return deleteDraftComments.apply(account, input).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot delete draft comments", e);
     }
diff --git a/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index 9d29888..012e6ce 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -133,7 +133,7 @@
       myQueryAccounts.setSuggest(true);
       myQueryAccounts.setQuery(r.getQuery());
       myQueryAccounts.setLimit(r.getLimit());
-      return myQueryAccounts.apply(TopLevelResource.INSTANCE);
+      return myQueryAccounts.apply(TopLevelResource.INSTANCE).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve suggested accounts", e);
     }
@@ -164,7 +164,7 @@
       for (ListAccountsOption option : r.getOptions()) {
         myQueryAccounts.addOption(option);
       }
-      return myQueryAccounts.apply(TopLevelResource.INSTANCE);
+      return myQueryAccounts.apply(TopLevelResource.INSTANCE).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve suggested accounts", e);
     }
diff --git a/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java b/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java
index 759f60c..f68142f 100644
--- a/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java
@@ -61,7 +61,7 @@
   @Override
   public EmailInfo get() throws RestApiException {
     try {
-      return get.apply(resource());
+      return get.apply(resource()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot read email", e);
     }
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index f027c92..a04be30 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -351,7 +351,7 @@
   @Override
   public ChangeApi revert(RevertInput in) throws RestApiException {
     try {
-      return changeApi.id(revert.apply(change, in)._number);
+      return changeApi.id(revert.apply(change, in).value()._number);
     } catch (Exception e) {
       throw asRestApiException("Cannot revert change", e);
     }
@@ -401,7 +401,11 @@
 
   @Override
   public String topic() throws RestApiException {
-    return getTopic.apply(change);
+    try {
+      return getTopic.apply(change).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get topic", e);
+    }
   }
 
   @Override
@@ -418,7 +422,7 @@
   @Override
   public IncludedInInfo includedIn() throws RestApiException {
     try {
-      return includedIn.apply(change);
+      return includedIn.apply(change).value();
     } catch (Exception e) {
       throw asRestApiException("Could not extract IncludedIn data", e);
     }
@@ -427,7 +431,7 @@
   @Override
   public AddReviewerResult addReviewer(AddReviewerInput in) throws RestApiException {
     try {
-      return postReviewers.apply(change, in);
+      return postReviewers.apply(change, in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot add change reviewer", e);
     }
@@ -448,7 +452,9 @@
     try {
       suggestReviewers.setQuery(r.getQuery());
       suggestReviewers.setLimit(r.getLimit());
-      return suggestReviewers.apply(change);
+      suggestReviewers.setExcludeGroups(r.getExcludeGroups());
+      suggestReviewers.setReviewerState(r.getReviewerState());
+      return suggestReviewers.apply(change).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve suggested reviewers", e);
     }
@@ -457,7 +463,7 @@
   @Override
   public List<ReviewerInfo> reviewers() throws RestApiException {
     try {
-      return listReviewers.apply(change);
+      return listReviewers.apply(change).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve reviewers", e);
     }
@@ -512,7 +518,7 @@
   @Override
   public AccountInfo setAssignee(AssigneeInput input) throws RestApiException {
     try {
-      return putAssignee.apply(change, input);
+      return putAssignee.apply(change, input).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot set assignee", e);
     }
@@ -550,7 +556,7 @@
   @Override
   public Map<String, List<CommentInfo>> comments() throws RestApiException {
     try {
-      return listComments.apply(change);
+      return listComments.apply(change).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get comments", e);
     }
@@ -568,7 +574,7 @@
   @Override
   public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
     try {
-      return listChangeRobotComments.apply(change);
+      return listChangeRobotComments.apply(change).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get robot comments", e);
     }
@@ -577,7 +583,7 @@
   @Override
   public Map<String, List<CommentInfo>> drafts() throws RestApiException {
     try {
-      return listDrafts.apply(change);
+      return listDrafts.apply(change).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get drafts", e);
     }
@@ -671,7 +677,7 @@
     try {
       GetPureRevert getPureRevert = getPureRevertProvider.get();
       getPureRevert.setClaimedOriginal(claimedOriginal);
-      return getPureRevert.apply(change);
+      return getPureRevert.apply(change).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot compute pure revert", e);
     }
@@ -680,7 +686,7 @@
   @Override
   public List<ChangeMessageInfo> messages() throws RestApiException {
     try {
-      return changeMessages.list().apply(change);
+      return changeMessages.list().apply(change).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list change messages", e);
     }
diff --git a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
index ffc6524..7f0feba 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -221,7 +221,7 @@
   public String getCommitMessage() throws RestApiException {
     try {
       try (BinaryResult binaryResult =
-          getChangeEditCommitMessageProvider.get().apply(changeResource)) {
+          getChangeEditCommitMessageProvider.get().apply(changeResource).value()) {
         return binaryResult.asString();
       }
     } catch (Exception e) {
diff --git a/java/com/google/gerrit/server/api/changes/ChangeMessageApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeMessageApiImpl.java
index 14310e8..490ec5b 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeMessageApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeMessageApiImpl.java
@@ -48,7 +48,7 @@
   @Override
   public ChangeMessageInfo get() throws RestApiException {
     try {
-      return getChangeMessage.apply(changeMessageResource);
+      return getChangeMessage.apply(changeMessageResource).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve change message", e);
     }
diff --git a/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index acff137..b9635fb 100644
--- a/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -20,6 +20,7 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -29,7 +30,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.api.changes.ChangeApiImpl.DynamicOptionParser;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.change.CreateChange;
@@ -92,7 +92,7 @@
   public ChangeApi create(ChangeInput in) throws RestApiException {
     try {
       ChangeInfo out = createChange.apply(TopLevelResource.INSTANCE, in).value();
-      return api.create(changes.parse(new Change.Id(out._number)));
+      return api.create(changes.parse(Change.id(out._number)));
     } catch (Exception e) {
       throw asRestApiException("Cannot create change", e);
     }
@@ -127,7 +127,7 @@
     dynamicOptionParser.parseDynamicOptions(qc, q.getPluginOptions());
 
     try {
-      List<?> result = qc.apply(TopLevelResource.INSTANCE);
+      List<?> result = qc.apply(TopLevelResource.INSTANCE).value();
       if (result.isEmpty()) {
         return ImmutableList.of();
       }
diff --git a/java/com/google/gerrit/server/api/changes/CommentApiImpl.java b/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
index 418187d..c5fcab1 100644
--- a/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
@@ -46,7 +46,7 @@
   @Override
   public CommentInfo get() throws RestApiException {
     try {
-      return getComment.apply(comment);
+      return getComment.apply(comment).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve comment", e);
     }
@@ -55,7 +55,7 @@
   @Override
   public CommentInfo delete(DeleteCommentInput input) throws RestApiException {
     try {
-      return deleteComment.apply(comment, input);
+      return deleteComment.apply(comment, input).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot delete comment", e);
     }
diff --git a/java/com/google/gerrit/server/api/changes/DraftApiImpl.java b/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
index 4d26b11..f6eb3c5 100644
--- a/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
@@ -54,7 +54,7 @@
   @Override
   public CommentInfo get() throws RestApiException {
     try {
-      return getDraft.apply(draft);
+      return getDraft.apply(draft).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve draft", e);
     }
diff --git a/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/java/com/google/gerrit/server/api/changes/FileApiImpl.java
index 883ab99..c506b2e 100644
--- a/java/com/google/gerrit/server/api/changes/FileApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -63,7 +63,7 @@
   @Override
   public BinaryResult content() throws RestApiException {
     try {
-      return getContent.apply(file);
+      return getContent.apply(file).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve file content", e);
     }
diff --git a/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java b/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
index 11536cb..2174ef0 100644
--- a/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
@@ -54,7 +54,7 @@
   @Override
   public Map<String, Short> votes() throws RestApiException {
     try {
-      return listVotes.apply(reviewer);
+      return listVotes.apply(reviewer).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list votes", e);
     }
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 2df7ae6..01dfe36 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder.ListMultimapBuilder;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -54,7 +55,6 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountLoader;
@@ -254,7 +254,7 @@
   public BinaryResult submitPreview(String format) throws RestApiException {
     try {
       submitPreview.setFormat(format);
-      return submitPreview.apply(revision);
+      return submitPreview.apply(revision).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get submit preview", e);
     }
@@ -263,7 +263,7 @@
   @Override
   public ChangeApi rebase(RebaseInput in) throws RestApiException {
     try {
-      return changes.id(rebase.apply(revision, in)._number);
+      return changes.id(rebase.apply(revision, in).value()._number);
     } catch (Exception e) {
       throw asRestApiException("Cannot rebase ps", e);
     }
@@ -282,7 +282,7 @@
   @Override
   public ChangeApi cherryPick(CherryPickInput in) throws RestApiException {
     try {
-      return changes.id(cherryPick.apply(revision, in)._number);
+      return changes.id(cherryPick.apply(revision, in).value()._number);
     } catch (Exception e) {
       throw asRestApiException("Cannot cherry pick", e);
     }
@@ -291,7 +291,7 @@
   @Override
   public CherryPickChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException {
     try {
-      return cherryPick.apply(revision, in);
+      return cherryPick.apply(revision, in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot cherry pick", e);
     }
@@ -336,7 +336,7 @@
   @Override
   public MergeableInfo mergeable() throws RestApiException {
     try {
-      return mergeable.apply(revision);
+      return mergeable.apply(revision).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot check mergeability", e);
     }
@@ -346,7 +346,7 @@
   public MergeableInfo mergeableOtherBranches() throws RestApiException {
     try {
       mergeable.setOtherBranches(true);
-      return mergeable.apply(revision);
+      return mergeable.apply(revision).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot check mergeability", e);
     }
@@ -400,7 +400,7 @@
   @Override
   public Map<String, List<CommentInfo>> comments() throws RestApiException {
     try {
-      return listComments.apply(revision);
+      return listComments.apply(revision).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve comments", e);
     }
@@ -409,7 +409,7 @@
   @Override
   public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
     try {
-      return listRobotComments.apply(revision);
+      return listRobotComments.apply(revision).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve robot comments", e);
     }
@@ -427,7 +427,7 @@
   @Override
   public Map<String, List<CommentInfo>> drafts() throws RestApiException {
     try {
-      return listDrafts.apply(revision);
+      return listDrafts.apply(revision).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve drafts", e);
     }
@@ -476,7 +476,7 @@
       // Reread change to pick up new notes refs.
       return changes
           .id(revision.getChange().getId().get())
-          .revision(revision.getPatchSet().getId().get())
+          .revision(revision.getPatchSet().id().get())
           .draft(id);
     } catch (Exception e) {
       throw asRestApiException("Cannot create draft", e);
@@ -504,7 +504,7 @@
   @Override
   public BinaryResult patch() throws RestApiException {
     try {
-      return getPatch.apply(revision);
+      return getPatch.apply(revision).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get patch", e);
     }
@@ -513,7 +513,7 @@
   @Override
   public BinaryResult patch(String path) throws RestApiException {
     try {
-      return getPatch.setPath(path).apply(revision);
+      return getPatch.setPath(path).apply(revision).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get patch", e);
     }
@@ -531,7 +531,7 @@
   @Override
   public SubmitType submitType() throws RestApiException {
     try {
-      return getSubmitType.apply(revision);
+      return getSubmitType.apply(revision).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get submit type", e);
     }
@@ -540,16 +540,16 @@
   @Override
   public SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException {
     try {
-      return testSubmitType.apply(revision, in);
+      return testSubmitType.apply(revision, in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot test submit type", e);
     }
   }
 
   @Override
-  public List<TestSubmitRuleInfo> testSubmitRule(TestSubmitRuleInput in) throws RestApiException {
+  public TestSubmitRuleInfo testSubmitRule(TestSubmitRuleInput in) throws RestApiException {
     try {
-      return testSubmitRule.get().apply(revision, in);
+      return testSubmitRule.get().apply(revision, in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot test submit rule", e);
     }
@@ -575,7 +575,7 @@
   @Override
   public RelatedChangesInfo related() throws RestApiException {
     try {
-      return getRelated.apply(revision);
+      return getRelated.apply(revision).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get related changes", e);
     }
@@ -587,20 +587,20 @@
         ListMultimapBuilder.treeKeys().arrayListValues().build();
     try {
       Iterable<PatchSetApproval> approvals =
-          approvalsUtil.byPatchSet(revision.getNotes(), revision.getPatchSet().getId(), null, null);
+          approvalsUtil.byPatchSet(revision.getNotes(), revision.getPatchSet().id(), null, null);
       AccountLoader accountLoader =
           accountLoaderFactory.create(
               EnumSet.of(
                   FillOptions.ID, FillOptions.NAME, FillOptions.EMAIL, FillOptions.USERNAME));
       for (PatchSetApproval approval : approvals) {
-        String label = approval.getLabel();
+        String label = approval.label();
         ApprovalInfo info =
             new ApprovalInfo(
-                approval.getAccountId().get(),
-                Integer.valueOf(approval.getValue()),
+                approval.accountId().get(),
+                Integer.valueOf(approval.value()),
                 null,
-                approval.getTag(),
-                approval.getGranted());
+                approval.tag().orElse(null),
+                approval.granted());
         accountLoader.put(info);
         result.get(label).add(info);
       }
@@ -624,7 +624,11 @@
 
   @Override
   public String description() throws RestApiException {
-    return getDescription.apply(revision);
+    try {
+      return getDescription.apply(revision).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get description", e);
+    }
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
index 8cad507..49c2d49 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
@@ -47,7 +47,7 @@
   @Override
   public Map<String, Short> votes() throws RestApiException {
     try {
-      return listVotes.apply(reviewer);
+      return listVotes.apply(reviewer).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list votes", e);
     }
diff --git a/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java b/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
index 37a56fe..ec13061 100644
--- a/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
@@ -41,7 +41,7 @@
   @Override
   public RobotCommentInfo get() throws RestApiException {
     try {
-      return getComment.apply(comment);
+      return getComment.apply(comment).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve robot comment", e);
     }
diff --git a/java/com/google/gerrit/server/api/config/ServerImpl.java b/java/com/google/gerrit/server/api/config/ServerImpl.java
index 6e78be2..ab40ec8 100644
--- a/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -83,7 +83,7 @@
   @Override
   public ServerInfo getInfo() throws RestApiException {
     try {
-      return getServerInfo.apply(new ConfigResource());
+      return getServerInfo.apply(new ConfigResource()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get server info", e);
     }
@@ -92,7 +92,7 @@
   @Override
   public GeneralPreferencesInfo getDefaultPreferences() throws RestApiException {
     try {
-      return getPreferences.apply(new ConfigResource());
+      return getPreferences.apply(new ConfigResource()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get default general preferences", e);
     }
@@ -102,7 +102,7 @@
   public GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in)
       throws RestApiException {
     try {
-      return setPreferences.apply(new ConfigResource(), in);
+      return setPreferences.apply(new ConfigResource(), in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot set default general preferences", e);
     }
@@ -111,7 +111,7 @@
   @Override
   public DiffPreferencesInfo getDefaultDiffPreferences() throws RestApiException {
     try {
-      return getDiffPreferences.apply(new ConfigResource());
+      return getDiffPreferences.apply(new ConfigResource()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get default diff preferences", e);
     }
@@ -121,7 +121,7 @@
   public DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in)
       throws RestApiException {
     try {
-      return setDiffPreferences.apply(new ConfigResource(), in);
+      return setDiffPreferences.apply(new ConfigResource(), in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot set default diff preferences", e);
     }
@@ -130,7 +130,7 @@
   @Override
   public EditPreferencesInfo getDefaultEditPreferences() throws RestApiException {
     try {
-      return getEditPreferences.apply(new ConfigResource());
+      return getEditPreferences.apply(new ConfigResource()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get default edit preferences", e);
     }
@@ -140,7 +140,7 @@
   public EditPreferencesInfo setDefaultEditPreferences(EditPreferencesInfo in)
       throws RestApiException {
     try {
-      return setEditPreferences.apply(new ConfigResource(), in);
+      return setEditPreferences.apply(new ConfigResource(), in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot set default edit preferences", e);
     }
@@ -149,14 +149,18 @@
   @Override
   public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException {
     try {
-      return checkConsistency.get().apply(new ConfigResource(), in);
+      return checkConsistency.get().apply(new ConfigResource(), in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot check consistency", e);
     }
   }
 
   @Override
-  public List<TopMenu.MenuEntry> topMenus() {
-    return listTopMenus.apply(new ConfigResource()).value();
+  public List<TopMenu.MenuEntry> topMenus() throws RestApiException {
+    try {
+      return listTopMenus.apply(new ConfigResource()).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get top menus", e);
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
index b70a029..bb04ab4 100644
--- a/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
+++ b/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -119,7 +119,7 @@
   @Override
   public GroupInfo get() throws RestApiException {
     try {
-      return getGroup.apply(rsrc);
+      return getGroup.apply(rsrc).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve group", e);
     }
@@ -128,7 +128,7 @@
   @Override
   public GroupInfo detail() throws RestApiException {
     try {
-      return getDetail.apply(rsrc);
+      return getDetail.apply(rsrc).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve group", e);
     }
@@ -136,7 +136,11 @@
 
   @Override
   public String name() throws RestApiException {
-    return getName.apply(rsrc);
+    try {
+      return getName.apply(rsrc).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get group name", e);
+    }
   }
 
   @Override
@@ -153,7 +157,7 @@
   @Override
   public GroupInfo owner() throws RestApiException {
     try {
-      return getOwner.apply(rsrc);
+      return getOwner.apply(rsrc).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get group owner", e);
     }
@@ -172,7 +176,11 @@
 
   @Override
   public String description() throws RestApiException {
-    return getDescription.apply(rsrc);
+    try {
+      return getDescription.apply(rsrc).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get group description", e);
+    }
   }
 
   @Override
@@ -188,7 +196,11 @@
 
   @Override
   public GroupOptionsInfo options() throws RestApiException {
-    return getOptions.apply(rsrc);
+    try {
+      return getOptions.apply(rsrc).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get group options", e);
+    }
   }
 
   @Override
@@ -209,7 +221,7 @@
   public List<AccountInfo> members(boolean recursive) throws RestApiException {
     listMembers.setRecursive(recursive);
     try {
-      return listMembers.apply(rsrc);
+      return listMembers.apply(rsrc).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list group members", e);
     }
@@ -236,7 +248,7 @@
   @Override
   public List<GroupInfo> includedGroups() throws RestApiException {
     try {
-      return listSubgroups.apply(rsrc);
+      return listSubgroups.apply(rsrc).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list subgroups", e);
     }
@@ -263,7 +275,7 @@
   @Override
   public List<? extends GroupAuditEventInfo> auditLog() throws RestApiException {
     try {
-      return getAuditLog.apply(rsrc);
+      return getAuditLog.apply(rsrc).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get audit log", e);
     }
diff --git a/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/java/com/google/gerrit/server/api/groups/GroupsImpl.java
index bae75db..95dcad4 100644
--- a/java/com/google/gerrit/server/api/groups/GroupsImpl.java
+++ b/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -98,7 +98,7 @@
           .currentUser()
           .checkAny(GlobalPermission.fromAnnotation(createGroup.getClass()));
       GroupInfo info =
-          createGroup.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(in.name), in);
+          createGroup.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(in.name), in).value();
       return id(info.id);
     } catch (Exception e) {
       throw asRestApiException("Cannot create group " + in.name, e);
@@ -141,7 +141,7 @@
 
     if (req.getUser() != null) {
       try {
-        list.setUser(accountResolver.resolve(req.getUser()).asUnique().getAccount().getId());
+        list.setUser(accountResolver.resolve(req.getUser()).asUnique().account().id());
       } catch (Exception e) {
         throw asRestApiException("Error looking up user " + req.getUser(), e);
       }
@@ -154,7 +154,7 @@
     list.setMatchRegex(req.getRegex());
     list.setSuggest(req.getSuggest());
     try {
-      return list.apply(tlr);
+      return list.apply(tlr).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list groups", e);
     }
@@ -184,7 +184,7 @@
       for (ListGroupsOption option : r.getOptions()) {
         myQueryGroups.addOption(option);
       }
-      return myQueryGroups.apply(TopLevelResource.INSTANCE);
+      return myQueryGroups.apply(TopLevelResource.INSTANCE).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot query groups", e);
     }
diff --git a/java/com/google/gerrit/server/api/plugins/PluginApiImpl.java b/java/com/google/gerrit/server/api/plugins/PluginApiImpl.java
index 71f7832..3932177 100644
--- a/java/com/google/gerrit/server/api/plugins/PluginApiImpl.java
+++ b/java/com/google/gerrit/server/api/plugins/PluginApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.plugins;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.plugins.PluginApi;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.common.PluginInfo;
@@ -53,7 +55,11 @@
 
   @Override
   public PluginInfo get() throws RestApiException {
-    return getStatus.apply(resource);
+    try {
+      return getStatus.apply(resource).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get status", e);
+    }
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/api/plugins/PluginsImpl.java b/java/com/google/gerrit/server/api/plugins/PluginsImpl.java
index e570655..c275093 100644
--- a/java/com/google/gerrit/server/api/plugins/PluginsImpl.java
+++ b/java/com/google/gerrit/server/api/plugins/PluginsImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.plugins;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
 import com.google.gerrit.extensions.api.plugins.PluginApi;
 import com.google.gerrit.extensions.api.plugins.Plugins;
@@ -27,7 +29,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 import java.util.SortedMap;
 
 @Singleton
@@ -59,7 +60,11 @@
     return new ListRequest() {
       @Override
       public SortedMap<String, PluginInfo> getAsMap() throws RestApiException {
-        return listProvider.get().request(this).apply(TopLevelResource.INSTANCE);
+        try {
+          return listProvider.get().request(this).apply(TopLevelResource.INSTANCE).value();
+        } catch (Exception e) {
+          throw asRestApiException("Cannot list plugins", e);
+        }
       }
     };
   }
@@ -87,8 +92,8 @@
       Response<PluginInfo> created =
           installProvider.get().setName(name).apply(TopLevelResource.INSTANCE, input);
       return pluginApi.create(plugins.parse(created.value().id));
-    } catch (IOException e) {
-      throw new RestApiException("could not install plugin", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot install plugin", e);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/api/projects/BranchApiImpl.java b/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
index b3506fc..c7cca6f 100644
--- a/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
@@ -90,7 +90,7 @@
   @Override
   public BranchInfo get() throws RestApiException {
     try {
-      return getBranch.apply(resource());
+      return getBranch.apply(resource()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot read branch", e);
     }
@@ -109,7 +109,7 @@
   public BinaryResult file(String path) throws RestApiException {
     try {
       FileResource resource = filesCollection.parse(resource(), IdString.fromDecoded(path));
-      return getContent.apply(resource);
+      return getContent.apply(resource).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve file", e);
     }
@@ -118,9 +118,9 @@
   @Override
   public List<ReflogEntryInfo> reflog() throws RestApiException {
     try {
-      return getReflog.apply(resource());
-    } catch (IOException | PermissionBackendException e) {
-      throw new RestApiException("Cannot retrieve reflog", e);
+      return getReflog.apply(resource()).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve reflog", e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
index d7c9bc7..25e56fe 100644
--- a/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.projects;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.projects.ChildProjectApi;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -43,7 +45,11 @@
 
   @Override
   public ProjectInfo get(boolean recursive) throws RestApiException {
-    getChildProject.setRecursive(recursive);
-    return getChildProject.apply(rsrc);
+    try {
+      getChildProject.setRecursive(recursive);
+      return getChildProject.apply(rsrc).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get child project", e);
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/api/projects/CommitApiImpl.java b/java/com/google/gerrit/server/api/projects/CommitApiImpl.java
index 0a3b236..5c7921a 100644
--- a/java/com/google/gerrit/server/api/projects/CommitApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/CommitApiImpl.java
@@ -58,7 +58,7 @@
   @Override
   public CommitInfo get() throws RestApiException {
     try {
-      return getCommit.apply(commitResource);
+      return getCommit.apply(commitResource).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get commit info", e);
     }
@@ -67,7 +67,7 @@
   @Override
   public ChangeApi cherryPick(CherryPickInput input) throws RestApiException {
     try {
-      return changes.id(cherryPickCommit.apply(commitResource, input)._number);
+      return changes.id(cherryPickCommit.apply(commitResource, input).value()._number);
     } catch (Exception e) {
       throw asRestApiException("Cannot cherry pick", e);
     }
@@ -76,7 +76,7 @@
   @Override
   public IncludedInInfo includedIn() throws RestApiException {
     try {
-      return includedIn.apply(commitResource);
+      return includedIn.apply(commitResource).value();
     } catch (Exception e) {
       throw asRestApiException("Could not extract IncludedIn data", e);
     }
diff --git a/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java b/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
index c44f5bb..61736f6 100644
--- a/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
@@ -67,8 +67,8 @@
   @Override
   public DashboardInfo get(boolean inherited) throws RestApiException {
     try {
-      return get.get().setInherited(inherited).apply(resource());
-    } catch (IOException | PermissionBackendException | ConfigInvalidException e) {
+      return get.get().setInherited(inherited).apply(resource()).value();
+    } catch (Exception e) {
       throw asRestApiException("Cannot read dashboard", e);
     }
   }
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 354331e..1ac905d 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.extensions.api.projects.TagApi;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -69,6 +70,7 @@
 import com.google.gerrit.server.restapi.project.GetHead;
 import com.google.gerrit.server.restapi.project.GetParent;
 import com.google.gerrit.server.restapi.project.Index;
+import com.google.gerrit.server.restapi.project.IndexChanges;
 import com.google.gerrit.server.restapi.project.ListBranches;
 import com.google.gerrit.server.restapi.project.ListDashboards;
 import com.google.gerrit.server.restapi.project.ListTags;
@@ -124,6 +126,7 @@
   private final GetParent getParent;
   private final SetParent setParent;
   private final Index index;
+  private final IndexChanges indexChanges;
 
   @AssistedInject
   ProjectApiImpl(
@@ -158,6 +161,7 @@
       GetParent getParent,
       SetParent setParent,
       Index index,
+      IndexChanges indexChanges,
       @Assisted ProjectResource project) {
     this(
         permissionBackend,
@@ -192,6 +196,7 @@
         getParent,
         setParent,
         index,
+        indexChanges,
         null);
   }
 
@@ -228,6 +233,7 @@
       GetParent getParent,
       SetParent setParent,
       Index index,
+      IndexChanges indexChanges,
       @Assisted String name) {
     this(
         permissionBackend,
@@ -262,6 +268,7 @@
         getParent,
         setParent,
         index,
+        indexChanges,
         name);
   }
 
@@ -298,6 +305,7 @@
       GetParent getParent,
       SetParent setParent,
       Index index,
+      IndexChanges indexChanges,
       String name) {
     this.permissionBackend = permissionBackend;
     this.createProject = createProject;
@@ -332,6 +340,7 @@
     this.setParent = setParent;
     this.name = name;
     this.index = index;
+    this.indexChanges = indexChanges;
   }
 
   @Override
@@ -368,13 +377,17 @@
 
   @Override
   public String description() throws RestApiException {
-    return getDescription.apply(checkExists());
+    try {
+      return getDescription.apply(checkExists()).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get description", e);
+    }
   }
 
   @Override
   public ProjectAccessInfo access() throws RestApiException {
     try {
-      return getAccess.apply(checkExists());
+      return getAccess.apply(checkExists()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get access rights", e);
     }
@@ -383,7 +396,7 @@
   @Override
   public ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException {
     try {
-      return setAccess.apply(checkExists(), p);
+      return setAccess.apply(checkExists(), p).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot put access rights", e);
     }
@@ -401,7 +414,7 @@
   @Override
   public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
     try {
-      return checkAccess.apply(checkExists(), in);
+      return checkAccess.apply(checkExists(), in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot check access rights", e);
     }
@@ -410,7 +423,7 @@
   @Override
   public CheckProjectResultInfo check(CheckProjectInput in) throws RestApiException {
     try {
-      return check.apply(checkExists(), in);
+      return check.apply(checkExists(), in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot check project", e);
     }
@@ -427,13 +440,17 @@
 
   @Override
   public ConfigInfo config() throws RestApiException {
-    return getConfig.apply(checkExists());
+    try {
+      return getConfig.apply(checkExists()).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get config", e);
+    }
   }
 
   @Override
   public ConfigInfo config(ConfigInput in) throws RestApiException {
     try {
-      return putConfig.apply(checkExists(), in);
+      return putConfig.apply(checkExists(), in).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list tags", e);
     }
@@ -445,7 +462,7 @@
       @Override
       public List<BranchInfo> get() throws RestApiException {
         try {
-          return listBranches.get().request(this).apply(checkExists());
+          return listBranches.get().request(this).apply(checkExists()).value();
         } catch (Exception e) {
           throw asRestApiException("Cannot list branches", e);
         }
@@ -459,7 +476,7 @@
       @Override
       public List<TagInfo> get() throws RestApiException {
         try {
-          return listTags.get().request(this).apply(checkExists());
+          return listTags.get().request(this).apply(checkExists()).value();
         } catch (Exception e) {
           throw asRestApiException("Cannot list tags", e);
         }
@@ -475,7 +492,7 @@
   @Override
   public List<ProjectInfo> children(boolean recursive) throws RestApiException {
     try {
-      return children.list().withRecursive(recursive).apply(checkExists());
+      return children.list().withRecursive(recursive).apply(checkExists()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list children", e);
     }
@@ -484,7 +501,7 @@
   @Override
   public List<ProjectInfo> children(int limit) throws RestApiException {
     try {
-      return children.list().withLimit(limit).apply(checkExists());
+      return children.list().withLimit(limit).apply(checkExists()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list children", e);
     }
@@ -574,7 +591,7 @@
       @Override
       public List<DashboardInfo> get() throws RestApiException {
         try {
-          List<?> r = listDashboards.get().apply(checkExists());
+          List<?> r = listDashboards.get().apply(checkExists()).value();
           if (r.isEmpty()) {
             return Collections.emptyList();
           }
@@ -592,7 +609,7 @@
   @Override
   public String head() throws RestApiException {
     try {
-      return getHead.apply(checkExists());
+      return getHead.apply(checkExists()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get HEAD", e);
     }
@@ -612,7 +629,7 @@
   @Override
   public String parent() throws RestApiException {
     try {
-      return getParent.apply(checkExists());
+      return getParent.apply(checkExists()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get parent", e);
     }
@@ -640,6 +657,15 @@
     }
   }
 
+  @Override
+  public void indexChanges() throws RestApiException {
+    try {
+      indexChanges.apply(checkExists(), new Input());
+    } catch (Exception e) {
+      throw asRestApiException("Cannot index changes", e);
+    }
+  }
+
   private ProjectResource checkExists() throws ResourceNotFoundException {
     if (project == null) {
       throw new ResourceNotFoundException(name);
diff --git a/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java b/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
index 46a22c0..66d0224 100644
--- a/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Inject;
@@ -45,7 +45,7 @@
   @Override
   public final int parseArguments(Parameters params) throws CmdLineException {
     final String n = params.getParameter(0);
-    Optional<InternalGroup> group = groupCache.get(new AccountGroup.NameKey(n));
+    Optional<InternalGroup> group = groupCache.get(AccountGroup.nameKey(n));
     if (!group.isPresent()) {
       throw new CmdLineException(owner, localizable("Group \"%s\" does not exist"), n);
     }
diff --git a/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java b/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
index 2dd0c7a..20e8441 100644
--- a/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupCache;
@@ -53,7 +53,7 @@
   @Override
   public final int parseArguments(Parameters params) throws CmdLineException {
     final String n = params.getParameter(0);
-    AccountGroup.UUID uuid = new AccountGroup.UUID(n);
+    AccountGroup.UUID uuid = AccountGroup.uuid(n);
     if (groupBackend.handles(uuid)) {
       GroupDescription.Basic d = groupBackend.get(uuid);
       if (d != null) {
diff --git a/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 37e52f4..73a970b 100644
--- a/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -17,10 +17,10 @@
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResolver;
@@ -65,7 +65,7 @@
     Account.Id accountId;
     try {
       try {
-        accountId = accountResolver.resolve(token).asUnique().getAccount().getId();
+        accountId = accountResolver.resolve(token).asUnique().account().id();
       } catch (UnprocessableEntityException e) {
         switch (authType) {
           case HTTP_LDAP:
diff --git a/java/com/google/gerrit/server/args4j/ChangeIdHandler.java b/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
index ceba3e6..448c654 100644
--- a/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -18,10 +18,10 @@
 
 import com.google.common.base.Splitter;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.inject.Inject;
@@ -62,8 +62,8 @@
 
     try {
       Change.Key key = Change.Key.parse(tokens.get(2));
-      Project.NameKey project = new Project.NameKey(tokens.get(0));
-      Branch.NameKey branch = new Branch.NameKey(project, tokens.get(1));
+      Project.NameKey project = Project.nameKey(tokens.get(0));
+      BranchNameKey branch = BranchNameKey.create(project, tokens.get(1));
       List<ChangeData> changes = queryProvider.get().byBranchKey(branch, key);
       if (!changes.isEmpty()) {
         if (changes.size() > 1) {
diff --git a/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java b/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java
index 4581fe0..8b7cbd6 100644
--- a/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.PatchSet;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import org.kohsuke.args4j.CmdLineException;
diff --git a/java/com/google/gerrit/server/args4j/ProjectHandler.java b/java/com/google/gerrit/server/args4j/ProjectHandler.java
index e3af82b..61dbd2c 100644
--- a/java/com/google/gerrit/server/args4j/ProjectHandler.java
+++ b/java/com/google/gerrit/server/args4j/ProjectHandler.java
@@ -17,8 +17,8 @@
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -72,7 +72,7 @@
     }
 
     String nameWithoutSuffix = ProjectUtil.stripGitSuffix(projectName);
-    Project.NameKey nameKey = new Project.NameKey(nameWithoutSuffix);
+    Project.NameKey nameKey = Project.nameKey(nameWithoutSuffix);
 
     ProjectState state;
     try {
diff --git a/java/com/google/gerrit/server/audit/AuditService.java b/java/com/google/gerrit/server/audit/AuditService.java
index 425e22a..2a5d868 100644
--- a/java/com/google/gerrit/server/audit/AuditService.java
+++ b/java/com/google/gerrit/server/audit/AuditService.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.audit;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.AuditEvent;
 import com.google.gerrit.server.audit.group.GroupAuditListener;
 import com.google.gerrit.server.audit.group.GroupMemberAuditEvent;
diff --git a/java/com/google/gerrit/server/audit/BUILD b/java/com/google/gerrit/server/audit/BUILD
index c8b71a9..95929d3 100644
--- a/java/com/google/gerrit/server/audit/BUILD
+++ b/java/com/google/gerrit/server/audit/BUILD
@@ -9,15 +9,9 @@
     resources = ["//resources/com/google/gerrit/server"],
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
-        "//java/com/google/gerrit/server/ioutil",
-        "//java/com/google/gerrit/server/logging",
-        "//java/com/google/gerrit/server/util/time",
-        "//java/com/google/gerrit/util/cli",
-        "//java/com/google/gerrit/util/ssl",
-        "//java/org/apache/commons/net",
         "//lib:args4j",
         "//lib:autolink",
         "//lib:automaton",
@@ -50,6 +44,8 @@
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
+        "//lib:jgit",
+        "//lib:jgit-archive",
         "//lib:jsch",
         "//lib:juniversalchardet",
         "//lib:mime-util",
@@ -71,8 +67,6 @@
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jsoup",
         "//lib/log:jsonevent-layout",
         "//lib/log:log4j",
diff --git a/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java b/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
index ae26e12..252a1e2 100644
--- a/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.audit.group;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import java.sql.Timestamp;
 
 /** An audit event for groups. */
@@ -23,14 +23,14 @@
   /**
    * Gets the acting user who is updating the group.
    *
-   * @return the {@link com.google.gerrit.reviewdb.client.Account.Id} of the acting user.
+   * @return the {@link com.google.gerrit.entities.Account.Id} of the acting user.
    */
   Account.Id getActor();
 
   /**
-   * Gets the {@link com.google.gerrit.reviewdb.client.AccountGroup.UUID} of the updated group.
+   * Gets the {@link com.google.gerrit.entities.AccountGroup.UUID} of the updated group.
    *
-   * @return the {@link com.google.gerrit.reviewdb.client.AccountGroup.UUID} of the updated group.
+   * @return the {@link com.google.gerrit.entities.AccountGroup.UUID} of the updated group.
    */
   AccountGroup.UUID getUpdatedGroup();
 
diff --git a/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java b/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java
index a5c11bc..eccfbf4 100644
--- a/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java
@@ -16,8 +16,8 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import java.sql.Timestamp;
 
 @AutoValue
diff --git a/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java b/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java
index 0d5b26f..0fe3962 100644
--- a/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java
@@ -16,8 +16,8 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import java.sql.Timestamp;
 
 @AutoValue
diff --git a/java/com/google/gerrit/server/auth/InternalAuthBackend.java b/java/com/google/gerrit/server/auth/InternalAuthBackend.java
index c06c66b..2f8886b 100644
--- a/java/com/google/gerrit/server/auth/InternalAuthBackend.java
+++ b/java/com/google/gerrit/server/auth/InternalAuthBackend.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.externalids.PasswordVerifier;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -55,14 +56,14 @@
 
     AccountState who = accountCache.getByUsername(username).orElseThrow(UnknownUserException::new);
 
-    if (!who.getAccount().isActive()) {
+    if (!who.account().isActive()) {
       throw new UserNotAllowedException(
           "Authentication failed for "
               + username
               + ": account inactive or not provisioned in Gerrit");
     }
 
-    if (!who.checkPassword(req.getPassword().get(), username)) {
+    if (!PasswordVerifier.checkPassword(who.externalIds(), username, req.getPassword().get())) {
       throw new InvalidCredentialsException();
     }
     return new AuthUser(AuthUser.UUID.create(username), username);
diff --git a/java/com/google/gerrit/server/auth/ldap/Helper.java b/java/com/google/gerrit/server/auth/ldap/Helper.java
index 4b14c38..b0f011a 100644
--- a/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -19,7 +19,11 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.ParameterizedString;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AuthenticationFailedException;
 import com.google.gerrit.server.auth.NoSuchUserException;
@@ -81,11 +85,16 @@
   private final String connectTimeoutMillis;
   private final boolean useConnectionPooling;
   private final boolean groupsVisibleToAll;
+  private final Timer0 loginLatencyTimer;
+  private final Timer0 userSearchLatencyTimer;
+  private final Timer0 groupSearchLatencyTimer;
+  private final Timer0 groupExpansionLatencyTimer;
 
   @Inject
   Helper(
       @GerritServerConfig Config config,
-      @Named(LdapModule.PARENT_GROUPS_CACHE) Cache<String, ImmutableSet<String>> parentGroups) {
+      @Named(LdapModule.PARENT_GROUPS_CACHE) Cache<String, ImmutableSet<String>> parentGroups,
+      MetricMaker metricMaker) {
     this.config = config;
     this.server = LdapRealm.optional(config, "server");
     this.username = LdapRealm.optional(config, "username");
@@ -112,6 +121,33 @@
     }
     this.parentGroups = parentGroups;
     this.useConnectionPooling = LdapRealm.optional(config, "useConnectionPooling", false);
+
+    this.loginLatencyTimer =
+        metricMaker.newTimer(
+            "ldap/login_latency",
+            new Description("Latency of logins").setCumulative().setUnit(Units.NANOSECONDS));
+    this.userSearchLatencyTimer =
+        metricMaker.newTimer(
+            "ldap/user_search_latency",
+            new Description("Latency for searching the user account")
+                .setCumulative()
+                .setUnit(Units.NANOSECONDS));
+    this.groupSearchLatencyTimer =
+        metricMaker.newTimer(
+            "ldap/group_search_latency",
+            new Description("Latency for querying the groups membership of an account")
+                .setCumulative()
+                .setUnit(Units.NANOSECONDS));
+    this.groupExpansionLatencyTimer =
+        metricMaker.newTimer(
+            "ldap/group_expansion_latency",
+            new Description("Latency for expanding nested groups")
+                .setCumulative()
+                .setUnit(Units.NANOSECONDS));
+  }
+
+  Timer0 getGroupSearchLatencyTimer() {
+    return groupSearchLatencyTimer;
   }
 
   private Properties createContextProperties() {
@@ -191,7 +227,9 @@
   private DirContext kerberosOpen(Properties env)
       throws IOException, LoginException, NamingException {
     LoginContext ctx = new LoginContext("KerberosLogin");
-    ctx.login();
+    try (Timer0.Context ignored = loginLatencyTimer.start()) {
+      ctx.login();
+    }
     Subject subject = ctx.getSubject();
     try {
       return Subject.doAs(
@@ -209,7 +247,7 @@
 
   DirContext authenticate(String dn, String password) throws AccountException {
     final Properties env = createContextProperties();
-    try {
+    try (Timer0.Context ignored = loginLatencyTimer.start()) {
       env.put(Context.REFERRAL, referral);
 
       if (!supportAnonymous) {
@@ -258,7 +296,7 @@
     }
 
     for (LdapQuery accountQuery : accountQueryList) {
-      List<LdapQuery.Result> res = accountQuery.query(ctx, params);
+      List<LdapQuery.Result> res = accountQuery.query(ctx, params, userSearchLatencyTimer);
       if (res.size() == 1) {
         return res.get(0);
       } else if (res.size() > 1) {
@@ -290,8 +328,10 @@
       params.put(LdapRealm.USERNAME, username);
 
       for (LdapQuery groupMemberQuery : schema.groupMemberQueryList) {
-        for (LdapQuery.Result r : groupMemberQuery.query(ctx, params)) {
-          recursivelyExpandGroups(groupDNs, schema, ctx, r.getDN());
+        for (LdapQuery.Result r : groupMemberQuery.query(ctx, params, groupSearchLatencyTimer)) {
+          try (Timer0.Context ignored = groupExpansionLatencyTimer.start()) {
+            recursivelyExpandGroups(groupDNs, schema, ctx, r.getDN());
+          }
         }
       }
     }
@@ -321,7 +361,7 @@
 
     final Set<AccountGroup.UUID> actual = new HashSet<>();
     for (String dn : groupDNs) {
-      actual.add(new AccountGroup.UUID(LDAP_UUID + dn));
+      actual.add(AccountGroup.uuid(LDAP_UUID + dn));
     }
 
     if (actual.isEmpty()) {
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 87a4abf..0d8f3f8 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -27,7 +27,7 @@
 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.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
@@ -92,7 +92,7 @@
   private static GroupReference groupReference(ParameterizedString p, LdapQuery.Result res)
       throws NamingException {
     return new GroupReference(
-        new AccountGroup.UUID(LDAP_UUID + res.getDN()), LDAP_NAME + LdapRealm.apply(p, res));
+        AccountGroup.uuid(LDAP_UUID + res.getDN()), LDAP_NAME + LdapRealm.apply(p, res));
   }
 
   private static String cnFor(String dn) {
@@ -164,7 +164,7 @@
 
   @Override
   public Collection<GroupReference> suggest(String name, ProjectState project) {
-    AccountGroup.UUID uuid = new AccountGroup.UUID(name);
+    AccountGroup.UUID uuid = AccountGroup.uuid(name);
     if (isLdapUUID(uuid)) {
       GroupDescription.Basic g = get(uuid);
       if (g == null) {
@@ -179,7 +179,7 @@
 
   @Override
   public GroupMembership membershipsOf(IdentifiedUser user) {
-    String id = findId(user.state().getExternalIds());
+    String id = findId(user.state().externalIds());
     if (id == null) {
       return GroupMembership.EMPTY;
     }
@@ -213,7 +213,8 @@
         Map<String, String> params = Collections.emptyMap();
         for (String groupBase : schema.groupBases) {
           LdapQuery query = new LdapQuery(groupBase, schema.groupScope, filter, returnAttrs);
-          for (LdapQuery.Result res : query.query(ctx, params)) {
+          for (LdapQuery.Result res :
+              query.query(ctx, params, helper.getGroupSearchLatencyTimer())) {
             out.add(groupReference(schema.groupName, res));
           }
         }
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java b/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java
index f5406c2..a6aa2f6 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.auth.ldap;
 
 import com.google.common.cache.LoadingCache;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.project.ProjectCache;
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 3fbf049..092b5ac 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.auth.ldap;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.cache.CacheModule;
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapQuery.java b/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
index 3d25e86..3e549f6 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.auth.ldap;
 
 import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.metrics.Timer0;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -61,13 +62,16 @@
     return pattern.getParameterNames();
   }
 
-  List<Result> query(DirContext ctx, Map<String, String> params) throws NamingException {
+  List<Result> query(DirContext ctx, Map<String, String> params, Timer0 queryTimer)
+      throws NamingException {
     final SearchControls sc = new SearchControls();
     final NamingEnumeration<SearchResult> res;
 
     sc.setSearchScope(searchScope.scope());
     sc.setReturningAttributes(returnAttributes);
-    res = ctx.search(base, pattern.getRawPattern(), pattern.bind(params), sc);
+    try (Timer0.Context ignored = queryTimer.start()) {
+      res = ctx.search(base, pattern.getRawPattern(), pattern.bind(params), sc);
+    }
     try {
       final List<Result> r = new ArrayList<>();
       try {
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index be6303d..1421f17 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -22,10 +22,10 @@
 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.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.client.AuthType;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AbstractRealm;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AuthRequest;
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.inject.Inject;
@@ -301,7 +302,7 @@
 
   @Override
   public void onCreateAccount(AuthRequest who, Account account) {
-    usernameCache.put(who.getLocalUser(), Optional.of(account.getId()));
+    usernameCache.put(who.getLocalUser(), Optional.of(account.id()));
   }
 
   @Override
@@ -353,7 +354,9 @@
 
     @Override
     public Optional<Account.Id> load(String username) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading account for username %s", username)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading account for username", Metadata.builder().username(username).build())) {
         return externalIds
             .get(ExternalId.Key.create(SCHEME_GERRIT, username))
             .map(ExternalId::accountId);
@@ -372,7 +375,9 @@
     @Override
     public Set<AccountGroup.UUID> load(String username) throws Exception {
       try (TraceTimer timer =
-          TraceContext.newTimer("Loading group for member with username %s", username)) {
+          TraceContext.newTimer(
+              "Loading group for member with username",
+              Metadata.builder().username(username).build())) {
         final DirContext ctx = helper.open();
         try {
           return helper.queryForGroups(ctx, username, null);
@@ -393,7 +398,9 @@
 
     @Override
     public Boolean load(String groupDn) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading groupDn %s", groupDn)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading groupDn", Metadata.builder().authDomainName(groupDn).build())) {
         final DirContext ctx = helper.open();
         try {
           Name compositeGroupName = new CompositeName().add(groupDn);
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java b/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
index 2cef62d..944bd44 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
@@ -17,11 +17,11 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AbstractRealm;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
index 0980116..f58b0f7 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -17,17 +17,18 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Converter;
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.proto.Protos;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache.OAuthTokenProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.IntKeyCacheSerializer;
+import com.google.gerrit.server.cache.serialize.IntegerCacheSerializer;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
@@ -45,7 +46,9 @@
       protected void configure() {
         persist(OAUTH_TOKENS, Account.Id.class, OAuthToken.class)
             .version(1)
-            .keySerializer(new IntKeyCacheSerializer<>(Account.Id::new))
+            .keySerializer(
+                CacheSerializer.convert(
+                    IntegerCacheSerializer.INSTANCE, Converter.from(Account.Id::get, Account::id)))
             .valueSerializer(new Serializer());
       }
     };
diff --git a/java/com/google/gerrit/server/cache/CacheMetrics.java b/java/com/google/gerrit/server/cache/CacheMetrics.java
index 063ddbc..12194e7 100644
--- a/java/com/google/gerrit/server/cache/CacheMetrics.java
+++ b/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Set;
@@ -33,7 +34,8 @@
 
 @Singleton
 public class CacheMetrics {
-  private static final Field<String> F_NAME = Field.ofString("cache_name");
+  private static final Field<String> F_NAME =
+      Field.ofString("cache_name", Metadata.Builder::cacheName).build();
 
   @Inject
   public CacheMetrics(
diff --git a/java/com/google/gerrit/server/cache/h2/BUILD b/java/com/google/gerrit/server/cache/h2/BUILD
index a191f75..5e64aa7 100644
--- a/java/com/google/gerrit/server/cache/h2/BUILD
+++ b/java/com/google/gerrit/server/cache/h2/BUILD
@@ -14,8 +14,8 @@
         "//java/com/google/gerrit/server/util/time",
         "//lib:guava",
         "//lib:h2",
+        "//lib:jgit",
         "//lib/flogger:api",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 3732e37..ef4e44c 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.cache.PersistentCache;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -237,7 +238,9 @@
 
     @Override
     public ValueHolder<V> load(K key) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading value for %s from cache", key)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading value from cache", Metadata.builder().cacheKey(key.toString()).build())) {
         if (store.mightContain(key)) {
           ValueHolder<V> h = store.getIfPresent(key);
           if (h != null) {
diff --git a/java/com/google/gerrit/server/cache/mem/BUILD b/java/com/google/gerrit/server/cache/mem/BUILD
index bc5b66a..a666df7 100644
--- a/java/com/google/gerrit/server/cache/mem/BUILD
+++ b/java/com/google/gerrit/server/cache/mem/BUILD
@@ -11,7 +11,7 @@
         "//lib:caffeine",
         "//lib:caffeine-guava",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/server/cache/serialize/BUILD b/java/com/google/gerrit/server/cache/serialize/BUILD
index 76dcbb1..aa9106b 100644
--- a/java/com/google/gerrit/server/cache/serialize/BUILD
+++ b/java/com/google/gerrit/server/cache/serialize/BUILD
@@ -6,10 +6,10 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/proto",
-        "//java/com/google/gwtorm",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:protobuf",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
index 2d41f2c..5377fc1 100644
--- a/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.cache.serialize;
 
+import com.google.common.base.Converter;
+
 /**
  * Interface for serializing/deserializing a type to/from a persistent cache.
  *
@@ -22,6 +24,27 @@
  */
 public interface CacheSerializer<T> {
   /**
+   * Convert a serializer of one type to another type using a {@link Converter}.
+   *
+   * @param delegate underlying serializer.
+   * @param converter converter between an arbitrary type {@code T} and {@code delegate}'s type.
+   * @return serializer of type {@code T}.
+   */
+  static <T, D> CacheSerializer<T> convert(CacheSerializer<D> delegate, Converter<T, D> converter) {
+    return new CacheSerializer<T>() {
+      @Override
+      public byte[] serialize(T object) {
+        return delegate.serialize(converter.convert(object));
+      }
+
+      @Override
+      public T deserialize(byte[] in) {
+        return converter.reverse().convert(delegate.deserialize(in));
+      }
+    };
+  }
+
+  /**
    * Serializes the object to a new byte array.
    *
    * @param object object to serialize.
diff --git a/java/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializer.java
deleted file mode 100644
index 85530f4..0000000
--- a/java/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializer.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.cache.serialize;
-
-import static java.util.Objects.requireNonNull;
-
-import com.google.gwtorm.client.IntKey;
-import java.util.function.Function;
-
-public class IntKeyCacheSerializer<K extends IntKey<?>> implements CacheSerializer<K> {
-  private final Function<Integer, K> factory;
-
-  public IntKeyCacheSerializer(Function<Integer, K> factory) {
-    this.factory = requireNonNull(factory);
-  }
-
-  @Override
-  public byte[] serialize(K object) {
-    return IntegerCacheSerializer.INSTANCE.serialize(object.get());
-  }
-
-  @Override
-  public K deserialize(byte[] in) {
-    return factory.apply(IntegerCacheSerializer.INSTANCE.deserialize(in));
-  }
-}
diff --git a/java/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializer.java
index 500875d..7c0f84f 100644
--- a/java/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializer.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.cache.serialize;
 
-import org.eclipse.jgit.lib.Constants;
+import com.google.gerrit.git.ObjectIds;
 import org.eclipse.jgit.lib.ObjectId;
 
 public enum ObjectIdCacheSerializer implements CacheSerializer<ObjectId> {
@@ -22,14 +22,14 @@
 
   @Override
   public byte[] serialize(ObjectId object) {
-    byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
+    byte[] buf = new byte[ObjectIds.LEN];
     object.copyRawTo(buf, 0);
     return buf;
   }
 
   @Override
   public ObjectId deserialize(byte[] in) {
-    if (in == null || in.length != Constants.OBJECT_ID_LENGTH) {
+    if (in == null || in.length != ObjectIds.LEN) {
       throw new IllegalArgumentException("Failed to deserialize ObjectId");
     }
     return ObjectId.fromRaw(in);
diff --git a/java/com/google/gerrit/server/cache/serialize/ObjectIdConverter.java b/java/com/google/gerrit/server/cache/serialize/ObjectIdConverter.java
index eb946a9..22654e5 100644
--- a/java/com/google/gerrit/server/cache/serialize/ObjectIdConverter.java
+++ b/java/com/google/gerrit/server/cache/serialize/ObjectIdConverter.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
 
+import com.google.gerrit.git.ObjectIds;
 import com.google.protobuf.ByteString;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -35,7 +35,7 @@
     return new ObjectIdConverter();
   }
 
-  private final byte[] buf = new byte[OBJECT_ID_LENGTH];
+  private final byte[] buf = new byte[ObjectIds.LEN];
 
   private ObjectIdConverter() {}
 
@@ -46,10 +46,7 @@
 
   public ObjectId fromByteString(ByteString in) {
     checkArgument(
-        in.size() == OBJECT_ID_LENGTH,
-        "expected ByteString of length %s: %s",
-        OBJECT_ID_LENGTH,
-        in);
+        in.size() == ObjectIds.LEN, "expected ByteString of length %s: %s", ObjectIds.LEN, in);
     in.copyTo(buf, 0);
     return ObjectId.fromRaw(buf);
   }
diff --git a/java/com/google/gerrit/server/change/AbandonOp.java b/java/com/google/gerrit/server/change/AbandonOp.java
index 5ee5bc7..eb6e8d7 100644
--- a/java/com/google/gerrit/server/change/AbandonOp.java
+++ b/java/com/google/gerrit/server/change/AbandonOp.java
@@ -17,10 +17,10 @@
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchSetUtil;
@@ -112,7 +112,7 @@
     try {
       ReplyToChangeSender cm = abandonedSenderFactory.create(ctx.getProject(), change.getId());
       if (accountState != null) {
-        cm.setFrom(accountState.getAccount().getId());
+        cm.setFrom(accountState.account().id());
       }
       cm.setChangeMessage(message.getMessage(), ctx.getWhen());
       cm.setNotify(notify);
diff --git a/java/com/google/gerrit/server/change/AbandonUtil.java b/java/com/google/gerrit/server/change/AbandonUtil.java
index 6f46498..1bc1fad 100644
--- a/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -17,9 +17,9 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.config.ChangeCleanupConfig;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -40,7 +40,10 @@
 
   private final ChangeCleanupConfig cfg;
   private final Provider<ChangeQueryProcessor> queryProvider;
-  private final ChangeQueryBuilder queryBuilder;
+  // Provider is needed, because AbandonUtil is singleton, but ChangeQueryBuilder accesses
+  // index collection, that is only provided when multiversion index module is started.
+  // TODO(davido); Remove provider again, when support for legacy numeric fields is removed.
+  private final Provider<ChangeQueryBuilder> queryBuilderProvider;
   private final BatchAbandon batchAbandon;
   private final InternalUser internalUser;
 
@@ -49,11 +52,11 @@
       ChangeCleanupConfig cfg,
       InternalUser.Factory internalUserFactory,
       Provider<ChangeQueryProcessor> queryProvider,
-      ChangeQueryBuilder queryBuilder,
+      Provider<ChangeQueryBuilder> queryBuilderProvider,
       BatchAbandon batchAbandon) {
     this.cfg = cfg;
     this.queryProvider = queryProvider;
-    this.queryBuilder = queryBuilder;
+    this.queryBuilderProvider = queryBuilderProvider;
     this.batchAbandon = batchAbandon;
     internalUser = internalUserFactory.create();
   }
@@ -71,7 +74,11 @@
       }
 
       List<ChangeData> changesToAbandon =
-          queryProvider.get().enforceVisibility(false).query(queryBuilder.parse(query)).entities();
+          queryProvider
+              .get()
+              .enforceVisibility(false)
+              .query(queryBuilderProvider.get().parse(query))
+              .entities();
       ImmutableListMultimap.Builder<Project.NameKey, ChangeData> builder =
           ImmutableListMultimap.builder();
       for (ChangeData cd : changesToAbandon) {
@@ -111,7 +118,7 @@
           queryProvider
               .get()
               .enforceVisibility(false)
-              .query(queryBuilder.parse(newQuery))
+              .query(queryBuilderProvider.get().parse(newQuery))
               .entities();
       if (!changesToAbandon.isEmpty()) {
         validChanges.add(cd);
diff --git a/java/com/google/gerrit/server/change/AccountPatchReviewStore.java b/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
index fff3274..8da2a90 100644
--- a/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
@@ -16,9 +16,9 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import java.util.Collection;
 import java.util.Optional;
 
@@ -30,7 +30,8 @@
  * number of reviewed flags is growing without bound. The store must be able handle this data volume
  * efficiently.
  *
- * <p>For a multi-master setup the store must replicate the data between the masters.
+ * <p>For a cluster setups with multiple primary nodes the store must replicate the data between the
+ * primary servers.
  */
 public interface AccountPatchReviewStore {
 
diff --git a/java/com/google/gerrit/server/change/AddReviewersEmail.java b/java/com/google/gerrit/server/change/AddReviewersEmail.java
index d9c5dad..664b84d 100644
--- a/java/com/google/gerrit/server/change/AddReviewersEmail.java
+++ b/java/com/google/gerrit/server/change/AddReviewersEmail.java
@@ -18,9 +18,9 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.mail.send.AddReviewerSender;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index 610290d..87d34a4 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -27,13 +27,13 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Streams;
+import com.google.gerrit.entities.Account;
+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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountCache;
@@ -64,10 +64,14 @@
      * @param accountIds account IDs to add.
      * @param addresses email addresses to add.
      * @param state resulting reviewer state.
+     * @param forGroup whether this reviewer addition adds accounts for a group
      * @return batch update operation.
      */
     AddReviewersOp create(
-        Set<Account.Id> accountIds, Collection<Address> addresses, ReviewerState state);
+        Set<Account.Id> accountIds,
+        Collection<Address> addresses,
+        ReviewerState state,
+        boolean forGroup);
   }
 
   @AutoValue
@@ -107,6 +111,7 @@
   private final Set<Account.Id> accountIds;
   private final Collection<Address> addresses;
   private final ReviewerState state;
+  private final boolean forGroup;
 
   // Unlike addedCCs, addedReviewers is a PatchSetApproval because the AddReviewerResult returned
   // via the REST API is supposed to include vote information.
@@ -130,7 +135,8 @@
       AddReviewersEmail addReviewersEmail,
       @Assisted Set<Account.Id> accountIds,
       @Assisted Collection<Address> addresses,
-      @Assisted ReviewerState state) {
+      @Assisted ReviewerState state,
+      @Assisted boolean forGroup) {
     checkArgument(state == REVIEWER || state == CC, "must be %s or %s: %s", REVIEWER, CC, state);
     this.approvalsUtil = approvalsUtil;
     this.psUtil = psUtil;
@@ -142,6 +148,7 @@
     this.accountIds = accountIds;
     this.addresses = addresses;
     this.state = state;
+    this.forGroup = forGroup;
   }
 
   // TODO(dborowitz): This mutable setter is ugly, but a) it's less ugly than adding boolean args
@@ -162,7 +169,7 @@
       if (state == CC) {
         addedCCs =
             approvalsUtil.addCcs(
-                ctx.getNotes(), ctx.getUpdate(change.currentPatchSetId()), accountIds);
+                ctx.getNotes(), ctx.getUpdate(change.currentPatchSetId()), accountIds, forGroup);
       } else {
         addedReviewers =
             approvalsUtil.addReviewers(
@@ -174,12 +181,11 @@
       }
     }
 
-    ImmutableList<Address> addressesToAdd = ImmutableList.of();
     ReviewerStateInternal internalState = ReviewerStateInternal.fromReviewerState(state);
 
     // TODO(dborowitz): This behavior should live in ApprovalsUtil or something, like addCcs does.
     ImmutableSet<Address> existing = ctx.getNotes().getReviewersByEmail().byState(internalState);
-    addressesToAdd =
+    ImmutableList<Address> addressesToAdd =
         addresses.stream().filter(a -> !existing.contains(a)).collect(toImmutableList());
 
     if (state == CC) {
@@ -240,7 +246,7 @@
       addReviewersEmail.emailReviewers(
           ctx.getUser().asIdentifiedUser(),
           change,
-          Lists.transform(addedReviewers, PatchSetApproval::getAccountId),
+          Lists.transform(addedReviewers, PatchSetApproval::accountId),
           addedCCs,
           addedReviewersByEmail,
           addedCCsByEmail,
@@ -249,7 +255,7 @@
     if (!addedReviewers.isEmpty()) {
       List<AccountState> reviewers =
           addedReviewers.stream()
-              .map(r -> accountCache.get(r.getAccountId()))
+              .map(r -> accountCache.get(r.accountId()))
               .flatMap(Streams::stream)
               .collect(toList());
       reviewerAdded.fire(change, patchSet, reviewers, ctx.getAccount(), ctx.getWhen());
diff --git a/java/com/google/gerrit/server/change/ArchiveFormat.java b/java/com/google/gerrit/server/change/ArchiveFormat.java
index 0316c5f..d895a66 100644
--- a/java/com/google/gerrit/server/change/ArchiveFormat.java
+++ b/java/com/google/gerrit/server/change/ArchiveFormat.java
@@ -35,7 +35,9 @@
   TXZ("application/x-xz", new TxzFormat()),
   ZIP("application/x-zip", new ZipFormat());
 
+  @SuppressWarnings("ImmutableEnumChecker") // ArchiveCommand.Format is effectively immutable.
   private final ArchiveCommand.Format<?> format;
+
   private final String mimeType;
 
   ArchiveFormat(String mimeType, ArchiveCommand.Format<?> format) {
diff --git a/java/com/google/gerrit/server/change/BatchAbandon.java b/java/com/google/gerrit/server/change/BatchAbandon.java
index bc29c5d..e0a72ac4 100644
--- a/java/com/google/gerrit/server/change/BatchAbandon.java
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.ChangeCleanupConfig;
diff --git a/java/com/google/gerrit/server/change/ChangeETagComputation.java b/java/com/google/gerrit/server/change/ChangeETagComputation.java
new file mode 100644
index 0000000..a5b7d49
--- /dev/null
+++ b/java/com/google/gerrit/server/change/ChangeETagComputation.java
@@ -0,0 +1,63 @@
+// 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.change;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Allows plugins to contribute a value to the change ETag computation.
+ *
+ * <p>Plugins can affect the result of the get change / get change details REST endpoints by:
+ *
+ * <ul>
+ *   <li>providing plugin defined attributes to {@link
+ *       com.google.gerrit.extensions.common.ChangeInfo#plugins} (see {@link
+ *       ChangeAttributeFactory})
+ *   <li>implementing a {@link com.google.gerrit.server.rules.SubmitRule} which affects the
+ *       computation of {@link com.google.gerrit.extensions.common.ChangeInfo#submittable}
+ * </ul>
+ *
+ * <p>If the plugin defined part of {@link com.google.gerrit.extensions.common.ChangeInfo} depends
+ * on plugin specific data, callers that use the change ETags to avoid unneeded recomputations of
+ * ChangeInfos may see outdated plugin attributes and/or outdated submittable information, because a
+ * ChangeInfo is only reloaded if the change ETag changes.
+ *
+ * <p>By implementating this interface plugins can contribute to the change ETag computation and
+ * thus ensure that the ETag changes when the plugin data was changed. This way it is ensured that
+ * callers do not see outdated ChangeInfos.
+ *
+ * @see ChangeResource#getETag()
+ */
+@ExtensionPoint
+public interface ChangeETagComputation {
+  /**
+   * Computes an ETag of plugin-specific data for the given change.
+   *
+   * <p><strong>Note:</strong> Change ETags are computed very frequently and the computation must be
+   * cheap. Take good care to not perform any expensive computations when implementing this.
+   *
+   * <p>If an error is encountered during the ETag computation the plugin can indicate this by
+   * throwing any RuntimeException. In this case no value will be included in the change ETag
+   * computation. This means if the error is transient, the ETag will differ when the computation
+   * succeeds on a follow-up run.
+   *
+   * @param projectName the name of the project that contains the change
+   * @param changeId ID of the change for which the ETag should be computed
+   * @return the ETag
+   */
+  String getETag(Project.NameKey projectName, Change.Id changeId);
+}
diff --git a/java/com/google/gerrit/server/change/ChangeFinder.java b/java/com/google/gerrit/server/change/ChangeFinder.java
index 751d4b8..8d2d83d 100644
--- a/java/com/google/gerrit/server/change/ChangeFinder.java
+++ b/java/com/google/gerrit/server/change/ChangeFinder.java
@@ -19,17 +19,18 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.metrics.Counter1;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -91,7 +92,8 @@
             new Description("Total number of API calls per identifier type.")
                 .setRate()
                 .setUnit("requests"),
-            Field.ofEnum(ChangeIdType.class, "change_id_type"));
+            Field.ofEnum(ChangeIdType.class, "change_id_type", Metadata.Builder::changeIdType)
+                .build());
   }
 
   public ChangeNotes findOne(String id) {
@@ -129,7 +131,7 @@
       Integer n = Ints.tryParse(id);
       if (n != null) {
         changeIdCounter.increment(ChangeIdType.NUMERIC_ID);
-        return find(new Change.Id(n));
+        return find(Change.id(n));
       }
     }
 
@@ -138,7 +140,7 @@
     InternalChangeQuery query = queryProvider.get().noFields();
 
     // Try commit hash
-    if (id.matches("^([0-9a-fA-F]{" + RevId.ABBREV_LEN + "," + RevId.LEN + "})$")) {
+    if (id.matches("^([0-9a-fA-F]{" + ObjectIds.ABBREV_STR_LEN + "," + ObjectIds.STR_LEN + "})$")) {
       changeIdCounter.increment(ChangeIdType.COMMIT_HASH);
       return asChangeNotes(query.byCommit(id));
     }
@@ -162,7 +164,7 @@
   }
 
   private List<ChangeNotes> fromProjectNumber(String project, int changeNumber) {
-    Change.Id cId = new Change.Id(changeNumber);
+    Change.Id cId = Change.id(changeNumber);
     try {
       return ImmutableList.of(
           changeNotesFactory.createChecked(Project.NameKey.parse(project), cId));
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index a3b5db0..a00f1f8 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -17,7 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
+import static com.google.gerrit.entities.Change.INITIAL_PATCH_SET_ID;
 import static com.google.gerrit.server.change.ReviewerAdder.newAddReviewerInputFromCommitIdentity;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static java.util.Objects.requireNonNull;
@@ -30,18 +30,18 @@
 import com.google.gerrit.common.FooterConstants;
 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.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.PatchSetInfo;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.PatchSetUtil;
@@ -168,7 +168,7 @@
     this.reviewerAdder = reviewerAdder;
 
     this.changeId = changeId;
-    this.psId = new PatchSet.Id(changeId, INITIAL_PATCH_SET_ID);
+    this.psId = PatchSet.id(changeId, INITIAL_PATCH_SET_ID);
     this.commitId = commitId.copy();
     this.refName = refName;
     this.reviewerInputs = ImmutableList.of();
@@ -185,7 +185,7 @@
             getChangeKey(ctx.getRevWalk(), commitId),
             changeId,
             ctx.getAccountId(),
-            new Branch.NameKey(ctx.getProject(), refName),
+            BranchNameKey.create(ctx.getProject(), refName),
             ctx.getWhen());
     change.setStatus(MoreObjects.firstNonNull(status, Change.Status.NEW));
     change.setTopic(topic);
@@ -201,7 +201,7 @@
     rw.parseBody(commit);
     List<String> idList = commit.getFooterLines(FooterConstants.CHANGE_ID);
     if (!idList.isEmpty()) {
-      return new Change.Key(idList.get(idList.size() - 1).trim());
+      return Change.key(idList.get(idList.size() - 1).trim());
     }
     // A Change-Id is generated for the review, but not appended to the commit message.
     // This can happen if requireChangeId is false.
@@ -370,7 +370,7 @@
     ChangeUpdate update = ctx.getUpdate(psId);
     update.setChangeId(change.getKey().get());
     update.setSubjectForCommit("Create change");
-    update.setBranch(change.getDest().get());
+    update.setBranch(change.getDest().branch());
     update.setTopic(change.getTopic());
     update.setPsDescription(patchSetDescription);
     update.setPrivate(isPrivate);
@@ -419,9 +419,9 @@
     if (message != null) {
       changeMessage =
           ChangeMessagesUtil.newMessage(
-              patchSet.getId(),
+              patchSet.id(),
               ctx.getUser(),
-              patchSet.getCreatedOn(),
+              patchSet.createdOn(),
               message,
               ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
       cmUtil.addChangeMessage(update, changeMessage);
@@ -446,7 +446,7 @@
                 cm.setNotify(notify);
                 cm.addReviewers(
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewers).stream()
-                        .map(PatchSetApproval::getAccountId)
+                        .map(PatchSetApproval::accountId)
                         .collect(toImmutableSet()));
                 cm.addReviewersByEmail(
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewersByEmail));
@@ -511,14 +511,14 @@
           new CommitReceivedEvent(
               cmd,
               projectState.getProject(),
-              change.getDest().get(),
+              change.getDest().branch(),
               ctx.getRevWalk().getObjectReader(),
               commitId,
               ctx.getIdentifiedUser())) {
         commitValidatorsFactory
             .forGerritCommits(
                 permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
-                new Branch.NameKey(ctx.getProject(), refName),
+                BranchNameKey.create(ctx.getProject(), refName),
                 ctx.getIdentifiedUser(),
                 new NoSshInfo(),
                 ctx.getRevWalk(),
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 8d57992..27e5347 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -28,6 +28,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES;
+import static com.google.gerrit.extensions.client.ListChangesOption.SKIP_DIFFSTAT;
 import static com.google.gerrit.extensions.client.ListChangesOption.SKIP_MERGEABLE;
 import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
 import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
@@ -48,6 +49,12 @@
 import com.google.gerrit.common.data.SubmitRecord.Status;
 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.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -69,12 +76,6 @@
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GpgException;
@@ -288,7 +289,7 @@
 
   public ChangeInfo format(RevisionResource rsrc) {
     ChangeData cd = changeDataFactory.create(rsrc.getNotes());
-    return format(cd, Optional.of(rsrc.getPatchSet().getId()), true, ChangeInfo::new);
+    return format(cd, Optional.of(rsrc.getPatchSet().id()), true, ChangeInfo::new);
   }
 
   public List<List<ChangeInfo>> format(List<QueryResult<ChangeData>> in)
@@ -436,7 +437,7 @@
           info = format(cd, Optional.empty(), false, ChangeInfo::new);
           changeInfos.add(info);
           if (isCacheable) {
-            cache.put(new Change.Id(info._number), info);
+            cache.put(Change.id(info._number), info);
           }
         } catch (RuntimeException e) {
           logger.atWarning().withCause(e).log(
@@ -467,7 +468,7 @@
     Change c = result.change();
     if (c != null) {
       info.project = c.getProject().get();
-      info.branch = c.getDest().getShortName();
+      info.branch = c.getDest().shortName();
       info.topic = c.getTopic();
       info.changeId = c.getKey().get();
       info.subject = c.getSubject();
@@ -516,7 +517,7 @@
 
     Change in = cd.change();
     out.project = in.getProject().get();
-    out.branch = in.getDest().getShortName();
+    out.branch = in.getDest().shortName();
     out.topic = in.getTopic();
     out.assignee = in.getAssignee() != null ? accountLoader.get(in.getAssignee()) : null;
     out.hashtags = cd.hashtags();
@@ -533,10 +534,12 @@
         out.submittable = submittable(cd);
       }
     }
-    Optional<ChangedLines> changedLines = cd.changedLines();
-    if (changedLines.isPresent()) {
-      out.insertions = changedLines.get().insertions;
-      out.deletions = changedLines.get().deletions;
+    if (!has(SKIP_DIFFSTAT)) {
+      Optional<ChangedLines> changedLines = cd.changedLines();
+      if (changedLines.isPresent()) {
+        out.insertions = changedLines.get().insertions;
+        out.deletions = changedLines.get().deletions;
+      }
     }
     out.isPrivate = in.isPrivate() ? true : null;
     out.workInProgress = in.isWorkInProgress() ? true : null;
@@ -673,8 +676,8 @@
     if (!s.isPresent()) {
       return;
     }
-    out.submitted = s.get().getGranted();
-    out.submitter = accountLoader.get(s.get().getAccountId());
+    out.submitted = s.get().granted();
+    out.submitter = accountLoader.get(s.get().accountId());
   }
 
   private Collection<ChangeMessageInfo> messages(ChangeData cd) {
@@ -716,7 +719,7 @@
         continue;
       }
       for (ApprovalInfo ai : label.all) {
-        Account.Id id = new Account.Id(ai._accountId);
+        Account.Id id = Account.id(ai._accountId);
 
         if (canRemoveAnyReviewer
             || removeReviewerControl.testRemoveReviewer(
@@ -736,7 +739,7 @@
     if (ccs != null) {
       for (AccountInfo ai : ccs) {
         if (ai._accountId != null) {
-          Account.Id id = new Account.Id(ai._accountId);
+          Account.Id id = Account.id(ai._accountId);
           if (canRemoveAnyReviewer
               || removeReviewerControl.testRemoveReviewer(cd, userProvider.get(), id, 0)) {
             removable.add(id);
@@ -801,7 +804,7 @@
     }
     Map<PatchSet.Id, PatchSet> map = Maps.newHashMapWithExpectedSize(src.size());
     for (PatchSet patchSet : src) {
-      map.put(patchSet.getId(), patchSet);
+      map.put(patchSet.id(), patchSet);
     }
     return map;
   }
diff --git a/java/com/google/gerrit/server/change/ChangeKeyAdapter.java b/java/com/google/gerrit/server/change/ChangeKeyAdapter.java
new file mode 100644
index 0000000..0db4cea
--- /dev/null
+++ b/java/com/google/gerrit/server/change/ChangeKeyAdapter.java
@@ -0,0 +1,51 @@
+// 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.change;
+
+import com.google.gerrit.entities.Change;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import java.lang.reflect.Type;
+
+/**
+ * Adapter that serializes {@link com.google.gerrit.entities.Change.Key}'s {@code key} field as
+ * {@code id}, for backwards compatibility in stream-events.
+ */
+// TODO(dborowitz): auto-value-gson should support this directly using @SerializedName on the
+// AutoValue method.
+public class ChangeKeyAdapter implements JsonSerializer<Change.Key>, JsonDeserializer<Change.Key> {
+  @Override
+  public JsonElement serialize(Change.Key src, Type typeOfSrc, JsonSerializationContext context) {
+    JsonObject obj = new JsonObject();
+    obj.addProperty("id", src.get());
+    return obj;
+  }
+
+  @Override
+  public Change.Key deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+      throws JsonParseException {
+    JsonElement keyJson = json.getAsJsonObject().get("id");
+    if (keyJson == null || !keyJson.isJsonPrimitive() || !keyJson.getAsJsonPrimitive().isString()) {
+      throw new JsonParseException("Key is not a string: " + keyJson);
+    }
+    String key = keyJson.getAsJsonPrimitive().getAsString();
+    return Change.key(key);
+  }
+}
diff --git a/java/com/google/gerrit/server/change/ChangeKindCache.java b/java/com/google/gerrit/server/change/ChangeKindCache.java
index 44da4d6..9bd7ad7 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCache.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCache.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.ChangeKind;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.query.change.ChangeData;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index c20c0a2..1e14954 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -23,12 +23,12 @@
 import com.google.common.collect.FluentIterable;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.proto.Protos;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
@@ -374,13 +374,13 @@
     ChangeKind kind = ChangeKind.REWORK;
     // Trivial case: if we're on the first patch, we don't need to use
     // the repository.
-    if (patch.getId().get() > 1) {
+    if (patch.id().get() > 1) {
       try {
         Collection<PatchSet> patchSetCollection = change.patchSets();
         PatchSet priorPs = patch;
         for (PatchSet ps : patchSetCollection) {
-          if (ps.getId().get() < patch.getId().get()
-              && (ps.getId().get() > priorPs.getId().get() || priorPs == patch)) {
+          if (ps.id().get() < patch.id().get()
+              && (ps.id().get() > priorPs.id().get() || priorPs == patch)) {
             // We only want the previous patch set, so walk until the last one
             priorPs = ps;
           }
@@ -393,22 +393,17 @@
         if (priorPs != patch) {
           kind =
               cache.getChangeKind(
-                  change.project(),
-                  rw,
-                  repoConfig,
-                  ObjectId.fromString(priorPs.getRevision().get()),
-                  ObjectId.fromString(patch.getRevision().get()));
+                  change.project(), rw, repoConfig, priorPs.commitId(), patch.commitId());
         }
       } catch (StorageException e) {
         // Do nothing; assume we have a complex change
         logger.atWarning().withCause(e).log(
             "Unable to get change kind for patchSet %s of change %s",
-            patch.getPatchSetId(), change.getId());
+            patch.number(), change.getId());
       }
     }
     logger.atFine().log(
-        "Change kind for patchSet %s of change %s: %s",
-        patch.getPatchSetId(), change.getId(), kind);
+        "Change kind for patchSet %s of change %s: %s", patch.number(), change.getId(), kind);
     return kind;
   }
 
@@ -422,7 +417,7 @@
     ChangeKind kind = ChangeKind.REWORK;
     // Trivial case: if we're on the first patch, we don't need to open
     // the repository.
-    if (patch.getId().get() > 1) {
+    if (patch.id().get() > 1) {
       try (Repository repo = repoManager.openRepository(change.getProject());
           RevWalk rw = new RevWalk(repo)) {
         kind =
@@ -432,12 +427,11 @@
         // Do nothing; assume we have a complex change
         logger.atWarning().withCause(e).log(
             "Unable to get change kind for patchSet %s of change %s",
-            patch.getPatchSetId(), change.getChangeId());
+            patch.number(), change.getChangeId());
       }
     }
     logger.atFine().log(
-        "Change kind for patchSet %s of change %s: %s",
-        patch.getPatchSetId(), change.getChangeId(), kind);
+        "Change kind for patchSet %s of change %s: %s", patch.number(), change.getChangeId(), kind);
     return kind;
   }
 }
diff --git a/java/com/google/gerrit/server/change/ChangeMessageResource.java b/java/com/google/gerrit/server/change/ChangeMessageResource.java
index 3c9ef34..25f952d 100644
--- a/java/com/google/gerrit/server/change/ChangeMessageResource.java
+++ b/java/com/google/gerrit/server/change/ChangeMessageResource.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.inject.TypeLiteral;
 
 /** A change message resource. */
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 98b728f..8b8ce54 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -21,23 +21,27 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestResource.HasETag;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
@@ -75,6 +79,7 @@
   private final PermissionBackend permissionBackend;
   private final StarredChangesUtil starredChangesUtil;
   private final ProjectCache projectCache;
+  private final PluginSetContext<ChangeETagComputation> changeETagComputation;
   private final ChangeNotes notes;
   private final CurrentUser user;
 
@@ -86,6 +91,7 @@
       PermissionBackend permissionBackend,
       StarredChangesUtil starredChangesUtil,
       ProjectCache projectCache,
+      PluginSetContext<ChangeETagComputation> changeETagComputation,
       @Assisted ChangeNotes notes,
       @Assisted CurrentUser user) {
     this.accountCache = accountCache;
@@ -94,6 +100,7 @@
     this.permissionBackend = permissionBackend;
     this.starredChangesUtil = starredChangesUtil;
     this.projectCache = projectCache;
+    this.changeETagComputation = changeETagComputation;
     this.notes = notes;
     this.user = user;
   }
@@ -149,7 +156,7 @@
       accounts.add(getChange().getAssignee());
     }
     try {
-      patchSetUtil.byChange(notes).stream().map(PatchSet::getUploader).forEach(accounts::add);
+      patchSetUtil.byChange(notes).stream().map(PatchSet::uploader).forEach(accounts::add);
 
       // It's intentional to include the states for *all* reviewers into the ETag computation.
       // We need the states of all current reviewers and CCs because they are part of ChangeInfo.
@@ -193,16 +200,32 @@
     for (ProjectState p : projectStateTree) {
       hashObjectId(h, p.getConfig().getRevision(), buf);
     }
+
+    changeETagComputation.runEach(
+        c -> {
+          String pluginETag = c.getETag(notes.getProjectName(), notes.getChangeId());
+          if (pluginETag != null) {
+            h.putString(pluginETag, UTF_8);
+          }
+        });
   }
 
   @Override
   public String getETag() {
-    Hasher h = Hashing.murmur3_128().newHasher();
-    if (user.isIdentifiedUser()) {
-      h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8);
+    try (TraceTimer ignored =
+        TraceContext.newTimer(
+            "Compute change ETag",
+            Metadata.builder()
+                .changeId(notes.getChangeId().get())
+                .projectName(notes.getProjectName().get())
+                .build())) {
+      Hasher h = Hashing.murmur3_128().newHasher();
+      if (user.isIdentifiedUser()) {
+        h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8);
+      }
+      prepareETag(h, user);
+      return h.hash().toString();
     }
-    prepareETag(h, user);
-    return h.hash().toString();
   }
 
   private void hashObjectId(Hasher h, ObjectId id, byte[] buf) {
@@ -211,9 +234,8 @@
   }
 
   private void hashAccount(Hasher h, AccountState accountState, byte[] buf) {
-    h.putInt(accountState.getAccount().getId().get());
-    h.putString(
-        MoreObjects.firstNonNull(accountState.getAccount().getMetaId(), ZERO_ID_STRING), UTF_8);
-    accountState.getExternalIds().stream().forEach(e -> hashObjectId(h, e.blobId(), buf));
+    h.putInt(accountState.account().id().get());
+    h.putString(MoreObjects.firstNonNull(accountState.account().metaId(), ZERO_ID_STRING), UTF_8);
+    accountState.externalIds().stream().forEach(e -> hashObjectId(h, e.blobId(), buf));
   }
 }
diff --git a/java/com/google/gerrit/server/change/ChangeTriplet.java b/java/com/google/gerrit/server/change/ChangeTriplet.java
index e4e6870..1074302 100644
--- a/java/com/google/gerrit/server/change/ChangeTriplet.java
+++ b/java/com/google/gerrit/server/change/ChangeTriplet.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.change;
 
 import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import java.util.Optional;
 
 @AutoValue
@@ -27,8 +27,8 @@
     return format(change.getDest(), change.getKey());
   }
 
-  private static String format(Branch.NameKey branch, Change.Key change) {
-    return branch.getParentKey().get() + "~" + branch.getShortName() + "~" + change.get();
+  private static String format(BranchNameKey branch, Change.Key change) {
+    return branch.project().get() + "~" + branch.shortName() + "~" + change.get();
   }
 
   /**
@@ -53,14 +53,14 @@
     String changeId = Url.decode(triplet.substring(z + 1));
     return Optional.of(
         new AutoValue_ChangeTriplet(
-            new Branch.NameKey(new Project.NameKey(project), branch), new Change.Key(changeId)));
+            BranchNameKey.create(Project.nameKey(project), branch), Change.key(changeId)));
   }
 
   public final Project.NameKey project() {
-    return branch().getParentKey();
+    return branch().project();
   }
 
-  public abstract Branch.NameKey branch();
+  public abstract BranchNameKey branch();
 
   public abstract Change.Key id();
 
diff --git a/java/com/google/gerrit/server/change/CommentResource.java b/java/com/google/gerrit/server/change/CommentResource.java
index 1b7cbf8..dbe7a76 100644
--- a/java/com/google/gerrit/server/change/CommentResource.java
+++ b/java/com/google/gerrit/server/change/CommentResource.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.inject.TypeLiteral;
 
 public class CommentResource implements RestResource {
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 80b7190..0374a1c 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
+import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
 import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
 import static java.util.Comparator.comparing;
 import static java.util.Objects.requireNonNull;
@@ -30,14 +30,14 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.extensions.common.ProblemInfo.Status;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -238,7 +238,7 @@
   }
 
   private boolean openRepo() {
-    Project.NameKey project = change().getDest().getParentKey();
+    Project.NameKey project = change().getDest().project();
     try {
       repo = repoManager.openRepository(project);
       oi = repo.newObjectInserter();
@@ -265,7 +265,7 @@
     try {
       refs =
           repo.getRefDatabase()
-              .exactRef(all.stream().map(ps -> ps.getId().toRefName()).toArray(String[]::new));
+              .exactRef(all.stream().map(ps -> ps.id().toRefName()).toArray(String[]::new));
     } catch (IOException e) {
       error("error reading refs", e);
       refs = Collections.emptyMap();
@@ -274,12 +274,9 @@
     List<DeletePatchSetFromDbOp> deletePatchSetOps = new ArrayList<>();
     for (PatchSet ps : all) {
       // Check revision format.
-      int psNum = ps.getId().get();
-      String refName = ps.getId().toRefName();
-      ObjectId objId = parseObjectId(ps.getRevision().get(), "patch set " + psNum);
-      if (objId == null) {
-        continue;
-      }
+      int psNum = ps.id().get();
+      String refName = ps.id().toRefName();
+      ObjectId objId = ps.commitId();
       patchSetsBySha.put(objId, ps);
 
       // Check ref existence.
@@ -299,13 +296,13 @@
       RevCommit psCommit = parseCommit(objId, String.format("patch set %d", psNum));
       if (psCommit == null) {
         if (fix != null && fix.deletePatchSetIfCommitMissing) {
-          deletePatchSetOps.add(new DeletePatchSetFromDbOp(lastProblem(), ps.getId()));
+          deletePatchSetOps.add(new DeletePatchSetFromDbOp(lastProblem(), ps.id()));
         }
         continue;
       } else if (refProblem != null && fix != null) {
         fixPatchSetRef(refProblem, ps);
       }
-      if (ps.getId().equals(change().currentPatchSetId())) {
+      if (ps.id().equals(change().currentPatchSetId())) {
         currPsCommit = psCommit;
       }
     }
@@ -319,7 +316,7 @@
         problem(
             String.format(
                 "Multiple patch sets pointing to %s: %s",
-                e.getKey().name(), Collections2.transform(e.getValue(), PatchSet::getPatchSetId)));
+                e.getKey().name(), Collections2.transform(e.getValue(), PatchSet::number)));
       }
     }
 
@@ -327,7 +324,7 @@
   }
 
   private void checkMerged() {
-    String refName = change().getDest().get();
+    String refName = change().getDest().branch();
     Ref dest;
     try {
       dest = repo.getRefDatabase().exactRef(refName);
@@ -351,15 +348,15 @@
       try {
         merged = rw.isMergedInto(currPsCommit, tip);
       } catch (IOException e) {
-        problem("Error checking whether patch set " + currPs.getId().get() + " is merged");
+        problem("Error checking whether patch set " + currPs.id().get() + " is merged");
         return;
       }
-      checkMergedBitMatchesStatus(currPs.getId(), currPsCommit, merged);
+      checkMergedBitMatchesStatus(currPs.id(), currPsCommit, merged);
     }
   }
 
   private ProblemInfo wrongChangeStatus(PatchSet.Id psId, RevCommit commit) {
-    String refName = change().getDest().get();
+    String refName = change().getDest().branch();
     return problem(
         formatProblemMessage(
             "Patch set %d (%s) is merged into destination ref %s (%s), but change"
@@ -368,7 +365,7 @@
   }
 
   private void checkMergedBitMatchesStatus(PatchSet.Id psId, RevCommit commit, boolean merged) {
-    String refName = change().getDest().get();
+    String refName = change().getDest().branch();
     if (merged && !change().isMerged()) {
       ProblemInfo p = wrongChangeStatus(psId, commit);
       if (fix != null) {
@@ -379,7 +376,7 @@
           formatProblemMessage(
               "Patch set %d (%s) is not merged into"
                   + " destination ref %s (%s), but change status is %s",
-              currPs.getId().get(), commit.name(), refName, tip.name()));
+              currPs.id().get(), commit.name(), refName, tip.name()));
     }
   }
 
@@ -395,7 +392,11 @@
   }
 
   private void checkExpectMergedAs() {
-    ObjectId objId = parseObjectId(fix.expectMergedAs, "expected merged commit");
+    if (!ObjectId.isId(fix.expectMergedAs)) {
+      problem("Invalid revision on expected merged commit: " + fix.expectMergedAs);
+      return;
+    }
+    ObjectId objId = ObjectId.fromString(fix.expectMergedAs);
     RevCommit commit = parseCommit(objId, "expected merged commit");
     if (commit == null) {
       return;
@@ -406,7 +407,7 @@
         problem(
             String.format(
                 "Expected merged commit %s is not merged into destination ref %s (%s)",
-                commit.name(), change().getDest().get(), tip.name()));
+                commit.name(), change().getDest().branch(), tip.name()));
         return;
       }
 
@@ -420,8 +421,7 @@
           continue;
         }
         try {
-          Change c =
-              notesFactory.createChecked(change().getProject(), psId.getParentKey()).getChange();
+          Change c = notesFactory.createChecked(change().getProject(), psId.changeId()).getChange();
           if (!c.getDest().equals(change().getDest())) {
             continue;
           }
@@ -601,9 +601,9 @@
 
   private void fixPatchSetRef(ProblemInfo p, PatchSet ps) {
     try {
-      RefUpdate ru = repo.updateRef(ps.getId().toRefName());
+      RefUpdate ru = repo.updateRef(ps.id().toRefName());
       ru.setForceUpdate(true);
-      ru.setNewObjectId(ObjectId.fromString(ps.getRevision().get()));
+      ru.setNewObjectId(ps.commitId());
       ru.setRefLogIdent(newRefLogIdent());
       ru.setRefLogMessage("Repair patch set ref", true);
       RefUpdate.Result result = ru.update();
@@ -630,7 +630,7 @@
       }
     } catch (IOException e) {
       String msg = "Error fixing patch set ref";
-      logger.atWarning().withCause(e).log("%s %s", msg, ps.getId().toRefName());
+      logger.atWarning().withCause(e).log("%s %s", msg, ps.id().toRefName());
       p.status = Status.FIX_FAILED;
       p.outcome = msg;
     }
@@ -640,7 +640,7 @@
     try (BatchUpdate bu = newBatchUpdate()) {
       bu.setRepository(repo, rw, oi);
       for (DeletePatchSetFromDbOp op : ops) {
-        checkArgument(op.psId.getParentKey().equals(notes.getChangeId()));
+        checkArgument(op.psId.changeId().equals(notes.getChangeId()));
         bu.addOp(notes.getChangeId(), op);
       }
       bu.addOp(notes.getChangeId(), new UpdateCurrentPatchSetOp(ops));
@@ -652,7 +652,7 @@
       }
     } catch (UpdateException | RestApiException e) {
       String msg = "Error deleting patch set";
-      logger.atWarning().withCause(e).log("%s of change %s", msg, ops.get(0).psId.getParentKey());
+      logger.atWarning().withCause(e).log("%s of change %s", msg, ops.get(0).psId.changeId());
       for (DeletePatchSetFromDbOp op : ops) {
         // Overwrite existing statuses that were set before the transaction was
         // rolled back.
@@ -714,8 +714,8 @@
       // and whether they are seen by this op; we are already given the full set
       // of patch sets that will eventually be deleted in this update.
       for (PatchSet ps : psUtil.byChange(ctx.getNotes())) {
-        if (!toDelete.contains(ps.getId())) {
-          all.add(ps.getId());
+        if (!toDelete.contains(ps.id())) {
+          all.add(ps.id());
         }
       }
       if (all.isEmpty()) {
@@ -734,15 +734,6 @@
     return serverIdent.get();
   }
 
-  private ObjectId parseObjectId(String objIdStr, String desc) {
-    try {
-      return ObjectId.fromString(objIdStr);
-    } catch (IllegalArgumentException e) {
-      problem(String.format("Invalid revision on %s: %s", desc, objIdStr));
-      return null;
-    }
-  }
-
   private RevCommit parseCommit(ObjectId objId, String desc) {
     try {
       return rw.parseCommit(objId);
diff --git a/java/com/google/gerrit/server/change/DeleteChangeOp.java b/java/com/google/gerrit/server/change/DeleteChangeOp.java
index ec20e87..14298d5 100644
--- a/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -14,16 +14,19 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.flogger.LazyArgs.lazy;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.extensions.events.ChangeDeleted;
 import com.google.gerrit.server.plugincontext.PluginItemContext;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.RepoContext;
@@ -37,6 +40,8 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 public class DeleteChangeOp implements BatchUpdateOp {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public interface Factory {
     DeleteChangeOp create(Change.Id id);
   }
@@ -71,8 +76,19 @@
     ensureDeletable(ctx, id, patchSets);
     // Cleaning up is only possible as long as the change and its elements are
     // still part of the database.
-    cleanUpReferences(ctx, id);
+    cleanUpReferences(id);
 
+    logger.atFine().log(
+        "Deleting change %s, current patch set %d is commit %s",
+        id,
+        ctx.getChange().currentPatchSetId().get(),
+        lazy(
+            () ->
+                patchSets.stream()
+                    .filter(p -> p.number() == ctx.getChange().currentPatchSetId().get())
+                    .findAny()
+                    .map(p -> p.commitId().name())
+                    .orElse("n/a")));
     ctx.deleteChange();
     changeDeleted.fire(ctx.getChange(), ctx.getAccount(), ctx.getWhen());
     return true;
@@ -87,37 +103,50 @@
       if (isPatchSetMerged(ctx, patchSet)) {
         throw new ResourceConflictException(
             String.format(
-                "Cannot delete change %s: patch set %s is already merged",
-                id, patchSet.getPatchSetId()));
+                "Cannot delete change %s: patch set %s is already merged", id, patchSet.number()));
       }
     }
   }
 
   private boolean isPatchSetMerged(ChangeContext ctx, PatchSet patchSet) throws IOException {
-    Optional<ObjectId> destId = ctx.getRepoView().getRef(ctx.getChange().getDest().get());
+    Optional<ObjectId> destId = ctx.getRepoView().getRef(ctx.getChange().getDest().branch());
     if (!destId.isPresent()) {
       return false;
     }
 
     RevWalk revWalk = ctx.getRevWalk();
-    ObjectId objectId = ObjectId.fromString(patchSet.getRevision().get());
-    return revWalk.isMergedInto(revWalk.parseCommit(objectId), revWalk.parseCommit(destId.get()));
+    return revWalk.isMergedInto(
+        revWalk.parseCommit(patchSet.commitId()), revWalk.parseCommit(destId.get()));
   }
 
-  private void cleanUpReferences(ChangeContext ctx, Change.Id id) throws NoSuchChangeException {
+  private void cleanUpReferences(Change.Id id) throws IOException {
     accountPatchReviewStore.run(s -> s.clearReviewed(id));
 
-    // Non-atomic operation on Accounts table; not much we can do to make it
-    // atomic.
-    starredChangesUtil.unstarAll(ctx.getChange().getProject(), id);
+    // Non-atomic operation on All-Users refs; not much we can do to make it atomic.
+    starredChangesUtil.unstarAllForChangeDeletion(id);
   }
 
   @Override
   public void updateRepo(RepoContext ctx) throws IOException {
-    String prefix = new PatchSet.Id(id, 1).toRefName();
-    prefix = prefix.substring(0, prefix.length() - 1);
-    for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(prefix).entrySet()) {
-      ctx.addRefUpdate(e.getValue(), ObjectId.zeroId(), prefix + e.getKey());
+    String changeRefPrefix = RefNames.changeRefPrefix(id);
+    for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(changeRefPrefix).entrySet()) {
+      removeRef(ctx, e, changeRefPrefix);
     }
+    removeUserEdits(ctx);
+  }
+
+  private void removeUserEdits(RepoContext ctx) throws IOException {
+    String prefix = RefNames.REFS_USERS;
+    String editRef = String.format("/edit-%s/", id);
+    for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(prefix).entrySet()) {
+      if (e.getKey().contains(editRef)) {
+        removeRef(ctx, e, prefix);
+      }
+    }
+  }
+
+  private void removeRef(RepoContext ctx, Map.Entry<String, ObjectId> entry, String prefix)
+      throws IOException {
+    ctx.addRefUpdate(entry.getValue(), ObjectId.zeroId(), prefix + entry.getKey());
   }
 }
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index 4b5572b..3bc9324 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.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.mail.send.DeleteReviewerSender;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -55,7 +55,7 @@
     String msg = "Removed reviewer " + reviewer;
     changeMessage =
         new ChangeMessage(
-            new ChangeMessage.Key(change.getId(), ChangeUtil.messageUuid()),
+            ChangeMessage.key(change.getId(), ChangeUtil.messageUuid()),
             ctx.getAccountId(),
             ctx.getWhen(),
             psId);
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 29458a8..c4de02c 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -18,17 +18,17 @@
 import com.google.common.flogger.FluentLogger;
 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.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
@@ -108,7 +108,7 @@
   @Override
   public boolean updateChange(ChangeContext ctx)
       throws AuthException, ResourceNotFoundException, PermissionBackendException, IOException {
-    Account.Id reviewerId = reviewer.getAccount().getId();
+    Account.Id reviewerId = reviewer.account().id();
     // Check of removing this reviewer (even if there is no vote processed by the loop below) is OK
     removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), reviewerId);
 
@@ -125,7 +125,7 @@
     }
 
     StringBuilder msg = new StringBuilder();
-    msg.append("Removed reviewer " + reviewer.getAccount().getFullName());
+    msg.append("Removed reviewer " + reviewer.account().fullName());
     StringBuilder removedVotesMsg = new StringBuilder();
     removedVotesMsg.append(" with the following votes:\n\n");
     List<PatchSetApproval> del = new ArrayList<>();
@@ -134,14 +134,14 @@
       // Check if removing this vote is OK
       removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), a);
       del.add(a);
-      if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) {
-        oldApprovals.put(a.getLabel(), a.getValue());
+      if (a.patchSetId().equals(currPs.id()) && a.value() != 0) {
+        oldApprovals.put(a.label(), a.value());
         removedVotesMsg
             .append("* ")
-            .append(a.getLabel())
-            .append(formatLabelValue(a.getValue()))
+            .append(a.label())
+            .append(formatLabelValue(a.value()))
             .append(" by ")
-            .append(userFactory.create(a.getAccountId()).getNameEmail())
+            .append(userFactory.create(a.accountId()).getNameEmail())
             .append("\n");
         votesRemoved = true;
       }
@@ -152,7 +152,7 @@
     } else {
       msg.append(".");
     }
-    ChangeUpdate update = ctx.getUpdate(currPs.getId());
+    ChangeUpdate update = ctx.getUpdate(currPs.id());
     update.removeReviewer(reviewerId);
 
     changeMessage =
@@ -195,7 +195,7 @@
   private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId) {
     Iterable<PatchSetApproval> approvals;
     approvals = approvalsUtil.byChange(ctx.getNotes()).values();
-    return Iterables.filter(approvals, psa -> accountId.equals(psa.getAccountId()));
+    return Iterables.filter(approvals, psa -> accountId.equals(psa.accountId()));
   }
 
   private String formatLabelValue(short value) {
@@ -206,16 +206,19 @@
   }
 
   private void emailReviewers(
-      NameKey projectName, Change change, ChangeMessage changeMessage, NotifyResolver.Result notify)
+      Project.NameKey projectName,
+      Change change,
+      ChangeMessage changeMessage,
+      NotifyResolver.Result notify)
       throws EmailException {
     Account.Id userId = user.get().getAccountId();
-    if (userId.equals(reviewer.getAccount().getId())) {
+    if (userId.equals(reviewer.account().id())) {
       // 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.getAccount().getId()));
+    cm.addReviewers(Collections.singleton(reviewer.account().id()));
     cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
     cm.setNotify(notify);
     cm.send();
diff --git a/java/com/google/gerrit/server/change/DraftCommentResource.java b/java/com/google/gerrit/server/change/DraftCommentResource.java
index ef31725..3d3e8f9 100644
--- a/java/com/google/gerrit/server/change/DraftCommentResource.java
+++ b/java/com/google/gerrit/server/change/DraftCommentResource.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.CurrentUser;
 import com.google.inject.TypeLiteral;
 
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index 8353501..f7e45e7 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -18,9 +18,9 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.SendEmailExecutor;
@@ -129,7 +129,7 @@
       cm.setNotify(notify);
       cm.send();
     } catch (Exception e) {
-      logger.atSevere().withCause(e).log("Cannot email comments for %s", patchSet.getId());
+      logger.atSevere().withCause(e).log("Cannot email comments for %s", patchSet.id());
     } finally {
       requestContext.setContext(old);
     }
diff --git a/java/com/google/gerrit/server/change/FileContentUtil.java b/java/com/google/gerrit/server/change/FileContentUtil.java
index a806f94..5c7946c 100644
--- a/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -21,10 +21,10 @@
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PatchScript.FileMode;
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mime.FileTypeRegistry;
 import com.google.gerrit.server.project.ProjectState;
diff --git a/java/com/google/gerrit/server/change/FileInfoJson.java b/java/com/google/gerrit/server/change/FileInfoJson.java
index 56cc8df..a823975 100644
--- a/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -15,13 +15,12 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.common.FileInfo;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -44,27 +43,20 @@
 
   public Map<String, FileInfo> toFileInfoMap(Change change, PatchSet patchSet)
       throws PatchListNotAvailableException {
-    return toFileInfoMap(change, patchSet.getRevision(), null);
-  }
-
-  public Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, @Nullable PatchSet base)
-      throws PatchListNotAvailableException {
-    ObjectId objectId = ObjectId.fromString(revision.get());
-    return toFileInfoMap(change, objectId, base);
+    return toFileInfoMap(change, patchSet.commitId(), null);
   }
 
   public Map<String, FileInfo> toFileInfoMap(
       Change change, ObjectId objectId, @Nullable PatchSet base)
       throws PatchListNotAvailableException {
-    ObjectId a = (base == null) ? null : ObjectId.fromString(base.getRevision().get());
+    ObjectId a = base != null ? base.commitId() : null;
     return toFileInfoMap(change, PatchListKey.againstCommit(a, objectId, Whitespace.IGNORE_NONE));
   }
 
-  public Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, int parent)
+  public Map<String, FileInfo> toFileInfoMap(Change change, ObjectId objectId, int parent)
       throws PatchListNotAvailableException {
-    ObjectId b = ObjectId.fromString(revision.get());
     return toFileInfoMap(
-        change, PatchListKey.againstParentNum(parent + 1, b, Whitespace.IGNORE_NONE));
+        change, PatchListKey.againstParentNum(parent + 1, objectId, Whitespace.IGNORE_NONE));
   }
 
   private Map<String, FileInfo> toFileInfoMap(Change change, PatchListKey key)
diff --git a/java/com/google/gerrit/server/change/FileResource.java b/java/com/google/gerrit/server/change/FileResource.java
index bd7557f..5402338 100644
--- a/java/com/google/gerrit/server/change/FileResource.java
+++ b/java/com/google/gerrit/server/change/FileResource.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Patch;
 import com.google.inject.TypeLiteral;
 
 public class FileResource implements RestResource {
@@ -29,7 +29,7 @@
 
   public FileResource(RevisionResource rev, String name) {
     this.rev = rev;
-    this.key = new Patch.Key(rev.getPatchSet().getId(), name);
+    this.key = Patch.key(rev.getPatchSet().id(), name);
   }
 
   public Patch.Key getPatchKey() {
diff --git a/java/com/google/gerrit/server/change/FixResource.java b/java/com/google/gerrit/server/change/FixResource.java
index 08e2785..b6b5894 100644
--- a/java/com/google/gerrit/server/change/FixResource.java
+++ b/java/com/google/gerrit/server/change/FixResource.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.entities.FixReplacement;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.FixReplacement;
 import com.google.inject.TypeLiteral;
 import java.util.List;
 
diff --git a/java/com/google/gerrit/server/change/IncludedIn.java b/java/com/google/gerrit/server/change/IncludedIn.java
index 3ac6959..a0b64da 100644
--- a/java/com/google/gerrit/server/change/IncludedIn.java
+++ b/java/com/google/gerrit/server/change/IncludedIn.java
@@ -14,22 +14,33 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
+import static java.util.Comparator.naturalOrder;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.IncludedInInfo;
 import com.google.gerrit.extensions.config.ExternalIncludedIn;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -37,17 +48,21 @@
 @Singleton
 public class IncludedIn {
   private final GitRepositoryManager repoManager;
+  private final PermissionBackend permissionBackend;
   private final PluginSetContext<ExternalIncludedIn> externalIncludedIn;
 
   @Inject
   IncludedIn(
-      GitRepositoryManager repoManager, PluginSetContext<ExternalIncludedIn> externalIncludedIn) {
+      GitRepositoryManager repoManager,
+      PermissionBackend permissionBackend,
+      PluginSetContext<ExternalIncludedIn> externalIncludedIn) {
     this.repoManager = repoManager;
+    this.permissionBackend = permissionBackend;
     this.externalIncludedIn = externalIncludedIn;
   }
 
   public IncludedInInfo apply(Project.NameKey project, String revisionId)
-      throws RestApiException, IOException {
+      throws RestApiException, IOException, PermissionBackendException {
     try (Repository r = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(r)) {
       rw.setRetainBody(false);
@@ -61,18 +76,45 @@
       }
 
       IncludedInResolver.Result d = IncludedInResolver.resolve(r, rw, rev);
+
+      // Filter branches and tags according to their visbility by the user
+      ImmutableSortedSet<String> filteredBranches =
+          sortedShortNames(filterReadableRefs(project, d.branches()).keySet());
+      ImmutableSortedSet<String> filteredTags =
+          sortedShortNames(filterReadableRefs(project, d.tags()).keySet());
+
       ListMultimap<String, String> external = MultimapBuilder.hashKeys().arrayListValues().build();
       externalIncludedIn.runEach(
           ext -> {
             ListMultimap<String, String> extIncludedIns =
-                ext.getIncludedIn(project.get(), rev.name(), d.tags(), d.branches());
+                ext.getIncludedIn(project.get(), rev.name(), filteredBranches, filteredTags);
             if (extIncludedIns != null) {
               external.putAll(extIncludedIns);
             }
           });
 
       return new IncludedInInfo(
-          d.branches(), d.tags(), (!external.isEmpty() ? external.asMap() : null));
+          filteredBranches, filteredTags, (!external.isEmpty() ? external.asMap() : null));
     }
   }
+
+  /**
+   * Filter readable branches or tags according to the caller's refs visibility.
+   *
+   * @param project specific Gerrit project.
+   * @param inputRefs a list of branches (in short name) as strings
+   */
+  private Map<String, Ref> filterReadableRefs(Project.NameKey project, ImmutableList<Ref> inputRefs)
+      throws IOException, PermissionBackendException {
+    PermissionBackend.ForProject perm = permissionBackend.currentUser().project(project);
+    try (Repository repo = repoManager.openRepository(project)) {
+      return perm.filter(inputRefs, repo, RefFilterOptions.defaults());
+    }
+  }
+
+  private ImmutableSortedSet<String> sortedShortNames(Collection<String> refs) {
+    return refs.stream()
+        .map(Repository::shortenRefName)
+        .collect(toImmutableSortedSet(naturalOrder()));
+  }
 }
diff --git a/java/com/google/gerrit/server/change/IncludedInResolver.java b/java/com/google/gerrit/server/change/IncludedInResolver.java
index 09ca258..3891700 100644
--- a/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -14,13 +14,11 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
 import static java.util.Comparator.comparing;
-import static java.util.Comparator.naturalOrder;
 import static java.util.stream.Collectors.toList;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
@@ -171,13 +169,12 @@
    * Returns the short names of refs which are as well in the matchingRefs list as well as in the
    * allRef list.
    */
-  private static ImmutableSortedSet<String> getMatchingRefNames(
+  private static ImmutableList<Ref> getMatchingRefNames(
       Set<String> matchingRefs, Collection<Ref> allRefs) {
     return allRefs.stream()
-        .map(Ref::getName)
-        .filter(matchingRefs::contains)
-        .map(Repository::shortenRefName)
-        .collect(toImmutableSortedSet(naturalOrder()));
+        .filter(r -> matchingRefs.contains(r.getName()))
+        .distinct()
+        .collect(ImmutableList.toImmutableList());
   }
 
   /** Parse commit of ref and store the relation between ref and commit. */
@@ -211,8 +208,8 @@
 
   @AutoValue
   public abstract static class Result {
-    public abstract ImmutableSortedSet<String> branches();
+    public abstract ImmutableList<Ref> branches();
 
-    public abstract ImmutableSortedSet<String> tags();
+    public abstract ImmutableList<Ref> tags();
   }
 }
diff --git a/java/com/google/gerrit/server/change/LabelNormalizer.java b/java/com/google/gerrit/server/change/LabelNormalizer.java
index 1ec1717..1ef3aee 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.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
@@ -88,24 +88,23 @@
     List<PatchSetApproval> deleted = Lists.newArrayListWithCapacity(approvals.size());
     LabelTypes labelTypes = projectCache.checkedGet(notes.getProjectName()).getLabelTypes(notes);
     for (PatchSetApproval psa : approvals) {
-      Change.Id changeId = psa.getKey().getParentKey().getParentKey();
+      Change.Id changeId = psa.key().patchSetId().changeId();
       checkArgument(
           changeId.equals(notes.getChangeId()),
           "Approval %s does not match change %s",
-          psa.getKey(),
+          psa.key(),
           notes.getChange().getKey());
       if (psa.isLegacySubmit()) {
         unchanged.add(psa);
         continue;
       }
-      LabelType label = labelTypes.byLabel(psa.getLabelId());
+      LabelType label = labelTypes.byLabel(psa.labelId());
       if (label == null) {
         deleted.add(psa);
         continue;
       }
-      PatchSetApproval copy = copy(psa);
-      applyTypeFloor(label, copy);
-      if (copy.getValue() != psa.getValue()) {
+      PatchSetApproval copy = applyTypeFloor(label, psa);
+      if (copy.value() != psa.value()) {
         updated.add(copy);
       } else {
         unchanged.add(psa);
@@ -114,18 +113,16 @@
     return Result.create(unchanged, updated, deleted);
   }
 
-  private PatchSetApproval copy(PatchSetApproval src) {
-    return new PatchSetApproval(src.getPatchSetId(), src);
-  }
-
-  private void applyTypeFloor(LabelType lt, PatchSetApproval a) {
+  private PatchSetApproval applyTypeFloor(LabelType lt, PatchSetApproval a) {
+    PatchSetApproval.Builder b = a.toBuilder();
     LabelValue atMin = lt.getMin();
-    if (atMin != null && a.getValue() < atMin.getValue()) {
-      a.setValue(atMin.getValue());
+    if (atMin != null && a.value() < atMin.getValue()) {
+      b.value(atMin.getValue());
     }
     LabelValue atMax = lt.getMax();
-    if (atMax != null && a.getValue() > atMax.getValue()) {
-      a.setValue(atMax.getValue());
+    if (atMax != null && a.value() > atMax.getValue()) {
+      b.value(atMax.getValue());
     }
+    return b.build();
   }
 }
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index 6fde5a5..c6f4969 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -36,12 +36,12 @@
 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.PatchSetApproval;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.VotingRangeInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.account.AccountLoader;
@@ -206,8 +206,8 @@
       if (standard) {
         for (PatchSetApproval psa : cd.currentApprovals()) {
           if (type.matches(psa)) {
-            short val = psa.getValue();
-            Account.Id accountId = psa.getAccountId();
+            short val = psa.value();
+            Account.Id accountId = psa.accountId();
             setLabelScores(accountLoader, type, e.getValue(), val, accountId);
           }
         }
@@ -260,7 +260,7 @@
             accountId,
             null,
             null)) {
-      result.put(psa.getLabel(), psa.getValue());
+      result.put(psa.label(), psa.value());
     }
     return result;
   }
@@ -279,7 +279,7 @@
       // we aren't including 0 votes for all users below, so we can just look at
       // the latest patch set (in the next loop).
       for (PatchSetApproval psa : cd.approvals().values()) {
-        allUsers.add(psa.getAccountId());
+        allUsers.add(psa.accountId());
       }
     }
 
@@ -287,13 +287,13 @@
     SetMultimap<Account.Id, PatchSetApproval> current =
         MultimapBuilder.hashKeys().hashSetValues().build();
     for (PatchSetApproval a : cd.currentApprovals()) {
-      allUsers.add(a.getAccountId());
-      LabelType type = labelTypes.byLabel(a.getLabelId());
+      allUsers.add(a.accountId());
+      LabelType type = labelTypes.byLabel(a.labelId());
       if (type != null) {
         labelNames.add(type.getName());
         // Not worth the effort to distinguish between votable/non-votable for 0
         // values on closed changes, since they can't vote anyway.
-        current.put(a.getAccountId(), a);
+        current.put(a.accountId(), a);
       }
     }
 
@@ -335,19 +335,19 @@
         }
       }
       for (PatchSetApproval psa : current.get(accountId)) {
-        LabelType type = labelTypes.byLabel(psa.getLabelId());
+        LabelType type = labelTypes.byLabel(psa.labelId());
         if (type == null) {
           continue;
         }
 
-        short val = psa.getValue();
+        short val = psa.value();
         ApprovalInfo info = byLabel.get(type.getName());
         if (info != null) {
           info.value = Integer.valueOf(val);
           info.permittedVotingRange = pvr.getOrDefault(type.getName(), null);
-          info.date = psa.getGranted();
-          info.tag = psa.getTag();
-          if (psa.isPostSubmit()) {
+          info.date = psa.granted();
+          info.tag = psa.tag().orElse(null);
+          if (psa.postSubmit()) {
             info.postSubmit = true;
           }
         }
@@ -441,13 +441,13 @@
     Set<Account.Id> allUsers = new HashSet<>();
     allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER));
     for (PatchSetApproval psa : cd.approvals().values()) {
-      allUsers.add(psa.getAccountId());
+      allUsers.add(psa.accountId());
     }
 
     Table<Account.Id, String, PatchSetApproval> current =
         HashBasedTable.create(allUsers.size(), cd.getLabelTypes().getLabelTypes().size());
     for (PatchSetApproval psa : cd.currentApprovals()) {
-      current.put(psa.getAccountId(), psa.getLabel(), psa);
+      current.put(psa.accountId(), psa.label(), psa);
     }
 
     LabelTypes labelTypes = cd.getLabelTypes();
@@ -467,16 +467,16 @@
         Timestamp date = null;
         PatchSetApproval psa = current.get(accountId, lt.getName());
         if (psa != null) {
-          value = Integer.valueOf(psa.getValue());
+          value = Integer.valueOf(psa.value());
           if (value == 0) {
             // This may be a dummy approval that was inserted when the reviewer
             // was added. Explicitly check whether the user can vote on this
             // label.
             value = perm.test(new LabelPermission(lt)) ? 0 : null;
           }
-          tag = psa.getTag();
-          date = psa.getGranted();
-          if (psa.isPostSubmit()) {
+          tag = psa.tag().orElse(null);
+          date = psa.granted();
+          if (psa.postSubmit()) {
             logger.atWarning().log("unexpected post-submit approval on open change: %s", psa);
           }
         } else {
diff --git a/java/com/google/gerrit/server/change/MergeabilityCache.java b/java/com/google/gerrit/server/change/MergeabilityCache.java
index 3a7f3ab..b432bc9 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCache.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCache.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Branch;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -29,7 +29,7 @@
         Ref intoRef,
         SubmitType submitType,
         String mergeStrategy,
-        Branch.NameKey dest,
+        BranchNameKey dest,
         Repository repo) {
       throw new UnsupportedOperationException("Mergeability checking disabled");
     }
@@ -46,7 +46,7 @@
       Ref intoRef,
       SubmitType submitType,
       String mergeStrategy,
-      Branch.NameKey dest,
+      BranchNameKey dest,
       Repository repo);
 
   Boolean getIfPresent(ObjectId commit, Ref intoRef, SubmitType submitType, String mergeStrategy);
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index d408519..44af1e4 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -24,9 +24,9 @@
 import com.google.common.cache.Weigher;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.proto.Protos;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache.MergeabilityKeyProto;
 import com.google.gerrit.server.cache.serialize.BooleanCacheSerializer;
@@ -191,7 +191,7 @@
       Ref intoRef,
       SubmitType submitType,
       String mergeStrategy,
-      Branch.NameKey dest,
+      BranchNameKey dest,
       Repository repo) {
     ObjectId into = intoRef != null ? intoRef.getObjectId() : ObjectId.zeroId();
     EntryKey key = new EntryKey(commit, into, submitType, mergeStrategy);
diff --git a/java/com/google/gerrit/server/change/NotifyResolver.java b/java/com/google/gerrit/server/change/NotifyResolver.java
index 5b15684..27951ca 100644
--- a/java/com/google/gerrit/server/change/NotifyResolver.java
+++ b/java/com/google/gerrit/server/change/NotifyResolver.java
@@ -21,12 +21,12 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.NotifyInfo;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -99,7 +99,7 @@
     List<String> problems = new ArrayList<>(inputs.size());
     for (String nameOrEmail : inputs) {
       try {
-        r.add(accountResolver.resolve(nameOrEmail).asUnique().getAccount().getId());
+        r.add(accountResolver.resolve(nameOrEmail).asUnique().account().id());
       } catch (UnprocessableEntityException e) {
         problems.add(e.getMessage());
       }
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index d3649f6..71c54b1 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -20,13 +20,13 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetInfo;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -208,7 +208,7 @@
     if (newGroups.isEmpty()) {
       PatchSet prevPs = psUtil.current(ctx.getNotes());
       if (prevPs != null) {
-        newGroups = prevPs.getGroups();
+        newGroups = prevPs.groups();
       }
     }
     patchSet =
@@ -222,7 +222,7 @@
     if (message != null) {
       changeMessage =
           ChangeMessagesUtil.newMessage(
-              patchSet.getId(),
+              patchSet.id(),
               ctx.getUser(),
               ctx.getWhen(),
               message,
@@ -288,7 +288,7 @@
                 commitId,
                 refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
             projectCache.checkedGet(origNotes.getProjectName()).getProject(),
-            origNotes.getChange().getDest().get(),
+            origNotes.getChange().getDest().branch(),
             ctx.getRevWalk().getObjectReader(),
             commitId,
             ctx.getIdentifiedUser())) {
diff --git a/java/com/google/gerrit/server/change/PureRevert.java b/java/com/google/gerrit/server/change/PureRevert.java
index acb3dd7..cb632dc 100644
--- a/java/com/google/gerrit/server/change/PureRevert.java
+++ b/java/com/google/gerrit/server/change/PureRevert.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.inject.Inject;
@@ -54,8 +54,6 @@
     }
 
     return pureRevertCache.isPureRevert(
-        notes.getProjectName(),
-        ObjectId.fromString(notes.getCurrentPatchSet().getRevision().get()),
-        claimedOriginalObjectId);
+        notes.getProjectName(), notes.getCurrentPatchSet().commitId(), claimedOriginalObjectId);
   }
 }
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index fccda7c..4723af8 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -16,11 +16,10 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -151,10 +150,8 @@
           NoSuchChangeException, PermissionBackendException {
     // Ok that originalPatchSet was not read in a transaction, since we just
     // need its revision.
-    RevId oldRev = originalPatchSet.getRevision();
-
     RevWalk rw = ctx.getRevWalk();
-    RevCommit original = rw.parseCommit(ObjectId.fromString(oldRev.get()));
+    RevCommit original = rw.parseCommit(originalPatchSet.commitId());
     rw.parseBody(original);
     RevCommit baseCommit = rw.parseCommit(baseCommitId);
     CurrentUser changeOwner = identifiedUserFactory.create(notes.getChange().getOwner());
@@ -164,7 +161,7 @@
       rw.parseBody(baseCommit);
       newCommitMessage =
           newMergeUtil()
-              .createCommitMessageOnSubmit(original, baseCommit, notes, originalPatchSet.getId());
+              .createCommitMessageOnSubmit(original, baseCommit, notes, originalPatchSet.id());
     } else {
       newCommitMessage = original.getFullMessage();
     }
@@ -178,9 +175,7 @@
 
     rebasedPatchSetId =
         ChangeUtil.nextPatchSetIdFromChangeRefs(
-            ctx.getRepoView()
-                .getRefs(originalPatchSet.getId().getParentKey().toRefPrefix())
-                .keySet(),
+            ctx.getRepoView().getRefs(originalPatchSet.id().changeId().toRefPrefix()).keySet(),
             notes.getChange().currentPatchSetId());
     patchSetInserter =
         patchSetInserterFactory
@@ -195,14 +190,14 @@
           "Patch Set "
               + rebasedPatchSetId.get()
               + ": Patch Set "
-              + originalPatchSet.getId().get()
+              + originalPatchSet.id().get()
               + " was rebased");
     }
 
     if (base != null && !base.notes().getChange().isMerged()) {
       if (!base.notes().getChange().isMerged()) {
         // Add to end of relation chain for open base change.
-        patchSetInserter.setGroups(base.patchSet().getGroups());
+        patchSetInserter.setGroups(base.patchSet().groups());
       } else {
         // If the base is merged, start a new relation chain.
         patchSetInserter.setGroups(GroupCollector.getDefaultGroups(rebasedCommit));
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index a4cf5ba..2d36df2 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -17,14 +17,14 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -56,7 +56,7 @@
     this.psUtil = psUtil;
   }
 
-  public boolean canRebase(PatchSet patchSet, Branch.NameKey dest, Repository git, RevWalk rw) {
+  public boolean canRebase(PatchSet patchSet, BranchNameKey dest, Repository git, RevWalk rw) {
     try {
       findBaseRevision(patchSet, dest, git, rw);
       return true;
@@ -64,7 +64,7 @@
       return false;
     } catch (StorageException | IOException e) {
       logger.atWarning().withCause(e).log(
-          "Error checking if patch set %s on %s can be rebased", patchSet.getId(), dest);
+          "Error checking if patch set %s on %s can be rebased", patchSet.id(), dest);
       return false;
     }
   }
@@ -87,18 +87,18 @@
     // Try parsing the base as a ref string.
     PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base);
     if (basePatchSetId != null) {
-      Change.Id baseChangeId = basePatchSetId.getParentKey();
+      Change.Id baseChangeId = basePatchSetId.changeId();
       ChangeNotes baseNotes = notesFor(rsrc, baseChangeId);
       if (baseNotes != null) {
         return Base.create(
-            notesFor(rsrc, basePatchSetId.getParentKey()), psUtil.get(baseNotes, basePatchSetId));
+            notesFor(rsrc, basePatchSetId.changeId()), psUtil.get(baseNotes, basePatchSetId));
       }
     }
 
     // Try parsing base as a change number (assume current patch set).
     Integer baseChangeId = Ints.tryParse(base);
     if (baseChangeId != null) {
-      ChangeNotes baseNotes = notesFor(rsrc, new Change.Id(baseChangeId));
+      ChangeNotes baseNotes = notesFor(rsrc, Change.id(baseChangeId));
       if (baseNotes != null) {
         return Base.create(baseNotes, psUtil.current(baseNotes));
       }
@@ -108,10 +108,10 @@
     Base ret = null;
     for (ChangeData cd : queryProvider.get().byProjectCommit(rsrc.getProject(), base)) {
       for (PatchSet ps : cd.patchSets()) {
-        if (!ps.getRevision().matches(base)) {
+        if (!ObjectIds.matchesAbbreviation(ps.commitId(), base)) {
           continue;
         }
-        if (ret == null || ret.patchSet().getId().get() < ps.getId().get()) {
+        if (ret == null || ret.patchSet().id().get() < ps.id().get()) {
           ret = Base.create(cd.notes(), ps);
         }
       }
@@ -141,10 +141,10 @@
    * @throws IOException if accessing the repository fails.
    */
   public ObjectId findBaseRevision(
-      PatchSet patchSet, Branch.NameKey destBranch, Repository git, RevWalk rw)
+      PatchSet patchSet, BranchNameKey destBranch, Repository git, RevWalk rw)
       throws RestApiException, IOException {
-    String baseRev = null;
-    RevCommit commit = rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
+    ObjectId baseId = null;
+    RevCommit commit = rw.parseCommit(patchSet.commitId());
 
     if (commit.getParentCount() > 1) {
       throw new UnprocessableEntityException("Cannot rebase a change with multiple parents.");
@@ -153,12 +153,12 @@
           "Cannot rebase a change without any parents (is this the initial commit?).");
     }
 
-    RevId parentRev = new RevId(commit.getParent(0).name());
+    ObjectId parentId = commit.getParent(0);
 
     CHANGES:
-    for (ChangeData cd : queryProvider.get().byBranchCommit(destBranch, parentRev.get())) {
+    for (ChangeData cd : queryProvider.get().byBranchCommit(destBranch, parentId.name())) {
       for (PatchSet depPatchSet : cd.patchSets()) {
-        if (!depPatchSet.getRevision().equals(parentRev)) {
+        if (!depPatchSet.commitId().equals(parentId)) {
           continue;
         }
         Change depChange = cd.change();
@@ -168,29 +168,29 @@
         }
 
         if (depChange.isNew()) {
-          if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
+          if (depPatchSet.id().equals(depChange.currentPatchSetId())) {
             throw new ResourceConflictException(
                 "Change is already based on the latest patch set of the dependent change.");
           }
-          baseRev = cd.currentPatchSet().getRevision().get();
+          baseId = cd.currentPatchSet().commitId();
         }
         break CHANGES;
       }
     }
 
-    if (baseRev == null) {
+    if (baseId == null) {
       // We are dependent on a merged PatchSet or have no PatchSet
       // dependencies at all.
-      Ref destRef = git.getRefDatabase().exactRef(destBranch.get());
+      Ref destRef = git.getRefDatabase().exactRef(destBranch.branch());
       if (destRef == null) {
         throw new UnprocessableEntityException(
-            "The destination branch does not exist: " + destBranch.get());
+            "The destination branch does not exist: " + destBranch.branch());
       }
-      baseRev = destRef.getObjectId().getName();
-      if (baseRev.equals(parentRev.get())) {
+      baseId = destRef.getObjectId();
+      if (baseId.equals(parentId)) {
         throw new ResourceConflictException("Change is already up to date.");
       }
     }
-    return ObjectId.fromString(baseRev);
+    return baseId;
   }
 }
diff --git a/java/com/google/gerrit/server/change/ReviewerAdder.java b/java/com/google/gerrit/server/change/ReviewerAdder.java
index a6ad559..ba6ba21 100644
--- a/java/com/google/gerrit/server/change/ReviewerAdder.java
+++ b/java/com/google/gerrit/server/change/ReviewerAdder.java
@@ -31,6 +31,13 @@
 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.BooleanProjectConfig;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -41,13 +48,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -237,7 +237,8 @@
         revision.getUser(),
         ImmutableSet.of(user.getAccountId()),
         null,
-        true);
+        true,
+        false);
   }
 
   @Nullable
@@ -260,7 +261,13 @@
 
     if (isValidReviewer(notes.getChange().getDest(), reviewerUser.getAccount())) {
       return new ReviewerAddition(
-          input, notes, user, ImmutableSet.of(reviewerUser.getAccountId()), null, exactMatchFound);
+          input,
+          notes,
+          user,
+          ImmutableSet.of(reviewerUser.getAccountId()),
+          null,
+          exactMatchFound,
+          false);
     }
     return fail(
         input,
@@ -340,11 +347,11 @@
 
     for (Account member : members) {
       if (isValidReviewer(notes.getChange().getDest(), member)) {
-        reviewers.add(member.getId());
+        reviewers.add(member.id());
       }
     }
 
-    return new ReviewerAddition(input, notes, user, reviewers, null, true);
+    return new ReviewerAddition(input, notes, user, reviewers, null, true, true);
   }
 
   @Nullable
@@ -366,16 +373,16 @@
           FailureType.NOT_FOUND,
           MessageFormat.format(ChangeMessages.get().reviewerInvalid, input.reviewer));
     }
-    return new ReviewerAddition(input, notes, user, null, ImmutableList.of(adr), true);
+    return new ReviewerAddition(input, notes, user, null, ImmutableList.of(adr), true, false);
   }
 
-  private boolean isValidReviewer(Branch.NameKey branch, Account member)
+  private boolean isValidReviewer(BranchNameKey branch, Account member)
       throws PermissionBackendException {
     try {
       // Check ref permission instead of change permission, since change permissions take into
       // account the private bit, whereas adding a user as a reviewer is explicitly allowing them to
       // see private changes.
-      permissionBackend.absentUser(member.getId()).ref(branch).check(RefPermission.READ);
+      permissionBackend.absentUser(member.id()).ref(branch).check(RefPermission.READ);
       return true;
     } catch (AuthException e) {
       return false;
@@ -421,7 +428,8 @@
         CurrentUser caller,
         @Nullable Iterable<Account.Id> reviewers,
         @Nullable Iterable<Address> reviewersByEmail,
-        boolean exactMatchFound) {
+        boolean exactMatchFound,
+        boolean forGroup) {
       checkArgument(
           reviewers != null || reviewersByEmail != null,
           "must have either reviewers or reviewersByEmail");
@@ -435,7 +443,7 @@
       this.reviewersByEmail =
           reviewersByEmail == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewersByEmail);
       this.caller = caller.asIdentifiedUser();
-      op = addReviewersOpFactory.create(this.reviewers, this.reviewersByEmail, state());
+      op = addReviewersOpFactory.create(this.reviewers, this.reviewersByEmail, state(), forGroup);
       this.exactMatchFound = exactMatchFound;
     }
 
@@ -469,8 +477,8 @@
           // New reviewers have value 0, don't bother normalizing.
           result.reviewers.add(
               json.format(
-                  new ReviewerInfo(psa.getAccountId().get()),
-                  psa.getAccountId(),
+                  new ReviewerInfo(psa.accountId().get()),
+                  psa.accountId(),
                   cd,
                   ImmutableList.of(psa)));
         }
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index 2742bb9..6686ed8 100644
--- a/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -21,12 +21,12 @@
 import com.google.gerrit.common.data.LabelType;
 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.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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.permissions.LabelPermission;
@@ -111,9 +111,9 @@
 
     out.approvals = new TreeMap<>(labelTypes.nameComparator());
     for (PatchSetApproval ca : approvals) {
-      LabelType at = labelTypes.byLabel(ca.getLabelId());
+      LabelType at = labelTypes.byLabel(ca.labelId());
       if (at != null) {
-        out.approvals.put(at.getName(), formatValue(ca.getValue()));
+        out.approvals.put(at.getName(), formatValue(ca.value()));
       }
     }
 
diff --git a/java/com/google/gerrit/server/change/ReviewerResource.java b/java/com/google/gerrit/server/change/ReviewerResource.java
index 52f3585..df0a03f 100644
--- a/java/com/google/gerrit/server/change/ReviewerResource.java
+++ b/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -17,11 +17,11 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
 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/ReviewerSuggestion.java b/java/com/google/gerrit/server/change/ReviewerSuggestion.java
index 198a5fd..1b2a008 100644
--- a/java/com/google/gerrit/server/change/ReviewerSuggestion.java
+++ b/java/com/google/gerrit/server/change/ReviewerSuggestion.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import java.util.Set;
 
 /**
@@ -35,9 +35,8 @@
    * @param changeId The changeId that the suggestion is for. Can be {@code null}.
    * @param query The query as typed by the user. Can be {@code null}.
    * @param candidates A set of candidates for the ranking. Can be empty.
-   * @return Set of {@link SuggestedReviewer}s. The {@link
-   *     com.google.gerrit.reviewdb.client.Account.Id}s listed here don't have to be included in
-   *     {@code candidates}.
+   * @return Set of {@link SuggestedReviewer}s. The {@link com.google.gerrit.entities.Account.Id}s
+   *     listed here don't have to be included in {@code candidates}.
    */
   Set<SuggestedReviewer> suggestReviewers(
       Project.NameKey project,
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index aa733cd..fbd14c4 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -28,10 +28,15 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
 import static com.google.gerrit.server.CommonConverters.toGitPerson;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -44,10 +49,6 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GpgException;
@@ -71,7 +72,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
@@ -182,7 +182,7 @@
     info.message = commit.getFullMessage();
 
     if (addLinks) {
-      List<WebLinkInfo> links = webLinks.getPatchSetLinks(project, commit.name());
+      ImmutableList<WebLinkInfo> links = webLinks.getPatchSetLinks(project, commit.name());
       info.webLinks = links.isEmpty() ? null : links;
     }
 
@@ -192,7 +192,7 @@
       i.commit = parent.name();
       i.subject = parent.getShortMessage();
       if (addLinks) {
-        List<WebLinkInfo> parentLinks = webLinks.getParentLinks(project, parent.name());
+        ImmutableList<WebLinkInfo> parentLinks = webLinks.getParentLinks(project, parent.name());
         i.webLinks = parentLinks.isEmpty() ? null : parentLinks;
       }
       info.parents.add(i);
@@ -216,7 +216,7 @@
     try (Repository repo = openRepoIfNecessary(cd.project());
         RevWalk rw = newRevWalk(repo)) {
       for (PatchSet in : map.values()) {
-        PatchSet.Id id = in.getId();
+        PatchSet.Id id = in.id();
         boolean want;
         if (has(ALL_REVISIONS)) {
           want = true;
@@ -227,7 +227,7 @@
         }
         if (want) {
           res.put(
-              in.getRevision().get(),
+              in.commitId().name(),
               toRevisionInfo(accountLoader, cd, in, repo, rw, false, changeInfo));
         }
       }
@@ -251,7 +251,7 @@
 
       String projectName = cd.project().get();
       String url = scheme.getUrl(projectName);
-      String refName = in.getRefName();
+      String refName = in.refName();
       FetchInfo fetchInfo = new FetchInfo(url, refName);
       r.put(schemeName, fetchInfo);
 
@@ -275,14 +275,14 @@
       throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     Change c = cd.change();
     RevisionInfo out = new RevisionInfo();
-    out.isCurrent = in.getId().equals(c.currentPatchSetId());
-    out._number = in.getId().get();
-    out.ref = in.getRefName();
-    out.created = in.getCreatedOn();
-    out.uploader = accountLoader.get(in.getUploader());
+    out.isCurrent = in.id().equals(c.currentPatchSetId());
+    out._number = in.id().get();
+    out.ref = in.refName();
+    out.created = in.createdOn();
+    out.uploader = accountLoader.get(in.uploader());
     out.fetch = makeFetchMap(cd, in);
     out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
-    out.description = in.getDescription();
+    out.description = in.description().orElse(null);
 
     boolean setCommit = has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT));
     boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS);
@@ -290,14 +290,14 @@
       checkState(rw != null);
       checkState(repo != null);
       Project.NameKey project = c.getProject();
-      String rev = in.getRevision().get();
+      String rev = in.commitId().name();
       RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
       rw.parseBody(commit);
       if (setCommit) {
         out.commit = getCommitInfo(project, rw, commit, has(WEB_LINKS), fillCommit);
       }
       if (addFooters) {
-        Ref ref = repo.exactRef(cd.change().getDest().get());
+        Ref ref = repo.exactRef(cd.change().getDest().branch());
         RevCommit mergeTip = null;
         if (ref != null) {
           mergeTip = rw.parseCommit(ref.getObjectId());
@@ -306,7 +306,7 @@
         out.commitWithFooters =
             mergeUtilFactory
                 .create(projectCache.get(project))
-                .createCommitMessageOnSubmit(commit, mergeTip, cd.notes(), in.getId());
+                .createCommitMessageOnSubmit(commit, mergeTip, cd.notes(), in.id());
       }
     }
 
@@ -324,10 +324,10 @@
     }
 
     if (gpgApi.isEnabled() && has(PUSH_CERTIFICATES)) {
-      if (in.getPushCertificate() != null) {
+      if (in.pushCertificate().isPresent()) {
         out.pushCertificate =
             gpgApi.checkPushCertificate(
-                in.getPushCertificate(), userFactory.create(in.getUploader()));
+                in.pushCertificate().get(), userFactory.create(in.uploader()));
       } else {
         out.pushCertificate = new PushCertificateInfo();
       }
diff --git a/java/com/google/gerrit/server/change/RevisionResource.java b/java/com/google/gerrit/server/change/RevisionResource.java
index caafe24..30fa593 100644
--- a/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/java/com/google/gerrit/server/change/RevisionResource.java
@@ -16,15 +16,18 @@
 
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestResource.HasETag;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.inject.TypeLiteral;
@@ -89,9 +92,18 @@
 
   @Override
   public String getETag() {
-    Hasher h = Hashing.murmur3_128().newHasher();
-    prepareETag(h, getUser());
-    return h.hash().toString();
+    try (TraceTimer ignored =
+        TraceContext.newTimer(
+            "Compute revision ETag",
+            Metadata.builder()
+                .changeId(change.getId().get())
+                .patchSetId(ps.number())
+                .projectName(change.getProject().get())
+                .build())) {
+      Hasher h = Hashing.murmur3_128().newHasher();
+      prepareETag(h, getUser());
+      return h.hash().toString();
+    }
   }
 
   public void prepareETag(Hasher h, CurrentUser user) {
@@ -114,7 +126,7 @@
 
   @Override
   public String toString() {
-    String s = ps.getId().toString();
+    String s = ps.id().toString();
     if (edit.isPresent()) {
       s = "edit:" + s;
     }
@@ -122,6 +134,6 @@
   }
 
   public boolean isCurrent() {
-    return ps.getId().equals(getChange().currentPatchSetId());
+    return ps.id().equals(getChange().currentPatchSetId());
   }
 }
diff --git a/java/com/google/gerrit/server/change/RobotCommentResource.java b/java/com/google/gerrit/server/change/RobotCommentResource.java
index c4fab58..b12727d 100644
--- a/java/com/google/gerrit/server/change/RobotCommentResource.java
+++ b/java/com/google/gerrit/server/change/RobotCommentResource.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.inject.TypeLiteral;
 
 public class RobotCommentResource implements RestResource {
diff --git a/java/com/google/gerrit/server/change/SetAssigneeOp.java b/java/com/google/gerrit/server/change/SetAssigneeOp.java
index 8d350c3..9848150 100644
--- a/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ b/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -17,10 +17,10 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.AssigneeChanged;
diff --git a/java/com/google/gerrit/server/change/SetHashtagsOp.java b/java/com/google/gerrit/server/change/SetHashtagsOp.java
index abc4eee..712e1f3 100644
--- a/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -21,12 +21,12 @@
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.change.HashtagsUtil.InvalidHashtagException;
 import com.google.gerrit.server.extensions.events.HashtagsEdited;
diff --git a/java/com/google/gerrit/server/change/SetPrivateOp.java b/java/com/google/gerrit/server/change/SetPrivateOp.java
index 1600fd5..28d178d 100644
--- a/java/com/google/gerrit/server/change/SetPrivateOp.java
+++ b/java/com/google/gerrit/server/change/SetPrivateOp.java
@@ -16,11 +16,11 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchSetUtil;
diff --git a/java/com/google/gerrit/server/change/SuggestedReviewer.java b/java/com/google/gerrit/server/change/SuggestedReviewer.java
index 353bf3b..1b30199 100644
--- a/java/com/google/gerrit/server/change/SuggestedReviewer.java
+++ b/java/com/google/gerrit/server/change/SuggestedReviewer.java
@@ -13,7 +13,7 @@
 // limitations under the License.
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 
 public class SuggestedReviewer {
 
diff --git a/java/com/google/gerrit/server/change/WalkSorter.java b/java/com/google/gerrit/server/change/WalkSorter.java
index 5945a0c..816a904 100644
--- a/java/com/google/gerrit/server/change/WalkSorter.java
+++ b/java/com/google/gerrit/server/change/WalkSorter.java
@@ -24,9 +24,9 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Ordering;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
@@ -41,7 +41,6 @@
 import java.util.Set;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
@@ -223,27 +222,26 @@
     for (ChangeData cd : in) {
       PatchSet maxPs = null;
       for (PatchSet ps : cd.patchSets()) {
-        if (shouldInclude(ps) && (maxPs == null || ps.getId().get() > maxPs.getId().get())) {
+        if (shouldInclude(ps) && (maxPs == null || ps.id().get() > maxPs.id().get())) {
           maxPs = ps;
         }
       }
       if (maxPs == null) {
         continue; // No patch sets matched.
       }
-      ObjectId id = ObjectId.fromString(maxPs.getRevision().get());
       try {
-        RevCommit c = rw.parseCommit(id);
+        RevCommit c = rw.parseCommit(maxPs.commitId());
         byCommit.put(c, PatchSetData.create(cd, maxPs, c));
       } catch (MissingObjectException | IncorrectObjectTypeException e) {
         logger.atWarning().withCause(e).log(
-            "missing commit %s for patch set %s", id.name(), maxPs.getId());
+            "missing commit %s for patch set %s", maxPs.commitId().name(), maxPs.id());
       }
     }
     return byCommit;
   }
 
   private boolean shouldInclude(PatchSet ps) {
-    return includePatchSets.isEmpty() || includePatchSets.contains(ps.getId());
+    return includePatchSets.isEmpty() || includePatchSets.contains(ps.id());
   }
 
   private static void markStart(RevWalk rw, Iterable<RevCommit> commits) throws IOException {
diff --git a/java/com/google/gerrit/server/change/WorkInProgressOp.java b/java/com/google/gerrit/server/change/WorkInProgressOp.java
index f3f1a29..78edadab 100644
--- a/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -17,10 +17,10 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
diff --git a/java/com/google/gerrit/server/change/testing/TestChangeETagComputation.java b/java/com/google/gerrit/server/change/testing/TestChangeETagComputation.java
new file mode 100644
index 0000000..344b9b3
--- /dev/null
+++ b/java/com/google/gerrit/server/change/testing/TestChangeETagComputation.java
@@ -0,0 +1,30 @@
+// 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.change.testing;
+
+import com.google.gerrit.server.change.ChangeETagComputation;
+
+public class TestChangeETagComputation {
+
+  public static ChangeETagComputation withETag(String etag) {
+    return (p, id) -> etag;
+  }
+
+  public static ChangeETagComputation withException(RuntimeException e) {
+    return (p, id) -> {
+      throw e;
+    };
+  }
+}
diff --git a/java/com/google/gerrit/server/config/AllProjectsName.java b/java/com/google/gerrit/server/config/AllProjectsName.java
index 7719e38..6d5525c 100644
--- a/java/com/google/gerrit/server/config/AllProjectsName.java
+++ b/java/com/google/gerrit/server/config/AllProjectsName.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 
 /** Special name of the project that all projects derive from. */
 public class AllProjectsName extends Project.NameKey {
diff --git a/java/com/google/gerrit/server/config/AllUsersName.java b/java/com/google/gerrit/server/config/AllUsersName.java
index 22d29a4..aa92db8 100644
--- a/java/com/google/gerrit/server/config/AllUsersName.java
+++ b/java/com/google/gerrit/server/config/AllUsersName.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 
 /** Special name of the project in which meta data for all users is stored. */
 public class AllUsersName extends Project.NameKey {
diff --git a/java/com/google/gerrit/server/config/DownloadConfig.java b/java/com/google/gerrit/server/config/DownloadConfig.java
index e9d5e5e..6dea07d 100644
--- a/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.CoreDownloadSchemes;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
-import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
 import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 5dad6a8..25f2b20 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.config.ExternalIncludedIn;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
+import com.google.gerrit.extensions.events.AccountActivationListener;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.AgreementSignupListener;
 import com.google.gerrit.extensions.events.AssigneeChangedListener;
@@ -61,6 +62,7 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.extensions.webui.BranchWebLink;
 import com.google.gerrit.extensions.webui.DiffWebLink;
 import com.google.gerrit.extensions.webui.FileHistoryWebLink;
@@ -76,7 +78,10 @@
 import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
 import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.RequestListener;
+import com.google.gerrit.server.TraceRequestListener;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountDeactivator;
@@ -98,6 +103,7 @@
 import com.google.gerrit.server.change.AbandonOp;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
 import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.gerrit.server.change.ChangeETagComputation;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
@@ -135,6 +141,7 @@
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.group.db.GroupDbModule;
 import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
+import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.mail.AutoReplyMailFilter;
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.ListMailFilter;
@@ -142,7 +149,8 @@
 import com.google.gerrit.server.mail.send.FromAddressGenerator;
 import com.google.gerrit.server.mail.send.FromAddressGeneratorProvider;
 import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
-import com.google.gerrit.server.mail.send.MailSoyTofuProvider;
+import com.google.gerrit.server.mail.send.MailSoySauceProvider;
+import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
 import com.google.gerrit.server.mail.send.MailTemplates;
 import com.google.gerrit.server.mime.FileTypeRegistry;
 import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
@@ -190,7 +198,7 @@
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
 import com.google.inject.internal.UniqueAnnotations;
-import com.google.template.soy.tofu.SoyTofu;
+import com.google.template.soy.jbcsrc.api.SoySauce;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.transport.PostReceiveHook;
@@ -281,7 +289,7 @@
 
     bind(ApprovalsUtil.class);
 
-    bind(SoyTofu.class).annotatedWith(MailTemplates.class).toProvider(MailSoyTofuProvider.class);
+    bind(SoySauce.class).annotatedWith(MailTemplates.class).toProvider(MailSoySauceProvider.class);
     bind(FromAddressGenerator.class).toProvider(FromAddressGeneratorProvider.class).in(SINGLETON);
     bind(Boolean.class)
         .annotatedWith(EnableReverseDnsLookup.class)
@@ -325,6 +333,7 @@
     DynamicSet.setOf(binder(), PostReceiveHook.class);
     DynamicSet.setOf(binder(), PreUploadHook.class);
     DynamicSet.setOf(binder(), PostUploadHook.class);
+    DynamicSet.setOf(binder(), AccountActivationListener.class);
     DynamicSet.setOf(binder(), AccountIndexedListener.class);
     DynamicSet.setOf(binder(), ChangeIndexedListener.class);
     DynamicSet.setOf(binder(), GroupIndexedListener.class);
@@ -341,6 +350,7 @@
     DynamicSet.bind(binder(), EventListener.class).to(EventsMetrics.class);
     DynamicSet.setOf(binder(), UserScopedEventListener.class);
     DynamicSet.setOf(binder(), CommitValidationListener.class);
+    DynamicSet.setOf(binder(), CommentValidator.class);
     DynamicSet.setOf(binder(), ChangeMessageModifier.class);
     DynamicSet.setOf(binder(), RefOperationValidationListener.class);
     DynamicSet.setOf(binder(), OnSubmitValidationListener.class);
@@ -380,6 +390,12 @@
     DynamicItem.itemOf(binder(), ProjectNameLockManager.class);
     DynamicSet.setOf(binder(), SubmitRule.class);
     DynamicSet.setOf(binder(), QuotaEnforcer.class);
+    DynamicSet.setOf(binder(), PerformanceLogger.class);
+    DynamicSet.setOf(binder(), RequestListener.class);
+    DynamicSet.bind(binder(), RequestListener.class).to(TraceRequestListener.class);
+    DynamicSet.setOf(binder(), ChangeETagComputation.class);
+    DynamicSet.setOf(binder(), ExceptionHook.class);
+    DynamicSet.setOf(binder(), MailSoyTemplateProvider.class);
 
     DynamicMap.mapOf(binder(), MailFilter.class);
     bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
diff --git a/java/com/google/gerrit/server/config/GerritIsReplica.java b/java/com/google/gerrit/server/config/GerritIsReplica.java
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 2255a67..7f487e1 100644
--- a/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -17,7 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 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/PluginConfigFactory.java b/java/com/google/gerrit/server/config/PluginConfigFactory.java
index 2164a86..9e45701 100644
--- a/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.plugins.Plugin;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.project.NoSuchProjectException;
diff --git a/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index 92ae10a..fcfa5e9 100644
--- a/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -17,14 +17,14 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.extensions.api.projects.ConfigValue;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.Extension;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
@@ -316,7 +316,7 @@
 
     @Override
     public void onGitReferenceUpdated(Event event) {
-      Project.NameKey p = new Project.NameKey(event.getProjectName());
+      Project.NameKey p = Project.nameKey(event.getProjectName());
       if (!event.getRefName().equals(RefNames.REFS_CONFIG)) {
         return;
       }
diff --git a/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index a2e0356..c2538ac 100644
--- a/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.util.ServerRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
diff --git a/java/com/google/gerrit/server/config/RepositoryConfig.java b/java/com/google/gerrit/server/config/RepositoryConfig.java
index d8c8468..f722321 100644
--- a/java/com/google/gerrit/server/config/RepositoryConfig.java
+++ b/java/com/google/gerrit/server/config/RepositoryConfig.java
@@ -19,8 +19,8 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.nio.file.Path;
diff --git a/java/com/google/gerrit/server/config/SitePaths.java b/java/com/google/gerrit/server/config/SitePaths.java
index 47b6336..ee95c6f 100644
--- a/java/com/google/gerrit/server/config/SitePaths.java
+++ b/java/com/google/gerrit/server/config/SitePaths.java
@@ -54,6 +54,8 @@
   public final Path secure_config;
   public final Path notedb_config;
 
+  public final Path jgit_config;
+
   public final Path ssl_keystore;
   public final Path ssh_key;
   public final Path ssh_rsa;
@@ -99,6 +101,8 @@
     secure_config = etc_dir.resolve("secure.config");
     notedb_config = etc_dir.resolve("notedb.config");
 
+    jgit_config = etc_dir.resolve("jgit.config");
+
     ssl_keystore = etc_dir.resolve("keystore");
     ssh_key = etc_dir.resolve("ssh_host_key");
     ssh_rsa = etc_dir.resolve("ssh_host_rsa_key");
diff --git a/java/com/google/gerrit/server/config/UrlFormatter.java b/java/com/google/gerrit/server/config/UrlFormatter.java
index eb1e0b2..6b2510e 100644
--- a/java/com/google/gerrit/server/config/UrlFormatter.java
+++ b/java/com/google/gerrit/server/config/UrlFormatter.java
@@ -16,8 +16,8 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import java.util.Optional;
 
 /**
diff --git a/java/com/google/gerrit/server/data/ChangeAttribute.java b/java/com/google/gerrit/server/data/ChangeAttribute.java
index fde5922..a6da2b9 100644
--- a/java/com/google/gerrit/server/data/ChangeAttribute.java
+++ b/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.data;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gson.annotations.SerializedName;
 import java.util.List;
 
diff --git a/java/com/google/gerrit/server/data/PatchAttribute.java b/java/com/google/gerrit/server/data/PatchAttribute.java
index 22f18af..2428c54 100644
--- a/java/com/google/gerrit/server/data/PatchAttribute.java
+++ b/java/com/google/gerrit/server/data/PatchAttribute.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.data;
 
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.entities.Patch.ChangeType;
 
 public class PatchAttribute {
   public String file;
diff --git a/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java b/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
index 5d3d1f2..59ae6f8 100644
--- a/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
+++ b/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
@@ -80,8 +80,7 @@
     }
     Query query = parser.parse(q);
     try {
-      // TODO(fishywang): Currently as we don't have much documentation, we just use MAX_VALUE here
-      // and skipped paging. Maybe add paging later.
+      // We don't have much documentation, so we just use MAX_VALUE here and skip paging.
       TopDocs results = searcher.search(query, Integer.MAX_VALUE);
       ScoreDoc[] hits = results.scoreDocs;
       long totalHits = results.totalHits;
diff --git a/java/com/google/gerrit/server/edit/ChangeEdit.java b/java/com/google/gerrit/server/edit/ChangeEdit.java
index 11dc380..c652289 100644
--- a/java/com/google/gerrit/server/edit/ChangeEdit.java
+++ b/java/com/google/gerrit/server/edit/ChangeEdit.java
@@ -16,8 +16,8 @@
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 /**
diff --git a/java/com/google/gerrit/server/edit/ChangeEditJson.java b/java/com/google/gerrit/server/edit/ChangeEditJson.java
index 55e0aef..25dcae0 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditJson.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditJson.java
@@ -51,8 +51,8 @@
   public EditInfo toEditInfo(ChangeEdit edit, boolean downloadCommands) {
     EditInfo out = new EditInfo();
     out.commit = fillCommit(edit.getEditCommit());
-    out.baseRevision = edit.getBasePatchSet().getRevision().get();
-    out.basePatchSetNumber = edit.getBasePatchSet().getPatchSetId();
+    out.baseRevision = edit.getBasePatchSet().commitId().name();
+    out.basePatchSetNumber = edit.getBasePatchSet().number();
     out.ref = edit.getRefName();
     if (downloadCommands) {
       out.fetch = fillFetchMap(edit);
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 9620b49..bd0b96f 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -15,16 +15,16 @@
 package com.google.gerrit.server.edit;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -127,7 +127,7 @@
     }
 
     PatchSet currentPatchSet = lookupCurrentPatchSet(notes);
-    ObjectId patchSetCommitId = getPatchSetCommitId(currentPatchSet);
+    ObjectId patchSetCommitId = currentPatchSet.commitId();
     createEdit(repository, notes, currentPatchSet, patchSetCommitId, TimeUtil.nowTs());
   }
 
@@ -160,7 +160,7 @@
       throw new InvalidChangeOperationException(
           String.format(
               "Change edit for change %s is already based on latest patch set %s",
-              notes.getChangeId(), currentPatchSet.getId()));
+              notes.getChangeId(), currentPatchSet.id()));
     }
 
     rebase(repository, changeEdit, currentPatchSet);
@@ -383,7 +383,7 @@
     if (optionalChangeEdit.isPresent()) {
       ChangeEdit changeEdit = optionalChangeEdit.get();
       newTreeId = merge(repository, changeEdit, newTreeId);
-      if (ObjectId.equals(newTreeId, changeEdit.getEditCommit().getTree())) {
+      if (ObjectId.isEqual(newTreeId, changeEdit.getEditCommit().getTree())) {
         // Modifications are already contained in the change edit.
         return changeEdit;
       }
@@ -433,10 +433,10 @@
             String.format(
                 "Only the patch set %s on which the existing change edit is based may be modified "
                     + "(specified patch set: %s)",
-                changeEdit.getBasePatchSet().getId(), patchSet.getId()));
+                changeEdit.getBasePatchSet().id(), patchSet.id()));
       }
     } else {
-      PatchSet.Id patchSetId = patchSet.getId();
+      PatchSet.Id patchSetId = patchSet.id();
       PatchSet.Id currentPatchSetId = notes.getChange().currentPatchSetId();
       if (!patchSetId.equals(currentPatchSetId)) {
         throw new InvalidChangeOperationException(
@@ -463,12 +463,12 @@
 
   private static boolean isBasedOn(ChangeEdit changeEdit, PatchSet patchSet) {
     PatchSet editBasePatchSet = changeEdit.getBasePatchSet();
-    return editBasePatchSet.getId().equals(patchSet.getId());
+    return editBasePatchSet.id().equals(patchSet.id());
   }
 
   private static RevCommit lookupCommit(Repository repository, PatchSet patchSet)
       throws IOException {
-    ObjectId patchSetCommitId = getPatchSetCommitId(patchSet);
+    ObjectId patchSetCommitId = patchSet.commitId();
     return lookupCommit(repository, patchSetCommitId);
   }
 
@@ -491,7 +491,7 @@
       throw new BadRequestException(e.getMessage());
     }
 
-    if (ObjectId.equals(newTreeId, baseCommit.getTree())) {
+    if (ObjectId.isEqual(newTreeId, baseCommit.getTree())) {
       throw new InvalidChangeOperationException("no changes were made");
     }
     return newTreeId;
@@ -500,7 +500,7 @@
   private static ObjectId merge(Repository repository, ChangeEdit changeEdit, ObjectId newTreeId)
       throws IOException, MergeConflictException {
     PatchSet basePatchSet = changeEdit.getBasePatchSet();
-    ObjectId basePatchSetCommitId = getPatchSetCommitId(basePatchSet);
+    ObjectId basePatchSetCommitId = basePatchSet.commitId();
     ObjectId editCommitId = changeEdit.getEditCommit();
 
     ThreeWayMerger threeWayMerger = MergeStrategy.RESOLVE.newMerger(repository, true);
@@ -539,10 +539,6 @@
     return user.newCommitterIdent(commitTimestamp, tz);
   }
 
-  private static ObjectId getPatchSetCommitId(PatchSet patchSet) {
-    return ObjectId.fromString(patchSet.getRevision().get());
-  }
-
   private ChangeEdit createEdit(
       Repository repository,
       ChangeNotes notes,
@@ -561,7 +557,7 @@
 
   private String getEditRefName(Change change, PatchSet basePatchSet) {
     IdentifiedUser me = currentUser.get().asIdentifiedUser();
-    return RefNames.refsEdit(me.getAccountId(), change.getId(), basePatchSet.getId());
+    return RefNames.refsEdit(me.getAccountId(), change.getId(), basePatchSet.id());
   }
 
   private ChangeEdit updateEdit(
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index ef1d880..ea34b76 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -16,14 +16,14 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -122,7 +122,7 @@
       String[] refNames = new String[n];
       for (int i = n; i > 0; i--) {
         refNames[i - 1] =
-            RefNames.refsEdit(u.getAccountId(), change.getId(), new PatchSet.Id(change.getId(), i));
+            RefNames.refsEdit(u.getAccountId(), change.getId(), PatchSet.id(change.getId(), i));
       }
       Ref ref = repo.getRefDatabase().firstExactRef(refNames);
       if (ref == null) {
@@ -162,7 +162,7 @@
         ObjectReader reader = oi.newReader();
         RevWalk rw = new RevWalk(reader)) {
       PatchSet basePatchSet = edit.getBasePatchSet();
-      if (!basePatchSet.getId().equals(change.currentPatchSetId())) {
+      if (!basePatchSet.id().equals(change.currentPatchSetId())) {
         throw new ResourceConflictException("only edit for current patch set can be published");
       }
 
@@ -177,17 +177,14 @@
           new StringBuilder("Patch Set ").append(inserter.getPatchSetId().get()).append(": ");
 
       // Previously checked that the base patch set is the current patch set.
-      ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get());
+      ObjectId prior = basePatchSet.commitId();
       ChangeKind kind =
           changeKindCache.getChangeKind(change.getProject(), rw, repo.getConfig(), prior, squashed);
       if (kind == ChangeKind.NO_CODE_CHANGE) {
         message.append("Commit message was updated.");
         inserter.setDescription("Edit commit message");
       } else {
-        message
-            .append("Published edit on patch set ")
-            .append(basePatchSet.getPatchSetId())
-            .append(".");
+        message.append("Published edit on patch set ").append(basePatchSet.number()).append(".");
       }
 
       try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.nowTs())) {
@@ -226,7 +223,7 @@
       int pos = ref.getName().lastIndexOf('/');
       checkArgument(pos > 0, "invalid edit ref: %s", ref.getName());
       String psId = ref.getName().substring(pos + 1);
-      return psUtil.get(notes, new PatchSet.Id(notes.getChange().getId(), Integer.parseInt(psId)));
+      return psUtil.get(notes, PatchSet.id(notes.getChange().getId(), Integer.parseInt(psId)));
     } catch (StorageException | NumberFormatException e) {
       throw new IOException(e);
     }
@@ -235,7 +232,7 @@
   private RevCommit squashEdit(
       RevWalk rw, ObjectInserter inserter, RevCommit edit, PatchSet basePatchSet)
       throws IOException, ResourceConflictException {
-    RevCommit parent = rw.parseCommit(ObjectId.fromString(basePatchSet.getRevision().get()));
+    RevCommit parent = rw.parseCommit(basePatchSet.commitId());
     if (parent.getTree().equals(edit.getTree())
         && edit.getFullMessage().equals(parent.getFullMessage())) {
       throw new ResourceConflictException("identical tree and message");
diff --git a/java/com/google/gerrit/server/events/AssigneeChangedEvent.java b/java/com/google/gerrit/server/events/AssigneeChangedEvent.java
index 60a0935..490d6d14 100644
--- a/java/com/google/gerrit/server/events/AssigneeChangedEvent.java
+++ b/java/com/google/gerrit/server/events/AssigneeChangedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class AssigneeChangedEvent extends ChangeEvent {
diff --git a/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java b/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
index 32b5b02..066f398 100644
--- a/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
+++ b/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class ChangeAbandonedEvent extends PatchSetEvent {
diff --git a/java/com/google/gerrit/server/events/ChangeDeletedEvent.java b/java/com/google/gerrit/server/events/ChangeDeletedEvent.java
index 63142fd..017a839b 100644
--- a/java/com/google/gerrit/server/events/ChangeDeletedEvent.java
+++ b/java/com/google/gerrit/server/events/ChangeDeletedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class ChangeDeletedEvent extends ChangeEvent {
diff --git a/java/com/google/gerrit/server/events/ChangeEvent.java b/java/com/google/gerrit/server/events/ChangeEvent.java
index 6029ded..d39099c 100644
--- a/java/com/google/gerrit/server/events/ChangeEvent.java
+++ b/java/com/google/gerrit/server/events/ChangeEvent.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.data.ChangeAttribute;
 
 public abstract class ChangeEvent extends RefEvent {
@@ -29,7 +29,7 @@
   protected ChangeEvent(String type, Change change) {
     super(type);
     this.project = change.getProject();
-    this.refName = RefNames.fullName(change.getDest().get());
+    this.refName = RefNames.fullName(change.getDest().branch());
     this.changeKey = change.getKey();
   }
 
diff --git a/java/com/google/gerrit/server/events/ChangeMergedEvent.java b/java/com/google/gerrit/server/events/ChangeMergedEvent.java
index 3fb2ac8..3b96328 100644
--- a/java/com/google/gerrit/server/events/ChangeMergedEvent.java
+++ b/java/com/google/gerrit/server/events/ChangeMergedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class ChangeMergedEvent extends PatchSetEvent {
diff --git a/java/com/google/gerrit/server/events/ChangeRestoredEvent.java b/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
index 7c86d70..48100a1 100644
--- a/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
+++ b/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class ChangeRestoredEvent extends PatchSetEvent {
diff --git a/java/com/google/gerrit/server/events/CommentAddedEvent.java b/java/com/google/gerrit/server/events/CommentAddedEvent.java
index bb1ac4d..dbbebe8 100644
--- a/java/com/google/gerrit/server/events/CommentAddedEvent.java
+++ b/java/com/google/gerrit/server/events/CommentAddedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ApprovalAttribute;
 
diff --git a/java/com/google/gerrit/server/events/CommitReceivedEvent.java b/java/com/google/gerrit/server/events/CommitReceivedEvent.java
index c0f9c29..6e43621 100644
--- a/java/com/google/gerrit/server/events/CommitReceivedEvent.java
+++ b/java/com/google/gerrit/server/events/CommitReceivedEvent.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.events;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import java.io.IOException;
 import org.eclipse.jgit.lib.ObjectId;
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index d5f548f..a6db081 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gson.Gson;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -47,6 +48,8 @@
     protected void configure() {
       DynamicItem.itemOf(binder(), EventDispatcher.class);
       DynamicItem.bind(binder(), EventDispatcher.class).to(EventBroker.class);
+
+      bind(Gson.class).annotatedWith(EventGson.class).toProvider(EventGsonProvider.class);
     }
   }
 
@@ -81,7 +84,7 @@
   }
 
   @Override
-  public void postEvent(Branch.NameKey branchName, RefEvent event)
+  public void postEvent(BranchNameKey branchName, RefEvent event)
       throws PermissionBackendException {
     fireEvent(branchName, event);
   }
@@ -120,7 +123,7 @@
     fireEventForUnrestrictedListeners(event);
   }
 
-  protected void fireEvent(Branch.NameKey branchName, RefEvent event)
+  protected void fireEvent(BranchNameKey branchName, RefEvent event)
       throws PermissionBackendException {
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
       CurrentUser user = c.call(UserScopedEventListener::getUser);
@@ -174,9 +177,9 @@
     }
   }
 
-  protected boolean isVisibleTo(Branch.NameKey branchName, CurrentUser user)
+  protected boolean isVisibleTo(BranchNameKey branchName, CurrentUser user)
       throws PermissionBackendException {
-    ProjectState pe = projectCache.get(branchName.getParentKey());
+    ProjectState pe = projectCache.get(branchName.project());
     if (pe == null || !pe.statePermitsRead()) {
       return false;
     }
@@ -194,13 +197,13 @@
       RefEvent refEvent = (RefEvent) event;
       String ref = refEvent.getRefName();
       if (PatchSet.isChangeRef(ref)) {
-        Change.Id cid = PatchSet.Id.fromRef(ref).getParentKey();
+        Change.Id cid = PatchSet.Id.fromRef(ref).changeId();
         try {
           Change change = notesFactory.createChecked(refEvent.getProjectNameKey(), cid).getChange();
           return isVisibleTo(change, user);
         } catch (NoSuchChangeException e) {
           logger.atFine().log(
-              "Change %s cannot be found, falling back on ref visibility check", cid.id);
+              "Change %s cannot be found, falling back on ref visibility check", cid.get());
         }
       }
       return isVisibleTo(refEvent.getBranchNameKey(), user);
diff --git a/java/com/google/gerrit/server/events/EventDispatcher.java b/java/com/google/gerrit/server/events/EventDispatcher.java
index e6735f2..a358aee 100644
--- a/java/com/google/gerrit/server/events/EventDispatcher.java
+++ b/java/com/google/gerrit/server/events/EventDispatcher.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.events;
 
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 
 /** Interface for posting (dispatching) Events */
@@ -37,7 +37,7 @@
    * @param event The event to post
    * @throws PermissionBackendException on failure of permission checks
    */
-  void postEvent(Branch.NameKey branchName, RefEvent event) throws PermissionBackendException;
+  void postEvent(BranchNameKey branchName, RefEvent event) throws PermissionBackendException;
 
   /**
    * Post a stream event that is related to a project.
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index efd9bb9..3f22d7f 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -24,18 +24,18 @@
 import com.google.gerrit.common.data.LabelTypes;
 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.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.UserIdentity;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
@@ -68,7 +68,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -127,7 +126,7 @@
   public ChangeAttribute asChangeAttribute(Change change) {
     ChangeAttribute a = new ChangeAttribute();
     a.project = change.getProject().get();
-    a.branch = change.getDest().getShortName();
+    a.branch = change.getDest().shortName();
     a.topic = change.getTopic();
     a.id = change.getKey().get();
     a.number = change.getId().get();
@@ -174,12 +173,12 @@
    * @return object suitable for serialization to JSON
    */
   public RefUpdateAttribute asRefUpdateAttribute(
-      ObjectId oldId, ObjectId newId, Branch.NameKey refName) {
+      ObjectId oldId, ObjectId newId, BranchNameKey refName) {
     RefUpdateAttribute ru = new RefUpdateAttribute();
     ru.newRev = newId != null ? newId.getName() : ObjectId.zeroId().getName();
     ru.oldRev = oldId != null ? oldId.getName() : ObjectId.zeroId().getName();
-    ru.project = refName.getParentKey().get();
-    ru.refName = refName.get();
+    ru.project = refName.project().get();
+    ru.refName = refName.branch();
     return ru;
   }
 
@@ -285,7 +284,7 @@
 
   private void addDependsOn(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs)
       throws IOException {
-    RevCommit commit = rw.parseCommit(ObjectId.fromString(currentPs.getRevision().get()));
+    RevCommit commit = rw.parseCommit(currentPs.commitId());
     final List<String> parentNames = new ArrayList<>(commit.getParentCount());
     for (RevCommit p : commit.getParents()) {
       parentNames.add(p.name());
@@ -296,7 +295,7 @@
     for (ChangeData cd : queryProvider.get().byProjectCommits(change.getProject(), parentNames)) {
       for (PatchSet ps : cd.patchSets()) {
         for (String p : parentNames) {
-          if (!ps.getRevision().get().equals(p)) {
+          if (!ps.commitId().name().equals(p)) {
             continue;
           }
           ca.dependsOn.add(newDependsOn(requireNonNull(cd.change()), ps));
@@ -318,18 +317,18 @@
 
   private void addNeededBy(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs)
       throws IOException {
-    if (currentPs.getGroups().isEmpty()) {
+    if (currentPs.groups().isEmpty()) {
       return;
     }
-    String rev = currentPs.getRevision().get();
+    String rev = currentPs.commitId().name();
     // Find changes in the same related group as this patch set, having a patch
     // set whose parent matches this patch set's revision.
     for (ChangeData cd :
         InternalChangeQuery.byProjectGroups(
-            queryProvider, indexConfig, change.getProject(), currentPs.getGroups())) {
+            queryProvider, indexConfig, change.getProject(), currentPs.groups())) {
       PATCH_SETS:
       for (PatchSet ps : cd.patchSets()) {
-        RevCommit commit = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+        RevCommit commit = rw.parseCommit(ps.commitId());
         for (RevCommit p : commit.getParents()) {
           if (!p.name().equals(rev)) {
             continue;
@@ -343,7 +342,7 @@
 
   private DependencyAttribute newDependsOn(Change c, PatchSet ps) {
     DependencyAttribute d = newDependencyAttribute(c, ps);
-    d.isCurrentPatchSet = ps.getId().equals(c.currentPatchSetId());
+    d.isCurrentPatchSet = ps.id().equals(c.currentPatchSetId());
     return d;
   }
 
@@ -355,8 +354,8 @@
     DependencyAttribute d = new DependencyAttribute();
     d.number = c.getId().get();
     d.id = c.getKey().toString();
-    d.revision = ps.getRevision().get();
-    d.ref = ps.getRefName();
+    d.revision = ps.commitId().name();
+    d.ref = ps.refName();
     return d;
   }
 
@@ -400,7 +399,7 @@
       for (PatchSet p : ps) {
         PatchSetAttribute psa = asPatchSetAttribute(revWalk, change, p);
         if (approvals != null) {
-          addApprovals(psa, p.getId(), approvals, labelTypes);
+          addApprovals(psa, p.id(), approvals, labelTypes);
         }
         ca.patchSets.add(psa);
         if (includeFiles) {
@@ -463,12 +462,12 @@
    */
   public PatchSetAttribute asPatchSetAttribute(RevWalk revWalk, Change change, PatchSet patchSet) {
     PatchSetAttribute p = new PatchSetAttribute();
-    p.revision = patchSet.getRevision().get();
-    p.number = patchSet.getPatchSetId();
-    p.ref = patchSet.getRefName();
-    p.uploader = asAccountAttribute(patchSet.getUploader());
-    p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
-    PatchSet.Id pId = patchSet.getId();
+    p.revision = patchSet.commitId().name();
+    p.number = patchSet.number();
+    p.ref = patchSet.refName();
+    p.uploader = asAccountAttribute(patchSet.uploader());
+    p.createdOn = patchSet.createdOn().getTime() / 1000L;
+    PatchSet.Id pId = patchSet.id();
     try {
       p.parents = new ArrayList<>();
       RevCommit c = revWalk.parseCommit(ObjectId.fromString(p.revision));
@@ -476,7 +475,7 @@
         p.parents.add(parent.name());
       }
 
-      UserIdentity author = toUserIdentity(c.getAuthorIdent());
+      UserIdentity author = emails.toUserIdentity(c.getAuthorIdent());
       if (author.getAccount() == null) {
         p.author = new AccountAttribute();
         p.author.email = author.getEmail();
@@ -495,7 +494,7 @@
       }
       p.kind = changeKindCache.getChangeKind(change, patchSet);
     } catch (IOException | StorageException e) {
-      logger.atSevere().withCause(e).log("Cannot load patch set data for %s", patchSet.getId());
+      logger.atSevere().withCause(e).log("Cannot load patch set data for %s", patchSet.id());
     } catch (PatchListObjectTooLargeException e) {
       logger.atWarning().log("Cannot get size information for %s: %s", pId, e.getMessage());
     } catch (PatchListNotAvailableException e) {
@@ -504,26 +503,6 @@
     return p;
   }
 
-  // TODO: The same method exists in PatchSetInfoFactory, find a common place
-  // for it
-  private UserIdentity toUserIdentity(PersonIdent who) throws IOException {
-    UserIdentity u = new UserIdentity();
-    u.setName(who.getName());
-    u.setEmail(who.getEmailAddress());
-    u.setDate(new Timestamp(who.getWhen().getTime()));
-    u.setTimeZone(who.getTimeZoneOffset());
-
-    // If only one account has access to this email address, select it
-    // as the identity of the user.
-    //
-    Set<Account.Id> a = emails.getAccountFor(u.getEmail());
-    if (a.size() == 1) {
-      u.setAccount(a.iterator().next());
-    }
-
-    return u;
-  }
-
   public void addApprovals(
       PatchSetAttribute p,
       PatchSet.Id id,
@@ -540,7 +519,7 @@
     if (!list.isEmpty()) {
       p.approvals = new ArrayList<>(list.size());
       for (PatchSetApproval a : list) {
-        if (a.getValue() != 0) {
+        if (a.value() != 0) {
           p.approvals.add(asApprovalAttribute(a, labelTypes));
         }
       }
@@ -571,9 +550,9 @@
    */
   public AccountAttribute asAccountAttribute(AccountState accountState) {
     AccountAttribute who = new AccountAttribute();
-    who.name = accountState.getAccount().getFullName();
-    who.email = accountState.getAccount().getPreferredEmail();
-    who.username = accountState.getUserName().orElse(null);
+    who.name = accountState.account().fullName();
+    who.email = accountState.account().preferredEmail();
+    who.username = accountState.userName().orElse(null);
     return who;
   }
 
@@ -599,13 +578,13 @@
    */
   public ApprovalAttribute asApprovalAttribute(PatchSetApproval approval, LabelTypes labelTypes) {
     ApprovalAttribute a = new ApprovalAttribute();
-    a.type = approval.getLabelId().get();
-    a.value = Short.toString(approval.getValue());
-    a.by = asAccountAttribute(approval.getAccountId());
-    a.grantedOn = approval.getGranted().getTime() / 1000L;
+    a.type = approval.labelId().get();
+    a.value = Short.toString(approval.value());
+    a.by = asAccountAttribute(approval.accountId());
+    a.grantedOn = approval.granted().getTime() / 1000L;
     a.oldValue = null;
 
-    LabelType lt = labelTypes.byLabel(approval.getLabelId());
+    LabelType lt = labelTypes.byLabel(approval.labelId());
     if (lt != null) {
       a.description = lt.getName();
     }
diff --git a/java/com/google/gerrit/server/events/EventGson.java b/java/com/google/gerrit/server/events/EventGson.java
new file mode 100644
index 0000000..87b45f6
--- /dev/null
+++ b/java/com/google/gerrit/server/events/EventGson.java
@@ -0,0 +1,28 @@
+// 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.events;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@BindingAnnotation
+@Retention(RUNTIME)
+@Target({PARAMETER, FIELD})
+public @interface EventGson {}
diff --git a/java/com/google/gerrit/server/events/EventGsonProvider.java b/java/com/google/gerrit/server/events/EventGsonProvider.java
new file mode 100644
index 0000000..688507b
--- /dev/null
+++ b/java/com/google/gerrit/server/events/EventGsonProvider.java
@@ -0,0 +1,36 @@
+// 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.events;
+
+import com.google.common.base.Supplier;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.change.ChangeKeyAdapter;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Provider;
+
+public class EventGsonProvider implements Provider<Gson> {
+  @Override
+  public Gson get() {
+    return new GsonBuilder()
+        .registerTypeAdapter(Event.class, new EventDeserializer())
+        .registerTypeAdapter(Supplier.class, new SupplierSerializer())
+        .registerTypeAdapter(Supplier.class, new SupplierDeserializer())
+        .registerTypeAdapter(Change.Key.class, new ChangeKeyAdapter())
+        .registerTypeAdapter(Project.NameKey.class, new ProjectNameKeyAdapter())
+        .create();
+  }
+}
diff --git a/java/com/google/gerrit/server/events/EventsMetrics.java b/java/com/google/gerrit/server/events/EventsMetrics.java
index f73d6de..3c87cca 100644
--- a/java/com/google/gerrit/server/events/EventsMetrics.java
+++ b/java/com/google/gerrit/server/events/EventsMetrics.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -31,7 +32,7 @@
         metricMaker.newCounter(
             "events",
             new Description("Triggered events").setRate().setUnit("triggered events"),
-            Field.ofString("type"));
+            Field.ofString("type", Metadata.Builder::eventType).build());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/events/HashtagsChangedEvent.java b/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
index 1de0fc3..d9e0445 100644
--- a/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
+++ b/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class HashtagsChangedEvent extends ChangeEvent {
diff --git a/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java b/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
index 8cea856..24f4709 100644
--- a/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
+++ b/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class PatchSetCreatedEvent extends PatchSetEvent {
diff --git a/java/com/google/gerrit/server/events/PatchSetEvent.java b/java/com/google/gerrit/server/events/PatchSetEvent.java
index f9dde66..c8e4591 100644
--- a/java/com/google/gerrit/server/events/PatchSetEvent.java
+++ b/java/com/google/gerrit/server/events/PatchSetEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.PatchSetAttribute;
 
 public class PatchSetEvent extends ChangeEvent {
diff --git a/java/com/google/gerrit/server/events/PrivateStateChangedEvent.java b/java/com/google/gerrit/server/events/PrivateStateChangedEvent.java
index d03eda4..2e6fe83 100644
--- a/java/com/google/gerrit/server/events/PrivateStateChangedEvent.java
+++ b/java/com/google/gerrit/server/events/PrivateStateChangedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class PrivateStateChangedEvent extends PatchSetEvent {
diff --git a/java/com/google/gerrit/server/events/ProjectCreatedEvent.java b/java/com/google/gerrit/server/events/ProjectCreatedEvent.java
index dc979ca..d092b39 100644
--- a/java/com/google/gerrit/server/events/ProjectCreatedEvent.java
+++ b/java/com/google/gerrit/server/events/ProjectCreatedEvent.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.events;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 
 public class ProjectCreatedEvent extends ProjectEvent {
   static final String TYPE = "project-created";
@@ -27,7 +27,7 @@
 
   @Override
   public Project.NameKey getProjectNameKey() {
-    return new Project.NameKey(projectName);
+    return Project.nameKey(projectName);
   }
 
   public String getHeadName() {
diff --git a/java/com/google/gerrit/server/events/ProjectEvent.java b/java/com/google/gerrit/server/events/ProjectEvent.java
index cba8e90..77085e6 100644
--- a/java/com/google/gerrit/server/events/ProjectEvent.java
+++ b/java/com/google/gerrit/server/events/ProjectEvent.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.events;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 
 public abstract class ProjectEvent extends Event {
   protected ProjectEvent(String type) {
diff --git a/java/com/google/gerrit/server/events/ProjectNameKeySerializer.java b/java/com/google/gerrit/server/events/ProjectNameKeyAdapter.java
similarity index 60%
rename from java/com/google/gerrit/server/events/ProjectNameKeySerializer.java
rename to java/com/google/gerrit/server/events/ProjectNameKeyAdapter.java
index 743b314..eeaa238 100644
--- a/java/com/google/gerrit/server/events/ProjectNameKeySerializer.java
+++ b/java/com/google/gerrit/server/events/ProjectNameKeyAdapter.java
@@ -14,17 +14,31 @@
 
 package com.google.gerrit.server.events;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
 import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
 import com.google.gson.JsonPrimitive;
 import com.google.gson.JsonSerializationContext;
 import com.google.gson.JsonSerializer;
 import java.lang.reflect.Type;
 
-public class ProjectNameKeySerializer implements JsonSerializer<Project.NameKey> {
+public class ProjectNameKeyAdapter
+    implements JsonSerializer<Project.NameKey>, JsonDeserializer<Project.NameKey> {
   @Override
   public JsonElement serialize(
       Project.NameKey project, Type typeOfSrc, JsonSerializationContext context) {
     return new JsonPrimitive(project.get());
   }
+
+  @Override
+  public Project.NameKey deserialize(
+      JsonElement json, Type typeOfT, JsonDeserializationContext context)
+      throws JsonParseException {
+    if (!json.isJsonPrimitive() || !json.getAsJsonPrimitive().isString()) {
+      throw new JsonParseException("Key is not a string: " + json);
+    }
+    return Project.nameKey(json.getAsString());
+  }
 }
diff --git a/java/com/google/gerrit/server/events/RefEvent.java b/java/com/google/gerrit/server/events/RefEvent.java
index 951940f..7a6bf6f 100644
--- a/java/com/google/gerrit/server/events/RefEvent.java
+++ b/java/com/google/gerrit/server/events/RefEvent.java
@@ -14,15 +14,15 @@
 
 package com.google.gerrit.server.events;
 
-import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.entities.BranchNameKey;
 
 public abstract class RefEvent extends ProjectEvent {
   protected RefEvent(String type) {
     super(type);
   }
 
-  public Branch.NameKey getBranchNameKey() {
-    return new Branch.NameKey(getProjectNameKey(), getRefName());
+  public BranchNameKey getBranchNameKey() {
+    return BranchNameKey.create(getProjectNameKey(), getRefName());
   }
 
   public abstract String getRefName();
diff --git a/java/com/google/gerrit/server/events/RefReceivedEvent.java b/java/com/google/gerrit/server/events/RefReceivedEvent.java
index aa02d11..18783aa 100644
--- a/java/com/google/gerrit/server/events/RefReceivedEvent.java
+++ b/java/com/google/gerrit/server/events/RefReceivedEvent.java
@@ -13,7 +13,7 @@
 // limitations under the License.
 package com.google.gerrit.server.events;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
diff --git a/java/com/google/gerrit/server/events/RefUpdatedEvent.java b/java/com/google/gerrit/server/events/RefUpdatedEvent.java
index d740543..1ca6392 100644
--- a/java/com/google/gerrit/server/events/RefUpdatedEvent.java
+++ b/java/com/google/gerrit/server/events/RefUpdatedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.RefUpdateAttribute;
 
@@ -30,7 +30,7 @@
 
   @Override
   public Project.NameKey getProjectNameKey() {
-    return new Project.NameKey(refUpdate.get().project);
+    return Project.nameKey(refUpdate.get().project);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/events/ReviewerAddedEvent.java b/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
index ea6bda3..5d2b7a5 100644
--- a/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
+++ b/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class ReviewerAddedEvent extends PatchSetEvent {
diff --git a/java/com/google/gerrit/server/events/ReviewerDeletedEvent.java b/java/com/google/gerrit/server/events/ReviewerDeletedEvent.java
index 02f9d43..2b0b6ac 100644
--- a/java/com/google/gerrit/server/events/ReviewerDeletedEvent.java
+++ b/java/com/google/gerrit/server/events/ReviewerDeletedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ApprovalAttribute;
 
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 5826dc7..1769f7d 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -20,6 +20,10 @@
 import com.google.common.flogger.FluentLogger;
 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.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -42,10 +46,6 @@
 import com.google.gerrit.extensions.events.VoteDeletedListener;
 import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow;
@@ -139,7 +139,7 @@
 
   private ChangeNotes getNotes(ChangeInfo info) {
     try {
-      return changeNotesFactory.createChecked(new Change.Id(info._number));
+      return changeNotesFactory.createChecked(Change.id(info._number));
     } catch (NoSuchChangeException e) {
       throw new StorageException(e);
     }
@@ -164,7 +164,7 @@
     return Suppliers.memoize(
         () ->
             account != null
-                ? eventFactory.asAccountAttribute(new Account.Id(account._accountId))
+                ? eventFactory.asAccountAttribute(Account.id(account._accountId))
                 : null);
   }
 
@@ -367,7 +367,7 @@
     if (ev.getUpdater() != null) {
       event.submitter = accountAttributeSupplier(ev.getUpdater());
     }
-    final Branch.NameKey refName = new Branch.NameKey(ev.getProjectName(), ev.getRefName());
+    final BranchNameKey refName = BranchNameKey.create(ev.getProjectName(), ev.getRefName());
     event.refUpdate =
         Suppliers.memoize(
             () ->
diff --git a/java/com/google/gerrit/server/events/TopicChangedEvent.java b/java/com/google/gerrit/server/events/TopicChangedEvent.java
index 0b6ecc5..b9cce66 100644
--- a/java/com/google/gerrit/server/events/TopicChangedEvent.java
+++ b/java/com/google/gerrit/server/events/TopicChangedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class TopicChangedEvent extends ChangeEvent {
diff --git a/java/com/google/gerrit/server/events/VoteDeletedEvent.java b/java/com/google/gerrit/server/events/VoteDeletedEvent.java
index 87c4c05..180b4a8 100644
--- a/java/com/google/gerrit/server/events/VoteDeletedEvent.java
+++ b/java/com/google/gerrit/server/events/VoteDeletedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ApprovalAttribute;
 
diff --git a/java/com/google/gerrit/server/events/WorkInProgressStateChangedEvent.java b/java/com/google/gerrit/server/events/WorkInProgressStateChangedEvent.java
index 5e52c7b..74f997b 100644
--- a/java/com/google/gerrit/server/events/WorkInProgressStateChangedEvent.java
+++ b/java/com/google/gerrit/server/events/WorkInProgressStateChangedEvent.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class WorkInProgressStateChangedEvent extends PatchSetEvent {
diff --git a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
index fdce1da..a76b69b 100644
--- a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.AssigneeChangedListener;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
index 252129b..ccf58cf 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -15,14 +15,14 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.ChangeAbandonedListener;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.cache.PerThreadCache;
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
index acb212a..a24873c 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.ChangeDeletedListener;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow;
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
index 1645333..5ce38ff 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -15,14 +15,14 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.ChangeMergedListener;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.cache.PerThreadCache;
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
index e8bed56..fa91dbd 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -15,14 +15,14 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.ChangeRestoredListener;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
index ccb17d5..5fb004f 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.ChangeRevertedListener;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
index e5d626f..e72b4d4 100644
--- a/java/com/google/gerrit/server/extensions/events/CommentAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -22,8 +24,6 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.CommentAddedListener;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.cache.PerThreadCache;
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index d00eb31..3dcf3b8 100644
--- a/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -16,19 +16,21 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.RevisionJson;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -39,42 +41,51 @@
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
+import org.eclipse.jgit.lib.Config;
 
+/**
+ * Formats change and revision info objects to serve as payload for Gerrit events.
+ *
+ * <p>Uses configurable options ({@code event.payload.listChangeOptions}) to decide which fields to
+ * populate.
+ */
 @Singleton
 public class EventUtil {
-  private static final ImmutableSet<ListChangesOption> CHANGE_OPTIONS;
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static final ImmutableSet<ListChangesOption> DEFAULT_CHANGE_OPTIONS;
 
   static {
     EnumSet<ListChangesOption> opts = EnumSet.allOf(ListChangesOption.class);
-
     // Some options, like actions, are expensive to compute because they potentially have to walk
     // lots of history and inspect lots of other changes.
     opts.remove(ListChangesOption.CHANGE_ACTIONS);
     opts.remove(ListChangesOption.CURRENT_ACTIONS);
-
     // CHECK suppresses some exceptions on corrupt changes, which is not appropriate for passing
     // through the event system as we would rather let them propagate.
     opts.remove(ListChangesOption.CHECK);
-
-    CHANGE_OPTIONS = Sets.immutableEnumSet(opts);
+    DEFAULT_CHANGE_OPTIONS = Sets.immutableEnumSet(opts);
   }
 
   private final ChangeData.Factory changeDataFactory;
   private final ChangeJson.Factory changeJsonFactory;
   private final RevisionJson.Factory revisionJsonFactory;
+  private final ImmutableSet<ListChangesOption> changeOptions;
 
   @Inject
   EventUtil(
       ChangeJson.Factory changeJsonFactory,
       RevisionJson.Factory revisionJsonFactory,
-      ChangeData.Factory changeDataFactory) {
+      ChangeData.Factory changeDataFactory,
+      @GerritServerConfig Config gerritConfig) {
     this.changeDataFactory = changeDataFactory;
     this.changeJsonFactory = changeJsonFactory;
     this.revisionJsonFactory = revisionJsonFactory;
+    this.changeOptions = parseChangeListOptions(gerritConfig);
   }
 
   public ChangeInfo changeInfo(Change change) {
-    return changeJsonFactory.create(CHANGE_OPTIONS).format(change);
+    return changeJsonFactory.create(changeOptions).format(change);
   }
 
   public RevisionInfo revisionInfo(Project project, PatchSet ps)
@@ -84,19 +95,19 @@
 
   public RevisionInfo revisionInfo(Project.NameKey project, PatchSet ps)
       throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
-    ChangeData cd = changeDataFactory.create(project, ps.getId().getParentKey());
-    return revisionJsonFactory.create(CHANGE_OPTIONS).getRevisionInfo(cd, ps);
+    ChangeData cd = changeDataFactory.create(project, ps.id().changeId());
+    return revisionJsonFactory.create(changeOptions).getRevisionInfo(cd, ps);
   }
 
   public AccountInfo accountInfo(AccountState accountState) {
-    if (accountState == null || accountState.getAccount().getId() == null) {
+    if (accountState == null || accountState.account().id() == null) {
       return null;
     }
-    Account account = accountState.getAccount();
-    AccountInfo accountInfo = new AccountInfo(account.getId().get());
-    accountInfo.email = account.getPreferredEmail();
-    accountInfo.name = account.getFullName();
-    accountInfo.username = accountState.getUserName().orElse(null);
+    Account account = accountState.account();
+    AccountInfo accountInfo = new AccountInfo(account.id().get());
+    accountInfo.email = account.preferredEmail();
+    accountInfo.name = account.fullName();
+    accountInfo.username = accountState.userName().orElse(null);
     return accountInfo;
   }
 
@@ -106,9 +117,25 @@
     for (Map.Entry<String, Short> e : approvals.entrySet()) {
       Integer value = e.getValue() != null ? Integer.valueOf(e.getValue()) : null;
       result.put(
-          e.getKey(),
-          new ApprovalInfo(accountState.getAccount().getId().get(), value, null, null, ts));
+          e.getKey(), new ApprovalInfo(accountState.account().id().get(), value, null, null, ts));
     }
     return result;
   }
+
+  private static ImmutableSet<ListChangesOption> parseChangeListOptions(Config gerritConfig) {
+    String[] config = gerritConfig.getStringList("event", "payload", "listChangeOptions");
+    if (config.length == 0) {
+      return DEFAULT_CHANGE_OPTIONS;
+    }
+
+    ImmutableSet.Builder<ListChangesOption> result = ImmutableSet.builder();
+    for (String c : config) {
+      try {
+        result.add(ListChangesOption.valueOf(c));
+      } catch (IllegalArgumentException e) {
+        logger.atWarning().withCause(e).log("could not parse list change option %s", c);
+      }
+    }
+    return result.build();
+  }
 }
diff --git a/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index bae17e7..99f105e 100644
--- a/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
index 65f5b8b..df60ec0 100644
--- a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -16,12 +16,12 @@
 
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.HashtagsEditedListener;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
index 49a6091..72adff7 100644
--- a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
@@ -15,14 +15,14 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.PrivateStateChangedListener;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
index f9f67f6..1af428cb 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -16,14 +16,14 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.ReviewerAddedListener;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
index b92f3e6..61632f2 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -22,8 +24,6 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.ReviewerDeletedListener;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
diff --git a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index f34e0ac..50fbaa1 100644
--- a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -15,14 +15,14 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.RevisionCreatedListener;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.cache.PerThreadCache;
diff --git a/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
index 9e1ae44..cb982a1 100644
--- a/java/com/google/gerrit/server/extensions/events/TopicEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.TopicEditedListener;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
index bd6873a..8533a65 100644
--- a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -22,8 +24,6 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.VoteDeletedListener;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
diff --git a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
index 785d6fe..16c5e25 100644
--- a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
@@ -15,14 +15,14 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
diff --git a/java/com/google/gerrit/server/extensions/webui/UiActions.java b/java/com/google/gerrit/server/extensions/webui/UiActions.java
index 3ca2bdb..0bc3d5c 100644
--- a/java/com/google/gerrit/server/extensions/webui/UiActions.java
+++ b/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -19,7 +19,6 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Predicate;
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -38,6 +37,7 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendCondition;
@@ -50,6 +50,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Predicate;
 
 @Singleton
 public class UiActions {
@@ -71,7 +72,7 @@
             new com.google.gerrit.metrics.Description("Latency for RestView#getDescription calls")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            Field.ofString("view"));
+            Field.ofString("view", Metadata.Builder::restViewName).build());
   }
 
   public <R extends RestResource> Iterable<UiAction.Description> from(
@@ -143,7 +144,7 @@
 
     String name = e.getExportName().substring(d + 1);
     UiAction.Description dsc;
-    try (Timer1.Context ignored = uiActionLatency.start(name)) {
+    try (Timer1.Context<String> ignored = uiActionLatency.start(name)) {
       dsc = ((UiAction<R>) view).getDescription(resource);
     }
 
diff --git a/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java b/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
index 65f14db..a1682fe 100644
--- a/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
+++ b/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
@@ -18,11 +18,11 @@
 import static java.util.stream.Collectors.groupingBy;
 
 import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.FixReplacement;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.FixReplacement;
 import com.google.gerrit.server.change.FileContentUtil;
 import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
 import com.google.gerrit.server.edit.tree.TreeModification;
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java
index d8aeece..4473ab7 100644
--- a/java/com/google/gerrit/server/git/BanCommit.java
+++ b/java/com/google/gerrit/server/git/BanCommit.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.git;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_REJECT_COMMITS;
+import static com.google.gerrit.entities.RefNames.REFS_REJECT_COMMITS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.git.LockFailureException;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/java/com/google/gerrit/server/git/BranchOrderSection.java b/java/com/google/gerrit/server/git/BranchOrderSection.java
index d4b537e..4c77b61 100644
--- a/java/com/google/gerrit/server/git/BranchOrderSection.java
+++ b/java/com/google/gerrit/server/git/BranchOrderSection.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.RefNames;
 import java.util.List;
 
 public class BranchOrderSection {
diff --git a/java/com/google/gerrit/server/git/ChangeMessageModifier.java b/java/com/google/gerrit/server/git/ChangeMessageModifier.java
index 7d4edcf..580c0b9 100644
--- a/java/com/google/gerrit/server/git/ChangeMessageModifier.java
+++ b/java/com/google/gerrit/server/git/ChangeMessageModifier.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.Branch;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 /**
@@ -47,5 +47,5 @@
    * @return a new not null commit message.
    */
   String onSubmit(
-      String newCommitMessage, RevCommit original, RevCommit mergeTip, Branch.NameKey destination);
+      String newCommitMessage, RevCommit original, RevCommit mergeTip, BranchNameKey destination);
 }
diff --git a/java/com/google/gerrit/server/git/ChangeReportFormatter.java b/java/com/google/gerrit/server/git/ChangeReportFormatter.java
index 3ce6b2b..f897a1d 100644
--- a/java/com/google/gerrit/server/git/ChangeReportFormatter.java
+++ b/java/com/google/gerrit/server/git/ChangeReportFormatter.java
@@ -16,7 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 
 public interface ChangeReportFormatter {
   @AutoValue
diff --git a/java/com/google/gerrit/server/git/CodeReviewCommit.java b/java/com/google/gerrit/server/git/CodeReviewCommit.java
index c210dcd..d7538ba 100644
--- a/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -19,8 +19,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.submit.CommitMergeStatus;
 import java.io.IOException;
@@ -48,7 +48,7 @@
       Ordering.natural()
           .onResultOf(
               (CodeReviewCommit c) ->
-                  c.getPatchsetId() != null ? c.getPatchsetId().getParentKey().get() : null)
+                  c.getPatchsetId() != null ? c.getPatchsetId().changeId().get() : null)
           .nullsFirst();
 
   public static CodeReviewRevWalk newRevWalk(Repository repo) {
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index b0f10f2..476037b 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -14,16 +14,49 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.server.CommonConverters;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.change.ChangeMessages;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import java.io.IOException;
+import java.sql.Timestamp;
+import java.text.MessageFormat;
 import java.util.ArrayList;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.ChangeIdUtil;
 
 /** Static utilities for working with {@link RevCommit}s. */
+@Singleton
 public class CommitUtil {
+  private final GitRepositoryManager repoManager;
+  private final Provider<PersonIdent> serverIdent;
+
+  @Inject
+  CommitUtil(
+      GitRepositoryManager repoManager, @GerritPersonIdent Provider<PersonIdent> serverIdent) {
+    this.repoManager = repoManager;
+    this.serverIdent = serverIdent;
+  }
+
   public static CommitInfo toCommitInfo(RevCommit commit) throws IOException {
     return toCommitInfo(commit, null);
   }
@@ -47,5 +80,84 @@
     return info;
   }
 
-  private CommitUtil() {}
+  /**
+   * Allows creating a revert commit.
+   *
+   * @param message Commit message for the revert commit.
+   * @param notes ChangeNotes of the change being reverted.
+   * @param user Current User performing the revert.
+   * @return ObjectId that represents the newly created commit.
+   * @throws ResourceConflictException Can't revert the initial commit.
+   * @throws IOException Thrown in case of I/O errors.
+   */
+  public ObjectId createRevertCommit(String message, ChangeNotes notes, CurrentUser user)
+      throws ResourceConflictException, IOException {
+    message = Strings.emptyToNull(message);
+
+    Project.NameKey project = notes.getProjectName();
+    try (Repository git = repoManager.openRepository(project);
+        ObjectInserter oi = git.newObjectInserter();
+        ObjectReader reader = oi.newReader();
+        RevWalk revWalk = new RevWalk(reader)) {
+      return createRevertCommit(message, notes, user, null, TimeUtil.nowTs(), oi, revWalk);
+    }
+  }
+
+  /**
+   * @param message Commit message for the revert commit.
+   * @param notes ChangeNotes of the change being reverted.
+   * @param user Current User performing the revert.
+   * @param generatedChangeId The changeId for the commit message, can be null since it is not
+   *     needed for commits, only for changes.
+   * @param ts Timestamp of creation for the commit.
+   * @param oi ObjectInserter for inserting the newly created commit.
+   * @param revWalk Used for parsing the original commit.
+   * @return ObjectId that represents the newly created commit.
+   * @throws ResourceConflictException Can't revert the initial commit.
+   * @throws IOException Thrown in case of I/O errors.
+   */
+  public ObjectId createRevertCommit(
+      String message,
+      ChangeNotes notes,
+      CurrentUser user,
+      @Nullable ObjectId generatedChangeId,
+      Timestamp ts,
+      ObjectInserter oi,
+      RevWalk revWalk)
+      throws ResourceConflictException, IOException {
+
+    PatchSet patch = notes.getCurrentPatchSet();
+    RevCommit commitToRevert = revWalk.parseCommit(patch.commitId());
+    if (commitToRevert.getParentCount() == 0) {
+      throw new ResourceConflictException("Cannot revert initial commit");
+    }
+
+    PersonIdent committerIdent = serverIdent.get();
+    PersonIdent authorIdent =
+        user.asIdentifiedUser().newCommitterIdent(ts, committerIdent.getTimeZone());
+
+    RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
+    revWalk.parseHeaders(parentToCommitToRevert);
+
+    CommitBuilder revertCommitBuilder = new CommitBuilder();
+    revertCommitBuilder.addParentId(commitToRevert);
+    revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree());
+    revertCommitBuilder.setAuthor(authorIdent);
+    revertCommitBuilder.setCommitter(authorIdent);
+
+    Change changeToRevert = notes.getChange();
+    if (message == null) {
+      message =
+          MessageFormat.format(
+              ChangeMessages.get().revertChangeDefaultMessage,
+              changeToRevert.getSubject(),
+              patch.commitId().name());
+    }
+    if (generatedChangeId != null) {
+      revertCommitBuilder.setMessage(ChangeIdUtil.insertId(message, generatedChangeId, true));
+    }
+    ObjectId id = oi.insert(revertCommitBuilder);
+    oi.flush();
+    return id;
+  }
 }
diff --git a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
deleted file mode 100644
index a8594e7..0000000
--- a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.git;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.AbstractAdvertiseRefsHook;
-import org.eclipse.jgit.transport.ServiceMayNotContinueException;
-
-/**
- * Wrapper around {@link com.google.gerrit.server.permissions.PermissionBackend.ForProject} that
- * implements {@link org.eclipse.jgit.transport.AdvertiseRefsHook}.
- */
-public class DefaultAdvertiseRefsHook extends AbstractAdvertiseRefsHook {
-  private final PermissionBackend.ForProject perm;
-  private final PermissionBackend.RefFilterOptions opts;
-
-  public DefaultAdvertiseRefsHook(
-      PermissionBackend.ForProject perm, PermissionBackend.RefFilterOptions opts) {
-    this.perm = perm;
-    this.opts = opts;
-  }
-
-  @Override
-  protected Map<String, Ref> getAdvertisedRefs(Repository repo, RevWalk revWalk)
-      throws ServiceMayNotContinueException {
-    try {
-      List<String> prefixes =
-          !opts.prefixes().isEmpty() ? opts.prefixes() : ImmutableList.of(RefDatabase.ALL);
-      return perm.filter(
-          repo.getRefDatabase().getRefsByPrefix(prefixes.toArray(new String[0])), repo, opts);
-    } catch (IOException | PermissionBackendException e) {
-      ServiceMayNotContinueException ex = new ServiceMayNotContinueException();
-      ex.initCause(e);
-      throw ex;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
index 5b9dffc..5866c57 100644
--- a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
+++ b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.UrlFormatter;
 import com.google.inject.Inject;
 import java.util.Optional;
diff --git a/java/com/google/gerrit/server/git/DelegateRefDatabase.java b/java/com/google/gerrit/server/git/DelegateRefDatabase.java
new file mode 100644
index 0000000..34dd6a9
--- /dev/null
+++ b/java/com/google/gerrit/server/git/DelegateRefDatabase.java
@@ -0,0 +1,86 @@
+// 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.git;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Wrapper around {@link RefDatabase} that delegates all calls to the wrapped {@link RefDatabase}.
+ */
+public class DelegateRefDatabase extends RefDatabase {
+
+  private Repository delegate;
+
+  DelegateRefDatabase(Repository delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public void create() throws IOException {
+    delegate.getRefDatabase().create();
+  }
+
+  @Override
+  public void close() {
+    delegate.close();
+  }
+
+  @Override
+  public boolean isNameConflicting(String name) throws IOException {
+    return delegate.getRefDatabase().isNameConflicting(name);
+  }
+
+  @Override
+  public RefUpdate newUpdate(String name, boolean detach) throws IOException {
+    return delegate.getRefDatabase().newUpdate(name, detach);
+  }
+
+  @Override
+  public RefRename newRename(String fromName, String toName) throws IOException {
+    return delegate.getRefDatabase().newRename(fromName, toName);
+  }
+
+  @Override
+  public Ref exactRef(String name) throws IOException {
+    return delegate.getRefDatabase().exactRef(name);
+  }
+
+  @SuppressWarnings("deprecation")
+  @Override
+  public Map<String, Ref> getRefs(String prefix) throws IOException {
+    return delegate.getRefDatabase().getRefs(prefix);
+  }
+
+  @Override
+  public List<Ref> getAdditionalRefs() throws IOException {
+    return delegate.getRefDatabase().getAdditionalRefs();
+  }
+
+  @Override
+  public Ref peel(Ref ref) throws IOException {
+    return delegate.getRefDatabase().peel(ref);
+  }
+
+  Repository getDelegate() {
+    return delegate;
+  }
+}
diff --git a/java/com/google/gerrit/server/git/DelegateRepository.java b/java/com/google/gerrit/server/git/DelegateRepository.java
new file mode 100644
index 0000000..800490d
--- /dev/null
+++ b/java/com/google/gerrit/server/git/DelegateRepository.java
@@ -0,0 +1,89 @@
+// 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.git;
+
+import java.io.IOException;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
+import org.eclipse.jgit.lib.BaseRepositoryBuilder;
+import org.eclipse.jgit.lib.ObjectDatabase;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.ReflogReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+
+/** Wrapper around {@link Repository} that delegates all calls to the wrapped {@link Repository}. */
+class DelegateRepository extends Repository {
+
+  private final Repository delegate;
+
+  DelegateRepository(Repository delegate) {
+    super(toBuilder(delegate));
+    this.delegate = delegate;
+  }
+
+  @Override
+  public void create(boolean bare) throws IOException {
+    delegate.create(bare);
+  }
+
+  @Override
+  public String getIdentifier() {
+    return delegate.getIdentifier();
+  }
+
+  @Override
+  public ObjectDatabase getObjectDatabase() {
+    return delegate.getObjectDatabase();
+  }
+
+  @Override
+  public RefDatabase getRefDatabase() {
+    return delegate.getRefDatabase();
+  }
+
+  @Override
+  public StoredConfig getConfig() {
+    return delegate.getConfig();
+  }
+
+  @Override
+  public AttributesNodeProvider createAttributesNodeProvider() {
+    return delegate.createAttributesNodeProvider();
+  }
+
+  @Override
+  public void scanForRepoChanges() throws IOException {
+    delegate.scanForRepoChanges();
+  }
+
+  @Override
+  public void notifyIndexChanged(boolean internal) {
+    delegate.notifyIndexChanged(internal);
+  }
+
+  @Override
+  public ReflogReader getReflogReader(String refName) throws IOException {
+    return delegate.getReflogReader(refName);
+  }
+
+  @SuppressWarnings("rawtypes")
+  private static BaseRepositoryBuilder toBuilder(Repository repo) {
+    if (!repo.isBare()) {
+      throw new IllegalArgumentException("non-bare repository is not supported");
+    }
+
+    return new BaseRepositoryBuilder<>().setFS(repo.getFS()).setGitDir(repo.getDirectory());
+  }
+}
diff --git a/java/com/google/gerrit/server/git/GarbageCollection.java b/java/com/google/gerrit/server/git/GarbageCollection.java
index 75c9012..090d439 100644
--- a/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -17,8 +17,8 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GarbageCollectionResult;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.events.GarbageCollectorListener;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GcConfig;
 import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionQueue.java b/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
index 31cd880..e3a923b 100644
--- a/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
+++ b/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.inject.Singleton;
 import java.util.Collection;
 import java.util.HashSet;
diff --git a/java/com/google/gerrit/server/git/GitRepositoryManager.java b/java/com/google/gerrit/server/git/GitRepositoryManager.java
index 01bbe89..e4d0696 100644
--- a/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.inject.ImplementedBy;
 import com.google.inject.Singleton;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/git/GroupCollector.java b/java/com/google/gerrit/server/git/GroupCollector.java
index e4fd803..dd39198 100644
--- a/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/java/com/google/gerrit/server/git/GroupCollector.java
@@ -28,8 +28,8 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.SortedSetMultimap;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -82,9 +82,9 @@
     if (rsrc.getEdit().isPresent()) {
       // Groups for an edit are just the base revision's groups, since they have
       // the same parent.
-      return rsrc.getEdit().get().getBasePatchSet().getGroups();
+      return rsrc.getEdit().get().getBasePatchSet().groups();
     }
-    return rsrc.getPatchSet().getGroups();
+    return rsrc.getPatchSet().groups();
   }
 
   private interface Lookup {
@@ -107,9 +107,9 @@
         transformRefs(changeRefsById),
         psId -> {
           // TODO(dborowitz): Reuse open repository from caller.
-          ChangeNotes notes = notesFactory.createChecked(project, psId.getParentKey());
+          ChangeNotes notes = notesFactory.createChecked(project, psId.changeId());
           PatchSet ps = psUtil.get(notes, psId);
-          return ps != null ? ps.getGroups() : null;
+          return ps != null ? ps.groups() : null;
         });
   }
 
diff --git a/java/com/google/gerrit/server/git/HookUtil.java b/java/com/google/gerrit/server/git/HookUtil.java
index 3bef7cc..fd29c8deb 100644
--- a/java/com/google/gerrit/server/git/HookUtil.java
+++ b/java/com/google/gerrit/server/git/HookUtil.java
@@ -19,8 +19,9 @@
 import java.io.IOException;
 import java.util.Map;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.transport.BaseReceivePack;
+import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+import org.eclipse.jgit.transport.UploadPack;
 
 /** Static utilities for writing git protocol hooks. */
 public class HookUtil {
@@ -32,8 +33,7 @@
    * @return map of refs that were advertised.
    * @throws ServiceMayNotContinueException if a problem occurred.
    */
-  @SuppressWarnings("deprecation")
-  public static Map<String, Ref> ensureAllRefsAdvertised(BaseReceivePack rp)
+  public static Map<String, Ref> ensureAllRefsAdvertised(ReceivePack rp)
       throws ServiceMayNotContinueException {
     Map<String, Ref> refs = rp.getAdvertisedRefs();
     if (refs != null) {
@@ -52,5 +52,32 @@
     return refs;
   }
 
+  /**
+   * Scan and advertise all refs in the repo if refs have not already been advertised; otherwise,
+   * just return the advertised map.
+   *
+   * @param up upload-pack handler.
+   * @return map of refs that were advertised.
+   * @throws ServiceMayNotContinueException if a problem occurred.
+   */
+  public static Map<String, Ref> ensureAllRefsAdvertised(UploadPack up)
+      throws ServiceMayNotContinueException {
+    Map<String, Ref> refs = up.getAdvertisedRefs();
+    if (refs != null) {
+      return refs;
+    }
+    try {
+      refs =
+          up.getRepository().getRefDatabase().getRefs().stream()
+              .collect(toMap(Ref::getName, r -> r));
+    } catch (ServiceMayNotContinueException e) {
+      throw e;
+    } catch (IOException e) {
+      throw new ServiceMayNotContinueException(e);
+    }
+    up.setAdvertisedRefs(refs);
+    return refs;
+  }
+
   private HookUtil() {}
 }
diff --git a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 1fdf194..d42ed25 100644
--- a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
@@ -259,7 +259,7 @@
       int newLen = projectName.length() - Constants.DOT_GIT_EXT.length();
       projectName = projectName.substring(0, newLen);
     }
-    return new Project.NameKey(projectName);
+    return Project.nameKey(projectName);
   }
 
   protected class ProjectVisitor extends SimpleFileVisitor<Path> {
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index c1d8ddd..ad87843 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -17,19 +17,30 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Comparator.naturalOrder;
 import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
 
-import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.exceptions.InvalidMergeStrategyException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -38,13 +49,6 @@
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -67,7 +71,6 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -117,6 +120,13 @@
 public class MergeUtil {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  /**
+   * Length of abbreviated hex SHA-1s in merged filenames.
+   *
+   * <p>This is a constant so output is stable over time even if the SHA-1 prefix becomes ambiguous.
+   */
+  private static final int NAME_ABBREV_LEN = 6;
+
   static class PluggableCommitMessageGenerator {
     private final DynamicSet<ChangeMessageModifier> changeMessageModifiers;
 
@@ -126,7 +136,7 @@
     }
 
     public String generate(
-        RevCommit original, RevCommit mergeTip, Branch.NameKey dest, String originalMessage) {
+        RevCommit original, RevCommit mergeTip, BranchNameKey dest, String originalMessage) {
       requireNonNull(original.getRawBuffer());
       if (mergeTip != null) {
         requireNonNull(mergeTip.getRawBuffer());
@@ -256,7 +266,8 @@
       boolean ignoreIdenticalTree,
       boolean allowConflicts)
       throws MissingObjectException, IncorrectObjectTypeException, IOException,
-          MergeIdenticalTreeException, MergeConflictException, MethodNotAllowedException {
+          MergeIdenticalTreeException, MergeConflictException, MethodNotAllowedException,
+          InvalidMergeStrategyException {
 
     ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
     m.setBase(originalCommit.getParent(parentIndex));
@@ -337,13 +348,13 @@
         String.format(
             "%0$-" + nameLength + "s (%s %s)",
             oursName,
-            ours.abbreviate(6).name(),
+            abbreviateName(ours, NAME_ABBREV_LEN),
             oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
     String theirsNameFormatted =
         String.format(
             "%0$-" + nameLength + "s (%s %s)",
             theirsName,
-            theirs.abbreviate(6).name(),
+            abbreviateName(theirs, NAME_ABBREV_LEN),
             theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));
 
     MergeFormatter fmt = new MergeFormatter();
@@ -422,7 +433,8 @@
       PersonIdent committerIndent,
       String commitMsg,
       RevWalk rw)
-      throws IOException, MergeIdenticalTreeException, MergeConflictException {
+      throws IOException, MergeIdenticalTreeException, MergeConflictException,
+          InvalidMergeStrategyException {
 
     if (!MergeStrategy.THEIRS.getName().equals(mergeStrategy)
         && rw.isMergedInto(originalCommit, mergeTip)) {
@@ -450,7 +462,7 @@
   }
 
   public static String createConflictMessage(List<String> conflicts) {
-    StringBuilder sb = new StringBuilder("merge conflict(s)");
+    StringBuilder sb = new StringBuilder("merge conflict(s):");
     for (String c : conflicts) {
       sb.append('\n').append(c);
     }
@@ -514,7 +526,7 @@
     PatchSetApproval submitAudit = null;
 
     for (PatchSetApproval a : safeGetApprovals(notes, psId)) {
-      if (a.getValue() <= 0) {
+      if (a.value() <= 0) {
         // Negative votes aren't counted.
         continue;
       }
@@ -522,29 +534,29 @@
       if (a.isLegacySubmit()) {
         // Submit is treated specially, below (becomes committer)
         //
-        if (submitAudit == null || a.getGranted().compareTo(submitAudit.getGranted()) > 0) {
+        if (submitAudit == null || a.granted().compareTo(submitAudit.granted()) > 0) {
           submitAudit = a;
         }
         continue;
       }
 
-      final Account acc = identifiedUserFactory.create(a.getAccountId()).getAccount();
+      final Account acc = identifiedUserFactory.create(a.accountId()).getAccount();
       final StringBuilder identbuf = new StringBuilder();
-      if (acc.getFullName() != null && acc.getFullName().length() > 0) {
+      if (acc.fullName() != null && acc.fullName().length() > 0) {
         if (identbuf.length() > 0) {
           identbuf.append(' ');
         }
-        identbuf.append(acc.getFullName());
+        identbuf.append(acc.fullName());
       }
-      if (acc.getPreferredEmail() != null && acc.getPreferredEmail().length() > 0) {
-        if (isSignedOffBy(footers, acc.getPreferredEmail())) {
+      if (acc.preferredEmail() != null && acc.preferredEmail().length() > 0) {
+        if (isSignedOffBy(footers, acc.preferredEmail())) {
           continue;
         }
         if (identbuf.length() > 0) {
           identbuf.append(' ');
         }
         identbuf.append('<');
-        identbuf.append(acc.getPreferredEmail());
+        identbuf.append(acc.preferredEmail());
         identbuf.append('>');
       }
       if (identbuf.length() == 0) {
@@ -553,12 +565,12 @@
       }
 
       final String tag;
-      if (isCodeReview(a.getLabelId())) {
+      if (isCodeReview(a.labelId())) {
         tag = "Reviewed-by";
-      } else if (isVerified(a.getLabelId())) {
+      } else if (isVerified(a.labelId())) {
         tag = "Tested-by";
       } else {
-        final LabelType lt = project.getLabelTypes().byLabel(a.getLabelId());
+        final LabelType lt = project.getLabelTypes().byLabel(a.labelId());
         if (lt == null) {
           continue;
         }
@@ -733,10 +745,10 @@
       CodeReviewRevWalk rw,
       ObjectInserter inserter,
       Config repoConfig,
-      Branch.NameKey destBranch,
+      BranchNameKey destBranch,
       CodeReviewCommit mergeTip,
       CodeReviewCommit n)
-      throws IntegrationException {
+      throws IntegrationException, InvalidMergeStrategyException {
     ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
     try {
       if (m.merge(mergeTip, n)) {
@@ -789,7 +801,7 @@
       PersonIdent committer,
       CodeReviewRevWalk rw,
       ObjectInserter inserter,
-      Branch.NameKey destBranch,
+      BranchNameKey destBranch,
       CodeReviewCommit mergeTip,
       ObjectId treeId,
       CodeReviewCommit n)
@@ -806,9 +818,9 @@
     }
 
     StringBuilder msgbuf = new StringBuilder().append(summarize(rw, merged));
-    if (!R_HEADS_MASTER.equals(destBranch.get())) {
+    if (!R_HEADS_MASTER.equals(destBranch.branch())) {
       msgbuf.append(" into ");
-      msgbuf.append(destBranch.getShortName());
+      msgbuf.append(destBranch.shortName());
     }
 
     if (merged.size() > 1) {
@@ -840,29 +852,26 @@
       return String.format("Merge \"%s\"", c.getShortMessage());
     }
 
-    LinkedHashSet<String> topics = new LinkedHashSet<>(4);
-    for (CodeReviewCommit c : merged) {
-      if (!Strings.isNullOrEmpty(c.change().getTopic())) {
-        topics.add(c.change().getTopic());
-      }
-    }
+    ImmutableSortedSet<String> topics =
+        merged.stream()
+            .map(c -> c.change().getTopic())
+            .filter(t -> !Strings.isNullOrEmpty(t))
+            .map(t -> "\"" + t + "\"")
+            .collect(toImmutableSortedSet(naturalOrder()));
 
-    if (topics.size() == 1) {
-      return String.format("Merge changes from topic \"%s\"", Iterables.getFirst(topics, null));
-    } else if (topics.size() > 1) {
-      return String.format("Merge changes from topics \"%s\"", Joiner.on("\", \"").join(topics));
-    } else {
+    if (!topics.isEmpty()) {
       return String.format(
-          "Merge changes %s%s",
-          FluentIterable.from(merged)
-              .limit(5)
-              .transform(c -> c.change().getKey().abbreviate())
-              .join(Joiner.on(',')),
-          merged.size() > 5 ? ", ..." : "");
+          "Merge changes from topic%s %s",
+          topics.size() > 1 ? "s" : "", topics.stream().collect(joining(", ")));
     }
+    return merged.stream()
+        .limit(5)
+        .map(c -> c.change().getKey().abbreviate())
+        .collect(joining(",", "Merge changes ", merged.size() > 5 ? ", ..." : ""));
   }
 
-  public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig) {
+  public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig)
+      throws InvalidMergeStrategyException {
     return newThreeWayMerger(inserter, repoConfig, mergeStrategyName());
   }
 
@@ -886,7 +895,8 @@
   }
 
   public static ThreeWayMerger newThreeWayMerger(
-      ObjectInserter inserter, Config repoConfig, String strategyName) {
+      ObjectInserter inserter, Config repoConfig, String strategyName)
+      throws InvalidMergeStrategyException {
     Merger m = newMerger(inserter, repoConfig, strategyName);
     checkArgument(
         m instanceof ThreeWayMerger,
@@ -895,9 +905,12 @@
     return (ThreeWayMerger) m;
   }
 
-  public static Merger newMerger(ObjectInserter inserter, Config repoConfig, String strategyName) {
+  public static Merger newMerger(ObjectInserter inserter, Config repoConfig, String strategyName)
+      throws InvalidMergeStrategyException {
     MergeStrategy strategy = MergeStrategy.get(strategyName);
-    checkArgument(strategy != null, "invalid merge strategy: %s", strategyName);
+    if (strategy == null) {
+      throw new InvalidMergeStrategyException(strategyName);
+    }
     return strategy.newMerger(
         new ObjectInserter.Filter() {
           @Override
@@ -975,7 +988,7 @@
         if (c.getPatchsetId() == null) {
           continue;
         }
-        Change.Id id = c.getPatchsetId().getParentKey();
+        Change.Id id = c.getPatchsetId().changeId();
         if (!expected.contains(id)) {
           continue;
         }
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index 3d64b82..858a55a 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -17,13 +17,11 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetInfo;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.config.SendEmailExecutor;
@@ -42,7 +40,6 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -112,7 +109,7 @@
   @Override
   public boolean updateChange(ChangeContext ctx) throws IOException {
     change = ctx.getChange();
-    correctBranch = refName.equals(change.getDest().get());
+    correctBranch = refName.equals(change.getDest().branch());
     if (!correctBranch) {
       return false;
     }
@@ -145,7 +142,7 @@
     }
     StringBuilder msgBuf = new StringBuilder();
     msgBuf.append("Change has been successfully pushed");
-    if (!refName.equals(change.getDest().get())) {
+    if (!refName.equals(change.getDest().branch())) {
       msgBuf.append(" into ");
       if (refName.startsWith(Constants.R_HEADS)) {
         msgBuf.append("branch ");
@@ -159,12 +156,7 @@
         ChangeMessagesUtil.newMessage(
             psId, ctx.getUser(), ctx.getWhen(), msgBuf.toString(), ChangeMessagesUtil.TAG_MERGED);
     cmUtil.addChangeMessage(update, msg);
-
-    PatchSetApproval submitter =
-        ApprovalsUtil.newApproval(
-            change.currentPatchSetId(), ctx.getUser(), LabelId.legacySubmit(), 1, ctx.getWhen());
-    update.putApproval(submitter.getLabel(), submitter.getValue());
-
+    update.putApproval(LabelId.legacySubmit().get(), (short) 1);
     return true;
   }
 
@@ -182,7 +174,7 @@
                   public void run() {
                     try {
                       MergedSender cm =
-                          mergedSenderFactory.create(ctx.getProject(), psId.getParentKey());
+                          mergedSenderFactory.create(ctx.getProject(), psId.changeId());
                       cm.setFrom(ctx.getAccountId());
                       cm.setPatchSet(patchSet, info);
                       cm.send();
@@ -203,8 +195,7 @@
 
   private PatchSetInfo getPatchSetInfo(ChangeContext ctx) throws IOException {
     RevWalk rw = ctx.getRevWalk();
-    RevCommit commit =
-        rw.parseCommit(ObjectId.fromString(requireNonNull(patchSet).getRevision().get()));
+    RevCommit commit = rw.parseCommit(requireNonNull(patchSet).commitId());
     return patchSetInfoFactory.get(rw, commit, psId);
   }
 }
diff --git a/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
index 01e85cf..32b5bb8 100644
--- a/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.config.SitePaths;
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index b72ea92..bfc5135 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -346,6 +346,8 @@
         out.write(Constants.encode(s.toString()));
         out.flush();
       } catch (IOException e) {
+        logger.atWarning().withCause(e).log(
+            "Sending progress to client failed. Stop sending updates for task %s", taskName);
         write = false;
       }
     }
diff --git a/java/com/google/gerrit/server/git/NotesBranchUtil.java b/java/com/google/gerrit/server/git/NotesBranchUtil.java
index 1636b85..8cb2b40 100644
--- a/java/com/google/gerrit/server/git/NotesBranchUtil.java
+++ b/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -16,9 +16,9 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/git/NotifyConfig.java b/java/com/google/gerrit/server/git/NotifyConfig.java
index 7f74f54..429f15a 100644
--- a/java/com/google/gerrit/server/git/NotifyConfig.java
+++ b/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.mail.Address;
@@ -113,6 +114,13 @@
 
   @Override
   public String toString() {
-    return "NotifyConfig[" + name + " = " + addresses + " + " + groups + "]";
+    return MoreObjects.toStringHelper(this)
+        .add("name", name)
+        .add("addresses", addresses)
+        .add("groups", groups)
+        .add("header", header)
+        .add("types", types)
+        .add("filter", filter)
+        .toString();
   }
 }
diff --git a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
new file mode 100644
index 0000000..c68842d
--- /dev/null
+++ b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
@@ -0,0 +1,185 @@
+// 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.git;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Wrapper around {@link DelegateRefDatabase} that filters all refs using {@link
+ * com.google.gerrit.server.permissions.PermissionBackend}.
+ */
+public class PermissionAwareReadOnlyRefDatabase extends DelegateRefDatabase {
+
+  private final PermissionBackend.ForProject forProject;
+
+  @Inject
+  PermissionAwareReadOnlyRefDatabase(
+      Repository delegateRepository, PermissionBackend.ForProject forProject) {
+    super(delegateRepository);
+    this.forProject = forProject;
+  }
+
+  @Override
+  public boolean isNameConflicting(String name) {
+    throw new UnsupportedOperationException("PermissionAwareReadOnlyRefDatabase is read-only");
+  }
+
+  @Override
+  public RefUpdate newUpdate(String name, boolean detach) {
+    throw new UnsupportedOperationException("PermissionAwareReadOnlyRefDatabase is read-only");
+  }
+
+  @Override
+  public RefRename newRename(String fromName, String toName) {
+    throw new UnsupportedOperationException("PermissionAwareReadOnlyRefDatabase is read-only");
+  }
+
+  @Override
+  public Ref exactRef(String name) throws IOException {
+    Ref ref = getDelegate().getRefDatabase().exactRef(name);
+    if (ref == null) {
+      return null;
+    }
+
+    Map<String, Ref> result;
+    try {
+      result =
+          forProject.filter(ImmutableMap.of(name, ref), getDelegate(), RefFilterOptions.defaults());
+    } catch (PermissionBackendException e) {
+      if (e.getCause() instanceof IOException) {
+        throw (IOException) e.getCause();
+      }
+      throw new IOException(e);
+    }
+    if (result.isEmpty()) {
+      return null;
+    }
+
+    Preconditions.checkState(
+        result.size() == 1, "Only one element expected, but was: " + result.size());
+    return Iterables.getOnlyElement(result.values());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Override
+  public Map<String, Ref> getRefs(String prefix) throws IOException {
+    Map<String, Ref> refs = getDelegate().getRefDatabase().getRefs(prefix);
+    if (refs.isEmpty()) {
+      return refs;
+    }
+
+    Map<String, Ref> result;
+    try {
+      // The security filtering assumes to receive the same refMap, independently from the ref
+      // prefix offset
+      result =
+          forProject.filter(
+              prefixIndependentRefMap(prefix, refs), getDelegate(), RefFilterOptions.defaults());
+    } catch (PermissionBackendException e) {
+      throw new IOException("", e);
+    }
+    return applyPrefixRefMap(prefix, result);
+  }
+
+  private Map<String, Ref> prefixIndependentRefMap(String prefix, Map<String, Ref> refs) {
+    if (prefix.length() > 0) {
+      return refs.values().stream().collect(Collectors.toMap(Ref::getName, Function.identity()));
+    }
+
+    return refs;
+  }
+
+  private Map<String, Ref> applyPrefixRefMap(String prefix, Map<String, Ref> refs) {
+    int prefixSlashPos = prefix.lastIndexOf('/') + 1;
+    if (prefixSlashPos > 0) {
+      return refs.values().stream()
+          .collect(
+              Collectors.toMap(
+                  (Ref ref) -> ref.getName().substring(prefixSlashPos), Function.identity()));
+    }
+
+    return refs;
+  }
+
+  @Override
+  public List<Ref> getRefsByPrefix(String prefix) throws IOException {
+    Map<String, Ref> coarseRefs;
+    int lastSlash = prefix.lastIndexOf('/');
+    if (lastSlash == -1) {
+      coarseRefs = getRefs(ALL);
+    } else {
+      coarseRefs = getRefs(prefix.substring(0, lastSlash + 1));
+    }
+
+    List<Ref> result;
+    if (lastSlash + 1 == prefix.length()) {
+      result = coarseRefs.values().stream().collect(toList());
+    } else {
+      String p = prefix.substring(lastSlash + 1);
+      result =
+          coarseRefs.entrySet().stream()
+              .filter(e -> e.getKey().startsWith(p))
+              .map(e -> e.getValue())
+              .collect(toList());
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  @Override
+  @NonNull
+  public Map<String, Ref> exactRef(String... refs) throws IOException {
+    Map<String, Ref> result = new HashMap<>(refs.length);
+    for (String name : refs) {
+      Ref ref = exactRef(name);
+      if (ref != null) {
+        result.put(name, ref);
+      }
+    }
+    return result;
+  }
+
+  @Override
+  @Nullable
+  public Ref firstExactRef(String... refs) throws IOException {
+    for (String name : refs) {
+      Ref ref = exactRef(name);
+      if (ref != null) {
+        return ref;
+      }
+    }
+    return null;
+  }
+}
diff --git a/java/com/google/gerrit/server/git/PermissionAwareRepository.java b/java/com/google/gerrit/server/git/PermissionAwareRepository.java
new file mode 100644
index 0000000..bb80cb5
--- /dev/null
+++ b/java/com/google/gerrit/server/git/PermissionAwareRepository.java
@@ -0,0 +1,39 @@
+// 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.git;
+
+import com.google.gerrit.server.permissions.PermissionBackend;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Wrapper around {@link DelegateRepository} that overwrites {@link #getRefDatabase()} to return a
+ * {@link PermissionAwareReadOnlyRefDatabase}.
+ */
+public class PermissionAwareRepository extends DelegateRepository {
+
+  private final PermissionAwareReadOnlyRefDatabase permissionAwareReadOnlyRefDatabase;
+
+  public PermissionAwareRepository(Repository delegate, PermissionBackend.ForProject forProject) {
+    super(delegate);
+    this.permissionAwareReadOnlyRefDatabase =
+        new PermissionAwareReadOnlyRefDatabase(delegate, forProject);
+  }
+
+  @Override
+  public RefDatabase getRefDatabase() {
+    return permissionAwareReadOnlyRefDatabase;
+  }
+}
diff --git a/java/com/google/gerrit/server/git/PermissionAwareRepositoryManager.java b/java/com/google/gerrit/server/git/PermissionAwareRepositoryManager.java
new file mode 100644
index 0000000..b11aa49
--- /dev/null
+++ b/java/com/google/gerrit/server/git/PermissionAwareRepositoryManager.java
@@ -0,0 +1,32 @@
+// 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.git;
+
+import com.google.common.base.Preconditions;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Wraps and unwraps existing repositories and makes them permission-aware by returning a {@link
+ * PermissionAwareReadOnlyRefDatabase}.
+ */
+public class PermissionAwareRepositoryManager {
+  public static Repository wrap(Repository delegate, PermissionBackend.ForProject forProject) {
+    Preconditions.checkState(
+        !(delegate instanceof PermissionAwareRepository),
+        "Cannot wrap PermissionAwareRepository instance");
+    return new PermissionAwareRepository(delegate, forProject);
+  }
+}
diff --git a/java/com/google/gerrit/server/git/ProjectRunnable.java b/java/com/google/gerrit/server/git/ProjectRunnable.java
index 23d2326..e74bf2d 100644
--- a/java/com/google/gerrit/server/git/ProjectRunnable.java
+++ b/java/com/google/gerrit/server/git/ProjectRunnable.java
@@ -13,7 +13,7 @@
 // limitations under the License.
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 
 /** Used to retrieve the project name from an operation * */
 public interface ProjectRunnable extends Runnable {
diff --git a/java/com/google/gerrit/server/git/PureRevertCache.java b/java/com/google/gerrit/server/git/PureRevertCache.java
index 16ac87f..9f9530c 100644
--- a/java/com/google/gerrit/server/git/PureRevertCache.java
+++ b/java/com/google/gerrit/server/git/PureRevertCache.java
@@ -18,15 +18,16 @@
 import com.google.common.base.Throwables;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache;
 import com.google.gerrit.server.cache.proto.Cache.PureRevertKeyProto;
 import com.google.gerrit.server.cache.serialize.BooleanCacheSerializer;
 import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ProjectCache;
@@ -35,7 +36,6 @@
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 import com.google.protobuf.ByteString;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
@@ -49,6 +49,7 @@
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
 
 /** Computes and caches if a change is a pure revert of another change. */
 @Singleton
@@ -97,8 +98,8 @@
             claimedRevert.getProjectName(), claimedRevert.getChange().getRevertOf());
     return isPureRevert(
         claimedRevert.getProjectName(),
-        ObjectId.fromString(claimedRevert.getCurrentPatchSet().getRevision().get()),
-        ObjectId.fromString(claimedOriginal.getCurrentPatchSet().getRevision().get()));
+        claimedRevert.getCurrentPatchSet().commitId(),
+        claimedOriginal.getCurrentPatchSet().commitId());
   }
 
   /**
@@ -151,10 +152,12 @@
     @Override
     public Boolean load(PureRevertKeyProto key) throws BadRequestException, IOException {
       try (TraceContext.TraceTimer ignored =
-          TraceContext.newTimer("Loading pure revert for %s", key)) {
+          TraceContext.newTimer(
+              "Loading pure revert",
+              Metadata.builder().cacheKey(key.toString()).projectName(key.getProject()).build())) {
         ObjectId original = ObjectIdConverter.create().fromByteString(key.getClaimedOriginal());
         ObjectId revert = ObjectIdConverter.create().fromByteString(key.getClaimedRevert());
-        Project.NameKey project = new Project.NameKey(key.getProject());
+        Project.NameKey project = Project.nameKey(key.getProject());
 
         try (Repository repo = repoManager.openRepository(project);
             ObjectInserter oi = repo.newObjectInserter();
@@ -185,9 +188,8 @@
           }
 
           // Any differences between claimed original's parent and the rebase result indicate that
-          // the
-          // claimedRevert is not a pure revert but made content changes
-          try (DiffFormatter df = new DiffFormatter(new ByteArrayOutputStream())) {
+          // the claimedRevert is not a pure revert but made content changes
+          try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
             df.setReader(oi.newReader(), repo.getConfig());
             List<DiffEntry> entries =
                 df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
diff --git a/java/com/google/gerrit/server/git/ReceivePackInitializer.java b/java/com/google/gerrit/server/git/ReceivePackInitializer.java
index f374215..d7af280 100644
--- a/java/com/google/gerrit/server/git/ReceivePackInitializer.java
+++ b/java/com/google/gerrit/server/git/ReceivePackInitializer.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.Project;
 import org.eclipse.jgit.transport.ReceivePack;
 
 @ExtensionPoint
diff --git a/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java b/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
index 3a7a125..8535cd2 100644
--- a/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
+++ b/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
 /**
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index d7f8982..196fc61 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -20,14 +20,15 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -136,7 +137,7 @@
   @Override
   public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
     if (event.getRefName().startsWith(RefNames.REFS_CHANGES)) {
-      cache.invalidate(new Project.NameKey(event.getProjectName()));
+      cache.invalidate(Project.nameKey(event.getProjectName()));
     }
   }
 
@@ -152,7 +153,9 @@
 
     @Override
     public List<CachedChange> load(Project.NameKey key) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading changes of project %s", key);
+      try (TraceTimer timer =
+              TraceContext.newTimer(
+                  "Loading changes of project", Metadata.builder().projectName(key.get()).build());
           ManualRequestContext ctx = requestContext.open()) {
         List<ChangeData> cds =
             queryProvider
diff --git a/java/com/google/gerrit/server/git/SystemReaderInstaller.java b/java/com/google/gerrit/server/git/SystemReaderInstaller.java
new file mode 100644
index 0000000..b6cc108
--- /dev/null
+++ b/java/com/google/gerrit/server/git/SystemReaderInstaller.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.util.git.DelegateSystemReader;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
+
+@Singleton
+public class SystemReaderInstaller implements LifecycleListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final SitePaths site;
+
+  @Inject
+  SystemReaderInstaller(SitePaths site) {
+    this.site = site;
+  }
+
+  @Override
+  public void start() {
+    SystemReader.setInstance(customReader());
+    logger.atInfo().log("Set JGit's SystemReader to read system config from %s", site.jgit_config);
+  }
+
+  @Override
+  public void stop() {}
+
+  private SystemReader customReader() {
+    SystemReader current = SystemReader.getInstance();
+
+    return new DelegateSystemReader(current) {
+      @Override
+      public FileBasedConfig openSystemConfig(Config parent, FS fs) {
+        return new FileBasedConfig(parent, site.jgit_config.toFile(), FS.DETECTED);
+      }
+    };
+  }
+}
diff --git a/java/com/google/gerrit/server/git/TagCache.java b/java/com/google/gerrit/server/git/TagCache.java
index 535644d..daf5ea5 100644
--- a/java/com/google/gerrit/server/git/TagCache.java
+++ b/java/com/google/gerrit/server/git/TagCache.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.cache.Cache;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.serialize.StringCacheSerializer;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/git/TagSet.java b/java/com/google/gerrit/server/git/TagSet.java
index 57637c89..43483bf 100644
--- a/java/com/google/gerrit/server/git/TagSet.java
+++ b/java/com/google/gerrit/server/git/TagSet.java
@@ -18,9 +18,9 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.CachedRefProto;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.TagProto;
@@ -232,7 +232,7 @@
                     new Tag(
                         idConverter.fromByteString(t.getId()),
                         BitSet.valueOf(t.getFlags().asReadOnlyByteBuffer()))));
-    return new TagSet(new Project.NameKey(proto.getProjectName()), refs, tags);
+    return new TagSet(Project.nameKey(proto.getProjectName()), refs, tags);
   }
 
   TagSetProto toProto() {
diff --git a/java/com/google/gerrit/server/git/TagSetHolder.java b/java/com/google/gerrit/server/git/TagSetHolder.java
index 194283e..a574308 100644
--- a/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -17,8 +17,8 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.proto.Protos;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import java.util.Collection;
@@ -117,7 +117,7 @@
     @Override
     public TagSetHolder deserialize(byte[] in) {
       TagSetHolderProto proto = Protos.parseUnchecked(TagSetHolderProto.parser(), in);
-      TagSetHolder holder = new TagSetHolder(new Project.NameKey(proto.getProjectName()));
+      TagSetHolder holder = new TagSetHolder(Project.nameKey(proto.getProjectName()));
       if (proto.hasTags()) {
         holder.tags = TagSet.fromProto(proto.getTags());
       }
diff --git a/java/com/google/gerrit/server/git/TracingHook.java b/java/com/google/gerrit/server/git/TracingHook.java
new file mode 100644
index 0000000..56eded0
--- /dev/null
+++ b/java/com/google/gerrit/server/git/TracingHook.java
@@ -0,0 +1,96 @@
+// 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.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.server.logging.TraceContext;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.transport.FetchV2Request;
+import org.eclipse.jgit.transport.LsRefsV2Request;
+import org.eclipse.jgit.transport.ProtocolV2Hook;
+
+/**
+ * Git hook for ls-refs and fetch that enables Gerrit request tracing if the user sets the 'trace'
+ * server option.
+ *
+ * <p>This hook is only invoked if Git protocol v2 is used.
+ *
+ * <p>If the 'trace' server option is specified without value, this means without providing a trace
+ * ID, a trace ID is generated, but it's not returned to the client. Hence users are advised to
+ * always provide a trace ID.
+ */
+public class TracingHook implements ProtocolV2Hook, AutoCloseable {
+  private TraceContext traceContext;
+
+  @Override
+  public void onLsRefs(LsRefsV2Request req) {
+    maybeStartTrace(req.getServerOptions());
+  }
+
+  @Override
+  public void onFetch(FetchV2Request req) {
+    maybeStartTrace(req.getServerOptions());
+  }
+
+  @Override
+  public void close() {
+    if (traceContext != null) {
+      traceContext.close();
+    }
+  }
+
+  /**
+   * Starts request tracing if 'trace' server option is set.
+   *
+   * @param serverOptionList list of provided server options
+   */
+  private void maybeStartTrace(List<String> serverOptionList) {
+    if (traceContext != null) {
+      // Trace was already started
+      return;
+    }
+
+    Optional<String> traceOption = parseTraceOption(serverOptionList);
+    traceContext =
+        TraceContext.newTrace(
+            traceOption.isPresent(),
+            traceOption.orElse(null),
+            (tagName, traceId) -> {
+              // TODO(ekempin): Return trace ID to client
+            });
+  }
+
+  private Optional<String> parseTraceOption(List<String> serverOptionList) {
+    if (serverOptionList == null || serverOptionList.isEmpty()) {
+      return Optional.empty();
+    }
+
+    Optional<String> traceOption =
+        serverOptionList.stream().filter(o -> o.startsWith("trace")).findAny();
+    if (!traceOption.isPresent()) {
+      return Optional.empty();
+    }
+
+    int e = traceOption.get().indexOf('=');
+    if (e > 0) {
+      // trace option was specified with trace ID: "--trace=<trace-ID>"
+      return Optional.of(traceOption.get().substring(e + 1));
+    }
+
+    // trace option was specified without trace ID: "--trace",
+    // return an empty string so that a trace ID is generated
+    return Optional.of("");
+  }
+}
diff --git a/java/com/google/gerrit/server/git/UploadPackInitializer.java b/java/com/google/gerrit/server/git/UploadPackInitializer.java
index b63c5b3..7e97d87 100644
--- a/java/com/google/gerrit/server/git/UploadPackInitializer.java
+++ b/java/com/google/gerrit/server/git/UploadPackInitializer.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.Project;
 import org.eclipse.jgit.transport.UploadPack;
 
 @ExtensionPoint
diff --git a/java/com/google/gerrit/server/git/UploadPackMetricsHook.java b/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
index aa02fba..4afff2b 100644
--- a/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
+++ b/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.metrics.Histogram1;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import org.eclipse.jgit.storage.pack.PackStatistics;
@@ -43,14 +44,15 @@
 
   @Inject
   UploadPackMetricsHook(MetricMaker metricMaker) {
-    Field<Operation> operation = Field.ofEnum(Operation.class, "operation");
+    Field<Operation> operationField =
+        Field.ofEnum(Operation.class, "operation", Metadata.Builder::gitOperation).build();
     requestCount =
         metricMaker.newCounter(
             "git/upload-pack/request_count",
             new Description("Total number of git-upload-pack requests")
                 .setRate()
                 .setUnit("requests"),
-            operation);
+            operationField);
 
     counting =
         metricMaker.newTimer(
@@ -58,7 +60,7 @@
             new Description("Time spent in the 'Counting...' phase")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            operation);
+            operationField);
 
     compressing =
         metricMaker.newTimer(
@@ -66,7 +68,7 @@
             new Description("Time spent in the 'Compressing...' phase")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            operation);
+            operationField);
 
     writing =
         metricMaker.newTimer(
@@ -74,7 +76,7 @@
             new Description("Time spent transferring bytes to client")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            operation);
+            operationField);
 
     packBytes =
         metricMaker.newHistogram(
@@ -82,7 +84,7 @@
             new Description("Distribution of sizes of packs sent to clients")
                 .setCumulative()
                 .setUnit(Units.BYTES),
-            operation);
+            operationField);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/git/UsersSelfAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/UsersSelfAdvertiseRefsHook.java
new file mode 100644
index 0000000..6c1879e
--- /dev/null
+++ b/java/com/google/gerrit/server/git/UsersSelfAdvertiseRefsHook.java
@@ -0,0 +1,93 @@
+// 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.git;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Map;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.transport.AdvertiseRefsHook;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+import org.eclipse.jgit.transport.UploadPack;
+
+/**
+ * Advertises {@code refs/users/self} for authenticated users when interacting with the {@code
+ * All-Users} repository.
+ */
+@Singleton
+public class UsersSelfAdvertiseRefsHook implements AdvertiseRefsHook {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final Provider<CurrentUser> userProvider;
+
+  @Inject
+  public UsersSelfAdvertiseRefsHook(Provider<CurrentUser> userProvider) {
+    this.userProvider = userProvider;
+  }
+
+  @Override
+  public void advertiseRefs(UploadPack uploadPack) throws ServiceMayNotContinueException {
+    CurrentUser user = userProvider.get();
+    if (!user.isIdentifiedUser()) {
+      return;
+    }
+
+    addSelfSymlinkIfNecessary(
+        uploadPack.getRepository().getRefDatabase(),
+        HookUtil.ensureAllRefsAdvertised(uploadPack),
+        user.getAccountId());
+  }
+
+  @Override
+  public void advertiseRefs(ReceivePack receivePack) throws ServiceMayNotContinueException {
+    CurrentUser user = userProvider.get();
+    if (!user.isIdentifiedUser()) {
+      return;
+    }
+
+    addSelfSymlinkIfNecessary(
+        receivePack.getRepository().getRefDatabase(),
+        HookUtil.ensureAllRefsAdvertised(receivePack),
+        user.getAccountId());
+  }
+
+  private static void addSelfSymlinkIfNecessary(
+      RefDatabase refDatabase, Map<String, Ref> advertisedRefs, Account.Id accountId)
+      throws ServiceMayNotContinueException {
+    String refName = RefNames.refsUsers(accountId);
+    try {
+      Ref r = refDatabase.exactRef(refName);
+      if (r == null) {
+        logger.atWarning().log("User ref %s not found", refName);
+        return;
+      }
+
+      SymbolicRef s = new SymbolicRef(RefNames.REFS_USERS_SELF, r);
+      advertisedRefs.put(s.getName(), s);
+      logger.atFinest().log("Added %s as alias for user ref %s", RefNames.REFS_USERS_SELF, refName);
+    } catch (IOException e) {
+      throw new ServiceMayNotContinueException(e);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 4522b0d..f2a0ff1 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -18,11 +18,11 @@
 
 import com.google.common.base.CaseFormat;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.ScheduleConfig.Schedule;
 import com.google.gerrit.server.logging.LoggingContext;
diff --git a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
index 97beefd..e90f58b 100644
--- a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
+++ b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git.meta;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index 5cda238..d588441 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -19,8 +19,11 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.git.GitUpdateFailureException;
 import com.google.gerrit.git.LockFailureException;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.git.ObjectIds;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.util.CommitMessageUtil;
@@ -114,7 +117,7 @@
   /** @return revision of the metadata that was loaded. */
   @Nullable
   public ObjectId getRevision() {
-    return revision != null ? revision.copy() : null;
+    return ObjectIds.copyOrNull(revision);
   }
 
   /**
@@ -487,18 +490,18 @@
           case REJECTED_MISSING_OBJECT:
           case REJECTED_OTHER_REASON:
           default:
-            throw new IOException(errorMsg(ru, db.getDirectory()));
+            throw new GitUpdateFailureException(errorMsg(ru, db.getDirectory()), ru);
         }
       }
-
-      private String errorMsg(RefUpdate ru, File location) {
-        return String.format(
-            "Cannot update %s in %s: %s (%s)",
-            ru.getName(), location, ru.getResult(), ru.getRefLogMessage());
-      }
     };
   }
 
+  private String errorMsg(RefUpdate ru, File location) {
+    return String.format(
+        "Cannot update %s in %s: %s (%s)",
+        ru.getName(), location, ru.getResult(), ru.getRefLogMessage());
+  }
+
   protected DirCache readTree(RevTree tree)
       throws IOException, MissingObjectException, IncorrectObjectTypeException {
     DirCache dc = DirCache.newInCore();
@@ -548,8 +551,13 @@
 
     try (TraceTimer timer =
             TraceContext.newTimer(
-                "Read file '%s' from ref '%s' of project '%s' from revision '%s'",
-                fileName, getRefName(), projectName, revision.name());
+                "Read file",
+                Metadata.builder()
+                    .projectName(projectName.get())
+                    .noteDbRefName(getRefName())
+                    .revision(revision.name())
+                    .noteDbFilePath(fileName)
+                    .build());
         TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree())) {
       if (tw != null) {
         ObjectLoader obj = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
@@ -624,7 +632,12 @@
   protected void saveFile(String fileName, byte[] raw) throws IOException {
     try (TraceTimer timer =
         TraceContext.newTimer(
-            "Save file '%s' in ref '%s' of project '%s'", fileName, getRefName(), projectName)) {
+            "Save file",
+            Metadata.builder()
+                .projectName(projectName.get())
+                .noteDbRefName(getRefName())
+                .noteDbFilePath(fileName)
+                .build())) {
       DirCacheEditor editor = newTree.editor();
       if (raw != null && 0 < raw.length) {
         final ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, raw);
diff --git a/java/com/google/gerrit/server/git/receive/AllRefsWatcher.java b/java/com/google/gerrit/server/git/receive/AllRefsWatcher.java
index c092c43..13ae54a 100644
--- a/java/com/google/gerrit/server/git/receive/AllRefsWatcher.java
+++ b/java/com/google/gerrit/server/git/receive/AllRefsWatcher.java
@@ -20,7 +20,7 @@
 import java.util.Map;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
-import org.eclipse.jgit.transport.BaseReceivePack;
+import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.ServiceMayNotContinueException;
 import org.eclipse.jgit.transport.UploadPack;
 
@@ -34,7 +34,7 @@
   private Map<String, Ref> allRefs;
 
   @Override
-  public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException {
+  public void advertiseRefs(ReceivePack rp) throws ServiceMayNotContinueException {
     allRefs = HookUtil.ensureAllRefsAdvertised(rp);
   }
 
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 4475465..c68ed30 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -21,6 +21,8 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.metrics.Counter0;
@@ -30,19 +32,19 @@
 import com.google.gerrit.metrics.Histogram1;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.cache.PerThreadCache;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.ReceiveCommitsExecutor;
-import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
 import com.google.gerrit.server.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.PermissionAwareRepositoryManager;
 import com.google.gerrit.server.git.ProjectRunnable;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.UsersSelfAdvertiseRefsHook;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
@@ -63,7 +65,6 @@
 import com.google.inject.name.Named;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
@@ -71,8 +72,6 @@
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.AdvertiseRefsHook;
-import org.eclipse.jgit.transport.AdvertiseRefsHookChain;
 import org.eclipse.jgit.transport.PreReceiveHook;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
@@ -204,11 +203,20 @@
 
     @Inject
     Metrics(MetricMaker metricMaker) {
+      // For the changes metric the push type field is never set to PushType.NORMAL, hence it is not
+      // mentioned in the field description.
       changes =
           metricMaker.newHistogram(
               "receivecommits/changes_per_push",
               new Description("number of changes uploaded in a single push.").setCumulative(),
-              Field.ofEnum(PushType.class, "type", "type of push (create/replace, autoclose)"));
+              Field.ofEnum(PushType.class, "type", Metadata.Builder::pushType)
+                  .description("type of push (create/replace, autoclose)")
+                  .build());
+
+      Field<PushType> pushTypeField =
+          Field.ofEnum(PushType.class, "type", Metadata.Builder::pushType)
+              .description("type of push (create/replace, autoclose, normal)")
+              .build();
 
       latencyPerChange =
           metricMaker.newTimer(
@@ -218,7 +226,7 @@
                           + "(Only includes pushes which contain changes.)")
                   .setUnit(Units.MILLISECONDS)
                   .setCumulative(),
-              Field.ofEnum(PushType.class, "type", "type of push (create/replace, autoclose)"));
+              pushTypeField);
 
       latencyPerPush =
           metricMaker.newTimer(
@@ -226,8 +234,7 @@
               new Description("processing delay for a processing single push")
                   .setUnit(Units.MILLISECONDS)
                   .setCumulative(),
-              Field.ofEnum(
-                  PushType.class, "type", "type of push (create/replace, autoclose, normal)"));
+              pushTypeField);
 
       timeouts =
           metricMaker.newCounter(
@@ -263,6 +270,8 @@
       ContributorAgreementsChecker contributorAgreements,
       Metrics metrics,
       QuotaBackend quotaBackend,
+      UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook,
+      AllUsersName allUsersName,
       @Named(TIMEOUT_NAME) long timeoutMillis,
       @Assisted ProjectState projectState,
       @Assisted IdentifiedUser user,
@@ -278,9 +287,12 @@
     this.user = user;
     this.repo = repo;
     this.metrics = metrics;
-
+    // If the user lacks READ permission, some references may be filtered and hidden from view.
+    // Check objects mentioned inside the incoming pack file are reachable from visible refs.
     Project.NameKey projectName = projectState.getNameKey();
-    receivePack = new ReceivePack(repo);
+    this.perm = permissionBackend.user(user).project(projectName);
+
+    receivePack = new ReceivePack(PermissionAwareRepositoryManager.wrap(repo, perm));
     receivePack.setAllowCreates(true);
     receivePack.setAllowDeletes(true);
     receivePack.setAllowNonFastForwards(true);
@@ -293,9 +305,6 @@
     receivePack.setPreReceiveHook(this);
     receivePack.setPostReceiveHook(lazyPostReceive.create(user, projectName));
 
-    // If the user lacks READ permission, some references may be filtered and hidden from view.
-    // Check objects mentioned inside the incoming pack file are reachable from visible refs.
-    this.perm = permissionBackend.user(user).project(projectName);
     try {
       projectState.checkStatePermitsRead();
       this.perm.check(ProjectPermission.READ);
@@ -304,19 +313,14 @@
           receiveConfig.checkReferencedObjectsAreReachable);
     }
 
-    List<AdvertiseRefsHook> advHooks = new ArrayList<>(4);
     allRefsWatcher = new AllRefsWatcher();
-    advHooks.add(allRefsWatcher);
-    advHooks.add(
-        new DefaultAdvertiseRefsHook(perm, RefFilterOptions.builder().setFilterMeta(true).build()));
-    advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName));
-    advHooks.add(new HackPushNegotiateHook());
-    receivePack.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
-
+    receivePack.setAdvertiseRefsHook(
+        ReceiveCommitsAdvertiseRefsHookChain.create(
+            allRefsWatcher, usersSelfAdvertiseRefsHook, allUsersName, queryProvider, projectName));
     resultChangeIds = new ResultChangeIds();
     receiveCommits =
         factory.create(
-            projectState, user, receivePack, allRefsWatcher, messageSender, resultChangeIds);
+            projectState, user, receivePack, repo, allRefsWatcher, messageSender, resultChangeIds);
     receiveCommits.init();
     QuotaResponse.Aggregated availableTokens =
         quotaBackend.user(user).project(projectName).availableTokens(REPOSITORY_SIZE_GROUP);
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index b1bf933..d89bb63 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -7,21 +7,22 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/metrics",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/util/cli",
         "//lib:args4j",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
index a2d8e94..7b5f90bd 100644
--- a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
+++ b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
@@ -14,27 +14,29 @@
 
 package com.google.gerrit.server.git.receive;
 
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.util.List;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -48,12 +50,29 @@
   private final IdentifiedUser user;
   private final PermissionBackend.ForProject permissions;
   private final Project project;
-  private final Branch.NameKey branch;
+  private final BranchNameKey branch;
   private final SshInfo sshInfo;
 
   interface Factory {
     BranchCommitValidator create(
-        ProjectState projectState, Branch.NameKey branch, IdentifiedUser user);
+        ProjectState projectState, BranchNameKey branch, IdentifiedUser user);
+  }
+
+  /** A boolean validation status and a list of additional messages. */
+  @AutoValue
+  abstract static class Result {
+    static Result create(boolean isValid, ImmutableList<CommitValidationMessage> messages) {
+      return new AutoValue_BranchCommitValidator_Result(isValid, messages);
+    }
+
+    /** Whether the commit is valid. */
+    abstract boolean isValid();
+
+    /**
+     * A list of messages related to the validation. Messages may be present regardless of the
+     * {@link #isValid()} status.
+     */
+    abstract ImmutableList<CommitValidationMessage> messages();
   }
 
   @Inject
@@ -62,7 +81,7 @@
       PermissionBackend permissionBackend,
       SshInfo sshInfo,
       @Assisted ProjectState projectState,
-      @Assisted Branch.NameKey branch,
+      @Assisted BranchNameKey branch,
       @Assisted IdentifiedUser user) {
     this.sshInfo = sshInfo;
     this.user = user;
@@ -80,17 +99,17 @@
    * @param commit the commit being validated.
    * @param isMerged whether this is a merge commit created by magicBranch --merge option
    * @param change the change for which this is a new patchset.
+   * @return The validation {@link Result}.
    */
-  public boolean validCommit(
+  Result validateCommit(
       ObjectReader objectReader,
       ReceiveCommand cmd,
       RevCommit commit,
       boolean isMerged,
-      List<ValidationMessage> messages,
       NoteMap rejectCommits,
       @Nullable Change change)
       throws IOException {
-    return validCommit(objectReader, cmd, commit, isMerged, messages, rejectCommits, change, false);
+    return validateCommit(objectReader, cmd, commit, isMerged, rejectCommits, change, false);
   }
 
   /**
@@ -102,55 +121,63 @@
    * @param isMerged whether this is a merge commit created by magicBranch --merge option
    * @param change the change for which this is a new patchset.
    * @param skipValidation whether 'skip-validation' was requested.
+   * @return The validation {@link Result}.
    */
-  public boolean validCommit(
+  Result validateCommit(
       ObjectReader objectReader,
       ReceiveCommand cmd,
       RevCommit commit,
       boolean isMerged,
-      List<ValidationMessage> messages,
       NoteMap rejectCommits,
       @Nullable Change change,
       boolean skipValidation)
       throws IOException {
-    try (CommitReceivedEvent receiveEvent =
-        new CommitReceivedEvent(cmd, project, branch.get(), objectReader, commit, user)) {
-      CommitValidators validators;
-      if (isMerged) {
-        validators =
-            commitValidatorsFactory.forMergedCommits(permissions, branch, user.asIdentifiedUser());
-      } else {
-        validators =
-            commitValidatorsFactory.forReceiveCommits(
-                permissions,
-                branch,
-                user.asIdentifiedUser(),
-                sshInfo,
-                rejectCommits,
-                receiveEvent.revWalk,
-                change,
-                skipValidation);
-      }
+    try (TraceTimer traceTimer = TraceContext.newTimer("BranchCommitValidator#validateCommit")) {
+      ImmutableList.Builder<CommitValidationMessage> messages = new ImmutableList.Builder<>();
+      try (CommitReceivedEvent receiveEvent =
+          new CommitReceivedEvent(cmd, project, branch.branch(), objectReader, commit, user)) {
+        CommitValidators validators;
+        if (isMerged) {
+          validators =
+              commitValidatorsFactory.forMergedCommits(
+                  permissions, branch, user.asIdentifiedUser());
+        } else {
+          validators =
+              commitValidatorsFactory.forReceiveCommits(
+                  permissions,
+                  branch,
+                  user.asIdentifiedUser(),
+                  sshInfo,
+                  rejectCommits,
+                  receiveEvent.revWalk,
+                  change,
+                  skipValidation);
+        }
 
-      for (CommitValidationMessage m : validators.validate(receiveEvent)) {
-        messages.add(
-            new CommitValidationMessage(messageForCommit(commit, m.getMessage()), m.getType()));
+        for (CommitValidationMessage m : validators.validate(receiveEvent)) {
+          messages.add(
+              new CommitValidationMessage(
+                  messageForCommit(commit, m.getMessage(), objectReader), m.getType()));
+        }
+      } catch (CommitValidationException e) {
+        logger.atFine().log("Commit validation failed on %s", commit.name());
+        for (CommitValidationMessage m : e.getMessages()) {
+          // The non-error messages may contain background explanation for the
+          // fatal error, so have to preserve all messages.
+          messages.add(
+              new CommitValidationMessage(
+                  messageForCommit(commit, m.getMessage(), objectReader), m.getType()));
+        }
+        cmd.setResult(
+            REJECTED_OTHER_REASON, messageForCommit(commit, e.getMessage(), objectReader));
+        return Result.create(false, messages.build());
       }
-    } catch (CommitValidationException e) {
-      logger.atFine().log("Commit validation failed on %s", commit.name());
-      for (CommitValidationMessage m : e.getMessages()) {
-        // The non-error messages may contain background explanation for the
-        // fatal error, so have to preserve all messages.
-        messages.add(
-            new CommitValidationMessage(messageForCommit(commit, m.getMessage()), m.getType()));
-      }
-      cmd.setResult(REJECTED_OTHER_REASON, messageForCommit(commit, e.getMessage()));
-      return false;
+      return Result.create(true, messages.build());
     }
-    return true;
   }
 
-  private String messageForCommit(RevCommit c, String msg) {
-    return String.format("commit %s: %s", c.abbreviate(RevId.ABBREV_LEN).name(), msg);
+  private String messageForCommit(RevCommit c, String msg, ObjectReader objectReader)
+      throws IOException {
+    return String.format("commit %s: %s", abbreviateName(c, objectReader), msg);
   }
 }
diff --git a/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
index 36d0eb7..72483af 100644
--- a/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
+++ b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
@@ -18,18 +18,17 @@
 
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.git.ObjectIds;
 import java.io.IOException;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
-import org.eclipse.jgit.transport.BaseReceivePack;
+import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.ServiceMayNotContinueException;
 import org.eclipse.jgit.transport.UploadPack;
 
@@ -49,7 +48,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /** Size of an additional ".have" line. */
-  private static final int HAVE_LINE_LEN = 4 + Constants.OBJECT_ID_STRING_LENGTH + 1 + 5 + 1;
+  private static final int HAVE_LINE_LEN = 4 + ObjectIds.STR_LEN + 1 + 5 + 1;
 
   /**
    * Maximum number of bytes to "waste" in the advertisement with a peek at this repository's
@@ -73,9 +72,8 @@
     throw new UnsupportedOperationException("HackPushNegotiateHook cannot be used for UploadPack");
   }
 
-  @SuppressWarnings("deprecation")
   @Override
-  public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException {
+  public void advertiseRefs(ReceivePack rp) throws ServiceMayNotContinueException {
     Map<String, Ref> r = rp.getAdvertisedRefs();
     if (r == null) {
       try {
@@ -91,38 +89,32 @@
     rp.setAdvertisedRefs(r, history(r.values(), rp));
   }
 
-  private Set<ObjectId> history(Collection<Ref> refs, BaseReceivePack rp) {
+  private Set<ObjectId> history(Collection<Ref> refs, ReceivePack rp) {
     Set<ObjectId> alreadySending = rp.getAdvertisedObjects();
-    if (alreadySending.isEmpty()) {
-      alreadySending = idsOf(refs);
-    }
-
-    int max = MAX_HISTORY - Math.max(0, alreadySending.size() - refs.size());
-    if (max <= 0) {
-      return Collections.emptySet();
+    if (MAX_HISTORY <= alreadySending.size()) {
+      return alreadySending;
     }
 
     // Scan history until the advertisement is full.
-    @SuppressWarnings("deprecation")
     RevWalk rw = rp.getRevWalk();
     rw.reset();
     try {
-      for (Ref ref : refs) {
+      Set<ObjectId> tips = idsOf(refs);
+      for (ObjectId tip : tips) {
         try {
-          if (ref.getObjectId() != null) {
-            rw.markStart(rw.parseCommit(ref.getObjectId()));
-          }
+          rw.markStart(rw.parseCommit(tip));
         } catch (IOException badCommit) {
           continue;
         }
       }
 
-      Set<ObjectId> history = Sets.newHashSetWithExpectedSize(max);
+      Set<ObjectId> history = Sets.newHashSetWithExpectedSize(MAX_HISTORY);
+      history.addAll(alreadySending);
       try {
         int stepCnt = 0;
-        for (RevCommit c; history.size() < max && (c = rw.next()) != null; ) {
+        for (RevCommit c; history.size() < MAX_HISTORY && (c = rw.next()) != null; ) {
           if (c.getParentCount() <= 1
-              && !alreadySending.contains(c)
+              && !tips.contains(c)
               && (history.size() < BASE_COMMITS || (++stepCnt % STEP_COMMITS) == 0)) {
             history.add(c);
           }
diff --git a/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java b/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
index 17d4a38..a19dbac 100644
--- a/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
+++ b/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.quota.QuotaBackend;
@@ -61,7 +61,7 @@
   @Override
   public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
     hooks.runEach(h -> h.onPostReceive(rp, commands));
-    if (affectsSize(rp, commands)) {
+    if (affectsSize(rp)) {
       QuotaResponse.Aggregated a =
           quotaBackend
               .user(user)
@@ -78,21 +78,7 @@
     }
   }
 
-  public static boolean affectsSize(ReceivePack rp, Collection<ReceiveCommand> commands) {
-    long packSize;
-    try {
-      packSize = rp.getPackSize();
-    } catch (IllegalStateException e) {
-      // No pack was received, i.e. ref deletion or wind back
-      return false;
-    }
-    if (packSize > 0L) {
-      for (ReceiveCommand cmd : commands) {
-        if (cmd.getType() != ReceiveCommand.Type.DELETE) {
-          return true;
-        }
-      }
-    }
-    return false;
+  public static boolean affectsSize(ReceivePack rp) {
+    return rp.hasReceivedPack() && rp.getPackSize() > 0L;
   }
 }
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 2de5dbc..012ba43 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -15,17 +15,17 @@
 package com.google.gerrit.server.git.receive;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.flogger.LazyArgs.lazy;
-import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
-import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
+import static com.google.gerrit.entities.RefNames.isConfigRef;
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static com.google.gerrit.server.change.HashtagsUtil.cleanupHashtag;
 import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
 import static com.google.gerrit.server.git.receive.ReceiveConstants.COMMAND_REJECTION_MESSAGE_FOOTER;
-import static com.google.gerrit.server.git.receive.ReceiveConstants.ONLY_CHANGE_OWNER_OR_PROJECT_OWNER_CAN_MODIFY_WIP;
+import static com.google.gerrit.server.git.receive.ReceiveConstants.ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP;
 import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
 import static com.google.gerrit.server.git.receive.ReceiveConstants.SAME_CHANGE_ID_IN_MULTIPLE_CHANGES;
 import static com.google.gerrit.server.git.validators.CommitValidators.NEW_PATCHSET_PATTERN;
@@ -40,7 +40,6 @@
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
 
-import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
@@ -48,6 +47,7 @@
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
@@ -62,37 +62,48 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 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.BooleanProjectConfig;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetInfo;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentForValidation.CommentType;
+import com.google.gerrit.extensions.validators.CommentValidationFailure;
+import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.PublishCommentUtil;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.NotifyResolver;
@@ -118,8 +129,12 @@
 import com.google.gerrit.server.git.validators.RefOperationValidators;
 import com.google.gerrit.server.git.validators.ValidationMessage;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.PerformanceLogContext;
+import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.Sequences;
@@ -180,11 +195,12 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Queue;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
-import java.util.regex.Matcher;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -223,9 +239,6 @@
 class ReceiveCommits {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private static final String CODE_REVIEW_ERROR =
-      "You need 'Push' rights to upload code review requests.\n"
-          + "Verify that you are pushing to the right branch.";
   private static final String CANNOT_DELETE_CHANGES = "Cannot delete from '" + REFS_CHANGES + "'";
   private static final String CANNOT_DELETE_CONFIG =
       "Cannot delete project configuration from '" + RefNames.REFS_CONFIG + "'";
@@ -236,6 +249,7 @@
         ProjectState projectState,
         IdentifiedUser user,
         ReceivePack receivePack,
+        Repository repository,
         AllRefsWatcher allRefsWatcher,
         MessageSender messageSender,
         ResultChangeIds resultChangeIds);
@@ -276,16 +290,14 @@
     }
   }
 
-  private static final Function<Exception, RestApiException> INSERT_EXCEPTION =
-      input -> {
-        if (input instanceof RestApiException) {
-          return (RestApiException) input;
-        } else if ((input instanceof ExecutionException)
-            && (input.getCause() instanceof RestApiException)) {
-          return (RestApiException) input.getCause();
-        }
-        return new RestApiException("Error inserting change/patchset", input);
-      };
+  private static RestApiException asRestApiException(Exception e) {
+    if (e instanceof RestApiException) {
+      return (RestApiException) e;
+    } else if ((e instanceof ExecutionException) && (e.getCause() instanceof RestApiException)) {
+      return (RestApiException) e.getCause();
+    }
+    return new RestApiException("Error inserting change/patchset", e);
+  }
 
   // ReceiveCommits has a lot of fields, sorry. Here and in the constructor they are split up
   // somewhat, and kept sorted lexicographically within sections, except where later assignments
@@ -301,7 +313,10 @@
   private final ChangeNotes.Factory notesFactory;
   private final ChangeReportFormatter changeFormatter;
   private final CmdLineParser.Factory optionParserFactory;
+  private final CommentsUtil commentsUtil;
+  private final PluginSetContext<CommentValidator> commentValidators;
   private final BranchCommitValidator.Factory commitValidatorFactory;
+  private final Config config;
   private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
   private final CreateRefControl createRefControl;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
@@ -309,6 +324,7 @@
   private final MergedByPushOp.Factory mergedByPushOpFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final PatchSetUtil psUtil;
+  private final DynamicSet<PerformanceLogger> performanceLoggers;
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
   private final Provider<InternalChangeQuery> queryProvider;
@@ -317,6 +333,7 @@
   private final ReceiveConfig receiveConfig;
   private final RefOperationValidators.Factory refValidatorsFactory;
   private final ReplaceOp.Factory replaceOpFactory;
+  private final PluginSetContext<RequestListener> requestListeners;
   private final RetryHelper retryHelper;
   private final RequestScopePropagator requestScopePropagator;
   private final Sequences seq;
@@ -334,7 +351,6 @@
 
   // Immutable fields derived from constructor arguments.
   private final boolean allowProjectOwnersToChangeParent;
-  private final boolean allowPushToRefsChanges;
   private final LabelTypes labelTypes;
   private final NoteMap rejectCommits;
   private final PermissionBackend.ForProject permissions;
@@ -343,7 +359,7 @@
 
   // Collections populated during processing.
   private final List<UpdateGroupsRequest> updateGroups;
-  private final List<ValidationMessage> messages;
+  private final Queue<ValidationMessage> messages;
   /** Multimap of error text to refnames that produced that error. */
   private final ListMultimap<String, String> errors;
 
@@ -363,6 +379,7 @@
 
   private MessageSender messageSender;
   private ResultChangeIds resultChangeIds;
+  private ImmutableMap<String, String> loggingTags;
 
   @Inject
   ReceiveCommits(
@@ -370,21 +387,24 @@
       AllProjectsName allProjectsName,
       BatchUpdate.Factory batchUpdateFactory,
       ProjectConfig.Factory projectConfigFactory,
-      @GerritServerConfig Config cfg,
+      @GerritServerConfig Config config,
       ChangeEditUtil editUtil,
       ChangeIndexer indexer,
       ChangeInserter.Factory changeInserterFactory,
       ChangeNotes.Factory notesFactory,
       DynamicItem<ChangeReportFormatter> changeFormatterProvider,
       CmdLineParser.Factory optionParserFactory,
+      CommentsUtil commentsUtil,
       BranchCommitValidator.Factory commitValidatorFactory,
       CreateGroupPermissionSyncer createGroupPermissionSyncer,
       CreateRefControl createRefControl,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginSetContext<ReceivePackInitializer> initializers,
+      PluginSetContext<CommentValidator> commentValidators,
       MergedByPushOp.Factory mergedByPushOpFactory,
       PatchSetInfoFactory patchSetInfoFactory,
       PatchSetUtil psUtil,
+      DynamicSet<PerformanceLogger> performanceLoggers,
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       Provider<InternalChangeQuery> queryProvider,
@@ -393,6 +413,7 @@
       ReceiveConfig receiveConfig,
       RefOperationValidators.Factory refValidatorsFactory,
       ReplaceOp.Factory replaceOpFactory,
+      PluginSetContext<RequestListener> requestListeners,
       RetryHelper retryHelper,
       RequestScopePropagator requestScopePropagator,
       Sequences seq,
@@ -403,6 +424,7 @@
       @Assisted ProjectState projectState,
       @Assisted IdentifiedUser user,
       @Assisted ReceivePack rp,
+      @Assisted Repository repository,
       @Assisted AllRefsWatcher allRefsWatcher,
       @Nullable @Assisted MessageSender messageSender,
       @Assisted ResultChangeIds resultChangeIds)
@@ -413,7 +435,10 @@
     this.batchUpdateFactory = batchUpdateFactory;
     this.changeFormatter = changeFormatterProvider.get();
     this.changeInserterFactory = changeInserterFactory;
+    this.commentsUtil = commentsUtil;
+    this.commentValidators = commentValidators;
     this.commitValidatorFactory = commitValidatorFactory;
+    this.config = config;
     this.createRefControl = createRefControl;
     this.createGroupPermissionSyncer = createGroupPermissionSyncer;
     this.editUtil = editUtil;
@@ -430,10 +455,12 @@
     this.pluginConfigEntries = pluginConfigEntries;
     this.projectCache = projectCache;
     this.psUtil = psUtil;
+    this.performanceLoggers = performanceLoggers;
     this.queryProvider = queryProvider;
     this.receiveConfig = receiveConfig;
     this.refValidatorsFactory = refValidatorsFactory;
     this.replaceOpFactory = replaceOpFactory;
+    this.requestListeners = requestListeners;
     this.retryHelper = retryHelper;
     this.requestScopePropagator = requestScopePropagator;
     this.seq = seq;
@@ -447,24 +474,25 @@
     this.projectState = projectState;
     this.user = user;
     this.receivePack = rp;
+    // This repository instance in unwrapped, while the repository instance in
+    // receivePack.getRepo() is wrapped in PermissionAwareRepository instance.
+    this.repo = repository;
 
     // Immutable fields derived from constructor arguments.
-    allowPushToRefsChanges = cfg.getBoolean("receive", "allowPushToRefsChanges", false);
-    repo = rp.getRepository();
     project = projectState.getProject();
     labelTypes = projectState.getLabelTypes();
     permissions = permissionBackend.user(user).project(project.getNameKey());
-    rejectCommits = BanCommit.loadRejectCommitsMap(rp.getRepository(), rp.getRevWalk());
+    rejectCommits = BanCommit.loadRejectCommitsMap(repo, rp.getRevWalk());
 
     // Collections populated during processing.
     errors = MultimapBuilder.linkedHashKeys().arrayListValues().build();
-    messages = new ArrayList<>();
+    messages = new ConcurrentLinkedQueue<>();
     pushOptions = LinkedListMultimap.create();
     replaceByChange = new LinkedHashMap<>();
     updateGroups = new ArrayList<>();
 
     this.allowProjectOwnersToChangeParent =
-        cfg.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
+        config.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
 
     // Other settings populated during processing.
     newChangeForAllNotInTarget =
@@ -473,6 +501,7 @@
     // Handles for outputting back over the wire to the end user.
     this.messageSender = messageSender != null ? messageSender : new ReceivePackMessageSender();
     this.resultChangeIds = resultChangeIds;
+    this.loggingTags = ImmutableMap.of();
   }
 
   void init() {
@@ -499,127 +528,150 @@
     addMessage(error, ValidationMessage.Type.ERROR);
   }
 
+  /**
+   * Sends all messages which have been collected while processing the push to the client.
+   *
+   * <p><strong>Attention:</strong>{@link AsyncReceiveCommits} may call this method while {@link
+   * #processCommands(Collection, MultiProgressMonitor)} is still running (if the execution of
+   * processCommands takes too long and AsyncReceiveCommits gets a timeout). This means that local
+   * variables that are accessed in this method must be thread-safe (otherwise we may hit a {@link
+   * java.util.ConcurrentModificationException} if we read a variable here that at the same time is
+   * updated by the background thread that still executes processCommands).
+   */
   void sendMessages() {
-    for (ValidationMessage m : messages) {
-      String msg = m.getType().getPrefix() + m.getMessage();
+    try (TraceContext traceContext =
+        TraceContext.newTrace(
+            loggingTags.containsKey(RequestId.Type.TRACE_ID.name()),
+            loggingTags.get(RequestId.Type.TRACE_ID.name()),
+            (tagName, traceId) -> {})) {
+      loggingTags.forEach((tagName, tagValue) -> traceContext.addTag(tagName, tagValue));
 
-      // Avoid calling sendError which will add its own error: prefix.
-      messageSender.sendMessage(msg);
+      for (ValidationMessage m : messages) {
+        String msg = m.getType().getPrefix() + m.getMessage();
+        logger.atFine().log("Sending message: %s", msg);
+
+        // Avoid calling sendError which will add its own error: prefix.
+        messageSender.sendMessage(msg);
+      }
     }
   }
 
   void processCommands(Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
-    Task commandProgress = progress.beginSubTask("refs", UNKNOWN);
-    commands = commands.stream().map(c -> wrapReceiveCommand(c, commandProgress)).collect(toList());
-    processCommandsUnsafe(commands, progress);
-    rejectRemaining(commands, INTERNAL_SERVER_ERROR);
-
-    // This sends error messages before the 'done' string of the progress monitor is sent.
-    // Currently, the test framework relies on this ordering to understand if pushes completed
-    // successfully.
-    sendErrorMessages();
-
-    commandProgress.end();
-    progress.end();
-  }
-
-  // Process as many commands as possible, but may leave some commands in state NOT_ATTEMPTED.
-  private void processCommandsUnsafe(
-      Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
     parsePushOptions();
+    int commandCount = commands.size();
     try (TraceContext traceContext =
-        TraceContext.newTrace(
-            tracePushOption.isPresent(),
-            tracePushOption.orElse(null),
-            (tagName, traceId) -> addMessage(tagName + ": " + traceId))) {
+            TraceContext.newTrace(
+                tracePushOption.isPresent(),
+                tracePushOption.orElse(null),
+                (tagName, traceId) -> addMessage(tagName + ": " + traceId));
+        TraceTimer traceTimer =
+            newTimer("processCommands", Metadata.builder().resourceCount(commandCount));
+        PerformanceLogContext performanceLogContext =
+            new PerformanceLogContext(config, performanceLoggers)) {
+      RequestInfo requestInfo =
+          RequestInfo.builder(RequestInfo.RequestType.GIT_RECEIVE, user, traceContext)
+              .project(project.getNameKey())
+              .build();
+      requestListeners.runEach(l -> l.onRequest(requestInfo));
       traceContext.addTag(RequestId.Type.RECEIVE_ID, new RequestId(project.getNameKey().get()));
 
-      logger.atFinest().log("Calling user: %s", user.getLoggableName());
-
       // Log the push options here, rather than in parsePushOptions(), so that they are included
       // into the trace if tracing is enabled.
       logger.atFine().log("push options: %s", receivePack.getPushOptions());
 
-      if (!projectState.getProject().getState().permitsWrite()) {
-        for (ReceiveCommand cmd : commands) {
-          reject(cmd, "prohibited by Gerrit: project state does not permit write");
-        }
-        return;
-      }
+      Task commandProgress = progress.beginSubTask("refs", UNKNOWN);
+      commands =
+          commands.stream().map(c -> wrapReceiveCommand(c, commandProgress)).collect(toList());
+      processCommandsUnsafe(commands, progress);
+      rejectRemaining(commands, INTERNAL_SERVER_ERROR);
 
-      logger.atFine().log("Parsing %d commands", commands.size());
+      // This sends error messages before the 'done' string of the progress monitor is sent.
+      // Currently, the test framework relies on this ordering to understand if pushes completed
+      // successfully.
+      sendErrorMessages();
 
-      List<ReceiveCommand> magicCommands = new ArrayList<>();
-      List<ReceiveCommand> directPatchSetPushCommands = new ArrayList<>();
-      List<ReceiveCommand> regularCommands = new ArrayList<>();
-
-      for (ReceiveCommand cmd : commands) {
-        if (MagicBranch.isMagicBranch(cmd.getRefName())) {
-          magicCommands.add(cmd);
-        } else if (isDirectChangesPush(cmd.getRefName())) {
-          directPatchSetPushCommands.add(cmd);
-        } else {
-          regularCommands.add(cmd);
-        }
-      }
-
-      int commandTypes =
-          (magicCommands.isEmpty() ? 0 : 1)
-              + (directPatchSetPushCommands.isEmpty() ? 0 : 1)
-              + (regularCommands.isEmpty() ? 0 : 1);
-
-      if (commandTypes > 1) {
-        rejectRemaining(commands, "cannot combine normal pushes and magic pushes");
-        return;
-      }
-
-      try {
-        if (!regularCommands.isEmpty()) {
-          handleRegularCommands(regularCommands, progress);
-          return;
-        }
-
-        for (ReceiveCommand cmd : directPatchSetPushCommands) {
-          parseDirectChangesPush(cmd);
-        }
-
-        boolean first = true;
-        for (ReceiveCommand cmd : magicCommands) {
-          if (first) {
-            parseMagicBranch(cmd);
-            first = false;
-          } else {
-            reject(cmd, "duplicate request");
-          }
-        }
-      } catch (PermissionBackendException | NoSuchProjectException | IOException err) {
-        logger.atSevere().withCause(err).log("Failed to process refs in %s", project.getName());
-        return;
-      }
-
-      Task newProgress = progress.beginSubTask("new", UNKNOWN);
-      Task replaceProgress = progress.beginSubTask("updated", UNKNOWN);
-
-      List<CreateRequest> newChanges = Collections.emptyList();
-      if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
-        newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress);
-      }
-
-      // Commit validation has already happened, so any changes without Change-Id are for the
-      // deprecated feature.
-      warnAboutMissingChangeId(newChanges);
-      preparePatchSetsForReplace(newChanges);
-      insertChangesAndPatchSets(newChanges, replaceProgress);
-      newProgress.end();
-      replaceProgress.end();
-      queueSuccessMessages(newChanges);
-
-      logger.atFine().log(
-          "Command results: %s",
-          lazy(() -> commands.stream().map(ReceiveCommits::commandToString).collect(joining(","))));
+      commandProgress.end();
+      progress.end();
+      loggingTags = traceContext.getTags();
+      logger.atFine().log("Processing commands done.");
     }
   }
 
+  // Process as many commands as possible, but may leave some commands in state NOT_ATTEMPTED.
+  private void processCommandsUnsafe(
+      Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
+    logger.atFine().log("Calling user: %s", user.getLoggableName());
+    logger.atFine().log("Groups: %s", user.getEffectiveGroups().getKnownGroups());
+
+    if (!projectState.getProject().getState().permitsWrite()) {
+      for (ReceiveCommand cmd : commands) {
+        reject(cmd, "prohibited by Gerrit: project state does not permit write");
+      }
+      return;
+    }
+
+    logger.atFine().log("Parsing %d commands", commands.size());
+
+    List<ReceiveCommand> magicCommands = new ArrayList<>();
+    List<ReceiveCommand> regularCommands = new ArrayList<>();
+
+    for (ReceiveCommand cmd : commands) {
+      if (MagicBranch.isMagicBranch(cmd.getRefName())) {
+        magicCommands.add(cmd);
+      } else {
+        regularCommands.add(cmd);
+      }
+    }
+
+    int commandTypes = (magicCommands.isEmpty() ? 0 : 1) + (regularCommands.isEmpty() ? 0 : 1);
+
+    if (commandTypes > 1) {
+      rejectRemaining(commands, "cannot combine normal pushes and magic pushes");
+      return;
+    }
+
+    try {
+      if (!regularCommands.isEmpty()) {
+        handleRegularCommands(regularCommands, progress);
+        return;
+      }
+
+      boolean first = true;
+      for (ReceiveCommand cmd : magicCommands) {
+        if (first) {
+          parseMagicBranch(cmd);
+          first = false;
+        } else {
+          reject(cmd, "duplicate request");
+        }
+      }
+    } catch (PermissionBackendException | NoSuchProjectException | IOException err) {
+      logger.atSevere().withCause(err).log("Failed to process refs in %s", project.getName());
+      return;
+    }
+
+    Task newProgress = progress.beginSubTask("new", UNKNOWN);
+    Task replaceProgress = progress.beginSubTask("updated", UNKNOWN);
+
+    List<CreateRequest> newChanges = Collections.emptyList();
+    if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
+      newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress);
+    }
+
+    // Commit validation has already happened, so any changes without Change-Id are for the
+    // deprecated feature.
+    warnAboutMissingChangeId(newChanges);
+    preparePatchSetsForReplace(newChanges);
+    insertChangesAndPatchSets(newChanges, replaceProgress);
+    newProgress.end();
+    replaceProgress.end();
+    queueSuccessMessages(newChanges);
+
+    logger.atFine().log(
+        "Command results: %s",
+        lazy(() -> commands.stream().map(ReceiveCommits::commandToString).collect(joining(","))));
+  }
+
   private void sendErrorMessages() {
     if (!errors.isEmpty()) {
       logger.atFine().log("Handling error conditions: %s", errors.keySet());
@@ -633,69 +685,72 @@
 
   private void handleRegularCommands(List<ReceiveCommand> cmds, MultiProgressMonitor progress)
       throws PermissionBackendException, IOException, NoSuchProjectException {
-    resultChangeIds.setMagicPush(false);
-    for (ReceiveCommand cmd : cmds) {
-      parseRegularCommand(cmd);
-    }
-
-    try (BatchUpdate bu =
-            batchUpdateFactory.create(
-                project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
-        ObjectInserter ins = repo.newObjectInserter();
-        ObjectReader reader = ins.newReader();
-        RevWalk rw = new RevWalk(reader)) {
-      bu.setRepository(repo, rw, ins);
-      bu.setRefLogMessage("push");
-
-      int added = 0;
+    try (TraceTimer traceTimer =
+        newTimer("handleRegularCommands", Metadata.builder().resourceCount(cmds.size()))) {
+      resultChangeIds.setMagicPush(false);
       for (ReceiveCommand cmd : cmds) {
-        if (cmd.getResult() == NOT_ATTEMPTED) {
-          bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
-          added++;
+        parseRegularCommand(cmd);
+      }
+
+      try (BatchUpdate bu =
+              batchUpdateFactory.create(
+                  project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
+          ObjectInserter ins = repo.newObjectInserter();
+          ObjectReader reader = ins.newReader();
+          RevWalk rw = new RevWalk(reader)) {
+        bu.setRepository(repo, rw, ins);
+        bu.setRefLogMessage("push");
+
+        int added = 0;
+        for (ReceiveCommand cmd : cmds) {
+          if (cmd.getResult() == NOT_ATTEMPTED) {
+            bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
+            added++;
+          }
+        }
+        logger.atFine().log("Added %d additional ref updates", added);
+        bu.execute();
+      } catch (UpdateException | RestApiException e) {
+        rejectRemaining(cmds, INTERNAL_SERVER_ERROR);
+        logger.atSevere().withCause(e).log("update failed:");
+      }
+
+      Set<BranchNameKey> branches = new HashSet<>();
+      for (ReceiveCommand c : cmds) {
+        // Most post-update steps should happen in UpdateOneRefOp#postUpdate. The only steps that
+        // should happen in this loop are things that can't happen within one BatchUpdate because
+        // they involve kicking off an additional BatchUpdate.
+        if (c.getResult() != OK) {
+          continue;
+        }
+        if (isHead(c) || isConfig(c)) {
+          switch (c.getType()) {
+            case CREATE:
+            case UPDATE:
+            case UPDATE_NONFASTFORWARD:
+              Task closeProgress = progress.beginSubTask("closed", UNKNOWN);
+              autoCloseChanges(c, closeProgress);
+              closeProgress.end();
+              branches.add(BranchNameKey.create(project.getNameKey(), c.getRefName()));
+              break;
+
+            case DELETE:
+              break;
+          }
         }
       }
-      logger.atFine().log("Added %d additional ref updates", added);
-      bu.execute();
-    } catch (UpdateException | RestApiException e) {
-      rejectRemaining(cmds, INTERNAL_SERVER_ERROR);
-      logger.atSevere().withCause(e).log("update failed:");
-    }
 
-    Set<Branch.NameKey> branches = new HashSet<>();
-    for (ReceiveCommand c : cmds) {
-      // Most post-update steps should happen in UpdateOneRefOp#postUpdate. The only steps that
-      // should happen in this loop are things that can't happen within one BatchUpdate because
-      // they involve kicking off an additional BatchUpdate.
-      if (c.getResult() != OK) {
-        continue;
-      }
-      if (isHead(c) || isConfig(c)) {
-        switch (c.getType()) {
-          case CREATE:
-          case UPDATE:
-          case UPDATE_NONFASTFORWARD:
-            Task closeProgress = progress.beginSubTask("closed", UNKNOWN);
-            autoCloseChanges(c, closeProgress);
-            closeProgress.end();
-            branches.add(new Branch.NameKey(project.getNameKey(), c.getRefName()));
-            break;
-
-          case DELETE:
-            break;
+      // Update superproject gitlinks if required.
+      if (!branches.isEmpty()) {
+        try (MergeOpRepoManager orm = ormProvider.get()) {
+          orm.setContext(TimeUtil.nowTs(), user, NotifyResolver.Result.none());
+          SubmoduleOp op = subOpFactory.create(branches, orm);
+          op.updateSuperProjects();
+        } catch (SubmoduleException e) {
+          logger.atSevere().withCause(e).log("Can't update the superprojects");
         }
       }
     }
-
-    // Update superproject gitlinks if required.
-    if (!branches.isEmpty()) {
-      try (MergeOpRepoManager orm = ormProvider.get()) {
-        orm.setContext(TimeUtil.nowTs(), user, NotifyResolver.Result.none());
-        SubmoduleOp op = subOpFactory.create(branches, orm);
-        op.updateSuperProjects();
-      } catch (SubmoduleException e) {
-        logger.atSevere().withCause(e).log("Can't update the superprojects");
-      }
-    }
   }
 
   /** Appends messages for successful change creation/updates. */
@@ -755,7 +810,7 @@
     Boolean isPrivate = null;
     Boolean wip = null;
     if (!updated.isEmpty()) {
-      edit = magicBranch != null && (magicBranch.edit || magicBranch.draft);
+      edit = magicBranch != null && magicBranch.edit;
       if (magicBranch != null) {
         if (magicBranch.isPrivate) {
           isPrivate = true;
@@ -814,97 +869,102 @@
   }
 
   private void insertChangesAndPatchSets(List<CreateRequest> newChanges, Task replaceProgress) {
-    ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
-    if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) {
-      logger.atWarning().log(
-          "Skipping change updates on %s because ref update failed: %s %s",
-          project.getName(),
-          magicBranchCmd.getResult(),
-          Strings.nullToEmpty(magicBranchCmd.getMessage()));
-      return;
-    }
-
-    try (BatchUpdate bu =
-            batchUpdateFactory.create(
-                project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
-        ObjectInserter ins = repo.newObjectInserter();
-        ObjectReader reader = ins.newReader();
-        RevWalk rw = new RevWalk(reader)) {
-      bu.setRepository(repo, rw, ins);
-      bu.setRefLogMessage("push");
-      if (magicBranch != null) {
-        bu.setNotify(magicBranch.getNotifyForNewChange());
+    try (TraceTimer traceTimer =
+        newTimer(
+            "insertChangesAndPatchSets", Metadata.builder().resourceCount(newChanges.size()))) {
+      ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
+      if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) {
+        logger.atWarning().log(
+            "Skipping change updates on %s because ref update failed: %s %s",
+            project.getName(),
+            magicBranchCmd.getResult(),
+            Strings.nullToEmpty(magicBranchCmd.getMessage()));
+        return;
       }
 
-      logger.atFine().log("Adding %d replace requests", newChanges.size());
-      for (ReplaceRequest replace : replaceByChange.values()) {
+      try (BatchUpdate bu =
+              batchUpdateFactory.create(
+                  project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
+          ObjectInserter ins = repo.newObjectInserter();
+          ObjectReader reader = ins.newReader();
+          RevWalk rw = new RevWalk(reader)) {
+        bu.setRepository(repo, rw, ins);
+        bu.setRefLogMessage("push");
         if (magicBranch != null) {
-          bu.setNotifyHandling(replace.ontoChange, magicBranch.getNotifyHandling(replace.notes));
+          bu.setNotify(magicBranch.getNotifyForNewChange());
         }
-        replace.addOps(bu, replaceProgress);
-      }
 
-      logger.atFine().log("Adding %d create requests", newChanges.size());
-      for (CreateRequest create : newChanges) {
-        create.addOps(bu);
-      }
-
-      logger.atFine().log("Adding %d group update requests", newChanges.size());
-      updateGroups.forEach(r -> r.addOps(bu));
-
-      logger.atFine().log("Executing batch");
-      try {
-        bu.execute();
-      } catch (UpdateException e) {
-        throw INSERT_EXCEPTION.apply(e);
-      }
-
-      replaceByChange.values().stream()
-          .forEach(req -> resultChangeIds.add(ResultChangeIds.Key.REPLACED, req.ontoChange));
-      newChanges.stream()
-          .forEach(req -> resultChangeIds.add(ResultChangeIds.Key.CREATED, req.changeId));
-
-      if (magicBranchCmd != null) {
-        magicBranchCmd.setResult(OK);
-      }
-      for (ReplaceRequest replace : replaceByChange.values()) {
-        String rejectMessage = replace.getRejectMessage();
-        if (rejectMessage == null) {
-          if (replace.inputCommand.getResult() == NOT_ATTEMPTED) {
-            // Not necessarily the magic branch, so need to set OK on the original value.
-            replace.inputCommand.setResult(OK);
+        logger.atFine().log("Adding %d replace requests", newChanges.size());
+        for (ReplaceRequest replace : replaceByChange.values()) {
+          if (magicBranch != null) {
+            bu.setNotifyHandling(replace.ontoChange, magicBranch.getNotifyHandling(replace.notes));
           }
-        } else {
-          logger.atFine().log("Rejecting due to message from ReplaceOp");
-          reject(replace.inputCommand, rejectMessage);
+          replace.addOps(bu, replaceProgress);
         }
-      }
 
-    } catch (ResourceConflictException e) {
-      addError(e.getMessage());
-      reject(magicBranchCmd, "conflict");
-    } catch (BadRequestException | UnprocessableEntityException | AuthException e) {
-      logger.atFine().withCause(e).log("Rejecting due to client error");
-      reject(magicBranchCmd, e.getMessage());
-    } catch (RestApiException | IOException e) {
-      logger.atSevere().withCause(e).log("Can't insert change/patch set for %s", project.getName());
-      reject(magicBranchCmd, String.format("%s: %s", INTERNAL_SERVER_ERROR, e.getMessage()));
-    }
+        logger.atFine().log("Adding %d create requests", newChanges.size());
+        for (CreateRequest create : newChanges) {
+          create.addOps(bu);
+        }
 
-    if (magicBranch != null && magicBranch.submit) {
-      try {
-        submit(newChanges, replaceByChange.values());
+        logger.atFine().log("Adding %d group update requests", newChanges.size());
+        updateGroups.forEach(r -> r.addOps(bu));
+
+        logger.atFine().log("Executing batch");
+        try {
+          bu.execute();
+        } catch (UpdateException e) {
+          throw asRestApiException(e);
+        }
+
+        replaceByChange.values().stream()
+            .forEach(req -> resultChangeIds.add(ResultChangeIds.Key.REPLACED, req.ontoChange));
+        newChanges.stream()
+            .forEach(req -> resultChangeIds.add(ResultChangeIds.Key.CREATED, req.changeId));
+
+        if (magicBranchCmd != null) {
+          magicBranchCmd.setResult(OK);
+        }
+        for (ReplaceRequest replace : replaceByChange.values()) {
+          String rejectMessage = replace.getRejectMessage();
+          if (rejectMessage == null) {
+            if (replace.inputCommand.getResult() == NOT_ATTEMPTED) {
+              // Not necessarily the magic branch, so need to set OK on the original value.
+              replace.inputCommand.setResult(OK);
+            }
+          } else {
+            logger.atFine().log("Rejecting due to message from ReplaceOp");
+            reject(replace.inputCommand, rejectMessage);
+          }
+        }
+
       } catch (ResourceConflictException e) {
         addError(e.getMessage());
         reject(magicBranchCmd, "conflict");
-      } catch (RestApiException
-          | StorageException
-          | UpdateException
-          | IOException
-          | ConfigInvalidException
-          | PermissionBackendException e) {
-        logger.atSevere().withCause(e).log("Error submitting changes to %s", project.getName());
-        reject(magicBranchCmd, "error during submit");
+      } catch (BadRequestException | UnprocessableEntityException | AuthException e) {
+        logger.atFine().withCause(e).log("Rejecting due to client error");
+        reject(magicBranchCmd, e.getMessage());
+      } catch (RestApiException | IOException e) {
+        logger.atSevere().withCause(e).log(
+            "Can't insert change/patch set for %s", project.getName());
+        reject(magicBranchCmd, String.format("%s: %s", INTERNAL_SERVER_ERROR, e.getMessage()));
+      }
+
+      if (magicBranch != null && magicBranch.submit) {
+        try {
+          submit(newChanges, replaceByChange.values());
+        } catch (ResourceConflictException e) {
+          addError(e.getMessage());
+          reject(magicBranchCmd, "conflict");
+        } catch (RestApiException
+            | StorageException
+            | UpdateException
+            | IOException
+            | ConfigInvalidException
+            | PermissionBackendException e) {
+          logger.atSevere().withCause(e).log("Error submitting changes to %s", project.getName());
+          reject(magicBranchCmd, "error during submit");
+        }
       }
     }
   }
@@ -938,7 +998,7 @@
     if (!noteDbValues.isEmpty()) {
       // These semantics for duplicates/errors are somewhat arbitrary and may not match e.g. the
       // CmdLineParser behavior used by MagicBranchInput.
-      String value = noteDbValues.get(noteDbValues.size() - 1);
+      String value = Iterables.getLast(noteDbValues);
       noteDbPushOption = NoteDbPushOption.parse(value);
       if (!noteDbPushOption.isPresent()) {
         addError("Invalid value in -o " + NoteDbPushOption.OPTION_NAME + "=" + value);
@@ -949,32 +1009,12 @@
 
     List<String> traceValues = pushOptions.get("trace");
     if (!traceValues.isEmpty()) {
-      String value = traceValues.get(traceValues.size() - 1);
-      tracePushOption = Optional.of(value);
+      tracePushOption = Optional.of(Iterables.getLast(traceValues));
     } else {
       tracePushOption = Optional.empty();
     }
   }
 
-  private static boolean isDirectChangesPush(String refname) {
-    Matcher m = NEW_PATCHSET_PATTERN.matcher(refname);
-    return m.matches();
-  }
-
-  private void parseDirectChangesPush(ReceiveCommand cmd) {
-    Matcher m = NEW_PATCHSET_PATTERN.matcher(cmd.getRefName());
-    checkArgument(m.matches());
-
-    if (allowPushToRefsChanges) {
-      // The referenced change must exist and must still be open.
-      Change.Id changeId = Change.Id.parse(m.group(1));
-      parseReplaceCommand(cmd, changeId);
-      messages.add(new ValidationMessage("warning: pushes to refs/changes are deprecated", false));
-    } else {
-      reject(cmd, "upload to refs/changes not allowed");
-    }
-  }
-
   // Wrap ReceiveCommand so the progress counter works automatically.
   private ReceiveCommand wrapReceiveCommand(ReceiveCommand cmd, Task progress) {
     String refname = cmd.getRefName();
@@ -1007,161 +1047,166 @@
    */
   private void parseRegularCommand(ReceiveCommand cmd)
       throws PermissionBackendException, NoSuchProjectException, IOException {
-    if (cmd.getResult() != NOT_ATTEMPTED) {
-      // Already rejected by the core receive process.
-      logger.atFine().log("Already processed by core: %s %s", cmd.getResult(), cmd);
-      return;
-    }
-
-    if (!Repository.isValidRefName(cmd.getRefName()) || cmd.getRefName().contains("//")) {
-      reject(cmd, "not valid ref");
-      return;
-    }
-    if (RefNames.isNoteDbMetaRef(cmd.getRefName())) {
-      // Reject pushes to NoteDb refs without a special option and permission. Note that this
-      // prohibition doesn't depend on NoteDb being enabled in any way, since all sites will
-      // migrate to NoteDb eventually, and we don't want garbage data waiting there when the
-      // migration finishes.
-      logger.atFine().log(
-          "%s NoteDb ref %s with %s=%s",
-          cmd.getType(), cmd.getRefName(), NoteDbPushOption.OPTION_NAME, noteDbPushOption);
-      if (!Optional.of(NoteDbPushOption.ALLOW).equals(noteDbPushOption)) {
-        // Only reject this command, not the whole push. This supports the use case of "git clone
-        // --mirror" followed by "git push --mirror", when the user doesn't really intend to clone
-        // or mirror the NoteDb data; there is no single refspec that describes all refs *except*
-        // NoteDb refs.
-        reject(
-            cmd,
-            "NoteDb update requires -o "
-                + NoteDbPushOption.OPTION_NAME
-                + "="
-                + NoteDbPushOption.ALLOW.value());
+    try (TraceTimer traceTimer = newTimer("parseRegularCommand")) {
+      if (cmd.getResult() != NOT_ATTEMPTED) {
+        // Already rejected by the core receive process.
+        logger.atFine().log("Already processed by core: %s %s", cmd.getResult(), cmd);
         return;
       }
-      try {
-        permissionBackend.user(user).check(GlobalPermission.ACCESS_DATABASE);
-      } catch (AuthException e) {
-        reject(cmd, "NoteDb update requires access database permission");
+
+      if (!Repository.isValidRefName(cmd.getRefName()) || cmd.getRefName().contains("//")) {
+        reject(cmd, "not valid ref");
         return;
       }
-    }
+      if (RefNames.isNoteDbMetaRef(cmd.getRefName())) {
+        // Reject pushes to NoteDb refs without a special option and permission. Note that this
+        // prohibition doesn't depend on NoteDb being enabled in any way, since all sites will
+        // migrate to NoteDb eventually, and we don't want garbage data waiting there when the
+        // migration finishes.
+        logger.atFine().log(
+            "%s NoteDb ref %s with %s=%s",
+            cmd.getType(), cmd.getRefName(), NoteDbPushOption.OPTION_NAME, noteDbPushOption);
+        if (!Optional.of(NoteDbPushOption.ALLOW).equals(noteDbPushOption)) {
+          // Only reject this command, not the whole push. This supports the use case of "git clone
+          // --mirror" followed by "git push --mirror", when the user doesn't really intend to clone
+          // or mirror the NoteDb data; there is no single refspec that describes all refs *except*
+          // NoteDb refs.
+          reject(
+              cmd,
+              "NoteDb update requires -o "
+                  + NoteDbPushOption.OPTION_NAME
+                  + "="
+                  + NoteDbPushOption.ALLOW.value());
+          return;
+        }
+        try {
+          permissionBackend.user(user).check(GlobalPermission.ACCESS_DATABASE);
+        } catch (AuthException e) {
+          reject(cmd, "NoteDb update requires access database permission");
+          return;
+        }
+      }
 
-    switch (cmd.getType()) {
-      case CREATE:
-        parseCreate(cmd);
-        break;
+      switch (cmd.getType()) {
+        case CREATE:
+          parseCreate(cmd);
+          break;
 
-      case UPDATE:
-        parseUpdate(cmd);
-        break;
+        case UPDATE:
+          parseUpdate(cmd);
+          break;
 
-      case DELETE:
-        parseDelete(cmd);
-        break;
+        case DELETE:
+          parseDelete(cmd);
+          break;
 
-      case UPDATE_NONFASTFORWARD:
-        parseRewind(cmd);
-        break;
+        case UPDATE_NONFASTFORWARD:
+          parseRewind(cmd);
+          break;
 
-      default:
-        reject(cmd, "prohibited by Gerrit: unknown command type " + cmd.getType());
+        default:
+          reject(cmd, "prohibited by Gerrit: unknown command type " + cmd.getType());
+          return;
+      }
+
+      if (cmd.getResult() != NOT_ATTEMPTED) {
         return;
-    }
+      }
 
-    if (cmd.getResult() != NOT_ATTEMPTED) {
-      return;
-    }
-
-    if (isConfig(cmd)) {
-      validateConfigPush(cmd);
+      if (isConfig(cmd)) {
+        validateConfigPush(cmd);
+      }
     }
   }
 
   /** Validates a push to refs/meta/config, and reject the command if it fails. */
   private void validateConfigPush(ReceiveCommand cmd) throws PermissionBackendException {
-    logger.atFine().log("Processing %s command", cmd.getRefName());
-    try {
-      permissions.check(ProjectPermission.WRITE_CONFIG);
-    } catch (AuthException e) {
-      reject(
-          cmd,
-          String.format(
-              "must be either project owner or have %s permission",
-              ProjectPermission.WRITE_CONFIG.describeForException()));
-      return;
-    }
+    try (TraceTimer traceTimer = newTimer("validateConfigPush")) {
+      logger.atFine().log("Processing %s command", cmd.getRefName());
+      try {
+        permissions.check(ProjectPermission.WRITE_CONFIG);
+      } catch (AuthException e) {
+        reject(
+            cmd,
+            String.format(
+                "must be either project owner or have %s permission",
+                ProjectPermission.WRITE_CONFIG.describeForException()));
+        return;
+      }
 
-    switch (cmd.getType()) {
-      case CREATE:
-      case UPDATE:
-      case UPDATE_NONFASTFORWARD:
-        try {
-          ProjectConfig cfg = projectConfigFactory.create(project.getNameKey());
-          cfg.load(project.getNameKey(), receivePack.getRevWalk(), cmd.getNewId());
-          if (!cfg.getValidationErrors().isEmpty()) {
-            addError("Invalid project configuration:");
-            for (ValidationError err : cfg.getValidationErrors()) {
-              addError("  " + err.getMessage());
+      switch (cmd.getType()) {
+        case CREATE:
+        case UPDATE:
+        case UPDATE_NONFASTFORWARD:
+          try {
+            ProjectConfig cfg = projectConfigFactory.create(project.getNameKey());
+            cfg.load(project.getNameKey(), receivePack.getRevWalk(), cmd.getNewId());
+            if (!cfg.getValidationErrors().isEmpty()) {
+              addError("Invalid project configuration:");
+              for (ValidationError err : cfg.getValidationErrors()) {
+                addError("  " + err.getMessage());
+              }
+              reject(cmd, "invalid project configuration");
+              logger.atSevere().log(
+                  "User %s tried to push invalid project configuration %s for %s",
+                  user.getLoggableName(), cmd.getNewId().name(), project.getName());
+              return;
             }
+            Project.NameKey newParent = cfg.getProject().getParent(allProjectsName);
+            Project.NameKey oldParent = project.getParent(allProjectsName);
+            if (oldParent == null) {
+              // update of the 'All-Projects' project
+              if (newParent != null) {
+                reject(cmd, "invalid project configuration: root project cannot have parent");
+                return;
+              }
+            } else {
+              if (!oldParent.equals(newParent)) {
+                if (allowProjectOwnersToChangeParent) {
+                  try {
+                    permissionBackend
+                        .user(user)
+                        .project(project.getNameKey())
+                        .check(ProjectPermission.WRITE_CONFIG);
+                  } catch (AuthException e) {
+                    reject(
+                        cmd, "invalid project configuration: only project owners can set parent");
+                    return;
+                  }
+                } else {
+                  try {
+                    permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+                  } catch (AuthException e) {
+                    reject(cmd, "invalid project configuration: only Gerrit admin can set parent");
+                    return;
+                  }
+                }
+              }
+
+              if (projectCache.get(newParent) == null) {
+                reject(cmd, "invalid project configuration: parent does not exist");
+                return;
+              }
+            }
+            validatePluginConfig(cmd, cfg);
+          } catch (Exception e) {
             reject(cmd, "invalid project configuration");
-            logger.atSevere().log(
+            logger.atSevere().withCause(e).log(
                 "User %s tried to push invalid project configuration %s for %s",
                 user.getLoggableName(), cmd.getNewId().name(), project.getName());
             return;
           }
-          Project.NameKey newParent = cfg.getProject().getParent(allProjectsName);
-          Project.NameKey oldParent = project.getParent(allProjectsName);
-          if (oldParent == null) {
-            // update of the 'All-Projects' project
-            if (newParent != null) {
-              reject(cmd, "invalid project configuration: root project cannot have parent");
-              return;
-            }
-          } else {
-            if (!oldParent.equals(newParent)) {
-              if (allowProjectOwnersToChangeParent) {
-                try {
-                  permissionBackend
-                      .user(user)
-                      .project(project.getNameKey())
-                      .check(ProjectPermission.WRITE_CONFIG);
-                } catch (AuthException e) {
-                  reject(cmd, "invalid project configuration: only project owners can set parent");
-                  return;
-                }
-              } else {
-                try {
-                  permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
-                } catch (AuthException e) {
-                  reject(cmd, "invalid project configuration: only Gerrit admin can set parent");
-                  return;
-                }
-              }
-            }
+          break;
 
-            if (projectCache.get(newParent) == null) {
-              reject(cmd, "invalid project configuration: parent does not exist");
-              return;
-            }
-          }
-          validatePluginConfig(cmd, cfg);
-        } catch (Exception e) {
-          reject(cmd, "invalid project configuration");
-          logger.atSevere().withCause(e).log(
-              "User %s tried to push invalid project configuration %s for %s",
-              user.getLoggableName(), cmd.getNewId().name(), project.getName());
-          return;
-        }
-        break;
+        case DELETE:
+          break;
 
-      case DELETE:
-        break;
-
-      default:
-        reject(
-            cmd,
-            "prohibited by Gerrit: don't know how to handle config update of type "
-                + cmd.getType());
+        default:
+          reject(
+              cmd,
+              "prohibited by Gerrit: don't know how to handle config update of type "
+                  + cmd.getType());
+      }
     }
   }
 
@@ -1212,52 +1257,59 @@
 
   private void parseCreate(ReceiveCommand cmd)
       throws PermissionBackendException, NoSuchProjectException, IOException {
-    RevObject obj;
-    try {
-      obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
-    } catch (IOException err) {
-      logger.atSevere().withCause(err).log(
-          "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName());
-      reject(cmd, "invalid object");
-      return;
-    }
-    logger.atFine().log("Creating %s", cmd);
+    try (TraceTimer traceTimer = newTimer("parseCreate")) {
+      RevObject obj;
+      try {
+        obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
+      } catch (IOException err) {
+        logger.atSevere().withCause(err).log(
+            "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName());
+        reject(cmd, "invalid object");
+        return;
+      }
+      logger.atFine().log("Creating %s", cmd);
 
-    if (isHead(cmd) && !isCommit(cmd)) {
-      return;
-    }
+      if (isHead(cmd) && !isCommit(cmd)) {
+        return;
+      }
 
-    Branch.NameKey branch = new Branch.NameKey(project.getName(), cmd.getRefName());
-    try {
-      // Must pass explicit user instead of injecting a provider into CreateRefControl, since
-      // Provider<CurrentUser> within ReceiveCommits will always return anonymous.
-      createRefControl.checkCreateRef(Providers.of(user), receivePack.getRepository(), branch, obj);
-    } catch (AuthException denied) {
-      rejectProhibited(cmd, denied);
-      return;
-    } catch (ResourceConflictException denied) {
-      reject(cmd, "prohibited by Gerrit: " + denied.getMessage());
-      return;
-    }
+      BranchNameKey branch = BranchNameKey.create(project.getName(), cmd.getRefName());
+      try {
+        // Must pass explicit user instead of injecting a provider into CreateRefControl, since
+        // Provider<CurrentUser> within ReceiveCommits will always return anonymous.
+        createRefControl.checkCreateRef(
+            Providers.of(user), receivePack.getRepository(), branch, obj);
+      } catch (AuthException denied) {
+        rejectProhibited(cmd, denied);
+        return;
+      } catch (ResourceConflictException denied) {
+        reject(cmd, "prohibited by Gerrit: " + denied.getMessage());
+        return;
+      }
 
-    if (validRefOperation(cmd)) {
-      validateRegularPushCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
+      if (validRefOperation(cmd)) {
+        validateRegularPushCommits(
+            BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
+      }
     }
   }
 
   private void parseUpdate(ReceiveCommand cmd) throws PermissionBackendException {
-    logger.atFine().log("Updating %s", cmd);
-    Optional<AuthException> err = checkRefPermission(cmd, RefPermission.UPDATE);
-    if (!err.isPresent()) {
-      if (isHead(cmd) && !isCommit(cmd)) {
-        reject(cmd, "head must point to commit");
-        return;
+    try (TraceTimer traceTimer = TraceContext.newTimer("parseUpdate")) {
+      logger.atFine().log("Updating %s", cmd);
+      Optional<AuthException> err = checkRefPermission(cmd, RefPermission.UPDATE);
+      if (!err.isPresent()) {
+        if (isHead(cmd) && !isCommit(cmd)) {
+          reject(cmd, "head must point to commit");
+          return;
+        }
+        if (validRefOperation(cmd)) {
+          validateRegularPushCommits(
+              BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
+        }
+      } else {
+        rejectProhibited(cmd, err.get());
       }
-      if (validRefOperation(cmd)) {
-        validateRegularPushCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
-      }
-    } else {
-      rejectProhibited(cmd, err.get());
     }
   }
 
@@ -1280,45 +1332,50 @@
   }
 
   private void parseDelete(ReceiveCommand cmd) throws PermissionBackendException {
-    logger.atFine().log("Deleting %s", cmd);
-    if (cmd.getRefName().startsWith(REFS_CHANGES)) {
-      errors.put(CANNOT_DELETE_CHANGES, cmd.getRefName());
-      reject(cmd, "cannot delete changes");
-    } else if (isConfigRef(cmd.getRefName())) {
-      errors.put(CANNOT_DELETE_CONFIG, cmd.getRefName());
-      reject(cmd, "cannot delete project configuration");
-    }
+    try (TraceTimer traceTimer = newTimer("parseDelete")) {
+      logger.atFine().log("Deleting %s", cmd);
+      if (cmd.getRefName().startsWith(REFS_CHANGES)) {
+        errors.put(CANNOT_DELETE_CHANGES, cmd.getRefName());
+        reject(cmd, "cannot delete changes");
+      } else if (isConfigRef(cmd.getRefName())) {
+        errors.put(CANNOT_DELETE_CONFIG, cmd.getRefName());
+        reject(cmd, "cannot delete project configuration");
+      }
 
-    Optional<AuthException> err = checkRefPermission(cmd, RefPermission.DELETE);
-    if (!err.isPresent()) {
-      validRefOperation(cmd);
-
-    } else {
-      rejectProhibited(cmd, err.get());
+      Optional<AuthException> err = checkRefPermission(cmd, RefPermission.DELETE);
+      if (!err.isPresent()) {
+        validRefOperation(cmd);
+      } else {
+        rejectProhibited(cmd, err.get());
+      }
     }
   }
 
   private void parseRewind(ReceiveCommand cmd) throws PermissionBackendException {
-    try {
-      receivePack.getRevWalk().parseCommit(cmd.getNewId());
-    } catch (IOException err) {
-      logger.atSevere().withCause(err).log(
-          "Invalid object %s for %s forced update", cmd.getNewId().name(), cmd.getRefName());
-      reject(cmd, "invalid object");
-      return;
-    }
-    logger.atFine().log("Rewinding %s", cmd);
+    try (TraceTimer traceTimer = newTimer("parseRewind")) {
+      try {
+        receivePack.getRevWalk().parseCommit(cmd.getNewId());
+      } catch (IOException err) {
+        logger.atSevere().withCause(err).log(
+            "Invalid object %s for %s forced update", cmd.getNewId().name(), cmd.getRefName());
+        reject(cmd, "invalid object");
+        return;
+      }
+      logger.atFine().log("Rewinding %s", cmd);
 
-    if (!validRefOperation(cmd)) {
-      return;
-    }
-    validateRegularPushCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
-    if (cmd.getResult() != NOT_ATTEMPTED) {
-      return;
-    }
+      if (!validRefOperation(cmd)) {
+        return;
+      }
+      validateRegularPushCommits(BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
+      if (cmd.getResult() != NOT_ATTEMPTED) {
+        return;
+      }
 
-    checkRefPermission(cmd, RefPermission.FORCE_UPDATE)
-        .ifPresent((AuthException err) -> rejectProhibited(cmd, err));
+      Optional<AuthException> err = checkRefPermission(cmd, RefPermission.FORCE_UPDATE);
+      if (err.isPresent()) {
+        rejectProhibited(cmd, err.get());
+      }
+    }
   }
 
   private Optional<AuthException> checkRefPermission(ReceiveCommand cmd, RefPermission perm)
@@ -1358,11 +1415,21 @@
   static class MagicBranchInput {
     private static final Splitter COMMAS = Splitter.on(',').omitEmptyStrings();
 
+    private final IdentifiedUser user;
+    private final ProjectState projectState;
+    private final boolean defaultPublishComments;
+
     boolean deprecatedTopicSeen;
     final ReceiveCommand cmd;
     final LabelTypes labelTypes;
-    private final boolean defaultPublishComments;
-    Branch.NameKey dest;
+    /**
+     * Result of running {@link CommentValidator}-s on drafts that are published with the commit
+     * (which happens iff {@code --publish-comments} is set). Remains {@code true} if none are
+     * installed.
+     */
+    private boolean commentsValid = true;
+
+    BranchNameKey dest;
     PermissionBackend.ForRef perm;
     Set<String> reviewer = Sets.newLinkedHashSet();
     Set<String> cc = Sets.newLinkedHashSet();
@@ -1381,13 +1448,6 @@
     @Option(name = "--topic", metaVar = "NAME", usage = "attach topic to changes")
     String topic;
 
-    @Option(
-        name = "--draft",
-        usage =
-            "Will be removed. Before that, this option will be mapped to '--private'"
-                + "for new changes and '--edit' for existing changes")
-    boolean draft;
-
     @Option(name = "--private", usage = "mark new/updated change as private")
     boolean isPrivate;
 
@@ -1511,18 +1571,23 @@
       if (!hashtag.isEmpty()) {
         hashtags.add(hashtag);
       }
-      // TODO(dpursehouse): validate hashtags
     }
 
-    MagicBranchInput(IdentifiedUser user, ReceiveCommand cmd, LabelTypes labelTypes) {
+    @UsedAt(UsedAt.Project.GOOGLE)
+    @Option(name = "--create-cod-token", usage = "create a token for consistency-on-demand")
+    private boolean createCodToken;
+
+    MagicBranchInput(
+        IdentifiedUser user, ProjectState projectState, ReceiveCommand cmd, LabelTypes labelTypes) {
+      this.user = user;
+      this.projectState = projectState;
       this.deprecatedTopicSeen = false;
       this.cmd = cmd;
-      this.draft = cmd.getRefName().startsWith(MagicBranch.NEW_DRAFT_CHANGE);
       this.labelTypes = labelTypes;
-      GeneralPreferencesInfo prefs = user.state().getGeneralPreferences();
+      GeneralPreferencesInfo prefs = user.state().generalPreferences();
       this.defaultPublishComments =
           prefs != null
-              ? firstNonNull(user.state().getGeneralPreferences().publishCommentsOnPush, false)
+              ? firstNonNull(user.state().generalPreferences().publishCommentsOnPush, false)
               : false;
     }
 
@@ -1560,7 +1625,15 @@
           .collect(toImmutableSet());
     }
 
+    void setCommentsValid(boolean commentsValid) {
+      this.commentsValid = commentsValid;
+    }
+
     boolean shouldPublishComments() {
+      if (!commentsValid) {
+        // Validation messages of type WARNING have already been added, now withhold the comments.
+        return false;
+      }
       if (publishComments) {
         return true;
       } else if (noPublishComments) {
@@ -1619,9 +1692,24 @@
       return ref.substring(0, split);
     }
 
+    public boolean shouldSetWorkInProgressOnNewChanges() {
+      // When wip or ready explicitly provided, leave it as is.
+      if (workInProgress) {
+        return true;
+      }
+      if (ready) {
+        return false;
+      }
+
+      return projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
+          || firstNonNull(user.state().generalPreferences().workInProgressByDefault, false);
+    }
+
     NotifyResolver.Result getNotifyForNewChange() {
       return NotifyResolver.Result.create(
-          firstNonNull(notifyHandling, workInProgress ? NotifyHandling.OWNER : NotifyHandling.ALL),
+          firstNonNull(
+              notifyHandling,
+              shouldSetWorkInProgressOnNewChanges() ? NotifyHandling.OWNER : NotifyHandling.ALL),
           ImmutableSetMultimap.<RecipientType, Account.Id>builder()
               .putAll(RecipientType.TO, notifyTo)
               .putAll(RecipientType.CC, notifyCc)
@@ -1648,202 +1736,198 @@
    * <p>Assumes we are handling a magic branch here.
    */
   private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException {
-    logger.atFine().log("Found magic branch %s", cmd.getRefName());
-    MagicBranchInput magicBranch = new MagicBranchInput(user, cmd, labelTypes);
+    try (TraceTimer traceTimer = newTimer("parseMagicBranch")) {
+      logger.atFine().log("Found magic branch %s", cmd.getRefName());
+      MagicBranchInput magicBranch = new MagicBranchInput(user, projectState, cmd, labelTypes);
 
-    String ref;
-    magicBranch.cmdLineParser = optionParserFactory.create(magicBranch);
+      String ref;
+      magicBranch.cmdLineParser = optionParserFactory.create(magicBranch);
 
-    try {
-      ref = magicBranch.parse(repo, receivePack.getAdvertisedRefs().keySet(), pushOptions);
-    } catch (CmdLineException e) {
-      if (!magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
-        logger.atFine().log("Invalid branch syntax");
-        reject(cmd, e.getMessage());
+      try {
+        ref = magicBranch.parse(repo, receivePack.getAdvertisedRefs().keySet(), pushOptions);
+      } catch (CmdLineException e) {
+        if (!magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
+          logger.atFine().log("Invalid branch syntax");
+          reject(cmd, e.getMessage());
+          return;
+        }
+        ref = null; // never happens
+      }
+
+      if (magicBranch.topic != null && magicBranch.topic.length() > ChangeUtil.TOPIC_MAX_LENGTH) {
+        reject(
+            cmd, String.format("topic length exceeds the limit (%d)", ChangeUtil.TOPIC_MAX_LENGTH));
+      }
+
+      if (magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
+        StringWriter w = new StringWriter();
+        w.write("\nHelp for refs/for/branch:\n\n");
+        magicBranch.cmdLineParser.printUsage(w, null);
+        addMessage(w.toString());
+        reject(cmd, "see help");
         return;
       }
-      ref = null; // never happens
-    }
-
-    if (magicBranch.topic != null && magicBranch.topic.length() > ChangeUtil.TOPIC_MAX_LENGTH) {
-      reject(
-          cmd, String.format("topic length exceeds the limit (%d)", ChangeUtil.TOPIC_MAX_LENGTH));
-    }
-
-    if (magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
-      StringWriter w = new StringWriter();
-      w.write("\nHelp for refs/for/branch:\n\n");
-      magicBranch.cmdLineParser.printUsage(w, null);
-      addMessage(w.toString());
-      reject(cmd, "see help");
-      return;
-    }
-    if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(ref)) {
-      logger.atFine().log("Handling %s", RefNames.REFS_USERS_SELF);
-      ref = RefNames.refsUsers(user.getAccountId());
-    }
-    // Pushing changes for review usually requires that the target branch exists, but there is an
-    // exception for the branch to which HEAD points to and for refs/meta/config. Pushing for
-    // review to these branches is allowed even if the branch does not exist yet. This allows to
-    // push initial code for review to an empty repository and to review an initial project
-    // configuration.
-    if (!receivePack.getAdvertisedRefs().containsKey(ref)
-        && !ref.equals(readHEAD(repo))
-        && !ref.equals(RefNames.REFS_CONFIG)) {
-      logger.atFine().log("Ref %s not found", ref);
-      if (ref.startsWith(Constants.R_HEADS)) {
-        String n = ref.substring(Constants.R_HEADS.length());
-        reject(cmd, "branch " + n + " not found");
-      } else {
-        reject(cmd, ref + " not found");
+      if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(ref)) {
+        logger.atFine().log("Handling %s", RefNames.REFS_USERS_SELF);
+        ref = RefNames.refsUsers(user.getAccountId());
       }
-      return;
-    }
+      // Pushing changes for review usually requires that the target branch exists, but there is an
+      // exception for the branch to which HEAD points to and for refs/meta/config. Pushing for
+      // review to these branches is allowed even if the branch does not exist yet. This allows to
+      // push initial code for review to an empty repository and to review an initial project
+      // configuration.
+      if (!receivePack.getAdvertisedRefs().containsKey(ref)
+          && !ref.equals(readHEAD(repo))
+          && !ref.equals(RefNames.REFS_CONFIG)) {
+        logger.atFine().log("Ref %s not found", ref);
+        if (ref.startsWith(Constants.R_HEADS)) {
+          String n = ref.substring(Constants.R_HEADS.length());
+          reject(cmd, "branch " + n + " not found");
+        } else {
+          reject(cmd, ref + " not found");
+        }
+        return;
+      }
 
-    magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref);
-    magicBranch.perm = permissions.ref(ref);
+      magicBranch.dest = BranchNameKey.create(project.getNameKey(), ref);
+      magicBranch.perm = permissions.ref(ref);
 
-    Optional<AuthException> err = checkRefPermission(magicBranch.perm, RefPermission.CREATE_CHANGE);
-    if (err.isPresent()) {
-      rejectProhibited(cmd, err.get());
-      return;
-    }
-
-    // TODO(davido): Remove legacy support for drafts magic branch option
-    // after repo-tool supports private and work-in-progress changes.
-    if (magicBranch.draft && !receiveConfig.allowDrafts) {
-      errors.put(CODE_REVIEW_ERROR, ref);
-      reject(cmd, "draft workflow is disabled");
-      return;
-    }
-
-    if (magicBranch.isPrivate && magicBranch.removePrivate) {
-      reject(cmd, "the options 'private' and 'remove-private' are mutually exclusive");
-      return;
-    }
-
-    boolean privateByDefault =
-        projectCache.get(project.getNameKey()).is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
-    setChangeAsPrivate =
-        magicBranch.draft
-            || magicBranch.isPrivate
-            || (privateByDefault && !magicBranch.removePrivate);
-
-    if (receiveConfig.disablePrivateChanges && setChangeAsPrivate) {
-      reject(cmd, "private changes are disabled");
-      return;
-    }
-
-    if (magicBranch.workInProgress && magicBranch.ready) {
-      reject(cmd, "the options 'wip' and 'ready' are mutually exclusive");
-      return;
-    }
-    if (magicBranch.publishComments && magicBranch.noPublishComments) {
-      reject(
-          cmd, "the options 'publish-comments' and 'no-publish-comments' are mutually exclusive");
-      return;
-    }
-
-    if (magicBranch.submit) {
-      err = checkRefPermission(magicBranch.perm, RefPermission.UPDATE_BY_SUBMIT);
+      Optional<AuthException> err =
+          checkRefPermission(magicBranch.perm, RefPermission.CREATE_CHANGE);
       if (err.isPresent()) {
         rejectProhibited(cmd, err.get());
         return;
       }
-    }
 
-    RevWalk walk = receivePack.getRevWalk();
-    RevCommit tip;
-    try {
-      tip = walk.parseCommit(magicBranch.cmd.getNewId());
-      logger.atFine().log("Tip of push: %s", tip.name());
-    } catch (IOException ex) {
-      magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
-      logger.atSevere().withCause(ex).log("Invalid pack upload; one or more objects weren't sent");
-      return;
-    }
+      if (magicBranch.isPrivate && magicBranch.removePrivate) {
+        reject(cmd, "the options 'private' and 'remove-private' are mutually exclusive");
+        return;
+      }
 
-    String destBranch = magicBranch.dest.get();
-    try {
-      if (magicBranch.merged) {
+      boolean privateByDefault =
+          projectCache.get(project.getNameKey()).is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
+      setChangeAsPrivate =
+          magicBranch.isPrivate || (privateByDefault && !magicBranch.removePrivate);
+
+      if (receiveConfig.disablePrivateChanges && setChangeAsPrivate) {
+        reject(cmd, "private changes are disabled");
+        return;
+      }
+
+      if (magicBranch.workInProgress && magicBranch.ready) {
+        reject(cmd, "the options 'wip' and 'ready' are mutually exclusive");
+        return;
+      }
+      if (magicBranch.publishComments && magicBranch.noPublishComments) {
+        reject(
+            cmd, "the options 'publish-comments' and 'no-publish-comments' are mutually exclusive");
+        return;
+      }
+
+      if (magicBranch.submit) {
+        err = checkRefPermission(magicBranch.perm, RefPermission.UPDATE_BY_SUBMIT);
+        if (err.isPresent()) {
+          rejectProhibited(cmd, err.get());
+          return;
+        }
+      }
+
+      RevWalk walk = receivePack.getRevWalk();
+      RevCommit tip;
+      try {
+        tip = walk.parseCommit(magicBranch.cmd.getNewId());
+        logger.atFine().log("Tip of push: %s", tip.name());
+      } catch (IOException ex) {
+        magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
+        logger.atSevere().withCause(ex).log(
+            "Invalid pack upload; one or more objects weren't sent");
+        return;
+      }
+
+      String destBranch = magicBranch.dest.branch();
+      try {
+        if (magicBranch.merged) {
+          if (magicBranch.base != null) {
+            reject(cmd, "cannot use merged with base");
+            return;
+          }
+          RevCommit branchTip = readBranchTip(magicBranch.dest);
+          if (branchTip == null) {
+            reject(cmd, magicBranch.dest.branch() + " not found");
+            return;
+          }
+          if (!walk.isMergedInto(tip, branchTip)) {
+            reject(cmd, "not merged into branch");
+            return;
+          }
+        }
+
+        // If tip is a merge commit, or the root commit or
+        // if %base or %merged was specified, ignore newChangeForAllNotInTarget.
+        if (tip.getParentCount() > 1
+            || magicBranch.base != null
+            || magicBranch.merged
+            || tip.getParentCount() == 0) {
+          logger.atFine().log("Forcing newChangeForAllNotInTarget = false");
+          newChangeForAllNotInTarget = false;
+        }
+
         if (magicBranch.base != null) {
-          reject(cmd, "cannot use merged with base");
-          return;
-        }
-        RevCommit branchTip = readBranchTip(magicBranch.dest);
-        if (branchTip == null) {
-          reject(cmd, magicBranch.dest.get() + " not found");
-          return;
-        }
-        if (!walk.isMergedInto(tip, branchTip)) {
-          reject(cmd, "not merged into branch");
-          return;
-        }
-      }
-
-      // If tip is a merge commit, or the root commit or
-      // if %base or %merged was specified, ignore newChangeForAllNotInTarget.
-      if (tip.getParentCount() > 1
-          || magicBranch.base != null
-          || magicBranch.merged
-          || tip.getParentCount() == 0) {
-        logger.atFine().log("Forcing newChangeForAllNotInTarget = false");
-        newChangeForAllNotInTarget = false;
-      }
-
-      if (magicBranch.base != null) {
-        logger.atFine().log("Handling %%base: %s", magicBranch.base);
-        magicBranch.baseCommit = Lists.newArrayListWithCapacity(magicBranch.base.size());
-        for (ObjectId id : magicBranch.base) {
-          try {
-            magicBranch.baseCommit.add(walk.parseCommit(id));
-          } catch (IncorrectObjectTypeException notCommit) {
-            reject(cmd, "base must be a commit");
-            return;
-          } catch (MissingObjectException e) {
-            reject(cmd, "base not found");
-            return;
-          } catch (IOException e) {
-            logger.atWarning().withCause(e).log(
-                "Project %s cannot read %s", project.getName(), id.name());
-            reject(cmd, INTERNAL_SERVER_ERROR);
-            return;
+          logger.atFine().log("Handling %%base: %s", magicBranch.base);
+          magicBranch.baseCommit = Lists.newArrayListWithCapacity(magicBranch.base.size());
+          for (ObjectId id : magicBranch.base) {
+            try {
+              magicBranch.baseCommit.add(walk.parseCommit(id));
+            } catch (IncorrectObjectTypeException notCommit) {
+              reject(cmd, "base must be a commit");
+              return;
+            } catch (MissingObjectException e) {
+              reject(cmd, "base not found");
+              return;
+            } catch (IOException e) {
+              logger.atWarning().withCause(e).log(
+                  "Project %s cannot read %s", project.getName(), id.name());
+              reject(cmd, INTERNAL_SERVER_ERROR);
+              return;
+            }
+          }
+        } else if (newChangeForAllNotInTarget) {
+          RevCommit branchTip = readBranchTip(magicBranch.dest);
+          if (branchTip != null) {
+            magicBranch.baseCommit = Collections.singletonList(branchTip);
+            logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
+          } else {
+            // The target branch does not exist. Usually pushing changes for review requires that
+            // the
+            // target branch exists, but there is an exception for the branch to which HEAD points
+            // to
+            // and for refs/meta/config. Pushing for review to these branches is allowed even if the
+            // branch does not exist yet. This allows to push initial code for review to an empty
+            // repository and to review an initial project configuration.
+            if (!ref.equals(readHEAD(repo)) && !ref.equals(RefNames.REFS_CONFIG)) {
+              reject(cmd, magicBranch.dest.branch() + " not found");
+              return;
+            }
           }
         }
-      } else if (newChangeForAllNotInTarget) {
-        RevCommit branchTip = readBranchTip(magicBranch.dest);
-        if (branchTip != null) {
-          magicBranch.baseCommit = Collections.singletonList(branchTip);
-          logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
-        } else {
-          // The target branch does not exist. Usually pushing changes for review requires that the
-          // target branch exists, but there is an exception for the branch to which HEAD points to
-          // and for refs/meta/config. Pushing for review to these branches is allowed even if the
-          // branch does not exist yet. This allows to push initial code for review to an empty
-          // repository and to review an initial project configuration.
-          if (!ref.equals(readHEAD(repo)) && !ref.equals(RefNames.REFS_CONFIG)) {
-            reject(cmd, magicBranch.dest.get() + " not found");
-            return;
-          }
-        }
+      } catch (IOException ex) {
+        logger.atWarning().withCause(ex).log(
+            "Error walking to %s in project %s", destBranch, project.getName());
+        reject(cmd, INTERNAL_SERVER_ERROR);
+        return;
       }
-    } catch (IOException ex) {
-      logger.atWarning().withCause(ex).log(
-          "Error walking to %s in project %s", destBranch, project.getName());
-      reject(cmd, INTERNAL_SERVER_ERROR);
-      return;
-    }
 
-    if (magicBranch.deprecatedTopicSeen) {
-      messages.add(
-          new ValidationMessage(
-              "WARNING: deprecated topic syntax. Use -o topic=TOPIC instead", false));
-      logger.atInfo().log("deprecated topic push seen for project %s", project.getName());
-    }
+      if (magicBranch.deprecatedTopicSeen) {
+        messages.add(
+            new ValidationMessage(
+                "WARNING: deprecated topic syntax. Use -o topic=TOPIC instead", false));
+        logger.atInfo().log("deprecated topic push seen for project %s", project.getName());
+      }
 
-    if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
-      this.magicBranch = magicBranch;
-      this.resultChangeIds.setMagicPush(true);
+      if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
+        this.magicBranch = magicBranch;
+        this.resultChangeIds.setMagicPush(true);
+      }
     }
   }
 
@@ -1851,42 +1935,45 @@
   // branch.  If they aren't, we want to abort. We do this check by
   // looking to see if we can compute a merge base between the new
   // commits and the target branch head.
-  private boolean validateConnected(ReceiveCommand cmd, Branch.NameKey dest, RevCommit tip) {
-    RevWalk walk = receivePack.getRevWalk();
-    try {
-      Ref targetRef = receivePack.getAdvertisedRefs().get(dest.get());
-      if (targetRef == null || targetRef.getObjectId() == null) {
-        // The destination branch does not yet exist. Assume the
-        // history being sent for review will start it and thus
-        // is "connected" to the branch.
-        logger.atFine().log("Branch is unborn");
-
-        // This is not an error condition.
-        return true;
-      }
-
-      RevCommit h = walk.parseCommit(targetRef.getObjectId());
-      logger.atFine().log("Current branch tip: %s", h.name());
-      RevFilter oldRevFilter = walk.getRevFilter();
+  private boolean validateConnected(ReceiveCommand cmd, BranchNameKey dest, RevCommit tip) {
+    try (TraceTimer traceTimer =
+        newTimer("validateConnected", Metadata.builder().branchName(dest.branch()))) {
+      RevWalk walk = receivePack.getRevWalk();
       try {
-        walk.reset();
-        walk.setRevFilter(RevFilter.MERGE_BASE);
-        walk.markStart(tip);
-        walk.markStart(h);
-        if (walk.next() == null) {
-          reject(cmd, "no common ancestry");
-          return false;
+        Ref targetRef = receivePack.getAdvertisedRefs().get(dest.branch());
+        if (targetRef == null || targetRef.getObjectId() == null) {
+          // The destination branch does not yet exist. Assume the
+          // history being sent for review will start it and thus
+          // is "connected" to the branch.
+          logger.atFine().log("Branch is unborn");
+
+          // This is not an error condition.
+          return true;
         }
-      } finally {
-        walk.reset();
-        walk.setRevFilter(oldRevFilter);
+
+        RevCommit h = walk.parseCommit(targetRef.getObjectId());
+        logger.atFine().log("Current branch tip: %s", h.name());
+        RevFilter oldRevFilter = walk.getRevFilter();
+        try {
+          walk.reset();
+          walk.setRevFilter(RevFilter.MERGE_BASE);
+          walk.markStart(tip);
+          walk.markStart(h);
+          if (walk.next() == null) {
+            reject(cmd, "no common ancestry");
+            return false;
+          }
+        } finally {
+          walk.reset();
+          walk.setRevFilter(oldRevFilter);
+        }
+      } catch (IOException e) {
+        cmd.setResult(REJECTED_MISSING_OBJECT);
+        logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
+        return false;
       }
-    } catch (IOException e) {
-      cmd.setResult(REJECTED_MISSING_OBJECT);
-      logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
-      return false;
+      return true;
     }
-    return true;
   }
 
   private static String readHEAD(Repository repo) {
@@ -1900,89 +1987,64 @@
     }
   }
 
-  private RevCommit readBranchTip(Branch.NameKey branch) throws IOException {
-    Ref r = allRefs().get(branch.get());
+  private RevCommit readBranchTip(BranchNameKey branch) throws IOException {
+    Ref r = allRefs().get(branch.branch());
     if (r == null) {
       return null;
     }
     return receivePack.getRevWalk().parseCommit(r.getObjectId());
   }
 
-  // Handle an upload to refs/changes/XX/CHANGED-NUMBER.
-  private void parseReplaceCommand(ReceiveCommand cmd, Change.Id changeId) {
-    logger.atFine().log("Parsing replace command");
-    if (cmd.getType() != ReceiveCommand.Type.CREATE) {
-      reject(cmd, "invalid usage");
-      return;
-    }
-
-    RevCommit newCommit;
-    try {
-      newCommit = receivePack.getRevWalk().parseCommit(cmd.getNewId());
-      logger.atFine().log("Replacing with %s", newCommit);
-    } catch (IOException e) {
-      logger.atSevere().withCause(e).log("Cannot parse %s as commit", cmd.getNewId().name());
-      reject(cmd, "invalid commit");
-      return;
-    }
-
-    Change changeEnt;
-    try {
-      changeEnt = notesFactory.createChecked(project.getNameKey(), changeId).getChange();
-    } catch (NoSuchChangeException e) {
-      logger.atSevere().withCause(e).log("Change not found %s", changeId);
-      reject(cmd, "change " + changeId + " not found");
-      return;
-    } catch (StorageException e) {
-      logger.atSevere().withCause(e).log("Cannot lookup existing change %s", changeId);
-      reject(cmd, "database error");
-      return;
-    }
-    if (!project.getNameKey().equals(changeEnt.getProject())) {
-      reject(cmd, "change " + changeId + " does not belong to project " + project.getName());
-      return;
-    }
-
-    BranchCommitValidator validator =
-        commitValidatorFactory.create(projectState, changeEnt.getDest(), user);
-    try {
-      if (validator.validCommit(
-          receivePack.getRevWalk().getObjectReader(),
-          cmd,
-          newCommit,
-          false,
-          messages,
-          rejectCommits,
-          changeEnt)) {
-        logger.atFine().log("Replacing change %s", changeEnt.getId());
-        requestReplace(cmd, true, changeEnt, newCommit);
-      }
-    } catch (IOException e) {
-      reject(cmd, "I/O exception validating commit");
-    }
-  }
-
   /**
-   * Add an update for an existing change. Returns true if it succeeded; rejects the command if it
-   * failed.
+   * Update an existing change. If draft comments are to be published, these are validated and may
+   * be withheld.
+   *
+   * @return True if the command succeeded, false if it was rejected.
    */
-  private boolean requestReplace(
+  private boolean requestReplaceAndValidateComments(
       ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit) {
-    if (change.isClosed()) {
-      reject(
-          cmd,
-          changeFormatter.changeClosed(
-              ChangeReportFormatter.Input.builder().setChange(change).build()));
-      return false;
-    }
+    try (TraceTimer traceTimer = newTimer("requestReplaceAndValidateComments")) {
+      if (change.isClosed()) {
+        reject(
+            cmd,
+            changeFormatter.changeClosed(
+                ChangeReportFormatter.Input.builder().setChange(change).build()));
+        return false;
+      }
 
-    ReplaceRequest req = new ReplaceRequest(change.getId(), newCommit, cmd, checkMergedInto);
-    if (replaceByChange.containsKey(req.ontoChange)) {
-      reject(cmd, "duplicate request");
-      return false;
+      ReplaceRequest req = new ReplaceRequest(change.getId(), newCommit, cmd, checkMergedInto);
+      if (replaceByChange.containsKey(req.ontoChange)) {
+        reject(cmd, "duplicate request");
+        return false;
+      }
+
+      if (magicBranch != null && magicBranch.shouldPublishComments()) {
+        List<Comment> drafts =
+            commentsUtil.draftByChangeAuthor(
+                notesFactory.createChecked(change), user.getAccountId());
+        ImmutableList<CommentForValidation> draftsForValidation =
+            drafts.stream()
+                .map(
+                    comment ->
+                        CommentForValidation.create(
+                            comment.lineNbr > 0
+                                ? CommentType.INLINE_COMMENT
+                                : CommentType.FILE_COMMENT,
+                            comment.message))
+                .collect(toImmutableList());
+        ImmutableList<CommentValidationFailure> commentValidationFailures =
+            PublishCommentUtil.findInvalidComments(commentValidators, draftsForValidation);
+        magicBranch.setCommentsValid(commentValidationFailures.isEmpty());
+        commentValidationFailures.forEach(
+            failure ->
+                addMessage(
+                    "Comment validation failure: " + failure.getMessage(),
+                    ValidationMessage.Type.WARNING));
+      }
+
+      replaceByChange.put(req.ontoChange, req);
+      return true;
     }
-    replaceByChange.put(req.ontoChange, req);
-    return true;
   }
 
   private void warnAboutMissingChangeId(List<CreateRequest> newChanges) {
@@ -2003,351 +2065,367 @@
   }
 
   private List<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress) {
-    logger.atFine().log("Finding new and replaced changes");
-    List<CreateRequest> newChanges = new ArrayList<>();
+    try (TraceTimer traceTimer = newTimer("selectNewAndReplacedChangesFromMagicBranch")) {
+      logger.atFine().log("Finding new and replaced changes");
+      List<CreateRequest> newChanges = new ArrayList<>();
 
-    ListMultimap<ObjectId, Ref> existing = changeRefsById();
-    GroupCollector groupCollector =
-        GroupCollector.create(changeRefsById(), psUtil, notesFactory, project.getNameKey());
+      ListMultimap<ObjectId, Ref> existing = changeRefsById();
+      GroupCollector groupCollector =
+          GroupCollector.create(changeRefsById(), psUtil, notesFactory, project.getNameKey());
 
-    BranchCommitValidator validator =
-        commitValidatorFactory.create(projectState, magicBranch.dest, user);
+      BranchCommitValidator validator =
+          commitValidatorFactory.create(projectState, magicBranch.dest, user);
 
-    try {
-      RevCommit start = setUpWalkForSelectingChanges();
-      if (start == null) {
-        return Collections.emptyList();
-      }
-
-      LinkedHashMap<RevCommit, ChangeLookup> pending = new LinkedHashMap<>();
-      Set<Change.Key> newChangeIds = new HashSet<>();
-      int maxBatchChanges = receiveConfig.getEffectiveMaxBatchChangesLimit(user);
-      int total = 0;
-      int alreadyTracked = 0;
-      boolean rejectImplicitMerges =
-          start.getParentCount() == 1
-              && projectCache
-                  .get(project.getNameKey())
-                  .is(BooleanProjectConfig.REJECT_IMPLICIT_MERGES)
-              // Don't worry about implicit merges when creating changes for
-              // already-merged commits; they're already in history, so it's too
-              // late.
-              && !magicBranch.merged;
-      Set<RevCommit> mergedParents;
-      if (rejectImplicitMerges) {
-        mergedParents = new HashSet<>();
-      } else {
-        mergedParents = null;
-      }
-
-      for (; ; ) {
-        RevCommit c = receivePack.getRevWalk().next();
-        if (c == null) {
-          break;
+      try {
+        RevCommit start = setUpWalkForSelectingChanges();
+        if (start == null) {
+          return Collections.emptyList();
         }
-        total++;
-        receivePack.getRevWalk().parseBody(c);
-        String name = c.name();
-        groupCollector.visit(c);
-        Collection<Ref> existingRefs = existing.get(c);
 
+        LinkedHashMap<RevCommit, ChangeLookup> pending = new LinkedHashMap<>();
+        Set<Change.Key> newChangeIds = new HashSet<>();
+        int maxBatchChanges = receiveConfig.getEffectiveMaxBatchChangesLimit(user);
+        int total = 0;
+        int alreadyTracked = 0;
+        boolean rejectImplicitMerges =
+            start.getParentCount() == 1
+                && projectCache
+                    .get(project.getNameKey())
+                    .is(BooleanProjectConfig.REJECT_IMPLICIT_MERGES)
+                // Don't worry about implicit merges when creating changes for
+                // already-merged commits; they're already in history, so it's too
+                // late.
+                && !magicBranch.merged;
+        Set<RevCommit> mergedParents;
         if (rejectImplicitMerges) {
-          Collections.addAll(mergedParents, c.getParents());
-          mergedParents.remove(c);
-        }
-
-        boolean commitAlreadyTracked = !existingRefs.isEmpty();
-        if (commitAlreadyTracked) {
-          alreadyTracked++;
-          // Corner cases where an existing commit might need a new group:
-          // A) Existing commit has a null group; wasn't assigned during schema
-          //    upgrade, or schema upgrade is performed on a running server.
-          // B) Let A<-B<-C, then:
-          //      1. Push A to refs/heads/master
-          //      2. Push B to refs/for/master
-          //      3. Force push A~ to refs/heads/master
-          //      4. Push C to refs/for/master.
-          //      B will be in existing so we aren't replacing the patch set. It
-          //      used to have its own group, but now needs to to be changed to
-          //      A's group.
-          // C) Commit is a PatchSet of a pre-existing change uploaded with a
-          //    different target branch.
-          for (Ref ref : existingRefs) {
-            updateGroups.add(new UpdateGroupsRequest(ref, c));
-          }
-          if (!(newChangeForAllNotInTarget || magicBranch.base != null)) {
-            continue;
-          }
-        }
-
-        List<String> idList = c.getFooterLines(CHANGE_ID);
-        if (!idList.isEmpty()) {
-          pending.put(
-              c, lookupByChangeKey(c, new Change.Key(idList.get(idList.size() - 1).trim())));
+          mergedParents = new HashSet<>();
         } else {
-          pending.put(c, lookupByCommit(c));
+          mergedParents = null;
         }
 
-        int n = pending.size() + newChanges.size();
-        if (maxBatchChanges != 0 && n > maxBatchChanges) {
-          logger.atFine().log("%d changes exceeds limit of %d", n, maxBatchChanges);
-          reject(
-              magicBranch.cmd,
-              "the number of pushed changes in a batch exceeds the max limit " + maxBatchChanges);
-          return Collections.emptyList();
-        }
+        for (; ; ) {
+          RevCommit c = receivePack.getRevWalk().next();
+          if (c == null) {
+            break;
+          }
+          total++;
+          receivePack.getRevWalk().parseBody(c);
+          String name = c.name();
+          groupCollector.visit(c);
+          Collection<Ref> existingRefs = existing.get(c);
 
-        if (commitAlreadyTracked) {
-          boolean changeExistsOnDestBranch = false;
-          for (ChangeData cd : pending.get(c).destChanges) {
-            if (cd.change().getDest().equals(magicBranch.dest)) {
-              changeExistsOnDestBranch = true;
-              break;
+          if (rejectImplicitMerges) {
+            Collections.addAll(mergedParents, c.getParents());
+            mergedParents.remove(c);
+          }
+
+          boolean commitAlreadyTracked = !existingRefs.isEmpty();
+          if (commitAlreadyTracked) {
+            alreadyTracked++;
+            // Corner cases where an existing commit might need a new group:
+            // A) Existing commit has a null group; wasn't assigned during schema
+            //    upgrade, or schema upgrade is performed on a running server.
+            // B) Let A<-B<-C, then:
+            //      1. Push A to refs/heads/master
+            //      2. Push B to refs/for/master
+            //      3. Force push A~ to refs/heads/master
+            //      4. Push C to refs/for/master.
+            //      B will be in existing so we aren't replacing the patch set. It
+            //      used to have its own group, but now needs to to be changed to
+            //      A's group.
+            // C) Commit is a PatchSet of a pre-existing change uploaded with a
+            //    different target branch.
+            for (Ref ref : existingRefs) {
+              updateGroups.add(new UpdateGroupsRequest(ref, c));
             }
-          }
-          if (changeExistsOnDestBranch) {
-            continue;
-          }
-
-          logger.atFine().log("Creating new change for %s even though it is already tracked", name);
-        }
-
-        if (!validator.validCommit(
-            receivePack.getRevWalk().getObjectReader(),
-            magicBranch.cmd,
-            c,
-            magicBranch.merged,
-            messages,
-            rejectCommits,
-            null)) {
-          // Not a change the user can propose? Abort as early as possible.
-          logger.atFine().log("Aborting early due to invalid commit");
-          return Collections.emptyList();
-        }
-
-        // Don't allow merges to be uploaded in commit chain via all-not-in-target
-        if (newChangeForAllNotInTarget && c.getParentCount() > 1) {
-          reject(
-              magicBranch.cmd,
-              "Pushing merges in commit chains with 'all not in target' is not allowed,\n"
-                  + "to override please set the base manually");
-          logger.atFine().log("Rejecting merge commit %s with newChangeForAllNotInTarget", name);
-          // TODO(dborowitz): Should we early return here?
-        }
-
-        if (idList.isEmpty()) {
-          newChanges.add(new CreateRequest(c, magicBranch.dest.get(), newProgress));
-          continue;
-        }
-      }
-      logger.atFine().log(
-          "Finished initial RevWalk with %d commits total: %d already"
-              + " tracked, %d new changes with no Change-Id, and %d deferred"
-              + " lookups",
-          total, alreadyTracked, newChanges.size(), pending.size());
-
-      if (rejectImplicitMerges) {
-        rejectImplicitMerges(mergedParents);
-      }
-
-      for (Iterator<ChangeLookup> itr = pending.values().iterator(); itr.hasNext(); ) {
-        ChangeLookup p = itr.next();
-        if (p.changeKey == null) {
-          continue;
-        }
-
-        if (newChangeIds.contains(p.changeKey)) {
-          logger.atFine().log("Multiple commits with Change-Id %s", p.changeKey);
-          reject(magicBranch.cmd, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
-          return Collections.emptyList();
-        }
-
-        List<ChangeData> changes = p.destChanges;
-        if (changes.size() > 1) {
-          logger.atFine().log(
-              "Multiple changes in branch %s with Change-Id %s: %s",
-              magicBranch.dest,
-              p.changeKey,
-              changes.stream().map(cd -> cd.getId().toString()).collect(joining()));
-          // WTF, multiple changes in this branch have the same key?
-          // Since the commit is new, the user should recreate it with
-          // a different Change-Id. In practice, we should never see
-          // this error message as Change-Id should be unique per branch.
-          //
-          reject(magicBranch.cmd, p.changeKey.get() + " has duplicates");
-          return Collections.emptyList();
-        }
-
-        if (changes.size() == 1) {
-          // Schedule as a replacement to this one matching change.
-          //
-
-          RevId currentPs = changes.get(0).currentPatchSet().getRevision();
-          // If Commit is already current PatchSet of target Change.
-          if (p.commit.name().equals(currentPs.get())) {
-            if (pending.size() == 1) {
-              // There are no commits left to check, all commits in pending were already
-              // current PatchSet of the corresponding target changes.
-              reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
-            } else {
-              // Commit is already current PatchSet.
-              // Remove from pending and try next commit.
-              itr.remove();
+            if (!(newChangeForAllNotInTarget || magicBranch.base != null)) {
               continue;
             }
           }
-          if (requestReplace(magicBranch.cmd, false, changes.get(0).change(), p.commit)) {
-            continue;
-          }
-          return Collections.emptyList();
-        }
 
-        if (changes.isEmpty()) {
-          if (!isValidChangeId(p.changeKey.get())) {
-            reject(magicBranch.cmd, "invalid Change-Id");
+          List<String> idList = c.getFooterLines(FooterConstants.CHANGE_ID);
+          if (!idList.isEmpty()) {
+            pending.put(c, lookupByChangeKey(c, Change.key(idList.get(idList.size() - 1).trim())));
+          } else {
+            pending.put(c, lookupByCommit(c));
+          }
+
+          int n = pending.size() + newChanges.size();
+          if (maxBatchChanges != 0 && n > maxBatchChanges) {
+            logger.atFine().log("%d changes exceeds limit of %d", n, maxBatchChanges);
+            reject(
+                magicBranch.cmd,
+                "the number of pushed changes in a batch exceeds the max limit " + maxBatchChanges);
             return Collections.emptyList();
           }
 
-          // In case the change look up from the index failed,
-          // double check against the existing refs
-          if (foundInExistingRef(existing.get(p.commit))) {
-            if (pending.size() == 1) {
-              reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
-              return Collections.emptyList();
+          if (commitAlreadyTracked) {
+            boolean changeExistsOnDestBranch = false;
+            for (ChangeData cd : pending.get(c).destChanges) {
+              if (cd.change().getDest().equals(magicBranch.dest)) {
+                changeExistsOnDestBranch = true;
+                break;
+              }
             }
-            itr.remove();
+            if (changeExistsOnDestBranch) {
+              continue;
+            }
+
+            logger.atFine().log(
+                "Creating new change for %s even though it is already tracked", name);
+          }
+
+          BranchCommitValidator.Result validationResult =
+              validator.validateCommit(
+                  receivePack.getRevWalk().getObjectReader(),
+                  magicBranch.cmd,
+                  c,
+                  magicBranch.merged,
+                  rejectCommits,
+                  null);
+          messages.addAll(validationResult.messages());
+          if (!validationResult.isValid()) {
+            // Not a change the user can propose? Abort as early as possible.
+            logger.atFine().log("Aborting early due to invalid commit");
+            return Collections.emptyList();
+          }
+
+          // Don't allow merges to be uploaded in commit chain via all-not-in-target
+          if (newChangeForAllNotInTarget && c.getParentCount() > 1) {
+            reject(
+                magicBranch.cmd,
+                "Pushing merges in commit chains with 'all not in target' is not allowed,\n"
+                    + "to override please set the base manually");
+            logger.atFine().log("Rejecting merge commit %s with newChangeForAllNotInTarget", name);
+            // TODO(dborowitz): Should we early return here?
+          }
+
+          if (idList.isEmpty()) {
+            newChanges.add(new CreateRequest(c, magicBranch.dest.branch(), newProgress));
             continue;
           }
-          newChangeIds.add(p.changeKey);
         }
-        newChanges.add(new CreateRequest(p.commit, magicBranch.dest.get(), newProgress));
-      }
-      logger.atFine().log(
-          "Finished deferred lookups with %d updates and %d new changes",
-          replaceByChange.size(), newChanges.size());
-    } catch (IOException e) {
-      // Should never happen, the core receive process would have
-      // identified the missing object earlier before we got control.
-      //
-      magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
-      logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
-      return Collections.emptyList();
-    } catch (StorageException e) {
-      logger.atSevere().withCause(e).log("Cannot query database to locate prior changes");
-      reject(magicBranch.cmd, "database error");
-      return Collections.emptyList();
-    }
+        logger.atFine().log(
+            "Finished initial RevWalk with %d commits total: %d already"
+                + " tracked, %d new changes with no Change-Id, and %d deferred"
+                + " lookups",
+            total, alreadyTracked, newChanges.size(), pending.size());
 
-    if (newChanges.isEmpty() && replaceByChange.isEmpty()) {
-      reject(magicBranch.cmd, "no new changes");
-      return Collections.emptyList();
-    }
-    if (!newChanges.isEmpty() && magicBranch.edit) {
-      reject(magicBranch.cmd, "edit is not supported for new changes");
+        if (rejectImplicitMerges) {
+          rejectImplicitMerges(mergedParents);
+        }
+
+        for (Iterator<ChangeLookup> itr = pending.values().iterator(); itr.hasNext(); ) {
+          ChangeLookup p = itr.next();
+          if (p.changeKey == null) {
+            continue;
+          }
+
+          if (newChangeIds.contains(p.changeKey)) {
+            logger.atFine().log("Multiple commits with Change-Id %s", p.changeKey);
+            reject(magicBranch.cmd, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
+            return Collections.emptyList();
+          }
+
+          List<ChangeData> changes = p.destChanges;
+          if (changes.size() > 1) {
+            logger.atFine().log(
+                "Multiple changes in branch %s with Change-Id %s: %s",
+                magicBranch.dest,
+                p.changeKey,
+                changes.stream().map(cd -> cd.getId().toString()).collect(joining()));
+            // WTF, multiple changes in this branch have the same key?
+            // Since the commit is new, the user should recreate it with
+            // a different Change-Id. In practice, we should never see
+            // this error message as Change-Id should be unique per branch.
+            //
+            reject(magicBranch.cmd, p.changeKey.get() + " has duplicates");
+            return Collections.emptyList();
+          }
+
+          if (changes.size() == 1) {
+            // Schedule as a replacement to this one matching change.
+            //
+
+            ObjectId currentPs = changes.get(0).currentPatchSet().commitId();
+            // If Commit is already current PatchSet of target Change.
+            if (p.commit.equals(currentPs)) {
+              if (pending.size() == 1) {
+                // There are no commits left to check, all commits in pending were already
+                // current PatchSet of the corresponding target changes.
+                reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
+              } else {
+                // Commit is already current PatchSet.
+                // Remove from pending and try next commit.
+                itr.remove();
+                continue;
+              }
+            }
+            if (requestReplaceAndValidateComments(
+                magicBranch.cmd, false, changes.get(0).change(), p.commit)) {
+              continue;
+            }
+            return Collections.emptyList();
+          }
+
+          if (changes.isEmpty()) {
+            if (!isValidChangeId(p.changeKey.get())) {
+              reject(magicBranch.cmd, "invalid Change-Id");
+              return Collections.emptyList();
+            }
+
+            // In case the change look up from the index failed,
+            // double check against the existing refs
+            if (foundInExistingRef(existing.get(p.commit))) {
+              if (pending.size() == 1) {
+                reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
+                return Collections.emptyList();
+              }
+              itr.remove();
+              continue;
+            }
+            newChangeIds.add(p.changeKey);
+          }
+          newChanges.add(new CreateRequest(p.commit, magicBranch.dest.branch(), newProgress));
+        }
+        logger.atFine().log(
+            "Finished deferred lookups with %d updates and %d new changes",
+            replaceByChange.size(), newChanges.size());
+      } catch (IOException e) {
+        // Should never happen, the core receive process would have
+        // identified the missing object earlier before we got control.
+        //
+        magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
+        logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
+        return Collections.emptyList();
+      } catch (StorageException e) {
+        logger.atSevere().withCause(e).log("Cannot query database to locate prior changes");
+        reject(magicBranch.cmd, "database error");
+        return Collections.emptyList();
+      }
+
+      if (newChanges.isEmpty() && replaceByChange.isEmpty()) {
+        reject(magicBranch.cmd, "no new changes");
+        return Collections.emptyList();
+      }
+      if (!newChanges.isEmpty() && magicBranch.edit) {
+        reject(magicBranch.cmd, "edit is not supported for new changes");
+        return newChanges;
+      }
+
+      try {
+        SortedSetMultimap<ObjectId, String> groups = groupCollector.getGroups();
+        List<Integer> newIds = seq.nextChangeIds(newChanges.size());
+        for (int i = 0; i < newChanges.size(); i++) {
+          CreateRequest create = newChanges.get(i);
+          create.setChangeId(newIds.get(i));
+          create.groups = ImmutableList.copyOf(groups.get(create.commit));
+        }
+        for (ReplaceRequest replace : replaceByChange.values()) {
+          replace.groups = ImmutableList.copyOf(groups.get(replace.newCommitId));
+        }
+        for (UpdateGroupsRequest update : updateGroups) {
+          update.groups = ImmutableList.copyOf((groups.get(update.commit)));
+        }
+        logger.atFine().log("Finished updating groups from GroupCollector");
+      } catch (StorageException e) {
+        logger.atSevere().withCause(e).log("Error collecting groups for changes");
+        reject(magicBranch.cmd, INTERNAL_SERVER_ERROR);
+      }
       return newChanges;
     }
-
-    try {
-      SortedSetMultimap<ObjectId, String> groups = groupCollector.getGroups();
-      List<Integer> newIds = seq.nextChangeIds(newChanges.size());
-      for (int i = 0; i < newChanges.size(); i++) {
-        CreateRequest create = newChanges.get(i);
-        create.setChangeId(newIds.get(i));
-        create.groups = ImmutableList.copyOf(groups.get(create.commit));
-      }
-      for (ReplaceRequest replace : replaceByChange.values()) {
-        replace.groups = ImmutableList.copyOf(groups.get(replace.newCommitId));
-      }
-      for (UpdateGroupsRequest update : updateGroups) {
-        update.groups = ImmutableList.copyOf((groups.get(update.commit)));
-      }
-      logger.atFine().log("Finished updating groups from GroupCollector");
-    } catch (StorageException e) {
-      logger.atSevere().withCause(e).log("Error collecting groups for changes");
-      reject(magicBranch.cmd, INTERNAL_SERVER_ERROR);
-    }
-    return newChanges;
   }
 
   private boolean foundInExistingRef(Collection<Ref> existingRefs) {
-    for (Ref ref : existingRefs) {
-      ChangeNotes notes =
-          notesFactory.create(project.getNameKey(), Change.Id.fromRef(ref.getName()));
-      Change change = notes.getChange();
-      if (change.getDest().equals(magicBranch.dest)) {
-        logger.atFine().log("Found change %s from existing refs.", change.getKey());
-        // Reindex the change asynchronously, ignoring errors.
-        @SuppressWarnings("unused")
-        Future<?> possiblyIgnoredError = indexer.indexAsync(project.getNameKey(), change.getId());
-        return true;
+    try (TraceTimer traceTimer = newTimer("foundInExistingRef")) {
+      for (Ref ref : existingRefs) {
+        ChangeNotes notes =
+            notesFactory.create(project.getNameKey(), Change.Id.fromRef(ref.getName()));
+        Change change = notes.getChange();
+        if (change.getDest().equals(magicBranch.dest)) {
+          logger.atFine().log("Found change %s from existing refs.", change.getKey());
+          // Reindex the change asynchronously, ignoring errors.
+          @SuppressWarnings("unused")
+          Future<?> possiblyIgnoredError = indexer.indexAsync(project.getNameKey(), change.getId());
+          return true;
+        }
       }
+      return false;
     }
-    return false;
   }
 
   private RevCommit setUpWalkForSelectingChanges() throws IOException {
-    RevWalk rw = receivePack.getRevWalk();
-    RevCommit start = rw.parseCommit(magicBranch.cmd.getNewId());
+    try (TraceTimer traceTimer = newTimer("setUpWalkForSelectingChanges")) {
+      RevWalk rw = receivePack.getRevWalk();
+      RevCommit start = rw.parseCommit(magicBranch.cmd.getNewId());
 
-    rw.reset();
-    rw.sort(RevSort.TOPO);
-    rw.sort(RevSort.REVERSE, true);
-    receivePack.getRevWalk().markStart(start);
-    if (magicBranch.baseCommit != null) {
-      markExplicitBasesUninteresting();
-    } else if (magicBranch.merged) {
-      logger.atFine().log("Marking parents of merged commit %s uninteresting", start.name());
-      for (RevCommit c : start.getParents()) {
-        rw.markUninteresting(c);
+      rw.reset();
+      rw.sort(RevSort.TOPO);
+      rw.sort(RevSort.REVERSE, true);
+      receivePack.getRevWalk().markStart(start);
+      if (magicBranch.baseCommit != null) {
+        markExplicitBasesUninteresting();
+      } else if (magicBranch.merged) {
+        logger.atFine().log("Marking parents of merged commit %s uninteresting", start.name());
+        for (RevCommit c : start.getParents()) {
+          rw.markUninteresting(c);
+        }
+      } else {
+        markHeadsAsUninteresting(rw, magicBranch.dest != null ? magicBranch.dest.branch() : null);
       }
-    } else {
-      markHeadsAsUninteresting(rw, magicBranch.dest != null ? magicBranch.dest.get() : null);
+      return start;
     }
-    return start;
   }
 
   private void markExplicitBasesUninteresting() throws IOException {
-    logger.atFine().log("Marking %d base commits uninteresting", magicBranch.baseCommit.size());
-    for (RevCommit c : magicBranch.baseCommit) {
-      receivePack.getRevWalk().markUninteresting(c);
-    }
-    Ref targetRef = allRefs().get(magicBranch.dest.get());
-    if (targetRef != null) {
-      logger.atFine().log(
-          "Marking target ref %s (%s) uninteresting",
-          magicBranch.dest.get(), targetRef.getObjectId().name());
-      receivePack
-          .getRevWalk()
-          .markUninteresting(receivePack.getRevWalk().parseCommit(targetRef.getObjectId()));
+    try (TraceTimer traceTimer = newTimer("markExplicitBasesUninteresting")) {
+      logger.atFine().log("Marking %d base commits uninteresting", magicBranch.baseCommit.size());
+      for (RevCommit c : magicBranch.baseCommit) {
+        receivePack.getRevWalk().markUninteresting(c);
+      }
+      Ref targetRef = allRefs().get(magicBranch.dest.branch());
+      if (targetRef != null) {
+        logger.atFine().log(
+            "Marking target ref %s (%s) uninteresting",
+            magicBranch.dest.branch(), targetRef.getObjectId().name());
+        receivePack
+            .getRevWalk()
+            .markUninteresting(receivePack.getRevWalk().parseCommit(targetRef.getObjectId()));
+      }
     }
   }
 
   private void rejectImplicitMerges(Set<RevCommit> mergedParents) throws IOException {
-    if (!mergedParents.isEmpty()) {
-      Ref targetRef = allRefs().get(magicBranch.dest.get());
-      if (targetRef != null) {
-        RevWalk rw = receivePack.getRevWalk();
-        RevCommit tip = rw.parseCommit(targetRef.getObjectId());
-        boolean containsImplicitMerges = true;
-        for (RevCommit p : mergedParents) {
-          containsImplicitMerges &= !rw.isMergedInto(p, tip);
-        }
-
-        if (containsImplicitMerges) {
-          rw.reset();
+    try (TraceTimer traceTimer = newTimer("rejectImplicitMerges")) {
+      if (!mergedParents.isEmpty()) {
+        Ref targetRef = allRefs().get(magicBranch.dest.branch());
+        if (targetRef != null) {
+          RevWalk rw = receivePack.getRevWalk();
+          RevCommit tip = rw.parseCommit(targetRef.getObjectId());
+          boolean containsImplicitMerges = true;
           for (RevCommit p : mergedParents) {
-            rw.markStart(p);
+            containsImplicitMerges &= !rw.isMergedInto(p, tip);
           }
-          rw.markUninteresting(tip);
-          RevCommit c;
-          while ((c = rw.next()) != null) {
-            rw.parseBody(c);
-            messages.add(
-                new CommitValidationMessage(
-                    "Implicit Merge of " + c.abbreviate(7).name() + " " + c.getShortMessage(),
-                    ValidationMessage.Type.ERROR));
+
+          if (containsImplicitMerges) {
+            rw.reset();
+            for (RevCommit p : mergedParents) {
+              rw.markStart(p);
+            }
+            rw.markUninteresting(tip);
+            RevCommit c;
+            while ((c = rw.next()) != null) {
+              rw.parseBody(c);
+              messages.add(
+                  new CommitValidationMessage(
+                      "Implicit Merge of "
+                          + abbreviateName(c, rw.getObjectReader())
+                          + " "
+                          + c.getShortMessage(),
+                      ValidationMessage.Type.ERROR));
+            }
+            reject(magicBranch.cmd, "implicit merges detected");
           }
-          reject(magicBranch.cmd, "implicit merges detected");
         }
       }
     }
@@ -2356,20 +2434,23 @@
   // Mark all branch tips as uninteresting in the given revwalk,
   // so we get only the new commits when walking rw.
   private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) {
-    int i = 0;
-    for (Ref ref : allRefs().values()) {
-      if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef))
-          && ref.getObjectId() != null) {
-        try {
-          rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
-          i++;
-        } catch (IOException e) {
-          logger.atWarning().withCause(e).log(
-              "Invalid ref %s in %s", ref.getName(), project.getName());
+    try (TraceTimer traceTimer =
+        newTimer("markHeadsAsUninteresting", Metadata.builder().branchName(forRef))) {
+      int i = 0;
+      for (Ref ref : allRefs().values()) {
+        if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef))
+            && ref.getObjectId() != null) {
+          try {
+            rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
+            i++;
+          } catch (IOException e) {
+            logger.atWarning().withCause(e).log(
+                "Invalid ref %s in %s", ref.getName(), project.getName());
+          }
         }
       }
+      logger.atFine().log("Marked %d heads as uninteresting", i);
     }
-    logger.atFine().log("Marked %d heads as uninteresting", i);
   }
 
   private static boolean isValidChangeId(String idStr) {
@@ -2390,12 +2471,16 @@
   }
 
   private ChangeLookup lookupByChangeKey(RevCommit c, Change.Key key) {
-    return new ChangeLookup(c, key, queryProvider.get().byBranchKey(magicBranch.dest, key));
+    try (TraceTimer traceTimer = newTimer("lookupByChangeKey")) {
+      return new ChangeLookup(c, key, queryProvider.get().byBranchKey(magicBranch.dest, key));
+    }
   }
 
   private ChangeLookup lookupByCommit(RevCommit c) {
-    return new ChangeLookup(
-        c, null, queryProvider.get().byBranchCommit(magicBranch.dest, c.getName()));
+    try (TraceTimer traceTimer = newTimer("lookupByCommit")) {
+      return new ChangeLookup(
+          c, null, queryProvider.get().byBranchCommit(magicBranch.dest, c.getName()));
+    }
   }
 
   /** Represents a commit for which a Change should be created. */
@@ -2418,103 +2503,97 @@
     }
 
     private void setChangeId(int id) {
-      possiblyOverrideWorkInProgress();
+      try (TraceTimer traceTimer = newTimer(CreateRequest.class, "setChangeId")) {
+        changeId = Change.id(id);
+        ins =
+            changeInserterFactory
+                .create(changeId, commit, refName)
+                .setTopic(magicBranch.topic)
+                .setPrivate(setChangeAsPrivate)
+                .setWorkInProgress(magicBranch.shouldSetWorkInProgressOnNewChanges())
+                // Changes already validated in validateNewCommits.
+                .setValidate(false);
 
-      changeId = new Change.Id(id);
-      ins =
-          changeInserterFactory
-              .create(changeId, commit, refName)
-              .setTopic(magicBranch.topic)
-              .setPrivate(setChangeAsPrivate)
-              .setWorkInProgress(magicBranch.workInProgress)
-              // Changes already validated in validateNewCommits.
-              .setValidate(false);
-
-      if (magicBranch.merged) {
-        ins.setStatus(Change.Status.MERGED);
+        if (magicBranch.merged) {
+          ins.setStatus(Change.Status.MERGED);
+        }
+        cmd = new ReceiveCommand(ObjectId.zeroId(), commit, ins.getPatchSetId().toRefName());
+        if (receivePack.getPushCertificate() != null) {
+          ins.setPushCertificate(receivePack.getPushCertificate().toTextWithSignature());
+        }
       }
-      cmd = new ReceiveCommand(ObjectId.zeroId(), commit, ins.getPatchSetId().toRefName());
-      if (receivePack.getPushCertificate() != null) {
-        ins.setPushCertificate(receivePack.getPushCertificate().toTextWithSignature());
-      }
-    }
-
-    private void possiblyOverrideWorkInProgress() {
-      // When wip or ready explicitly provided, leave it as is.
-      if (magicBranch.workInProgress || magicBranch.ready) {
-        return;
-      }
-      magicBranch.workInProgress =
-          projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
-              || firstNonNull(user.state().getGeneralPreferences().workInProgressByDefault, false);
     }
 
     private void addOps(BatchUpdate bu) throws RestApiException {
-      checkState(changeId != null, "must call setChangeId before addOps");
-      try {
-        RevWalk rw = receivePack.getRevWalk();
-        rw.parseBody(commit);
-        final PatchSet.Id psId = ins.setGroups(groups).getPatchSetId();
-        Account.Id me = user.getAccountId();
-        List<FooterLine> footerLines = commit.getFooterLines();
-        requireNonNull(magicBranch);
+      try (TraceTimer traceTimer = newTimer(CreateRequest.class, "addOps")) {
+        checkState(changeId != null, "must call setChangeId before addOps");
+        try {
+          RevWalk rw = receivePack.getRevWalk();
+          rw.parseBody(commit);
+          final PatchSet.Id psId = ins.setGroups(groups).getPatchSetId();
+          Account.Id me = user.getAccountId();
+          List<FooterLine> footerLines = commit.getFooterLines();
+          requireNonNull(magicBranch);
 
-        // TODO(dborowitz): Support reviewers by email from footers? Maybe not: kernel developers
-        // with AOSP accounts already complain about these notifications, and that would make it
-        // worse. Might be better to get rid of the feature entirely:
-        // https://groups.google.com/d/topic/repo-discuss/tIFxY7L4DXk/discussion
-        MailRecipients fromFooters = getRecipientsFromFooters(accountResolver, footerLines);
-        fromFooters.remove(me);
+          // TODO(dborowitz): Support reviewers by email from footers? Maybe not: kernel developers
+          // with AOSP accounts already complain about these notifications, and that would make it
+          // worse. Might be better to get rid of the feature entirely:
+          // https://groups.google.com/d/topic/repo-discuss/tIFxY7L4DXk/discussion
+          MailRecipients fromFooters = getRecipientsFromFooters(accountResolver, footerLines);
+          fromFooters.remove(me);
 
-        Map<String, Short> approvals = magicBranch.labels;
-        StringBuilder msg =
-            new StringBuilder(
-                ApprovalsUtil.renderMessageWithApprovals(
-                    psId.get(), approvals, Collections.emptyMap()));
-        msg.append('.');
-        if (!Strings.isNullOrEmpty(magicBranch.message)) {
-          msg.append("\n").append(magicBranch.message);
-        }
+          Map<String, Short> approvals = magicBranch.labels;
+          StringBuilder msg =
+              new StringBuilder(
+                  ApprovalsUtil.renderMessageWithApprovals(
+                      psId.get(), approvals, Collections.emptyMap()));
+          msg.append('.');
+          if (!Strings.isNullOrEmpty(magicBranch.message)) {
+            msg.append("\n").append(magicBranch.message);
+          }
 
-        bu.setNotify(magicBranch.getNotifyForNewChange());
-        bu.insertChange(
-            ins.setReviewersAndCcsAsStrings(
-                    magicBranch.getCombinedReviewers(fromFooters),
-                    magicBranch.getCombinedCcs(fromFooters))
-                .setApprovals(approvals)
-                .setMessage(msg.toString())
-                .setRequestScopePropagator(requestScopePropagator)
-                .setSendMail(true)
-                .setPatchSetDescription(magicBranch.message));
-        if (!magicBranch.hashtags.isEmpty()) {
-          // Any change owner is allowed to add hashtags when creating a change.
-          bu.addOp(
-              changeId,
-              hashtagsFactory.create(new HashtagsInput(magicBranch.hashtags)).setFireEvent(false));
-        }
-        if (!Strings.isNullOrEmpty(magicBranch.topic)) {
+          bu.setNotify(magicBranch.getNotifyForNewChange());
+          bu.insertChange(
+              ins.setReviewersAndCcsAsStrings(
+                      magicBranch.getCombinedReviewers(fromFooters),
+                      magicBranch.getCombinedCcs(fromFooters))
+                  .setApprovals(approvals)
+                  .setMessage(msg.toString())
+                  .setRequestScopePropagator(requestScopePropagator)
+                  .setSendMail(true)
+                  .setPatchSetDescription(magicBranch.message));
+          if (!magicBranch.hashtags.isEmpty()) {
+            // Any change owner is allowed to add hashtags when creating a change.
+            bu.addOp(
+                changeId,
+                hashtagsFactory
+                    .create(new HashtagsInput(magicBranch.hashtags))
+                    .setFireEvent(false));
+          }
+          if (!Strings.isNullOrEmpty(magicBranch.topic)) {
+            bu.addOp(
+                changeId,
+                new BatchUpdateOp() {
+                  @Override
+                  public boolean updateChange(ChangeContext ctx) {
+                    ctx.getUpdate(psId).setTopic(magicBranch.topic);
+                    return true;
+                  }
+                });
+          }
           bu.addOp(
               changeId,
               new BatchUpdateOp() {
                 @Override
                 public boolean updateChange(ChangeContext ctx) {
-                  ctx.getUpdate(psId).setTopic(magicBranch.topic);
-                  return true;
+                  CreateRequest.this.change = ctx.getChange();
+                  return false;
                 }
               });
+          bu.addOp(changeId, new ChangeProgressOp(progress));
+        } catch (Exception e) {
+          throw asRestApiException(e);
         }
-        bu.addOp(
-            changeId,
-            new BatchUpdateOp() {
-              @Override
-              public boolean updateChange(ChangeContext ctx) {
-                CreateRequest.this.change = ctx.getChange();
-                return false;
-              }
-            });
-        bu.addOp(changeId, new ChangeProgressOp(progress));
-      } catch (Exception e) {
-        throw INSERT_EXCEPTION.apply(e);
       }
     }
   }
@@ -2522,67 +2601,88 @@
   private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
       throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
-    for (CreateRequest r : create) {
+    try (TraceTimer traceTimer = newTimer("submit")) {
+      Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
+      for (CreateRequest r : create) {
+        requireNonNull(
+            r.change,
+            () -> String.format("cannot submit new change %s; op may not have run", r.changeId));
+        bySha.put(r.commit, r.change);
+      }
+      for (ReplaceRequest r : replace) {
+        bySha.put(r.newCommitId, r.notes.getChange());
+      }
+      Change tipChange = bySha.get(magicBranch.cmd.getNewId());
       requireNonNull(
-          r.change,
-          () -> String.format("cannot submit new change %s; op may not have run", r.changeId));
-      bySha.put(r.commit, r.change);
-    }
-    for (ReplaceRequest r : replace) {
-      bySha.put(r.newCommitId, r.notes.getChange());
-    }
-    Change tipChange = bySha.get(magicBranch.cmd.getNewId());
-    requireNonNull(
-        tipChange,
-        () ->
-            String.format(
-                "tip of push does not correspond to a change; found these changes: %s", bySha));
-    logger.atFine().log(
-        "Processing submit with tip change %s (%s)", tipChange.getId(), magicBranch.cmd.getNewId());
-    try (MergeOp op = mergeOpProvider.get()) {
-      op.merge(tipChange, user, false, new SubmitInput(), false);
+          tipChange,
+          () ->
+              String.format(
+                  "tip of push does not correspond to a change; found these changes: %s", bySha));
+      logger.atFine().log(
+          "Processing submit with tip change %s (%s)",
+          tipChange.getId(), magicBranch.cmd.getNewId());
+      try (MergeOp op = mergeOpProvider.get()) {
+        SubmitInput submitInput = new SubmitInput();
+        submitInput.notify = magicBranch.notifyHandling;
+        submitInput.notifyDetails = new HashMap<>();
+        submitInput.notifyDetails.put(
+            RecipientType.TO,
+            new NotifyInfo(magicBranch.notifyTo.stream().map(Object::toString).collect(toList())));
+        submitInput.notifyDetails.put(
+            RecipientType.CC,
+            new NotifyInfo(magicBranch.notifyCc.stream().map(Object::toString).collect(toList())));
+        submitInput.notifyDetails.put(
+            RecipientType.BCC,
+            new NotifyInfo(magicBranch.notifyBcc.stream().map(Object::toString).collect(toList())));
+        op.merge(tipChange, user, false, submitInput, false);
+      }
     }
   }
 
   private void preparePatchSetsForReplace(List<CreateRequest> newChanges) {
-    try {
-      readChangesForReplace();
-      for (ReplaceRequest req : replaceByChange.values()) {
-        if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
-          req.validateNewPatchSet();
+    try (TraceTimer traceTimer =
+        newTimer(
+            "preparePatchSetsForReplace", Metadata.builder().resourceCount(newChanges.size()))) {
+      try {
+        readChangesForReplace();
+        for (ReplaceRequest req : replaceByChange.values()) {
+          if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
+            req.validateNewPatchSet();
+          }
         }
+      } catch (StorageException err) {
+        logger.atSevere().withCause(err).log(
+            "Cannot read database before replacement for project %s", project.getName());
+        rejectRemainingRequests(replaceByChange.values(), INTERNAL_SERVER_ERROR);
+      } catch (IOException | PermissionBackendException err) {
+        logger.atSevere().withCause(err).log(
+            "Cannot read repository before replacement for project %s", project.getName());
+        rejectRemainingRequests(replaceByChange.values(), INTERNAL_SERVER_ERROR);
       }
-    } catch (StorageException err) {
-      logger.atSevere().withCause(err).log(
-          "Cannot read database before replacement for project %s", project.getName());
-      rejectRemainingRequests(replaceByChange.values(), INTERNAL_SERVER_ERROR);
-    } catch (IOException | PermissionBackendException err) {
-      logger.atSevere().withCause(err).log(
-          "Cannot read repository before replacement for project %s", project.getName());
-      rejectRemainingRequests(replaceByChange.values(), INTERNAL_SERVER_ERROR);
-    }
-    logger.atFine().log("Read %d changes to replace", replaceByChange.size());
+      logger.atFine().log("Read %d changes to replace", replaceByChange.size());
 
-    if (magicBranch != null && magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
-      // Cancel creations tied to refs/for/ or refs/drafts/ command.
-      for (ReplaceRequest req : replaceByChange.values()) {
-        if (req.inputCommand == magicBranch.cmd && req.cmd != null) {
+      if (magicBranch != null && magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
+        // Cancel creations tied to refs/for/ command.
+        for (ReplaceRequest req : replaceByChange.values()) {
+          if (req.inputCommand == magicBranch.cmd && req.cmd != null) {
+            req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted");
+          }
+        }
+        for (CreateRequest req : newChanges) {
           req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted");
         }
       }
-      for (CreateRequest req : newChanges) {
-        req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted");
-      }
     }
   }
 
   private void readChangesForReplace() {
-    Collection<ChangeNotes> allNotes =
-        notesFactory.create(
-            replaceByChange.values().stream().map(r -> r.ontoChange).collect(toList()));
-    for (ChangeNotes notes : allNotes) {
-      replaceByChange.get(notes.getChangeId()).notes = notes;
+    try (TraceTimer traceTimer = newTimer("readChangesForReplace")) {
+      Collection<ChangeNotes> allNotes =
+          notesFactory.create(
+              replaceByChange.values().stream().map(r -> r.ontoChange).collect(toList()));
+      for (ChangeNotes notes : allNotes) {
+        replaceByChange.get(notes.getChangeId()).notes = notes;
+      }
     }
   }
 
@@ -2644,24 +2744,26 @@
      * @throws PermissionBackendException
      */
     boolean validateNewPatchSet() throws IOException, PermissionBackendException {
-      if (!validateNewPatchSetNoteDb()) {
-        return false;
-      }
-      sameTreeWarning();
-
-      if (magicBranch != null) {
-        validateMagicBranchWipStatusChange();
-        if (inputCommand.getResult() != NOT_ATTEMPTED) {
+      try (TraceTimer traceTimer = newTimer("validateNewPatchSet")) {
+        if (!validateNewPatchSetNoteDb()) {
           return false;
         }
+        sameTreeWarning();
 
-        if (magicBranch.edit || magicBranch.draft) {
-          return newEdit();
+        if (magicBranch != null) {
+          validateMagicBranchWipStatusChange();
+          if (inputCommand.getResult() != NOT_ATTEMPTED) {
+            return false;
+          }
+
+          if (magicBranch.edit) {
+            return newEdit();
+          }
         }
-      }
 
-      newPatchSet();
-      return true;
+        newPatchSet();
+        return true;
+      }
     }
 
     boolean validateNewPatchSetForAutoClose() throws IOException, PermissionBackendException {
@@ -2675,59 +2777,63 @@
 
     /** Validates the new PS against permissions and notedb status. */
     private boolean validateNewPatchSetNoteDb() throws IOException, PermissionBackendException {
-      if (notes == null) {
-        reject(inputCommand, "change " + ontoChange + " not found");
-        return false;
-      }
-
-      Change change = notes.getChange();
-      priorPatchSet = change.currentPatchSetId();
-      if (!revisions.containsValue(priorPatchSet)) {
-        reject(inputCommand, "change " + ontoChange + " missing revisions");
-        return false;
-      }
-
-      RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
-
-      // Not allowed to create a new patch set if the current patch set is locked.
-      if (psUtil.isPatchSetLocked(notes)) {
-        reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
-        return false;
-      }
-
-      try {
-        permissions.change(notes).check(ChangePermission.ADD_PATCH_SET);
-      } catch (AuthException no) {
-        reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
-        return false;
-      }
-
-      if (change.isClosed()) {
-        reject(inputCommand, "change " + ontoChange + " closed");
-        return false;
-      } else if (revisions.containsKey(newCommit)) {
-        reject(inputCommand, "commit already exists (in the change)");
-        return false;
-      }
-
-      for (Ref r : receivePack.getRepository().getRefDatabase().getRefsByPrefix("refs/changes")) {
-        if (r.getObjectId().equals(newCommit)) {
-          reject(inputCommand, "commit already exists (in the project)");
+      try (TraceTimer traceTimer = newTimer("validateNewPatchSetNoteDb")) {
+        if (notes == null) {
+          reject(inputCommand, "change " + ontoChange + " not found");
           return false;
         }
-      }
 
-      for (RevCommit prior : revisions.keySet()) {
-        // Don't allow a change to directly depend upon itself. This is a
-        // very common error due to users making a new commit rather than
-        // amending when trying to address review comments.
-        if (receivePack.getRevWalk().isMergedInto(prior, newCommit)) {
-          reject(inputCommand, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
+        Change change = notes.getChange();
+        priorPatchSet = change.currentPatchSetId();
+        if (!revisions.containsValue(priorPatchSet)) {
+          reject(inputCommand, "change " + ontoChange + " missing revisions");
           return false;
         }
-      }
 
-      return true;
+        RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+
+        // Not allowed to create a new patch set if the current patch set is locked.
+        if (psUtil.isPatchSetLocked(notes)) {
+          reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
+          return false;
+        }
+
+        try {
+          permissions.change(notes).check(ChangePermission.ADD_PATCH_SET);
+        } catch (AuthException no) {
+          reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
+          return false;
+        }
+
+        if (change.isClosed()) {
+          reject(inputCommand, "change " + ontoChange + " closed");
+          return false;
+        } else if (revisions.containsKey(newCommit)) {
+          reject(inputCommand, "commit already exists (in the change)");
+          return false;
+        }
+
+        for (Ref r : receivePack.getRepository().getRefDatabase().getRefsByPrefix("refs/changes")) {
+          if (r.getObjectId().equals(newCommit)) {
+            reject(inputCommand, "commit already exists (in the project)");
+            return false;
+          }
+        }
+
+        try (TraceTimer traceTimer2 = newTimer("validateNewPatchSetNoteDb#isMergedInto")) {
+          for (RevCommit prior : revisions.keySet()) {
+            // Don't allow a change to directly depend upon itself. This is a
+            // very common error due to users making a new commit rather than
+            // amending when trying to address review comments.
+            if (receivePack.getRevWalk().isMergedInto(prior, newCommit)) {
+              reject(inputCommand, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
+              return false;
+            }
+          }
+        }
+
+        return true;
+      }
     }
 
     /** Validates whether the WIP change is allowed. Rejects inputCommand if not. */
@@ -2746,9 +2852,9 @@
 
         if (!hasWriteConfigPermission) {
           try {
-            permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+            permissions.change(notes).check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
           } catch (AuthException e1) {
-            reject(inputCommand, ONLY_CHANGE_OWNER_OR_PROJECT_OWNER_CAN_MODIFY_WIP);
+            reject(inputCommand, ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP);
           }
         }
       }
@@ -2756,39 +2862,41 @@
 
     /** prints a warning if the new PS has the same tree as the previous commit. */
     private void sameTreeWarning() throws IOException {
-      RevWalk rw = receivePack.getRevWalk();
-      RevCommit newCommit = rw.parseCommit(newCommitId);
-      RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
+      try (TraceTimer traceTimer = newTimer("sameTreeWarning")) {
+        RevWalk rw = receivePack.getRevWalk();
+        RevCommit newCommit = rw.parseCommit(newCommitId);
+        RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
 
-      if (newCommit.getTree().equals(priorCommit.getTree())) {
-        rw.parseBody(newCommit);
-        rw.parseBody(priorCommit);
-        boolean messageEq =
-            Objects.equals(newCommit.getFullMessage(), priorCommit.getFullMessage());
-        boolean parentsEq = parentsEqual(newCommit, priorCommit);
-        boolean authorEq = authorEqual(newCommit, priorCommit);
-        ObjectReader reader = receivePack.getRevWalk().getObjectReader();
+        if (newCommit.getTree().equals(priorCommit.getTree())) {
+          rw.parseBody(newCommit);
+          rw.parseBody(priorCommit);
+          boolean messageEq =
+              Objects.equals(newCommit.getFullMessage(), priorCommit.getFullMessage());
+          boolean parentsEq = parentsEqual(newCommit, priorCommit);
+          boolean authorEq = authorEqual(newCommit, priorCommit);
+          ObjectReader reader = receivePack.getRevWalk().getObjectReader();
 
-        if (messageEq && parentsEq && authorEq) {
-          addMessage(
-              String.format(
-                  "warning: no changes between prior commit %s and new commit %s",
-                  reader.abbreviate(priorCommit).name(), reader.abbreviate(newCommit).name()));
-        } else {
-          StringBuilder msg = new StringBuilder();
-          msg.append("warning: ").append(reader.abbreviate(newCommit).name());
-          msg.append(":");
-          msg.append(" no files changed");
-          if (!authorEq) {
-            msg.append(", author changed");
+          if (messageEq && parentsEq && authorEq) {
+            addMessage(
+                String.format(
+                    "warning: no changes between prior commit %s and new commit %s",
+                    abbreviateName(priorCommit, reader), abbreviateName(newCommit, reader)));
+          } else {
+            StringBuilder msg = new StringBuilder();
+            msg.append("warning: ").append(abbreviateName(newCommit, reader));
+            msg.append(":");
+            msg.append(" no files changed");
+            if (!authorEq) {
+              msg.append(", author changed");
+            }
+            if (!messageEq) {
+              msg.append(", message updated");
+            }
+            if (!parentsEq) {
+              msg.append(", was rebased");
+            }
+            addMessage(msg.toString());
           }
-          if (!messageEq) {
-            msg.append(", message updated");
-          }
-          if (!parentsEq) {
-            msg.append(", was rebased");
-          }
-          addMessage(msg.toString());
         }
       }
     }
@@ -2798,33 +2906,36 @@
      * failure.
      */
     private boolean newEdit() {
-      psId = notes.getChange().currentPatchSetId();
-      Optional<ChangeEdit> edit;
+      try (TraceTimer traceTimer = newTimer("newEdit")) {
+        psId = notes.getChange().currentPatchSetId();
+        Optional<ChangeEdit> edit;
 
-      try {
-        edit = editUtil.byChange(notes, user);
-      } catch (AuthException | IOException e) {
-        logger.atSevere().withCause(e).log("Cannot retrieve edit");
-        return false;
-      }
+        try {
+          edit = editUtil.byChange(notes, user);
+        } catch (AuthException | IOException e) {
+          logger.atSevere().withCause(e).log("Cannot retrieve edit");
+          return false;
+        }
 
-      if (edit.isPresent()) {
-        if (edit.get().getBasePatchSet().getId().equals(psId)) {
-          // replace edit
-          cmd =
-              new ReceiveCommand(edit.get().getEditCommit(), newCommitId, edit.get().getRefName());
+        if (edit.isPresent()) {
+          if (edit.get().getBasePatchSet().id().equals(psId)) {
+            // replace edit
+            cmd =
+                new ReceiveCommand(
+                    edit.get().getEditCommit(), newCommitId, edit.get().getRefName());
+          } else {
+            // delete old edit ref on rebase
+            prev =
+                new ReceiveCommand(
+                    edit.get().getEditCommit(), ObjectId.zeroId(), edit.get().getRefName());
+            createEditCommand();
+          }
         } else {
-          // delete old edit ref on rebase
-          prev =
-              new ReceiveCommand(
-                  edit.get().getEditCommit(), ObjectId.zeroId(), edit.get().getRefName());
           createEditCommand();
         }
-      } else {
-        createEditCommand();
-      }
 
-      return true;
+        return true;
+      }
     }
 
     /** Creates a ReceiveCommand for a new edit. */
@@ -2838,47 +2949,52 @@
 
     /** Updates 'this' to add a new patchset. */
     private void newPatchSet() throws IOException {
-      RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
-      psId =
-          ChangeUtil.nextPatchSetIdFromAllRefsMap(allRefs(), notes.getChange().currentPatchSetId());
-      info = patchSetInfoFactory.get(receivePack.getRevWalk(), newCommit, psId);
-      cmd = new ReceiveCommand(ObjectId.zeroId(), newCommitId, psId.toRefName());
+      try (TraceTimer traceTimer = newTimer("newPatchSet")) {
+        RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+        psId =
+            ChangeUtil.nextPatchSetIdFromAllRefsMap(
+                allRefs(), notes.getChange().currentPatchSetId());
+        info = patchSetInfoFactory.get(receivePack.getRevWalk(), newCommit, psId);
+        cmd = new ReceiveCommand(ObjectId.zeroId(), newCommitId, psId.toRefName());
+      }
     }
 
     void addOps(BatchUpdate bu, @Nullable Task progress) throws IOException {
-      if (magicBranch != null && (magicBranch.edit || magicBranch.draft)) {
-        bu.addOp(notes.getChangeId(), new ReindexOnlyOp());
-        if (prev != null) {
-          bu.addRepoOnlyOp(new UpdateOneRefOp(prev));
+      try (TraceTimer traceTimer = newTimer("addOps")) {
+        if (magicBranch != null && magicBranch.edit) {
+          bu.addOp(notes.getChangeId(), new ReindexOnlyOp());
+          if (prev != null) {
+            bu.addRepoOnlyOp(new UpdateOneRefOp(prev));
+          }
+          bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
+          return;
         }
-        bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
-        return;
-      }
-      RevWalk rw = receivePack.getRevWalk();
-      // TODO(dborowitz): Move to ReplaceOp#updateRepo.
-      RevCommit newCommit = rw.parseCommit(newCommitId);
-      rw.parseBody(newCommit);
+        RevWalk rw = receivePack.getRevWalk();
+        // TODO(dborowitz): Move to ReplaceOp#updateRepo.
+        RevCommit newCommit = rw.parseCommit(newCommitId);
+        rw.parseBody(newCommit);
 
-      RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
-      replaceOp =
-          replaceOpFactory
-              .create(
-                  projectState,
-                  notes.getChange().getDest(),
-                  checkMergedInto,
-                  checkMergedInto ? inputCommand.getNewId().name() : null,
-                  priorPatchSet,
-                  priorCommit,
-                  psId,
-                  newCommit,
-                  info,
-                  groups,
-                  magicBranch,
-                  receivePack.getPushCertificate())
-              .setRequestScopePropagator(requestScopePropagator);
-      bu.addOp(notes.getChangeId(), replaceOp);
-      if (progress != null) {
-        bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress));
+        RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
+        replaceOp =
+            replaceOpFactory
+                .create(
+                    projectState,
+                    notes.getChange().getDest(),
+                    checkMergedInto,
+                    checkMergedInto ? inputCommand.getNewId().name() : null,
+                    priorPatchSet,
+                    priorCommit,
+                    psId,
+                    newCommit,
+                    info,
+                    groups,
+                    magicBranch,
+                    receivePack.getPushCertificate())
+                .setRequestScopePropagator(requestScopePropagator);
+        bu.addOp(notes.getChangeId(), replaceOp);
+        if (progress != null) {
+          bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress));
+        }
       }
     }
 
@@ -2899,12 +3015,12 @@
 
     private void addOps(BatchUpdate bu) {
       bu.addOp(
-          psId.getParentKey(),
+          psId.changeId(),
           new BatchUpdateOp() {
             @Override
             public boolean updateChange(ChangeContext ctx) {
               PatchSet ps = psUtil.get(ctx.getNotes(), psId);
-              List<String> oldGroups = ps.getGroups();
+              List<String> oldGroups = ps.groups();
               if (oldGroups == null) {
                 if (groups == null) {
                   return false;
@@ -2912,7 +3028,7 @@
               } else if (sameGroups(oldGroups, groups)) {
                 return false;
               }
-              psUtil.setGroups(ctx.getUpdate(psId), ps, groups);
+              ctx.getUpdate(psId).setGroups(groups);
               return true;
             }
           });
@@ -2976,7 +3092,11 @@
   }
 
   private void initChangeRefMaps() {
-    if (refsByChange == null) {
+    if (refsByChange != null) {
+      return;
+    }
+
+    try (TraceTimer traceTimer = newTimer("initChangeRefMaps")) {
       int estRefsPerChange = 4;
       refsById = MultimapBuilder.hashKeys().arrayListValues().build();
       refsByChange =
@@ -2989,7 +3109,7 @@
           PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
           if (psId != null) {
             refsById.put(obj, ref);
-            refsByChange.put(psId.getParentKey(), ref);
+            refsByChange.put(psId.changeId(), ref);
           }
         }
       }
@@ -3035,17 +3155,19 @@
   // Run RefValidators on the command. If any validator fails, the command status is set to
   // REJECTED, and the return value is 'false'
   private boolean validRefOperation(ReceiveCommand cmd) {
-    RefOperationValidators refValidators = refValidatorsFactory.create(getProject(), user, cmd);
+    try (TraceTimer traceTimer = newTimer("validRefOperation")) {
+      RefOperationValidators refValidators = refValidatorsFactory.create(getProject(), user, cmd);
 
-    try {
-      messages.addAll(refValidators.validateForRefOperation());
-    } catch (RefOperationValidationException e) {
-      messages.addAll(Lists.newArrayList(e.getMessages()));
-      reject(cmd, e.getMessage());
-      return false;
+      try {
+        messages.addAll(refValidators.validateForRefOperation());
+      } catch (RefOperationValidationException e) {
+        messages.addAll(e.getMessages());
+        reject(cmd, e.getMessage());
+        return false;
+      }
+
+      return true;
     }
-
-    return true;
   }
 
   /**
@@ -3053,185 +3175,196 @@
    *
    * <p>On validation failure, the command is rejected.
    */
-  private void validateRegularPushCommits(Branch.NameKey branch, ReceiveCommand cmd)
+  private void validateRegularPushCommits(BranchNameKey branch, ReceiveCommand cmd)
       throws PermissionBackendException {
-    boolean skipValidation =
-        !RefNames.REFS_CONFIG.equals(cmd.getRefName())
-            && !(MagicBranch.isMagicBranch(cmd.getRefName())
-                || NEW_PATCHSET_PATTERN.matcher(cmd.getRefName()).matches())
-            && pushOptions.containsKey(PUSH_OPTION_SKIP_VALIDATION);
-    if (skipValidation) {
-      if (projectState.is(BooleanProjectConfig.USE_SIGNED_OFF_BY)) {
-        reject(cmd, "requireSignedOffBy prevents option " + PUSH_OPTION_SKIP_VALIDATION);
-        return;
-      }
-
-      Optional<AuthException> err =
-          checkRefPermission(permissions.ref(branch.get()), RefPermission.SKIP_VALIDATION);
-      if (err.isPresent()) {
-        rejectProhibited(cmd, err.get());
-        return;
-      }
-      if (!Iterables.isEmpty(rejectCommits)) {
-        reject(cmd, "reject-commits prevents " + PUSH_OPTION_SKIP_VALIDATION);
-      }
-    }
-
-    BranchCommitValidator validator = commitValidatorFactory.create(projectState, branch, user);
-    RevWalk walk = receivePack.getRevWalk();
-    walk.reset();
-    walk.sort(RevSort.NONE);
-    try {
-      RevObject parsedObject = walk.parseAny(cmd.getNewId());
-      if (!(parsedObject instanceof RevCommit)) {
-        return;
-      }
-      ListMultimap<ObjectId, Ref> existing = changeRefsById();
-      walk.markStart((RevCommit) parsedObject);
-      markHeadsAsUninteresting(walk, cmd.getRefName());
-      int limit = receiveConfig.maxBatchCommits;
-      int n = 0;
-      for (RevCommit c; (c = walk.next()) != null; ) {
-        // Even if skipValidation is set, we still get here when at least one plugin
-        // commit validator requires to validate all commits. In this case, however,
-        // we don't need to check the commit limit.
-        if (++n > limit && !skipValidation) {
-          logger.atFine().log("Number of new commits exceeds limit of %d", limit);
-          reject(
-              cmd,
-              String.format(
-                  "more than %d commits, and %s not set", limit, PUSH_OPTION_SKIP_VALIDATION));
+    try (TraceTimer traceTimer =
+        newTimer("validateRegularPushCommits", Metadata.builder().branchName(branch.branch()))) {
+      boolean skipValidation =
+          !RefNames.REFS_CONFIG.equals(cmd.getRefName())
+              && !(MagicBranch.isMagicBranch(cmd.getRefName())
+                  || NEW_PATCHSET_PATTERN.matcher(cmd.getRefName()).matches())
+              && pushOptions.containsKey(PUSH_OPTION_SKIP_VALIDATION);
+      if (skipValidation) {
+        if (projectState.is(BooleanProjectConfig.USE_SIGNED_OFF_BY)) {
+          reject(cmd, "requireSignedOffBy prevents option " + PUSH_OPTION_SKIP_VALIDATION);
           return;
         }
-        if (existing.keySet().contains(c)) {
-          continue;
-        }
 
-        if (!validator.validCommit(
-            walk.getObjectReader(), cmd, c, false, messages, rejectCommits, null, skipValidation)) {
-          break;
+        Optional<AuthException> err =
+            checkRefPermission(permissions.ref(branch.branch()), RefPermission.SKIP_VALIDATION);
+        if (err.isPresent()) {
+          rejectProhibited(cmd, err.get());
+          return;
+        }
+        if (!Iterables.isEmpty(rejectCommits)) {
+          reject(cmd, "reject-commits prevents " + PUSH_OPTION_SKIP_VALIDATION);
         }
       }
-      logger.atFine().log("Validated %d new commits", n);
-    } catch (IOException err) {
-      cmd.setResult(REJECTED_MISSING_OBJECT);
-      logger.atSevere().withCause(err).log("Invalid pack upload; one or more objects weren't sent");
+
+      BranchCommitValidator validator = commitValidatorFactory.create(projectState, branch, user);
+      RevWalk walk = receivePack.getRevWalk();
+      walk.reset();
+      walk.sort(RevSort.NONE);
+      try {
+        RevObject parsedObject = walk.parseAny(cmd.getNewId());
+        if (!(parsedObject instanceof RevCommit)) {
+          return;
+        }
+        ListMultimap<ObjectId, Ref> existing = changeRefsById();
+        walk.markStart((RevCommit) parsedObject);
+        markHeadsAsUninteresting(walk, cmd.getRefName());
+        int limit = receiveConfig.maxBatchCommits;
+        int n = 0;
+        for (RevCommit c; (c = walk.next()) != null; ) {
+          // Even if skipValidation is set, we still get here when at least one plugin
+          // commit validator requires to validate all commits. In this case, however,
+          // we don't need to check the commit limit.
+          if (++n > limit && !skipValidation) {
+            logger.atFine().log("Number of new commits exceeds limit of %d", limit);
+            reject(
+                cmd,
+                String.format(
+                    "more than %d commits, and %s not set", limit, PUSH_OPTION_SKIP_VALIDATION));
+            return;
+          }
+          if (existing.keySet().contains(c)) {
+            continue;
+          }
+
+          BranchCommitValidator.Result validationResult =
+              validator.validateCommit(
+                  walk.getObjectReader(), cmd, c, false, rejectCommits, null, skipValidation);
+          messages.addAll(validationResult.messages());
+          if (!validationResult.isValid()) {
+            break;
+          }
+        }
+        logger.atFine().log("Validated %d new commits", n);
+      } catch (IOException err) {
+        cmd.setResult(REJECTED_MISSING_OBJECT);
+        logger.atSevere().withCause(err).log(
+            "Invalid pack upload; one or more objects weren't sent");
+      }
     }
   }
 
   private void autoCloseChanges(ReceiveCommand cmd, Task progress) {
-    logger.atFine().log("Starting auto-closing of changes");
-    String refName = cmd.getRefName();
-    Set<Change.Id> ids = new HashSet<>();
+    try (TraceTimer traceTimer = newTimer("autoCloseChanges")) {
+      logger.atFine().log("Starting auto-closing of changes");
+      String refName = cmd.getRefName();
+      Set<Change.Id> ids = new HashSet<>();
 
-    // TODO(dborowitz): Combine this BatchUpdate with the main one in
-    // handleRegularCommands
-    try {
-      retryHelper.execute(
-          updateFactory -> {
-            try (BatchUpdate bu =
-                    updateFactory.create(projectState.getNameKey(), user, TimeUtil.nowTs());
-                ObjectInserter ins = repo.newObjectInserter();
-                ObjectReader reader = ins.newReader();
-                RevWalk rw = new RevWalk(reader)) {
-              bu.setRepository(repo, rw, ins);
-              // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
+      // TODO(dborowitz): Combine this BatchUpdate with the main one in
+      // handleRegularCommands
+      try {
+        retryHelper.execute(
+            updateFactory -> {
+              try (BatchUpdate bu =
+                      updateFactory.create(projectState.getNameKey(), user, TimeUtil.nowTs());
+                  ObjectInserter ins = repo.newObjectInserter();
+                  ObjectReader reader = ins.newReader();
+                  RevWalk rw = new RevWalk(reader)) {
+                bu.setRepository(repo, rw, ins);
+                // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
 
-              RevCommit newTip = rw.parseCommit(cmd.getNewId());
-              Branch.NameKey branch = new Branch.NameKey(project.getNameKey(), refName);
+                RevCommit newTip = rw.parseCommit(cmd.getNewId());
+                BranchNameKey branch = BranchNameKey.create(project.getNameKey(), refName);
 
-              rw.reset();
-              rw.sort(RevSort.REVERSE);
-              rw.markStart(newTip);
-              if (!ObjectId.zeroId().equals(cmd.getOldId())) {
-                rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
-              }
+                rw.reset();
+                rw.sort(RevSort.REVERSE);
+                rw.markStart(newTip);
+                if (!ObjectId.zeroId().equals(cmd.getOldId())) {
+                  rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
+                }
 
-              ListMultimap<ObjectId, Ref> byCommit = changeRefsById();
-              Map<Change.Key, ChangeData> changeDataByKey = null;
-              List<ReplaceRequest> replaceAndClose = new ArrayList<>();
+                ListMultimap<ObjectId, Ref> byCommit = changeRefsById();
+                Map<Change.Key, ChangeData> changeDataByKey = null;
+                List<ReplaceRequest> replaceAndClose = new ArrayList<>();
 
-              int existingPatchSets = 0;
-              int newPatchSets = 0;
-              COMMIT:
-              for (RevCommit c; (c = rw.next()) != null; ) {
-                rw.parseBody(c);
+                int existingPatchSets = 0;
+                int newPatchSets = 0;
+                COMMIT:
+                for (RevCommit c; (c = rw.next()) != null; ) {
+                  rw.parseBody(c);
 
-                for (Ref ref : byCommit.get(c.copy())) {
-                  PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
-                  Optional<ChangeNotes> notes = getChangeNotes(psId.getParentKey());
-                  if (notes.isPresent() && notes.get().getChange().getDest().equals(branch)) {
-                    existingPatchSets++;
-                    bu.addOp(notes.get().getChangeId(), setPrivateOpFactory.create(false, null));
-                    bu.addOp(
-                        psId.getParentKey(),
-                        mergedByPushOpFactory.create(
-                            requestScopePropagator, psId, refName, newTip.getId().getName()));
-                    continue COMMIT;
+                  for (Ref ref : byCommit.get(c.copy())) {
+                    PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
+                    Optional<ChangeNotes> notes = getChangeNotes(psId.changeId());
+                    if (notes.isPresent() && notes.get().getChange().getDest().equals(branch)) {
+                      existingPatchSets++;
+                      bu.addOp(notes.get().getChangeId(), setPrivateOpFactory.create(false, null));
+                      bu.addOp(
+                          psId.changeId(),
+                          mergedByPushOpFactory.create(
+                              requestScopePropagator, psId, refName, newTip.getId().getName()));
+                      continue COMMIT;
+                    }
+                  }
+
+                  for (String changeId : c.getFooterLines(FooterConstants.CHANGE_ID)) {
+                    if (changeDataByKey == null) {
+                      changeDataByKey = executeIndexQuery(() -> openChangesByKeyByBranch(branch));
+                    }
+
+                    ChangeData onto = changeDataByKey.get(Change.key(changeId.trim()));
+                    if (onto != null) {
+                      newPatchSets++;
+                      // Hold onto this until we're done with the walk, as the call to
+                      // req.validate below calls isMergedInto which resets the walk.
+                      ChangeNotes ontoNotes = onto.notes();
+                      ReplaceRequest req =
+                          new ReplaceRequest(ontoNotes.getChangeId(), c, cmd, false);
+                      req.notes = ontoNotes;
+                      replaceAndClose.add(req);
+                      continue COMMIT;
+                    }
                   }
                 }
 
-                for (String changeId : c.getFooterLines(CHANGE_ID)) {
-                  if (changeDataByKey == null) {
-                    changeDataByKey = executeIndexQuery(() -> openChangesByKeyByBranch(branch));
+                for (ReplaceRequest req : replaceAndClose) {
+                  Change.Id id = req.notes.getChangeId();
+                  if (!req.validateNewPatchSetForAutoClose()) {
+                    logger.atFine().log("Not closing %s because validation failed", id);
+                    continue;
                   }
-
-                  ChangeData onto = changeDataByKey.get(new Change.Key(changeId.trim()));
-                  if (onto != null) {
-                    newPatchSets++;
-                    // Hold onto this until we're done with the walk, as the call to
-                    // req.validate below calls isMergedInto which resets the walk.
-                    ChangeNotes ontoNotes = onto.notes();
-                    ReplaceRequest req = new ReplaceRequest(ontoNotes.getChangeId(), c, cmd, false);
-                    req.notes = ontoNotes;
-                    replaceAndClose.add(req);
-                    continue COMMIT;
-                  }
+                  req.addOps(bu, null);
+                  bu.addOp(id, setPrivateOpFactory.create(false, null));
+                  bu.addOp(
+                      id,
+                      mergedByPushOpFactory
+                          .create(
+                              requestScopePropagator, req.psId, refName, newTip.getId().getName())
+                          .setPatchSetProvider(req.replaceOp::getPatchSet));
+                  bu.addOp(id, new ChangeProgressOp(progress));
+                  ids.add(id);
                 }
+
+                logger.atFine().log(
+                    "Auto-closing %d changes with existing patch sets and %d with new patch sets",
+                    existingPatchSets, newPatchSets);
+                bu.execute();
+              } catch (IOException | StorageException | PermissionBackendException e) {
+                logger.atSevere().withCause(e).log("Failed to auto-close changes");
+                return null;
               }
 
-              for (ReplaceRequest req : replaceAndClose) {
-                Change.Id id = req.notes.getChangeId();
-                if (!req.validateNewPatchSetForAutoClose()) {
-                  logger.atFine().log("Not closing %s because validation failed", id);
-                  continue;
-                }
-                req.addOps(bu, null);
-                bu.addOp(id, setPrivateOpFactory.create(false, null));
-                bu.addOp(
-                    id,
-                    mergedByPushOpFactory
-                        .create(requestScopePropagator, req.psId, refName, newTip.getId().getName())
-                        .setPatchSetProvider(req.replaceOp::getPatchSet));
-                bu.addOp(id, new ChangeProgressOp(progress));
-                ids.add(id);
-              }
+              // If we are here, we didn't throw UpdateException. Record the result.
+              // The ordering is indeterminate due to the HashSet; unfortunately, Change.Id doesn't
+              // fit into TreeSet.
+              ids.stream().forEach(id -> resultChangeIds.add(ResultChangeIds.Key.AUTOCLOSED, id));
 
-              logger.atFine().log(
-                  "Auto-closing %s changes with existing patch sets and %s with new patch sets",
-                  existingPatchSets, newPatchSets);
-              bu.execute();
-            } catch (IOException | StorageException | PermissionBackendException e) {
-              logger.atSevere().withCause(e).log("Failed to auto-close changes");
               return null;
-            }
-
-            // If we are here, we didn't throw UpdateException. Record the result.
-            // The ordering is indeterminate due to the HashSet; unfortunately, Change.Id doesn't
-            // fit into TreeSet.
-            ids.stream().forEach(id -> resultChangeIds.add(ResultChangeIds.Key.AUTOCLOSED, id));
-
-            return null;
-          },
-          // Use a multiple of the default timeout to account for inner retries that may otherwise
-          // eat up the whole timeout so that no time is left to retry this outer action.
-          RetryHelper.options()
-              .timeout(retryHelper.getDefaultTimeout(ActionType.CHANGE_UPDATE).multipliedBy(5))
-              .build());
-    } catch (RestApiException e) {
-      logger.atSevere().withCause(e).log("Can't insert patchset");
-    } catch (UpdateException e) {
-      logger.atSevere().withCause(e).log("Failed to auto-close changes");
+            },
+            // Use a multiple of the default timeout to account for inner retries that may otherwise
+            // eat up the whole timeout so that no time is left to retry this outer action.
+            RetryHelper.options()
+                .timeout(retryHelper.getDefaultTimeout(ActionType.CHANGE_UPDATE).multipliedBy(5))
+                .build());
+      } catch (RestApiException e) {
+        logger.atSevere().withCause(e).log("Can't insert patchset");
+      } catch (UpdateException e) {
+        logger.atSevere().withCause(e).log("Failed to auto-close changes");
+      }
     }
   }
 
@@ -3244,7 +3377,7 @@
   }
 
   private <T> T executeIndexQuery(Action<T> action) {
-    try {
+    try (TraceTimer traceTimer = newTimer("executeIndexQuery")) {
       return retryHelper.execute(
           ActionType.INDEX_QUERY, action, StorageException.class::isInstance);
     } catch (Exception e) {
@@ -3253,19 +3386,22 @@
     }
   }
 
-  private Map<Change.Key, ChangeData> openChangesByKeyByBranch(Branch.NameKey branch) {
-    Map<Change.Key, ChangeData> r = new HashMap<>();
-    for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) {
-      try {
-        // ChangeData is not materialised into a ChangeNotes for avoiding
-        // to load a potentially large number of changes meta-data into memory
-        // which would cause unnecessary disk I/O, CPU and heap utilisation.
-        r.put(cd.change().getKey(), cd);
-      } catch (NoSuchChangeException e) {
-        // Ignore deleted change
+  private Map<Change.Key, ChangeData> openChangesByKeyByBranch(BranchNameKey branch) {
+    try (TraceTimer traceTimer =
+        newTimer("openChangesByKeyByBranch", Metadata.builder().branchName(branch.branch()))) {
+      Map<Change.Key, ChangeData> r = new HashMap<>();
+      for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) {
+        try {
+          // ChangeData is not materialised into a ChangeNotes for avoiding
+          // to load a potentially large number of changes meta-data into memory
+          // which would cause unnecessary disk I/O, CPU and heap utilisation.
+          r.put(cd.change().getKey(), cd);
+        } catch (NoSuchChangeException e) {
+          // Ignore deleted change
+        }
       }
+      return r;
     }
-    return r;
   }
 
   // allRefsWatcher hooks into the protocol negotation to get a list of all known refs.
@@ -3275,7 +3411,25 @@
     return allRefsWatcher.getAllRefs();
   }
 
+  private TraceTimer newTimer(String name) {
+    return newTimer(getClass(), name);
+  }
+
+  private TraceTimer newTimer(Class<?> clazz, String name) {
+    return newTimer(clazz, name, Metadata.builder());
+  }
+
+  private TraceTimer newTimer(String name, Metadata.Builder metadataBuilder) {
+    return newTimer(getClass(), name, metadataBuilder);
+  }
+
+  private TraceTimer newTimer(Class<?> clazz, String name, Metadata.Builder metadataBuilder) {
+    metadataBuilder.projectName(project.getName());
+    return TraceContext.newTimer(clazz.getSimpleName() + "#" + name, metadataBuilder.build());
+  }
+
   private static void reject(ReceiveCommand cmd, String why) {
+    logger.atFine().log("Rejecting command '%s': %s", cmd, why);
     cmd.setResult(REJECTED_OTHER_REASON, why);
   }
 
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index cfe06ea..83bf554 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -14,43 +14,55 @@
 
 package com.google.gerrit.server.git.receive;
 
-import com.google.auto.value.AutoValue;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Maps;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.HookUtil;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Provider;
+import java.io.IOException;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
-import org.eclipse.jgit.transport.BaseReceivePack;
+import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.ServiceMayNotContinueException;
 import org.eclipse.jgit.transport.UploadPack;
 
-/** Exposes only the non refs/changes/ reference names. */
+/**
+ * Exposes only the non refs/changes/ reference names and provide additional haves.
+ *
+ * <p>Negotiation on Git push is suboptimal in that it tends to send more objects to the server than
+ * it should. This results in increased latencies for {@code git push}.
+ *
+ * <p>Ref advertisement for Git pushes still works in a "the server speaks first fashion" as Git
+ * Protocol V2 only addressed fetches Therefore the server needs to send all available references.
+ * For large repositories, this can be in the tens of megabytes to send to the client. We therefore
+ * remove all refs in refs/changes/* to scale down that footprint. Trivially, this would increase
+ * the unnecessary objects that the client has to send to the server because the common ancestor it
+ * finds in negotiation might be further back in history.
+ *
+ * <p>To work around this, we advertise the last 32 changes in that repository as additional {@code
+ * .haves}. This is a heuristical approach that aims at scaling down the number of unnecessary
+ * objects that client sends to the server. Unnecessary here refers to objects that the server
+ * already has.
+ *
+ * <p>TODO(hiesel): Instrument this heuristic and proof its value.
+ */
 public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  @VisibleForTesting
-  @AutoValue
-  public abstract static class Result {
-    public abstract Map<String, Ref> allRefs();
-
-    public abstract Set<ObjectId> additionalHaves();
-  }
-
   private final Provider<InternalChangeQuery> queryProvider;
   private final Project.NameKey projectName;
 
@@ -66,31 +78,18 @@
         "ReceiveCommitsAdvertiseRefsHook cannot be used for UploadPack");
   }
 
-  @SuppressWarnings("deprecation")
   @Override
-  public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException {
-    Result r = advertiseRefs(HookUtil.ensureAllRefsAdvertised(rp));
-    rp.setAdvertisedRefs(r.allRefs(), r.additionalHaves());
+  public void advertiseRefs(ReceivePack rp) throws ServiceMayNotContinueException {
+    Map<String, Ref> advertisedRefs = HookUtil.ensureAllRefsAdvertised(rp);
+    advertisedRefs.keySet().stream()
+        .filter(ReceiveCommitsAdvertiseRefsHook::skip)
+        .collect(toImmutableList())
+        .forEach(r -> advertisedRefs.remove(r));
+    rp.setAdvertisedRefs(advertisedRefs, advertiseOpenChanges(rp.getRepository()));
   }
 
-  @VisibleForTesting
-  public Result advertiseRefs(Map<String, Ref> oldRefs) {
-    Map<String, Ref> r = Maps.newHashMapWithExpectedSize(oldRefs.size());
-    Set<ObjectId> allPatchSets = Sets.newHashSetWithExpectedSize(oldRefs.size());
-    for (Map.Entry<String, Ref> e : oldRefs.entrySet()) {
-      String name = e.getKey();
-      if (!skip(name)) {
-        r.put(name, e.getValue());
-      }
-      if (name.startsWith(RefNames.REFS_CHANGES)) {
-        allPatchSets.add(e.getValue().getObjectId());
-      }
-    }
-    return new AutoValue_ReceiveCommitsAdvertiseRefsHook_Result(
-        r, advertiseOpenChanges(allPatchSets));
-  }
-
-  private Set<ObjectId> advertiseOpenChanges(Set<ObjectId> allPatchSets) {
+  private Set<ObjectId> advertiseOpenChanges(Repository repo)
+      throws ServiceMayNotContinueException {
     // Advertise some recent open changes, in case a commit is based on one.
     int limit = 32;
     try {
@@ -109,15 +108,20 @@
               .byProjectOpen(projectName)) {
         PatchSet ps = cd.currentPatchSet();
         if (ps != null) {
-          ObjectId id = ObjectId.fromString(ps.getRevision().get());
           // Ensure we actually observed a patch set ref pointing to this
           // object, in case the database is out of sync with the repo and the
           // object doesn't actually exist.
-          if (allPatchSets.contains(id)) {
-            r.add(id);
+          try {
+            Ref psRef = repo.getRefDatabase().exactRef(RefNames.patchSetRef(ps.id()));
+            if (psRef != null) {
+              r.add(ps.commitId());
+            }
+          } catch (IOException e) {
+            throw new ServiceMayNotContinueException(e);
           }
         }
       }
+
       return r;
     } catch (StorageException err) {
       logger.atSevere().withCause(err).log("Cannot list open changes of %s", projectName);
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java
new file mode 100644
index 0000000..d574466
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java
@@ -0,0 +1,91 @@
+// 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.git.receive;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.git.UsersSelfAdvertiseRefsHook;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.transport.AdvertiseRefsHook;
+import org.eclipse.jgit.transport.AdvertiseRefsHookChain;
+
+/**
+ * Helper to ensure that the chain for advertising refs is the same in tests and production code.
+ */
+public class ReceiveCommitsAdvertiseRefsHookChain {
+
+  /**
+   * Returns a single {@link AdvertiseRefsHook} that encompasses a chain of {@link
+   * AdvertiseRefsHook} to be used for advertising when processing a Git push.
+   */
+  public static AdvertiseRefsHook create(
+      AllRefsWatcher allRefsWatcher,
+      UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook,
+      AllUsersName allUsersName,
+      Provider<InternalChangeQuery> queryProvider,
+      Project.NameKey projectName) {
+    return create(
+        allRefsWatcher,
+        usersSelfAdvertiseRefsHook,
+        allUsersName,
+        queryProvider,
+        projectName,
+        false);
+  }
+
+  /**
+   * Returns a single {@link AdvertiseRefsHook} that encompasses a chain of {@link
+   * AdvertiseRefsHook} to be used for advertising when processing a Git push. Omits {@link
+   * HackPushNegotiateHook} as that does not advertise refs on it's own but adds {@code .have} based
+   * on history which is not relevant for the tests we have.
+   */
+  @VisibleForTesting
+  public static AdvertiseRefsHook createForTest(
+      Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName, CurrentUser user) {
+    return create(
+        new AllRefsWatcher(),
+        new UsersSelfAdvertiseRefsHook(Providers.of(user)),
+        new AllUsersName(AllUsersNameProvider.DEFAULT),
+        queryProvider,
+        projectName,
+        true);
+  }
+
+  private static AdvertiseRefsHook create(
+      AllRefsWatcher allRefsWatcher,
+      UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook,
+      AllUsersName allUsersName,
+      Provider<InternalChangeQuery> queryProvider,
+      Project.NameKey projectName,
+      boolean skipHackPushNegotiateHook) {
+    List<AdvertiseRefsHook> advHooks = new ArrayList<>();
+    advHooks.add(allRefsWatcher);
+    advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName));
+    if (!skipHackPushNegotiateHook) {
+      advHooks.add(new HackPushNegotiateHook());
+    }
+    if (projectName.equals(allUsersName)) {
+      advHooks.add(usersSelfAdvertiseRefsHook);
+    }
+    return AdvertiseRefsHookChain.newChain(advHooks);
+  }
+}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveConfig.java b/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
index b1c9f13..cdbf310 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
@@ -27,7 +27,6 @@
 class ReceiveConfig {
   final boolean checkMagicRefs;
   final boolean checkReferencedObjectsAreReachable;
-  final boolean allowDrafts;
   final int maxBatchCommits;
   final boolean disablePrivateChanges;
   private final int systemMaxBatchChanges;
@@ -38,7 +37,6 @@
     checkMagicRefs = config.getBoolean("receive", null, "checkMagicRefs", true);
     checkReferencedObjectsAreReachable =
         config.getBoolean("receive", null, "checkReferencedObjectsAreReachable", true);
-    allowDrafts = config.getBoolean("change", null, "allowDrafts", false);
     maxBatchCommits = config.getInt("receive", null, "maxBatchCommits", 10000);
     systemMaxBatchChanges = config.getInt("receive", "maxBatchChanges", 0);
     disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
index 03a1b33..df1888b 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
@@ -20,8 +20,8 @@
   public static final String PUSH_OPTION_SKIP_VALIDATION = "skip-validation";
 
   @VisibleForTesting
-  public static final String ONLY_CHANGE_OWNER_OR_PROJECT_OWNER_CAN_MODIFY_WIP =
-      "only change owner or project owner can modify Work-in-Progress";
+  public static final String ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP =
+      "only users with Toogle-Wip-State permission can modify Work-in-Progress";
 
   static final String COMMAND_REJECTION_MESSAGE_FOOTER =
       "Contact an administrator to fix the permissions";
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveRefFilter.java b/java/com/google/gerrit/server/git/receive/ReceiveRefFilter.java
index 16cba53..24a89f7 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveRefFilter.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveRefFilter.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.git.receive;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CACHE_AUTOMERGE;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
+import static com.google.gerrit.entities.RefNames.REFS_CACHE_AUTOMERGE;
+import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
 
 import com.google.common.collect.Maps;
 import java.util.Map;
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 4d0d2c3..e95cf3b 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -29,19 +29,19 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.PatchSetInfo;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
@@ -99,7 +99,7 @@
   public interface Factory {
     ReplaceOp create(
         ProjectState projectState,
-        Branch.NameKey dest,
+        BranchNameKey dest,
         boolean checkMergedInto,
         @Nullable String mergeResultRevId,
         @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
@@ -132,7 +132,7 @@
   private final ReviewerAdder reviewerAdder;
 
   private final ProjectState projectState;
-  private final Branch.NameKey dest;
+  private final BranchNameKey dest;
   private final boolean checkMergedInto;
   private final String mergeResultRevId;
   private final PatchSet.Id priorPatchSetId;
@@ -177,7 +177,7 @@
       @SendEmailExecutor ExecutorService sendEmailExecutor,
       ReviewerAdder reviewerAdder,
       @Assisted ProjectState projectState,
-      @Assisted Branch.NameKey dest,
+      @Assisted BranchNameKey dest,
       @Assisted boolean checkMergedInto,
       @Assisted @Nullable String mergeResultRevId,
       @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
@@ -232,7 +232,7 @@
             commitId);
 
     if (checkMergedInto) {
-      String mergedInto = findMergedInto(ctx, dest.get(), commit);
+      String mergedInto = findMergedInto(ctx, dest.branch(), commit);
       if (mergedInto != null) {
         mergedByPushOp =
             mergedByPushOpFactory.create(
@@ -255,7 +255,7 @@
     }
     if (groups.isEmpty()) {
       PatchSet prevPs = psUtil.current(notes);
-      groups = prevPs != null ? prevPs.getGroups() : ImmutableList.of();
+      groups = prevPs != null ? prevPs.groups() : ImmutableList.of();
     }
 
     ChangeData cd = changeDataFactory.create(ctx.getNotes());
@@ -295,7 +295,7 @@
       }
       if (shouldPublishComments()) {
         boolean workInProgress = change.isWorkInProgress();
-        if (magicBranch != null && magicBranch.workInProgress) {
+        if (magicBranch.workInProgress) {
           workInProgress = true;
         }
         comments = publishComments(ctx, workInProgress);
@@ -459,7 +459,7 @@
           continue;
         }
 
-        LabelType lt = projectState.getLabelTypes().byLabel(a.getLabelId());
+        LabelType lt = projectState.getLabelTypes().byLabel(a.labelId());
         if (lt != null) {
           current.put(lt.getName(), a);
         }
@@ -481,11 +481,7 @@
     change.setCurrentPatchSet(info);
 
     List<String> idList = commit.getFooterLines(CHANGE_ID);
-    if (idList.isEmpty()) {
-      change.setKey(new Change.Key("I" + commitId.name()));
-    } else {
-      change.setKey(new Change.Key(idList.get(idList.size() - 1).trim()));
-    }
+    change.setKey(Change.key(idList.get(idList.size() - 1).trim()));
   }
 
   private List<Comment> publishComments(ChangeContext ctx, boolean workInProgress) {
@@ -548,7 +544,7 @@
       try {
         ReplacePatchSetSender cm =
             replacePatchSetFactory.create(projectState.getNameKey(), notes.getChangeId());
-        cm.setFrom(ctx.getAccount().getAccount().getId());
+        cm.setFrom(ctx.getAccount().account().id());
         cm.setPatchSet(newPatchSet, info);
         cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
         cm.setNotify(ctx.getNotify(notes.getChangeId()));
@@ -556,7 +552,7 @@
             Streams.concat(
                     oldRecipients.getReviewers().stream(),
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewers).stream()
-                        .map(PatchSetApproval::getAccountId))
+                        .map(PatchSetApproval::accountId))
                 .collect(toImmutableSet()));
         cm.addExtraCC(
             Streams.concat(
@@ -567,7 +563,7 @@
         cm.send();
       } catch (Exception e) {
         logger.atSevere().withCause(e).log(
-            "Cannot send email for new patch set %s", newPatchSet.getId());
+            "Cannot send email for new patch set %s", newPatchSet.id());
       }
     }
 
diff --git a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
index e326141..805822c 100644
--- a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
+++ b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git.receive;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import java.util.ArrayList;
 import java.util.EnumMap;
 import java.util.List;
diff --git a/java/com/google/gerrit/server/git/receive/testing/BUILD b/java/com/google/gerrit/server/git/receive/testing/BUILD
new file mode 100644
index 0000000..06407ae
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/testing/BUILD
@@ -0,0 +1,14 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+    name = "testing",
+    testonly = 1,
+    srcs = glob(["**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//lib:guava",
+        "//lib:jgit",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
+    ],
+)
diff --git a/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java b/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java
new file mode 100644
index 0000000..c54ab25
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java
@@ -0,0 +1,87 @@
+// 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.git.receive.testing;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.StreamSupport;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.RefAdvertiser;
+
+/** Helper to collect advertised refs and additonal haves and verify them in tests. */
+public class TestRefAdvertiser extends RefAdvertiser {
+
+  @VisibleForTesting
+  @AutoValue
+  public abstract static class Result {
+    public abstract Map<String, Ref> allRefs();
+
+    public abstract Set<ObjectId> additionalHaves();
+
+    public static Result create(Map<String, Ref> allRefs, Set<ObjectId> additionalHaves) {
+      return new AutoValue_TestRefAdvertiser_Result(allRefs, additionalHaves);
+    }
+  }
+
+  private final Map<String, Ref> advertisedRefs;
+  private final Set<ObjectId> additionalHaves;
+  private final Repository repo;
+
+  public TestRefAdvertiser(Repository repo) {
+    advertisedRefs = new HashMap<>();
+    additionalHaves = new HashSet<>();
+    this.repo = repo;
+  }
+
+  @Override
+  protected void writeOne(CharSequence line) throws IOException {
+    List<String> lineParts =
+        StreamSupport.stream(Splitter.on(' ').split(line).spliterator(), false)
+            .map(String::trim)
+            .collect(toImmutableList());
+    if (".have".equals(lineParts.get(1))) {
+      additionalHaves.add(ObjectId.fromString(lineParts.get(0)));
+    } else {
+      ObjectId id = ObjectId.fromString(lineParts.get(0));
+      Ref ref =
+          repo.getRefDatabase().getRefs().stream()
+              .filter(r -> r.getObjectId().equals(id))
+              .findAny()
+              .orElseThrow(
+                  () ->
+                      new RuntimeException(
+                          line.toString() + " does not conform to expected pattern"));
+      advertisedRefs.put(lineParts.get(1), ref);
+    }
+  }
+
+  @Override
+  protected void end() {}
+
+  public Result result() {
+    return Result.create(advertisedRefs, additionalHaves);
+  }
+}
diff --git a/java/com/google/gerrit/server/git/validators/AccountValidator.java b/java/com/google/gerrit/server/git/validators/AccountValidator.java
index 08870d3..c6af49c 100644
--- a/java/com/google/gerrit/server/git/validators/AccountValidator.java
+++ b/java/com/google/gerrit/server/git/validators/AccountValidator.java
@@ -18,7 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountConfig;
 import com.google.gerrit.server.account.AccountProperties;
@@ -87,10 +87,10 @@
       messages.add("cannot deactivate own account");
     }
 
-    String newPreferredEmail = newAccount.get().getPreferredEmail();
+    String newPreferredEmail = newAccount.get().preferredEmail();
     if (newPreferredEmail != null
         && (!oldAccount.isPresent()
-            || !newPreferredEmail.equals(oldAccount.get().getPreferredEmail()))) {
+            || !newPreferredEmail.equals(oldAccount.get().preferredEmail()))) {
       if (!emailValidator.isValid(newPreferredEmail)) {
         messages.add(
             String.format(
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index ff21b2d..6773d29 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.git.validators;
 
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
+import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
+import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
+import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -25,14 +25,14 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
@@ -127,7 +127,7 @@
 
     public CommitValidators forReceiveCommits(
         PermissionBackend.ForProject forProject,
-        Branch.NameKey branch,
+        BranchNameKey branch,
         IdentifiedUser user,
         SshInfo sshInfo,
         NoteMap rejectCommits,
@@ -135,8 +135,8 @@
         @Nullable Change change,
         boolean skipValidation)
         throws IOException {
-      PermissionBackend.ForRef perm = forProject.ref(branch.get());
-      ProjectState projectState = projectCache.checkedGet(branch.getParentKey());
+      PermissionBackend.ForRef perm = forProject.ref(branch.branch());
+      ProjectState projectState = projectCache.checkedGet(branch.project());
       ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
       validators
           .add(new UploadMergesPermissionValidator(perm))
@@ -164,21 +164,21 @@
 
     public CommitValidators forGerritCommits(
         PermissionBackend.ForProject forProject,
-        Branch.NameKey branch,
+        BranchNameKey branch,
         IdentifiedUser user,
         SshInfo sshInfo,
         RevWalk rw,
         @Nullable Change change)
         throws IOException {
-      PermissionBackend.ForRef perm = forProject.ref(branch.get());
-      ProjectState projectState = projectCache.checkedGet(branch.getParentKey());
+      PermissionBackend.ForRef perm = forProject.ref(branch.branch());
+      ProjectState projectState = projectCache.checkedGet(branch.project());
       ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
       validators
           .add(new UploadMergesPermissionValidator(perm))
           .add(new ProjectStateValidationListener(projectState))
           .add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
           .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
-          .add(new SignedOffByValidator(user, perm, projectCache.checkedGet(branch.getParentKey())))
+          .add(new SignedOffByValidator(user, perm, projectCache.checkedGet(branch.project())))
           .add(
               new ChangeIdValidator(
                   projectState,
@@ -196,7 +196,7 @@
     }
 
     public CommitValidators forMergedCommits(
-        PermissionBackend.ForProject forProject, Branch.NameKey branch, IdentifiedUser user)
+        PermissionBackend.ForProject forProject, BranchNameKey branch, IdentifiedUser user)
         throws IOException {
       // Generally only include validators that are based on permissions of the
       // user creating a change for a merged commit; generally exclude
@@ -211,11 +211,11 @@
       //    discuss what to do about it.
       //  - Plugin validators may do things like require certain commit message
       //    formats, so we play it safe and exclude them.
-      PermissionBackend.ForRef perm = forProject.ref(branch.get());
+      PermissionBackend.ForRef perm = forProject.ref(branch.branch());
       ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
       validators
           .add(new UploadMergesPermissionValidator(perm))
-          .add(new ProjectStateValidationListener(projectCache.checkedGet(branch.getParentKey())))
+          .add(new ProjectStateValidationListener(projectCache.checkedGet(branch.project())))
           .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
           .add(new CommitterUploaderValidator(user, perm, urlFormatter.get()));
       return new CommitValidators(validators.build());
@@ -395,7 +395,7 @@
   /** If this is the special project configuration branch, validate the config. */
   public static class ConfigValidator implements CommitValidationListener {
     private final ProjectConfig.Factory projectConfigFactory;
-    private final Branch.NameKey branch;
+    private final BranchNameKey branch;
     private final IdentifiedUser user;
     private final RevWalk rw;
     private final AllUsersName allUsers;
@@ -403,7 +403,7 @@
 
     public ConfigValidator(
         ProjectConfig.Factory projectConfigFactory,
-        Branch.NameKey branch,
+        BranchNameKey branch,
         IdentifiedUser user,
         RevWalk rw,
         AllUsersName allUsers,
@@ -419,7 +419,7 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
         throws CommitValidationException {
-      if (REFS_CONFIG.equals(branch.get())) {
+      if (REFS_CONFIG.equals(branch.branch())) {
         List<CommitValidationMessage> messages = new ArrayList<>();
 
         try {
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidationListener.java b/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
index 6edd04e..b47d7d6 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.git.validators;
 
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.project.ProjectState;
@@ -44,7 +44,7 @@
       Repository repo,
       CodeReviewCommit commit,
       ProjectState destProject,
-      Branch.NameKey destBranch,
+      BranchNameKey destBranch,
       PatchSet.Id patchSetId,
       IdentifiedUser caller)
       throws MergeValidationException;
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 75be8f3..e17e129 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -17,16 +17,16 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountProperties;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -80,7 +80,7 @@
       Repository repo,
       CodeReviewCommit commit,
       ProjectState destProject,
-      Branch.NameKey destBranch,
+      BranchNameKey destBranch,
       PatchSet.Id patchSetId,
       IdentifiedUser caller)
       throws MergeValidationException {
@@ -156,11 +156,11 @@
         final Repository repo,
         final CodeReviewCommit commit,
         final ProjectState destProject,
-        final Branch.NameKey destBranch,
+        final BranchNameKey destBranch,
         final PatchSet.Id patchSetId,
         IdentifiedUser caller)
         throws MergeValidationException {
-      if (RefNames.REFS_CONFIG.equals(destBranch.get())) {
+      if (RefNames.REFS_CONFIG.equals(destBranch.branch())) {
         final Project.NameKey newParent;
         try {
           ProjectConfig cfg = projectConfigFactory.create(destProject.getNameKey());
@@ -251,7 +251,7 @@
         Repository repo,
         CodeReviewCommit commit,
         ProjectState destProject,
-        Branch.NameKey destBranch,
+        BranchNameKey destBranch,
         PatchSet.Id patchSetId,
         IdentifiedUser caller)
         throws MergeValidationException {
@@ -285,18 +285,17 @@
         Repository repo,
         CodeReviewCommit commit,
         ProjectState destProject,
-        Branch.NameKey destBranch,
+        BranchNameKey destBranch,
         PatchSet.Id patchSetId,
         IdentifiedUser caller)
         throws MergeValidationException {
-      Account.Id accountId = Account.Id.fromRef(destBranch.get());
+      Account.Id accountId = Account.Id.fromRef(destBranch.branch());
       if (!allUsersName.equals(destProject.getNameKey()) || accountId == null) {
         return;
       }
 
       ChangeData cd =
-          changeDataFactory.create(
-              destProject.getProject().getNameKey(), patchSetId.getParentKey());
+          changeDataFactory.create(destProject.getProject().getNameKey(), patchSetId.changeId());
       try {
         if (!cd.currentFilePaths().contains(AccountProperties.ACCOUNT_CONFIG)) {
           return;
@@ -336,13 +335,13 @@
         Repository repo,
         CodeReviewCommit commit,
         ProjectState destProject,
-        Branch.NameKey destBranch,
+        BranchNameKey destBranch,
         PatchSet.Id patchSetId,
         IdentifiedUser caller)
         throws MergeValidationException {
       // Groups are stored inside the 'All-Users' repository.
       if (!allUsersName.equals(destProject.getNameKey())
-          || !RefNames.isGroupRef(destBranch.get())) {
+          || !RefNames.isGroupRef(destBranch.branch())) {
         return;
       }
 
diff --git a/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java b/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
index 308fdc0..432dda3 100644
--- a/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
+++ b/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
@@ -17,8 +17,8 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.RefCache;
 import com.google.gerrit.server.update.ChainedReceiveCommands;
 import com.google.gerrit.server.validators.ValidationException;
diff --git a/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java b/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java
index 409240e..6faa7af 100644
--- a/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git.validators;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.validators.OnSubmitValidationListener.Arguments;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.submit.IntegrationException;
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java b/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java
index 9eaf2d2..d27cc38 100644
--- a/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java
+++ b/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java
@@ -14,27 +14,28 @@
 
 package com.google.gerrit.server.git.validators;
 
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.validators.ValidationException;
 
 public class RefOperationValidationException extends ValidationException {
   private static final long serialVersionUID = 1L;
-  private final Iterable<ValidationMessage> messages;
+  private final ImmutableList<ValidationMessage> messages;
 
-  public RefOperationValidationException(String reason, Iterable<ValidationMessage> messages) {
+  public RefOperationValidationException(String reason, ImmutableList<ValidationMessage> messages) {
     super(reason);
     this.messages = messages;
   }
 
-  public Iterable<ValidationMessage> getMessages() {
+  public ImmutableList<ValidationMessage> getMessages() {
     return messages;
   }
 
   @Override
   public String getMessage() {
-    StringBuilder msg = new StringBuilder(super.getMessage());
-    for (ValidationMessage error : messages) {
-      msg.append("\n").append(error.getMessage());
-    }
-    return msg.toString();
+    return messages.stream()
+        .map(ValidationMessage::getMessage)
+        .collect(joining("\n", super.getMessage() + "\n", ""));
   }
 }
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index 3c0208c..e9734a3 100644
--- a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
+++ b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -13,14 +13,14 @@
 // limitations under the License.
 package com.google.gerrit.server.git.validators;
 
-import com.google.common.base.Predicate;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.events.RefReceivedEvent;
@@ -39,8 +39,6 @@
 public class RefOperationValidators {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private static final GetErrorMessages GET_ERRORS = new GetErrorMessages();
-
   public interface Factory {
     RefOperationValidators create(Project project, IdentifiedUser user, ReceiveCommand cmd);
   }
@@ -93,22 +91,15 @@
     return messages;
   }
 
-  private void throwException(Iterable<ValidationMessage> messages, RefReceivedEvent event)
+  private void throwException(List<ValidationMessage> messages, RefReceivedEvent event)
       throws RefOperationValidationException {
-    Iterable<ValidationMessage> errors = Iterables.filter(messages, GET_ERRORS);
     String header =
         String.format(
             "Ref \"%s\" %S in project %s validation failed",
             event.command.getRefName(), event.command.getType(), event.project.getName());
     logger.atSevere().log(header);
-    throw new RefOperationValidationException(header, errors);
-  }
-
-  private static class GetErrorMessages implements Predicate<ValidationMessage> {
-    @Override
-    public boolean apply(ValidationMessage input) {
-      return input.isError();
-    }
+    throw new RefOperationValidationException(
+        header, messages.stream().filter(ValidationMessage::isError).collect(toImmutableList()));
   }
 
   private static class DisallowCreationAndDeletionOfGerritMaintainedBranches
diff --git a/java/com/google/gerrit/server/git/validators/UploadValidationListener.java b/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
index 13065e4..8f1d5e4 100644
--- a/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
+++ b/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.git.validators;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.validators.ValidationException;
 import java.util.Collection;
 import org.eclipse.jgit.lib.ObjectId;
diff --git a/java/com/google/gerrit/server/git/validators/UploadValidators.java b/java/com/google/gerrit/server/git/validators/UploadValidators.java
index 2595283..6847a28 100644
--- a/java/com/google/gerrit/server/git/validators/UploadValidators.java
+++ b/java/com/google/gerrit/server/git/validators/UploadValidators.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git.validators;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/group/GroupAuditService.java b/java/com/google/gerrit/server/group/GroupAuditService.java
index 4c02ada..30e5d3c 100644
--- a/java/com/google/gerrit/server/group/GroupAuditService.java
+++ b/java/com/google/gerrit/server/group/GroupAuditService.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.AuditEvent;
 import java.sql.Timestamp;
 
diff --git a/java/com/google/gerrit/server/group/GroupResolver.java b/java/com/google/gerrit/server/group/GroupResolver.java
index 5fe3e8e..a54f465 100644
--- a/java/com/google/gerrit/server/group/GroupResolver.java
+++ b/java/com/google/gerrit/server/group/GroupResolver.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupCache;
@@ -81,7 +81,7 @@
    * @return the group, null if no group is found for the given group ID
    */
   public GroupDescription.Basic parseId(String id) {
-    AccountGroup.UUID uuid = new AccountGroup.UUID(id);
+    AccountGroup.UUID uuid = AccountGroup.uuid(id);
     if (groupBackend.handles(uuid)) {
       GroupDescription.Basic d = groupBackend.get(uuid);
       if (d != null) {
diff --git a/java/com/google/gerrit/server/group/InternalGroup.java b/java/com/google/gerrit/server/group/InternalGroup.java
index 7828586..639cd7a 100644
--- a/java/com/google/gerrit/server/group/InternalGroup.java
+++ b/java/com/google/gerrit/server/group/InternalGroup.java
@@ -17,8 +17,8 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import java.io.Serializable;
 import java.sql.Timestamp;
 import org.eclipse.jgit.lib.ObjectId;
diff --git a/java/com/google/gerrit/server/group/InternalGroupDescription.java b/java/com/google/gerrit/server/group/InternalGroupDescription.java
index 1d2252d..c70c8bf 100644
--- a/java/com/google/gerrit/server/group/InternalGroupDescription.java
+++ b/java/com/google/gerrit/server/group/InternalGroupDescription.java
@@ -20,8 +20,8 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 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 2a9538d..b2d9632 100644
--- a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
+++ b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
@@ -20,9 +20,9 @@
 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.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.ScheduleConfig;
diff --git a/java/com/google/gerrit/server/group/SubgroupResource.java b/java/com/google/gerrit/server/group/SubgroupResource.java
index a33e96b..ceea2dc 100644
--- a/java/com/google/gerrit/server/group/SubgroupResource.java
+++ b/java/com/google/gerrit/server/group/SubgroupResource.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.group;
 
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.inject.TypeLiteral;
 
 public class SubgroupResource extends GroupResource {
diff --git a/java/com/google/gerrit/server/group/SystemGroupBackend.java b/java/com/google/gerrit/server/group/SystemGroupBackend.java
index 85c1e73..7821a01 100644
--- a/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -23,7 +23,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.StartupCheck;
 import com.google.gerrit.server.StartupException;
@@ -56,19 +56,19 @@
 
   /** Common UUID assigned to the "Anonymous Users" group. */
   public static final AccountGroup.UUID ANONYMOUS_USERS =
-      new AccountGroup.UUID(SYSTEM_GROUP_SCHEME + "Anonymous-Users");
+      AccountGroup.uuid(SYSTEM_GROUP_SCHEME + "Anonymous-Users");
 
   /** Common UUID assigned to the "Registered Users" group. */
   public static final AccountGroup.UUID REGISTERED_USERS =
-      new AccountGroup.UUID(SYSTEM_GROUP_SCHEME + "Registered-Users");
+      AccountGroup.uuid(SYSTEM_GROUP_SCHEME + "Registered-Users");
 
   /** Common UUID assigned to the "Project Owners" placeholder group. */
   public static final AccountGroup.UUID PROJECT_OWNERS =
-      new AccountGroup.UUID(SYSTEM_GROUP_SCHEME + "Project-Owners");
+      AccountGroup.uuid(SYSTEM_GROUP_SCHEME + "Project-Owners");
 
   /** Common UUID assigned to the "Change Owner" placeholder group. */
   public static final AccountGroup.UUID CHANGE_OWNER =
-      new AccountGroup.UUID(SYSTEM_GROUP_SCHEME + "Change-Owner");
+      AccountGroup.uuid(SYSTEM_GROUP_SCHEME + "Change-Owner");
 
   private static final AccountGroup.UUID[] all = {
     ANONYMOUS_USERS, REGISTERED_USERS, PROJECT_OWNERS, CHANGE_OWNER,
diff --git a/java/com/google/gerrit/server/group/db/AuditLogFormatter.java b/java/com/google/gerrit/server/group/db/AuditLogFormatter.java
index 508f5ef..ec4c0fc 100644
--- a/java/com/google/gerrit/server/group/db/AuditLogFormatter.java
+++ b/java/com/google/gerrit/server/group/db/AuditLogFormatter.java
@@ -20,8 +20,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.GroupBackend;
@@ -51,7 +51,7 @@
   }
 
   private static Optional<Account> getAccount(AccountCache accountCache, Account.Id accountId) {
-    return accountCache.get(accountId).map(AccountState::getAccount);
+    return accountCache.get(accountId).map(AccountState::account);
   }
 
   private static Optional<GroupDescription.Basic> getGroup(
@@ -72,7 +72,7 @@
   }
 
   private static Optional<Account> getAccount(ImmutableSet<Account> accounts, Account.Id id) {
-    return accounts.stream().filter(account -> account.getId().equals(id)).findAny();
+    return accounts.stream().filter(account -> account.id().equals(id)).findAny();
   }
 
   public static AuditLogFormatter createPartiallyWorkingFallBack() {
@@ -119,7 +119,7 @@
    * @return a {@code PersonIdent} which can be used for the author of a commit
    */
   public PersonIdent getParsableAuthorIdent(Account account, PersonIdent personIdent) {
-    return getParsableAuthorIdent(account.getName(), account.getId(), personIdent);
+    return getParsableAuthorIdent(account.getName(), account.id(), personIdent);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/group/db/AuditLogReader.java b/java/com/google/gerrit/server/group/db/AuditLogReader.java
index 106ee6b..d8f0a0f 100644
--- a/java/com/google/gerrit/server/group/db/AuditLogReader.java
+++ b/java/com/google/gerrit/server/group/db/AuditLogReader.java
@@ -14,18 +14,19 @@
 
 package com.google.gerrit.server.group.db;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.AccountGroupByIdAudit;
+import com.google.gerrit.entities.AccountGroupMemberAudit;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.notedb.NoteDbUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -49,12 +50,10 @@
 public class AuditLogReader {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final String serverId;
   private final AllUsersName allUsersName;
 
   @Inject
-  public AuditLogReader(@GerritServerId String serverId, AllUsersName allUsersName) {
-    this.serverId = serverId;
+  public AuditLogReader(AllUsersName allUsersName) {
     this.allUsersName = allUsersName;
   }
 
@@ -69,70 +68,79 @@
 
   private ImmutableList<AccountGroupMemberAudit> getMembersAudit(
       AccountGroup.Id groupId, List<ParsedCommit> commits) {
-    ListMultimap<MemberKey, AccountGroupMemberAudit> audits =
+    ListMultimap<MemberKey, AccountGroupMemberAudit.Builder> audits =
         MultimapBuilder.hashKeys().linkedListValues().build();
-    ImmutableList.Builder<AccountGroupMemberAudit> result = ImmutableList.builder();
+    List<AccountGroupMemberAudit.Builder> result = new ArrayList<>();
     for (ParsedCommit pc : commits) {
       for (Account.Id id : pc.addedMembers()) {
         MemberKey key = MemberKey.create(groupId, id);
-        AccountGroupMemberAudit audit =
-            new AccountGroupMemberAudit(
-                new AccountGroupMemberAudit.Key(id, groupId, pc.when()), pc.authorId());
+        AccountGroupMemberAudit.Builder audit =
+            AccountGroupMemberAudit.builder()
+                .memberId(id)
+                .groupId(groupId)
+                .addedOn(pc.when())
+                .addedBy(pc.authorId());
         audits.put(key, audit);
         result.add(audit);
       }
       for (Account.Id id : pc.removedMembers()) {
-        List<AccountGroupMemberAudit> adds = audits.get(MemberKey.create(groupId, id));
+        List<AccountGroupMemberAudit.Builder> adds = audits.get(MemberKey.create(groupId, id));
         if (!adds.isEmpty()) {
-          AccountGroupMemberAudit audit = adds.remove(0);
+          AccountGroupMemberAudit.Builder audit = adds.remove(0);
           audit.removed(pc.authorId(), pc.when());
         } else {
           // Match old behavior of DbGroupAuditListener and add a "legacy" add/remove pair.
-          AccountGroupMemberAudit audit =
-              new AccountGroupMemberAudit(
-                  new AccountGroupMemberAudit.Key(id, groupId, pc.when()), pc.authorId());
-          audit.removedLegacy();
+          AccountGroupMemberAudit.Builder audit =
+              AccountGroupMemberAudit.builder()
+                  .groupId(groupId)
+                  .memberId(id)
+                  .addedOn(pc.when())
+                  .addedBy(pc.authorId())
+                  .removedLegacy();
           result.add(audit);
         }
       }
     }
-    return result.build();
+    return result.stream().map(AccountGroupMemberAudit.Builder::build).collect(toImmutableList());
   }
 
-  public ImmutableList<AccountGroupByIdAud> getSubgroupsAudit(
+  public ImmutableList<AccountGroupByIdAudit> getSubgroupsAudit(
       Repository repo, AccountGroup.UUID uuid) throws IOException, ConfigInvalidException {
     return getSubgroupsAudit(getGroupId(repo, uuid), parseCommits(repo, uuid));
   }
 
-  private ImmutableList<AccountGroupByIdAud> getSubgroupsAudit(
+  private ImmutableList<AccountGroupByIdAudit> getSubgroupsAudit(
       AccountGroup.Id groupId, List<ParsedCommit> commits) {
-    ListMultimap<SubgroupKey, AccountGroupByIdAud> audits =
+    ListMultimap<SubgroupKey, AccountGroupByIdAudit.Builder> audits =
         MultimapBuilder.hashKeys().linkedListValues().build();
-    ImmutableList.Builder<AccountGroupByIdAud> result = ImmutableList.builder();
+    List<AccountGroupByIdAudit.Builder> result = new ArrayList<>();
     for (ParsedCommit pc : commits) {
       for (AccountGroup.UUID uuid : pc.addedSubgroups()) {
         SubgroupKey key = SubgroupKey.create(groupId, uuid);
-        AccountGroupByIdAud audit =
-            new AccountGroupByIdAud(
-                new AccountGroupByIdAud.Key(groupId, uuid, pc.when()), pc.authorId());
+        AccountGroupByIdAudit.Builder audit =
+            AccountGroupByIdAudit.builder()
+                .groupId(groupId)
+                .includeUuid(uuid)
+                .addedOn(pc.when())
+                .addedBy(pc.authorId());
         audits.put(key, audit);
         result.add(audit);
       }
       for (AccountGroup.UUID uuid : pc.removedSubgroups()) {
-        List<AccountGroupByIdAud> adds = audits.get(SubgroupKey.create(groupId, uuid));
+        List<AccountGroupByIdAudit.Builder> adds = audits.get(SubgroupKey.create(groupId, uuid));
         if (!adds.isEmpty()) {
-          AccountGroupByIdAud audit = adds.remove(0);
+          AccountGroupByIdAudit.Builder audit = adds.remove(0);
           audit.removed(pc.authorId(), pc.when());
         } else {
           // Unlike members, DbGroupAuditListener didn't insert an add/remove pair here.
         }
       }
     }
-    return result.build();
+    return result.stream().map(AccountGroupByIdAudit.Builder::build).collect(toImmutableList());
   }
 
   private Optional<ParsedCommit> parse(AccountGroup.UUID uuid, RevCommit c) {
-    Optional<Account.Id> authorId = NoteDbUtil.parseIdent(c.getAuthorIdent(), serverId);
+    Optional<Account.Id> authorId = NoteDbUtil.parseIdent(c.getAuthorIdent());
     if (!authorId.isPresent()) {
       // Only report audit events from identified users, since this was a non-nullable field in
       // ReviewDb. May be revisited.
@@ -168,7 +176,7 @@
   private Optional<Account.Id> parseAccount(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
     Optional<Account.Id> result =
         Optional.ofNullable(RawParseUtils.parsePersonIdent(line.getValue()))
-            .flatMap(ident -> NoteDbUtil.parseIdent(ident, serverId));
+            .flatMap(ident -> NoteDbUtil.parseIdent(ident));
     if (!result.isPresent()) {
       logInvalid(uuid, c, line);
     }
@@ -182,7 +190,7 @@
       logInvalid(uuid, c, line);
       return Optional.empty();
     }
-    return Optional.of(new AccountGroup.UUID(ident.getEmailAddress()));
+    return Optional.of(AccountGroup.uuid(ident.getEmailAddress()));
   }
 
   private static void logInvalid(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index 903b9c0..ab5c9b8 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -24,11 +24,11 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.DuplicateKeyException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.group.InternalGroup;
@@ -411,12 +411,12 @@
   }
 
   private ImmutableSet<Account.Id> readMembers() throws IOException, ConfigInvalidException {
-    return readFromFile(MEMBERS_FILE, entry -> new Account.Id(Integer.parseInt(entry)));
+    return readFromFile(MEMBERS_FILE, entry -> Account.id(Integer.parseInt(entry)));
   }
 
   private ImmutableSet<AccountGroup.UUID> readSubgroups()
       throws IOException, ConfigInvalidException {
-    return readFromFile(SUBGROUPS_FILE, AccountGroup.UUID::new);
+    return readFromFile(SUBGROUPS_FILE, AccountGroup::uuid);
   }
 
   private <E> ImmutableSet<E> readFromFile(String filePath, Function<String, E> fromStringFunction)
diff --git a/java/com/google/gerrit/server/group/db/GroupConfigEntry.java b/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
index f7a104d..81f5c7e 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.group.db;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
@@ -45,7 +45,7 @@
             String.format(
                 "ID of the group %s must not be negative, found %d", groupUuid.get(), id));
       }
-      group.setId(new AccountGroup.Id(id));
+      group.setId(AccountGroup.id(id));
     }
 
     @Override
@@ -77,7 +77,7 @@
       // the NoteDb migration converted such groups faithfully, so we need to be able to read them
       // back here.
       name = Strings.nullToEmpty(name);
-      group.setNameKey(new AccountGroup.NameKey(name));
+      group.setNameKey(AccountGroup.nameKey(name));
     }
 
     @Override
@@ -135,7 +135,7 @@
         throw new ConfigInvalidException(
             String.format("Owner UUID of the group %s must be defined", groupUuid.get()));
       }
-      group.setOwnerGroupUUID(new AccountGroup.UUID(ownerGroupUuid));
+      group.setOwnerGroupUUID(AccountGroup.uuid(ownerGroupUuid));
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/group/db/GroupNameNotes.java b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
index eda7153..70d7a1a 100644
--- a/java/com/google/gerrit/server/group/db/GroupNameNotes.java
+++ b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
@@ -29,10 +29,11 @@
 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.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.DuplicateKeyException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import java.io.IOException;
 import java.util.Collection;
@@ -266,7 +267,7 @@
       RevCommit oldCommit = ref != null ? rw.parseCommit(ref.getObjectId()) : null;
 
       for (Map.Entry<AccountGroup.UUID, String> e : biMap.entrySet()) {
-        AccountGroup.NameKey nameKey = new AccountGroup.NameKey(e.getValue());
+        AccountGroup.NameKey nameKey = AccountGroup.nameKey(e.getValue());
         ObjectId noteKey = getNoteKey(nameKey);
         noteMap.set(noteKey, getAsNoteData(e.getKey(), nameKey), inserter);
       }
@@ -286,7 +287,7 @@
       cb.setMessage("Store " + n + " group name" + (n != 1 ? "s" : ""));
       ObjectId newId = inserter.insert(cb).copy();
 
-      ObjectId oldId = oldCommit != null ? oldCommit.copy() : ObjectId.zeroId();
+      ObjectId oldId = ObjectIds.copyOrZero(oldCommit);
       bru.addCommand(new ReceiveCommand(oldId, newId, RefNames.REFS_GROUPNAMES));
     }
   }
@@ -442,7 +443,7 @@
       throw new ConfigInvalidException(String.format("UUID for group '%s' must be defined", name));
     }
 
-    return new GroupReference(new AccountGroup.UUID(uuid), name);
+    return new GroupReference(AccountGroup.uuid(uuid), name);
   }
 
   private String getCommitMessage() {
diff --git a/java/com/google/gerrit/server/group/db/Groups.java b/java/com/google/gerrit/server/group/db/Groups.java
index 37de011..2925cb3 100644
--- a/java/com/google/gerrit/server/group/db/Groups.java
+++ b/java/com/google/gerrit/server/group/db/Groups.java
@@ -17,9 +17,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.AccountGroupByIdAudit;
+import com.google.gerrit.entities.AccountGroupMemberAudit;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.group.InternalGroup;
@@ -152,7 +152,7 @@
    * @throws IOException if an error occurs while reading from NoteDb
    * @throws ConfigInvalidException if the group couldn't be retrieved from NoteDb
    */
-  public List<AccountGroupByIdAud> getSubgroupsAudit(Repository repo, AccountGroup.UUID groupUuid)
+  public List<AccountGroupByIdAudit> getSubgroupsAudit(Repository repo, AccountGroup.UUID groupUuid)
       throws IOException, ConfigInvalidException {
     return auditLogReader.getSubgroupsAudit(repo, groupUuid);
   }
diff --git a/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
index 3afb793..8a6cd94 100644
--- a/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
@@ -17,9 +17,9 @@
 import static com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo.error;
 import static com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo.warning;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.GroupBackend;
diff --git a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
index c3ca60b..0414304 100644
--- a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
@@ -24,10 +24,10 @@
 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.RefNames;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Inject;
@@ -163,7 +163,7 @@
           continue;
         }
 
-        ObjectId nameKey = GroupNameNotes.getNoteKey(new AccountGroup.NameKey(gRef.getName()));
+        ObjectId nameKey = GroupNameNotes.getNoteKey(AccountGroup.nameKey(gRef.getName()));
         if (!Objects.equals(nameKey, note)) {
           result.problems.add(
               error("notename entry %s does not match name %s", note, gRef.getName()));
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index b450ab8..7f1ba6a 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -21,13 +21,13 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.update.RetryHelper;
@@ -265,7 +266,10 @@
       throws DuplicateKeyException, IOException, ConfigInvalidException {
     try (TraceTimer timer =
         TraceContext.newTimer(
-            "Creating group '%s'", groupUpdate.getName().orElseGet(groupCreation::getNameKey))) {
+            "Creating group",
+            Metadata.builder()
+                .groupName(groupUpdate.getName().orElseGet(groupCreation::getNameKey).get())
+                .build())) {
       InternalGroup createdGroup = createGroupInNoteDbWithRetry(groupCreation, groupUpdate);
       evictCachesOnGroupCreation(createdGroup);
       dispatchAuditEventsOnGroupCreation(createdGroup);
@@ -285,7 +289,9 @@
    */
   public void updateGroup(AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
       throws DuplicateKeyException, IOException, NoSuchGroupException, ConfigInvalidException {
-    try (TraceTimer timer = TraceContext.newTimer("Updating group %s", groupUuid)) {
+    try (TraceTimer timer =
+        TraceContext.newTimer(
+            "Updating group", Metadata.builder().groupUuid(groupUuid.get()).build())) {
       Optional<Timestamp> updatedOn = groupUpdate.getUpdatedOn();
       if (!updatedOn.isPresent()) {
         updatedOn = Optional.of(TimeUtil.nowTs());
diff --git a/java/com/google/gerrit/server/group/db/InternalGroupCreation.java b/java/com/google/gerrit/server/group/db/InternalGroupCreation.java
index bb21d62..8988547 100644
--- a/java/com/google/gerrit/server/group/db/InternalGroupCreation.java
+++ b/java/com/google/gerrit/server/group/db/InternalGroupCreation.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.group.db;
 
 import com.google.auto.value.AutoValue;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 
 /**
  * Definition of all properties necessary for a group creation.
diff --git a/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java b/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java
index bff2952..9e6539a 100644
--- a/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java
+++ b/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java
@@ -16,8 +16,8 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import java.sql.Timestamp;
 import java.util.Optional;
 import java.util.Set;
diff --git a/java/com/google/gerrit/server/group/db/RenameGroupOp.java b/java/com/google/gerrit/server/group/db/RenameGroupOp.java
index e002192..35ff513 100644
--- a/java/com/google/gerrit/server/group/db/RenameGroupOp.java
+++ b/java/com/google/gerrit/server/group/db/RenameGroupOp.java
@@ -16,8 +16,8 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.DefaultQueueOp;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
diff --git a/java/com/google/gerrit/server/group/db/testing/BUILD b/java/com/google/gerrit/server/group/db/testing/BUILD
index 0cc45fd..8f33f98 100644
--- a/java/com/google/gerrit/server/group/db/testing/BUILD
+++ b/java/com/google/gerrit/server/group/db/testing/BUILD
@@ -9,7 +9,7 @@
     deps = [
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib:jgit",
+        "//lib:jgit-junit",
     ],
 )
diff --git a/java/com/google/gerrit/server/group/testing/BUILD b/java/com/google/gerrit/server/group/testing/BUILD
index 9b6d8de..fd61dff 100644
--- a/java/com/google/gerrit/server/group/testing/BUILD
+++ b/java/com/google/gerrit/server/group/testing/BUILD
@@ -8,10 +8,10 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
         "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
index 2f91394..8f20e92 100644
--- a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
+++ b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
@@ -18,17 +18,16 @@
 
 import com.google.common.truth.BooleanSubject;
 import com.google.common.truth.ComparableSubject;
-import com.google.common.truth.DefaultSubject;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IterableSubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import java.sql.Timestamp;
 import org.eclipse.jgit.lib.ObjectId;
 
-public class InternalGroupSubject extends Subject<InternalGroupSubject, InternalGroup> {
+public class InternalGroupSubject extends Subject {
 
   public static InternalGroupSubject assertThat(InternalGroup group) {
     return assertAbout(internalGroups()).that(group);
@@ -38,73 +37,65 @@
     return InternalGroupSubject::new;
   }
 
-  private InternalGroupSubject(FailureMetadata metadata, InternalGroup actual) {
-    super(metadata, actual);
+  private final InternalGroup group;
+
+  private InternalGroupSubject(FailureMetadata metadata, InternalGroup group) {
+    super(metadata, group);
+    this.group = group;
   }
 
-  public ComparableSubject<?, AccountGroup.UUID> groupUuid() {
+  public ComparableSubject<AccountGroup.UUID> groupUuid() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("groupUuid()").that(group.getGroupUUID());
+    return check("getGroupUUID()").that(group.getGroupUUID());
   }
 
-  public ComparableSubject<?, AccountGroup.NameKey> nameKey() {
+  public ComparableSubject<AccountGroup.NameKey> nameKey() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("nameKey()").that(group.getNameKey());
+    return check("getNameKey()").that(group.getNameKey());
   }
 
   public StringSubject name() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("name()").that(group.getName());
+    return check("getName()").that(group.getName());
   }
 
-  public Subject<DefaultSubject, Object> id() {
+  public Subject id() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("id()").that(group.getId());
+    return check("getId()").that(group.getId());
   }
 
   public StringSubject description() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("description()").that(group.getDescription());
+    return check("getDescription()").that(group.getDescription());
   }
 
-  public ComparableSubject<?, AccountGroup.UUID> ownerGroupUuid() {
+  public ComparableSubject<AccountGroup.UUID> ownerGroupUuid() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("ownerGroupUuid()").that(group.getOwnerGroupUUID());
+    return check("getOwnerGroupUUID()").that(group.getOwnerGroupUUID());
   }
 
   public BooleanSubject visibleToAll() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("visibleToAll()").that(group.isVisibleToAll());
+    return check("isVisibleToAll()").that(group.isVisibleToAll());
   }
 
-  public ComparableSubject<?, Timestamp> createdOn() {
+  public ComparableSubject<Timestamp> createdOn() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("createdOn()").that(group.getCreatedOn());
+    return check("getCreatedOn()").that(group.getCreatedOn());
   }
 
   public IterableSubject members() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("members()").that(group.getMembers());
+    return check("getMembers()").that(group.getMembers());
   }
 
   public IterableSubject subgroups() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("subgroups()").that(group.getSubgroups());
+    return check("getSubgroups()").that(group.getSubgroups());
   }
 
-  public ComparableSubject<?, ObjectId> refState() {
+  public ComparableSubject<ObjectId> refState() {
     isNotNull();
-    InternalGroup group = actual();
-    return check("refState()").that(group.getRefState());
+    return check("getRefState()").that(group.getRefState());
   }
 }
diff --git a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
index 550b15c..51c7ca3 100644
--- a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
+++ b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
@@ -20,7 +20,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
@@ -43,7 +43,7 @@
    */
   public GroupDescription.Basic create(String name) {
     requireNonNull(name);
-    return create(new AccountGroup.UUID(name.startsWith(PREFIX) ? name : PREFIX + name));
+    return create(AccountGroup.uuid(name.startsWith(PREFIX) ? name : PREFIX + name));
   }
 
   /**
diff --git a/java/com/google/gerrit/server/index/AbstractIndexModule.java b/java/com/google/gerrit/server/index/AbstractIndexModule.java
index 352ea4b..995b4b6 100644
--- a/java/com/google/gerrit/server/index/AbstractIndexModule.java
+++ b/java/com/google/gerrit/server/index/AbstractIndexModule.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.index;
 
 import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.project.ProjectIndex;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -44,9 +43,21 @@
   @Override
   protected void configure() {
     if (slave) {
-      bind(AccountIndex.Factory.class).toInstance(AbstractIndexModule::createDummyIndexFactory);
-      bind(ChangeIndex.Factory.class).toInstance(AbstractIndexModule::createDummyIndexFactory);
-      bind(ProjectIndex.Factory.class).toInstance(AbstractIndexModule::createDummyIndexFactory);
+      bind(AccountIndex.Factory.class)
+          .toInstance(
+              s -> {
+                throw new UnsupportedOperationException();
+              });
+      bind(ChangeIndex.Factory.class)
+          .toInstance(
+              s -> {
+                throw new UnsupportedOperationException();
+              });
+      bind(ProjectIndex.Factory.class)
+          .toInstance(
+              s -> {
+                throw new UnsupportedOperationException();
+              });
     } else {
       install(
           new FactoryModuleBuilder()
@@ -74,11 +85,6 @@
     }
   }
 
-  @SuppressWarnings("unused")
-  private static <T> T createDummyIndexFactory(Schema<?> schema) {
-    throw new UnsupportedOperationException();
-  }
-
   protected abstract Class<? extends AccountIndex> getAccountIndex();
 
   protected abstract Class<? extends ChangeIndex> getChangeIndex();
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index 612c637..17665c0 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -22,10 +22,10 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.index.IndexType;
 import com.google.gerrit.index.SchemaDefinitions;
 import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.index.project.ProjectIndexRewriter;
@@ -71,11 +71,6 @@
  * (e.g. Lucene).
  */
 public class IndexModule extends LifecycleModule {
-  public enum IndexType {
-    LUCENE,
-    ELASTICSEARCH
-  }
-
   public static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
       ImmutableList.of(
           AccountSchemaDefinitions.INSTANCE,
@@ -86,19 +81,8 @@
   /** Type of secondary index. */
   public static IndexType getIndexType(Injector injector) {
     Config cfg = injector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-    if (cfg != null) {
-      return cfg.getEnum("index", null, "type", IndexType.LUCENE);
-    }
-    return IndexType.LUCENE;
-  }
-
-  /** Type of secondary index. */
-  // TODO: stop relying on this method fostering error-prone string comparisons.
-  public static String getIndexType(@Nullable Config cfg) {
-    if (cfg != null) {
-      return cfg.getString("index", null, "type");
-    }
-    return IndexType.LUCENE.name();
+    String configValue = cfg != null ? cfg.getString("index", null, "type") : null;
+    return new IndexType(configValue);
   }
 
   private final int threads;
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index 4b5cd49..a45777e 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -16,18 +16,21 @@
 
 import static com.google.gerrit.server.index.change.ChangeField.CHANGE;
 import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
+import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID_STR;
 import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.project.ProjectField;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.group.GroupField;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.SingleGroupUser;
 import java.io.IOException;
 import java.util.Set;
@@ -56,17 +59,18 @@
     }
   }
 
-  public static Set<String> accountFields(QueryOptions opts) {
-    return accountFields(opts.fields());
+  public static Set<String> accountFields(QueryOptions opts, boolean useLegacyNumericFields) {
+    return accountFields(opts.fields(), useLegacyNumericFields);
   }
 
-  public static Set<String> accountFields(Set<String> fields) {
-    return fields.contains(AccountField.ID.getName())
-        ? fields
-        : Sets.union(fields, ImmutableSet.of(AccountField.ID.getName()));
+  public static Set<String> accountFields(Set<String> fields, boolean useLegacyNumericFields) {
+    String idFieldName =
+        useLegacyNumericFields ? AccountField.ID.getName() : AccountField.ID_STR.getName();
+    return fields.contains(idFieldName) ? fields : Sets.union(fields, ImmutableSet.of(idFieldName));
   }
 
-  public static Set<String> changeFields(QueryOptions opts) {
+  public static Set<String> changeFields(QueryOptions opts, boolean useLegacyNumericFields) {
+    FieldDef<ChangeData, ?> idField = useLegacyNumericFields ? LEGACY_ID : LEGACY_ID_STR;
     // Ensure we request enough fields to construct a ChangeData. We need both
     // change ID and project, which can either come via the Change field or
     // separate fields.
@@ -75,10 +79,10 @@
       // A Change is always sufficient.
       return fs;
     }
-    if (fs.contains(PROJECT.getName()) && fs.contains(LEGACY_ID.getName())) {
+    if (fs.contains(PROJECT.getName()) && fs.contains(idField.getName())) {
       return fs;
     }
-    return Sets.union(fs, ImmutableSet.of(LEGACY_ID.getName(), PROJECT.getName()));
+    return Sets.union(fs, ImmutableSet.of(idField.getName(), PROJECT.getName()));
   }
 
   public static Set<String> groupFields(QueryOptions opts) {
diff --git a/java/com/google/gerrit/server/index/account/AccountField.java b/java/com/google/gerrit/server/index/account/AccountField.java
index f67a41d..0dd22ce 100644
--- a/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/java/com/google/gerrit/server/index/account/AccountField.java
@@ -21,28 +21,33 @@
 import static com.google.gerrit.index.FieldDef.timestamp;
 import static java.util.stream.Collectors.toSet;
 
-import com.google.common.base.Predicates;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.RefState;
 import com.google.gerrit.index.SchemaUtil;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 
 /** Secondary index schemas for accounts. */
 public class AccountField {
   public static final FieldDef<AccountState, Integer> ID =
-      integer("id").stored().build(a -> a.getAccount().getId().get());
+      integer("id").stored().build(a -> a.account().id().get());
+
+  public static final FieldDef<AccountState, String> ID_STR =
+      exact("id_str").stored().build(a -> String.valueOf(a.account().id().get()));
 
   /**
    * External IDs.
@@ -52,7 +57,7 @@
    */
   public static final FieldDef<AccountState, Iterable<String>> EXTERNAL_ID =
       exact("external_id")
-          .buildRepeatable(a -> Iterables.transform(a.getExternalIds(), id -> id.key().get()));
+          .buildRepeatable(a -> Iterables.transform(a.externalIds(), id -> id.key().get()));
 
   /**
    * Fuzzy prefix match on name and email parts.
@@ -67,7 +72,7 @@
   public static final FieldDef<AccountState, Iterable<String>> NAME_PART =
       prefix("name")
           .buildRepeatable(
-              a -> getNameParts(a, Iterables.transform(a.getExternalIds(), ExternalId::email)));
+              a -> getNameParts(a, Iterables.transform(a.externalIds(), ExternalId::email)));
 
   /**
    * Fuzzy prefix match on name and preferred email parts. Parts of secondary emails are not
@@ -75,13 +80,13 @@
    */
   public static final FieldDef<AccountState, Iterable<String>> NAME_PART_NO_SECONDARY_EMAIL =
       prefix("name2")
-          .buildRepeatable(a -> getNameParts(a, Arrays.asList(a.getAccount().getPreferredEmail())));
+          .buildRepeatable(a -> getNameParts(a, Arrays.asList(a.account().preferredEmail())));
 
   public static final FieldDef<AccountState, String> FULL_NAME =
-      exact("full_name").build(a -> a.getAccount().getFullName());
+      exact("full_name").build(a -> a.account().fullName());
 
   public static final FieldDef<AccountState, String> ACTIVE =
-      exact("inactive").build(a -> a.getAccount().isActive() ? "1" : "0");
+      exact("inactive").build(a -> a.account().isActive() ? "1" : "0");
 
   /**
    * All emails (preferred email + secondary emails). Use this field only if the current user is
@@ -93,10 +98,10 @@
       prefix("email")
           .buildRepeatable(
               a ->
-                  FluentIterable.from(a.getExternalIds())
+                  FluentIterable.from(a.externalIds())
                       .transform(ExternalId::email)
-                      .append(Collections.singleton(a.getAccount().getPreferredEmail()))
-                      .filter(Predicates.notNull())
+                      .append(Collections.singleton(a.account().preferredEmail()))
+                      .filter(Objects::nonNull)
                       .transform(String::toLowerCase)
                       .toSet());
 
@@ -104,24 +109,24 @@
       prefix("preferredemail")
           .build(
               a -> {
-                String preferredEmail = a.getAccount().getPreferredEmail();
+                String preferredEmail = a.account().preferredEmail();
                 return preferredEmail != null ? preferredEmail.toLowerCase() : null;
               });
 
   public static final FieldDef<AccountState, String> PREFERRED_EMAIL_EXACT =
-      exact("preferredemail_exact").build(a -> a.getAccount().getPreferredEmail());
+      exact("preferredemail_exact").build(a -> a.account().preferredEmail());
 
   public static final FieldDef<AccountState, Timestamp> REGISTERED =
-      timestamp("registered").build(a -> a.getAccount().getRegisteredOn());
+      timestamp("registered").build(a -> a.account().registeredOn());
 
   public static final FieldDef<AccountState, String> USERNAME =
-      exact("username").build(a -> a.getUserName().map(String::toLowerCase).orElse(""));
+      exact("username").build(a -> a.userName().map(String::toLowerCase).orElse(""));
 
   public static final FieldDef<AccountState, Iterable<String>> WATCHED_PROJECT =
       exact("watchedproject")
           .buildRepeatable(
               a ->
-                  FluentIterable.from(a.getProjectWatches().keySet())
+                  FluentIterable.from(a.projectWatches().keySet())
                       .transform(k -> k.project().get())
                       .toSet());
 
@@ -136,15 +141,19 @@
       storedOnly("ref_state")
           .buildRepeatable(
               a -> {
-                if (a.getAccount().getMetaId() == null) {
+                if (a.account().metaId() == null) {
                   return ImmutableList.of();
                 }
 
                 return ImmutableList.of(
                     RefState.create(
-                            RefNames.refsUsers(a.getAccount().getId()),
-                            ObjectId.fromString(a.getAccount().getMetaId()))
-                        .toByteArray(a.getAllUsersNameForIndexing()));
+                            RefNames.refsUsers(a.account().id()),
+                            ObjectId.fromString(a.account().metaId()))
+                        // We use the default AllUsers name to avoid having to pass around that
+                        // variable just for indexing.
+                        // This field is only used for staleness detection which will discover the
+                        // default name and replace it with the actually configured name.
+                        .toByteArray(new AllUsersName(AllUsersNameProvider.DEFAULT)));
               });
 
   /**
@@ -157,13 +166,13 @@
       storedOnly("external_id_state")
           .buildRepeatable(
               a ->
-                  a.getExternalIds().stream()
+                  a.externalIds().stream()
                       .filter(e -> e.blobId() != null)
                       .map(ExternalId::toByteArray)
                       .collect(toSet()));
 
   private static final Set<String> getNameParts(AccountState a, Iterable<String> emails) {
-    String fullName = a.getAccount().getFullName();
+    String fullName = a.account().fullName();
     Set<String> parts = SchemaUtil.getNameParts(fullName, emails);
 
     // Additional values not currently added by getPersonParts.
diff --git a/java/com/google/gerrit/server/index/account/AccountIndex.java b/java/com/google/gerrit/server/index/account/AccountIndex.java
index 5c1b3dc..1838edf 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndex.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndex.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.index.account;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.query.account.AccountPredicates;
 
@@ -27,6 +27,6 @@
 
   @Override
   default Predicate<AccountState> keyPredicate(Account.Id id) {
-    return AccountPredicates.id(id);
+    return AccountPredicates.id(getSchema(), id);
   }
 }
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexCollection.java b/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
index 2a14f9b..eb1f784 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.index.account;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.index.IndexCollection;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountState;
 import com.google.inject.Singleton;
 
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java b/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java
index af23b52..3a34d47e 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.index.account;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.index.IndexDefinition;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountState;
 import com.google.inject.Inject;
 
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java b/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java
index 35b967c..643c249 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java
@@ -16,22 +16,27 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.IndexRewriter;
 import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.index.query.TooManyTermsInQueryException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import org.eclipse.jgit.util.MutableInteger;
 
 @Singleton
 public class AccountIndexRewriter implements IndexRewriter<AccountState> {
-
   private final AccountIndexCollection indexes;
+  private final IndexConfig config;
 
   @Inject
-  AccountIndexRewriter(AccountIndexCollection indexes) {
+  AccountIndexRewriter(AccountIndexCollection indexes, IndexConfig config) {
     this.indexes = indexes;
+    this.config = config;
   }
 
   @Override
@@ -39,6 +44,32 @@
       throws QueryParseException {
     AccountIndex index = indexes.getSearchIndex();
     requireNonNull(index, "no active search index configured for accounts");
+    validateMaxTermsInQuery(in);
     return new IndexedAccountQuery(index, in, opts);
   }
+
+  /**
+   * Validates whether a query has too many terms.
+   *
+   * @param predicate the predicate for which the leaf predicates should be counted
+   * @throws QueryParseException thrown if the query has too many terms
+   */
+  public void validateMaxTermsInQuery(Predicate<AccountState> predicate)
+      throws QueryParseException {
+    MutableInteger leafTerms = new MutableInteger();
+    validateMaxTermsInQuery(predicate, leafTerms);
+  }
+
+  private void validateMaxTermsInQuery(Predicate<AccountState> predicate, MutableInteger leafTerms)
+      throws TooManyTermsInQueryException {
+    if (!(predicate instanceof IndexPredicate)) {
+      if (++leafTerms.value > config.maxTerms()) {
+        throw new TooManyTermsInQueryException();
+      }
+    }
+
+    for (Predicate<AccountState> childPredicate : predicate.getChildren()) {
+      validateMaxTermsInQuery(childPredicate, leafTerms);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexer.java b/java/com/google/gerrit/server/index/account/AccountIndexer.java
index 7f4f295..8ced005 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexer.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexer.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.index.account;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 
 public interface AccountIndexer {
 
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index 932e2c3..b908846 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -17,12 +17,13 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.index.Index;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -90,13 +91,21 @@
       if (accountState.isPresent()) {
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
-                "Replacing account %d in index version %d", id.get(), i.getSchema().getVersion())) {
+                "Replacing account in index",
+                Metadata.builder()
+                    .accountId(id.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.replace(accountState.get());
         }
       } else {
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
-                "Deleting account %d in index version %d", id.get(), i.getSchema().getVersion())) {
+                "Deleting account in index",
+                Metadata.builder()
+                    .accountId(id.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.delete(id);
         }
       }
diff --git a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
index c41814f..157e290 100644
--- a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -49,7 +49,18 @@
   @Deprecated static final Schema<AccountState> V9 = schema(V8);
 
   // Lucene index was changed to add additional fields for sorting.
-  static final Schema<AccountState> V10 = schema(V9);
+  @Deprecated static final Schema<AccountState> V10 = schema(V9);
+
+  // New numeric types: use dimensional points using the k-d tree geo-spatial data structure
+  // to offer fast single- and multi-dimensional numeric range. As the consequense, integer
+  // document id type is replaced with string document id type.
+  static final Schema<AccountState> V11 =
+      new Schema.Builder<AccountState>()
+          .add(V10)
+          .remove(AccountField.ID)
+          .add(AccountField.ID_STR)
+          .legacyNumericFields(false)
+          .build();
 
   public static final String NAME = "accounts";
   public static final AccountSchemaDefinitions INSTANCE = new AccountSchemaDefinitions();
diff --git a/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java b/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
index acb7236..c1077b1 100644
--- a/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
+++ b/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
@@ -21,8 +21,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.entities.Account;
 import com.google.gerrit.index.SiteIndexer;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Accounts;
diff --git a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
index 8b9fa27..b3d961a0 100644
--- a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
+++ b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.query.DataSource;
@@ -23,7 +24,6 @@
 import com.google.gerrit.index.query.Matchable;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountState;
 
 public class IndexedAccountQuery extends IndexedQuery<Account.Id, AccountState>
diff --git a/java/com/google/gerrit/server/index/account/StalenessChecker.java b/java/com/google/gerrit/server/index/account/StalenessChecker.java
index 4664700..aad9527 100644
--- a/java/com/google/gerrit/server/index/account/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/account/StalenessChecker.java
@@ -22,16 +22,17 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.RefState;
 import com.google.gerrit.index.query.FieldBundle;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.inject.Inject;
@@ -60,6 +61,12 @@
           AccountField.REF_STATE.getName(),
           AccountField.EXTERNAL_ID_STATE.getName());
 
+  public static final ImmutableSet<String> FIELDS2 =
+      ImmutableSet.of(
+          AccountField.ID_STR.getName(),
+          AccountField.REF_STATE.getName(),
+          AccountField.EXTERNAL_ID_STATE.getName());
+
   private final AccountIndexCollection indexes;
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
@@ -92,8 +99,13 @@
       return false;
     }
 
+    boolean useLegacyNumericFields = i.getSchema().useLegacyNumericFields();
+    ImmutableSet<String> fields = useLegacyNumericFields ? FIELDS : FIELDS2;
     Optional<FieldBundle> result =
-        i.getRaw(id, QueryOptions.create(indexConfig, 0, 1, IndexUtils.accountFields(FIELDS)));
+        i.getRaw(
+            id,
+            QueryOptions.create(
+                indexConfig, 0, 1, IndexUtils.accountFields(fields, useLegacyNumericFields)));
     if (!result.isPresent()) {
       // The document is missing in the index.
       try (Repository repo = repoManager.openRepository(allUsersName)) {
@@ -106,7 +118,11 @@
 
     for (Map.Entry<Project.NameKey, RefState> e :
         RefState.parseStates(result.get().getValue(AccountField.REF_STATE)).entries()) {
-      try (Repository repo = repoManager.openRepository(e.getKey())) {
+      // Custom All-Users repository names are not indexed. Instead, the default name is used.
+      // Therefore, defer to the currently configured All-Users name.
+      Project.NameKey repoName =
+          e.getKey().get().equals(AllUsersNameProvider.DEFAULT) ? allUsersName : e.getKey();
+      try (Repository repo = repoManager.openRepository(repoName)) {
         if (!e.getValue().match(repo)) {
           // Ref was modified since the account was indexed.
           return true;
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index a9980ef..005f4c5 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -25,10 +25,10 @@
 import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.index.SiteIndexer;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 394761b..07e9b9e4 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.index.FieldDef.exact;
 import static com.google.gerrit.index.FieldDef.fullText;
 import static com.google.gerrit.index.FieldDef.intRange;
@@ -42,23 +43,22 @@
 import com.google.common.primitives.Longs;
 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.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.converter.ChangeProtoConverter;
+import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
+import com.google.gerrit.entities.converter.PatchSetProtoConverter;
+import com.google.gerrit.entities.converter.ProtoConverter;
 import com.google.gerrit.index.FieldDef;
 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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.converter.ChangeProtoConverter;
-import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
-import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
-import com.google.gerrit.reviewdb.converter.ProtoConverter;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.StarredChangesUtil;
@@ -107,6 +107,9 @@
   public static final FieldDef<ChangeData, Integer> LEGACY_ID =
       integer("legacy_id").stored().build(cd -> cd.getId().get());
 
+  public static final FieldDef<ChangeData, String> LEGACY_ID_STR =
+      exact("legacy_id_str").stored().build(cd -> String.valueOf(cd.getId().get()));
+
   /** Newer style Change-Id key. */
   public static final FieldDef<ChangeData, String> ID =
       prefix(ChangeQueryBuilder.FIELD_CHANGE_ID).build(changeGetter(c -> c.getKey().get()));
@@ -128,7 +131,7 @@
 
   /** Reference (aka branch) the change will submit onto. */
   public static final FieldDef<ChangeData, String> REF =
-      exact(ChangeQueryBuilder.FIELD_REF).build(changeGetter(c -> c.getDest().get()));
+      exact(ChangeQueryBuilder.FIELD_REF).build(changeGetter(c -> c.getDest().branch()));
 
   /** Topic, a short annotation on the branch. */
   public static final FieldDef<ChangeData, String> EXACT_TOPIC =
@@ -237,7 +240,6 @@
     Set<String> r = new HashSet<>();
     for (String path : paths) {
       StringBuilder directory = new StringBuilder();
-      directory.append("");
       r.add(directory.toString());
       String nextPart = null;
       for (String part : s.split(path)) {
@@ -450,14 +452,8 @@
   public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMIT =
       exact(ChangeQueryBuilder.FIELD_EXACTCOMMIT).buildRepeatable(ChangeField::getRevisions);
 
-  private static Set<String> getRevisions(ChangeData cd) {
-    Set<String> revisions = new HashSet<>();
-    for (PatchSet ps : cd.patchSets()) {
-      if (ps.getRevision() != null) {
-        revisions.add(ps.getRevision().get());
-      }
-    }
-    return revisions;
+  private static ImmutableSet<String> getRevisions(ChangeData cd) {
+    return cd.patchSets().stream().map(ps -> ps.commitId().name()).collect(toImmutableSet());
   }
 
   /** Tracking id extracted from a footer. */
@@ -473,13 +469,12 @@
     Set<String> allApprovals = new HashSet<>();
     Set<String> distinctApprovals = new HashSet<>();
     for (PatchSetApproval a : cd.currentApprovals()) {
-      if (a.getValue() != 0 && !a.isLegacySubmit()) {
-        allApprovals.add(formatLabel(a.getLabel(), a.getValue(), a.getAccountId()));
-        if (owners && cd.change().getOwner().equals(a.getAccountId())) {
-          allApprovals.add(
-              formatLabel(a.getLabel(), a.getValue(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
+      if (a.value() != 0 && !a.isLegacySubmit()) {
+        allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
+        if (owners && cd.change().getOwner().equals(a.accountId())) {
+          allApprovals.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
         }
-        distinctApprovals.add(formatLabel(a.getLabel(), a.getValue()));
+        distinctApprovals.add(formatLabel(a.label(), a.value()));
       }
     }
     allApprovals.addAll(distinctApprovals);
@@ -669,8 +664,7 @@
   public static final FieldDef<ChangeData, Iterable<String>> GROUP =
       exact(ChangeQueryBuilder.FIELD_GROUP)
           .buildRepeatable(
-              cd ->
-                  cd.patchSets().stream().flatMap(ps -> ps.getGroups().stream()).collect(toSet()));
+              cd -> cd.patchSets().stream().flatMap(ps -> ps.groups().stream()).collect(toSet()));
 
   /** Serialized patch set object, used for pre-populating results. */
   public static final FieldDef<ChangeData, Iterable<byte[]>> PATCH_SET =
@@ -776,7 +770,7 @@
           SubmitRecord.Label srl = new SubmitRecord.Label();
           srl.label = label.label;
           srl.status = label.status;
-          srl.appliedBy = label.appliedBy != null ? new Account.Id(label.appliedBy) : null;
+          srl.appliedBy = label.appliedBy != null ? Account.id(label.appliedBy) : null;
           rec.labels.add(srl);
         }
       }
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndex.java b/java/com/google/gerrit/server/index/change/ChangeIndex.java
index 855bfe9..29bff0a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.index.change;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.LegacyChangeIdPredicate;
+import com.google.gerrit.server.query.change.LegacyChangeIdStrPredicate;
 
 public interface ChangeIndex extends Index<Change.Id, ChangeData> {
   public interface Factory
@@ -27,6 +28,8 @@
 
   @Override
   default Predicate<ChangeData> keyPredicate(Change.Id id) {
-    return new LegacyChangeIdPredicate(id);
+    return getSchema().useLegacyNumericFields()
+        ? new LegacyChangeIdPredicate(id)
+        : new LegacyChangeIdStrPredicate(id);
   }
 }
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java b/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java
index a353a2a..b8e2f3e 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.index.change;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.IndexCollection;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Singleton;
 
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java b/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java
index 7945429..7de9e74 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.index.change;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.IndexDefinition;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index 9e81e75..63c5297 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -19,6 +19,8 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Change.Status;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.IndexRewriter;
@@ -31,8 +33,7 @@
 import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.index.query.TooManyTermsInQueryException;
 import com.google.gerrit.server.query.change.AndChangeSource;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
@@ -155,7 +156,7 @@
 
     MutableInteger leafTerms = new MutableInteger();
     Predicate<ChangeData> out = rewriteImpl(in, index, opts, leafTerms);
-    if (in == out || out instanceof IndexPredicate) {
+    if (isSameInstance(in, out) || out instanceof IndexPredicate) {
       return new IndexedChangeQuery(index, out, opts);
     } else if (out == null /* cannot rewrite */) {
       return in;
@@ -183,7 +184,7 @@
       throws QueryParseException {
     if (isIndexPredicate(in, index)) {
       if (++leafTerms.value > config.maxTerms()) {
-        throw new QueryParseException("too many terms in query");
+        throw new TooManyTermsInQueryException();
       }
       return in;
     } else if (in instanceof LimitPredicate) {
@@ -207,7 +208,7 @@
     for (int i = 0; i < n; i++) {
       Predicate<ChangeData> c = in.getChild(i);
       Predicate<ChangeData> nc = rewriteImpl(c, index, opts, leafTerms);
-      if (nc == c) {
+      if (isSameInstance(nc, c)) {
         isIndexed.set(i);
         newChildren.add(c);
       } else if (nc == null /* cannot rewrite c */) {
@@ -291,4 +292,9 @@
     return p.getChildCount() > 0
         && (p instanceof AndPredicate || p instanceof OrPredicate || p instanceof NotPredicate);
   }
+
+  @SuppressWarnings("ReferenceEquality")
+  private static <T> boolean isSameInstance(T a, T b) {
+    return a == b;
+  }
 }
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 4597cbd..f6d86bf 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -22,15 +22,18 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.index.Index;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -66,6 +69,7 @@
   @Nullable private final ChangeIndexCollection indexes;
   @Nullable private final ChangeIndex index;
   private final ChangeData.Factory changeDataFactory;
+  private final ChangeNotes.Factory notesFactory;
   private final ThreadLocalRequestContext context;
   private final ListeningExecutorService batchExecutor;
   private final ListeningExecutorService executor;
@@ -82,6 +86,7 @@
   ChangeIndexer(
       @GerritServerConfig Config cfg,
       ChangeData.Factory changeDataFactory,
+      ChangeNotes.Factory notesFactory,
       ThreadLocalRequestContext context,
       PluginSetContext<ChangeIndexedListener> indexedListeners,
       StalenessChecker stalenessChecker,
@@ -90,6 +95,7 @@
       @Assisted ChangeIndex index) {
     this.executor = executor;
     this.changeDataFactory = changeDataFactory;
+    this.notesFactory = notesFactory;
     this.context = context;
     this.indexedListeners = indexedListeners;
     this.stalenessChecker = stalenessChecker;
@@ -103,6 +109,7 @@
   ChangeIndexer(
       @GerritServerConfig Config cfg,
       ChangeData.Factory changeDataFactory,
+      ChangeNotes.Factory notesFactory,
       ThreadLocalRequestContext context,
       PluginSetContext<ChangeIndexedListener> indexedListeners,
       StalenessChecker stalenessChecker,
@@ -111,6 +118,7 @@
       @Assisted ChangeIndexCollection indexes) {
     this.executor = executor;
     this.changeDataFactory = changeDataFactory;
+    this.notesFactory = notesFactory;
     this.context = context;
     this.indexedListeners = indexedListeners;
     this.stalenessChecker = stalenessChecker;
@@ -133,6 +141,7 @@
   public ListenableFuture<?> indexAsync(Project.NameKey project, Change.Id id) {
     IndexTask task = new IndexTask(project, id);
     if (queuedIndexTasks.add(task)) {
+      fireChangeScheduledForIndexingEvent(project.get(), id.get());
       return submit(task);
     }
     return Futures.immediateFuture(null);
@@ -158,6 +167,11 @@
    * @param cd change to index.
    */
   public void index(ChangeData cd) {
+    fireChangeScheduledForIndexingEvent(cd.project().get(), cd.getId().get());
+    doIndex(cd);
+  }
+
+  private void doIndex(ChangeData cd) {
     indexImpl(cd);
 
     // Always double-check whether the change might be stale immediately after
@@ -186,18 +200,30 @@
     for (Index<?, ChangeData> i : getWriteIndexes()) {
       try (TraceTimer traceTimer =
           TraceContext.newTimer(
-              "Replacing change %d in index version %d",
-              cd.getId().get(), i.getSchema().getVersion())) {
+              "Replacing change in index",
+              Metadata.builder()
+                  .changeId(cd.getId().get())
+                  .patchSetId(cd.currentPatchSet().number())
+                  .indexVersion(i.getSchema().getVersion())
+                  .build())) {
         i.replace(cd);
       }
     }
     fireChangeIndexedEvent(cd.project().get(), cd.getId().get());
   }
 
+  private void fireChangeScheduledForIndexingEvent(String projectName, int id) {
+    indexedListeners.runEach(l -> l.onChangeScheduledForIndexing(projectName, id));
+  }
+
   private void fireChangeIndexedEvent(String projectName, int id) {
     indexedListeners.runEach(l -> l.onChangeIndexed(projectName, id));
   }
 
+  private void fireChangeScheduledForDeletionFromIndexEvent(int id) {
+    indexedListeners.runEach(l -> l.onChangeScheduledForDeletionFromIndex(id));
+  }
+
   private void fireChangeDeletedFromIndexEvent(int id) {
     indexedListeners.runEach(l -> l.onChangeDeleted(id));
   }
@@ -228,6 +254,7 @@
    * @return future for the deleting task.
    */
   public ListenableFuture<?> deleteAsync(Change.Id id) {
+    fireChangeScheduledForDeletionFromIndexEvent(id.get());
     return submit(new DeleteTask(id));
   }
 
@@ -237,6 +264,11 @@
    * @param id change ID to delete.
    */
   public void delete(Change.Id id) {
+    fireChangeScheduledForDeletionFromIndexEvent(id.get());
+    doDelete(id);
+  }
+
+  private void doDelete(Change.Id id) {
     new DeleteTask(id).call();
   }
 
@@ -327,8 +359,12 @@
     @Override
     public Void callImpl() throws Exception {
       remove();
-      ChangeData cd = changeDataFactory.create(project, id);
-      index(cd);
+      try {
+        ChangeNotes changeNotes = notesFactory.createChecked(project, id);
+        doIndex(changeDataFactory.create(changeNotes));
+      } catch (NoSuchChangeException e) {
+        doDelete(id);
+      }
       return null;
     }
 
@@ -374,7 +410,11 @@
       for (ChangeIndex i : getWriteIndexes()) {
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
-                "Deleting change %d in index version %d", id.get(), i.getSchema().getVersion())) {
+                "Deleting change in index",
+                Metadata.builder()
+                    .changeId(id.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.delete(id);
         }
       }
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index cde6a64..72153c4 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -22,7 +22,7 @@
 
 public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> {
   @Deprecated
-  static final Schema<ChangeData> V39 =
+  static final Schema<ChangeData> V55 =
       schema(
           ChangeField.ADDED,
           ChangeField.APPROVAL,
@@ -36,11 +36,16 @@
           ChangeField.COMMIT_MESSAGE,
           ChangeField.DELETED,
           ChangeField.DELTA,
+          ChangeField.DIRECTORY,
           ChangeField.DRAFTBY,
           ChangeField.EDITBY,
+          ChangeField.EXACT_AUTHOR,
           ChangeField.EXACT_COMMIT,
+          ChangeField.EXACT_COMMITTER,
           ChangeField.EXACT_TOPIC,
+          ChangeField.EXTENSION,
           ChangeField.FILE_PART,
+          ChangeField.FOOTER,
           ChangeField.FUZZY_TOPIC,
           ChangeField.GROUP,
           ChangeField.HASHTAG,
@@ -49,70 +54,49 @@
           ChangeField.LABEL,
           ChangeField.LEGACY_ID,
           ChangeField.MERGEABLE,
+          ChangeField.ONLY_EXTENSIONS,
           ChangeField.OWNER,
           ChangeField.PATCH_SET,
           ChangeField.PATH,
+          ChangeField.PENDING_REVIEWER,
+          ChangeField.PENDING_REVIEWER_BY_EMAIL,
+          ChangeField.PRIVATE,
           ChangeField.PROJECT,
           ChangeField.PROJECTS,
           ChangeField.REF,
           ChangeField.REF_STATE,
           ChangeField.REF_STATE_PATTERN,
+          ChangeField.REVERT_OF,
           ChangeField.REVIEWEDBY,
           ChangeField.REVIEWER,
+          ChangeField.REVIEWER_BY_EMAIL,
           ChangeField.STAR,
           ChangeField.STARBY,
+          ChangeField.STARTED,
           ChangeField.STATUS,
           ChangeField.STORED_SUBMIT_RECORD_LENIENT,
           ChangeField.STORED_SUBMIT_RECORD_STRICT,
           ChangeField.SUBMISSIONID,
           ChangeField.SUBMIT_RECORD,
+          ChangeField.TOTAL_COMMENT_COUNT,
           ChangeField.TR,
           ChangeField.UNRESOLVED_COMMENT_COUNT,
-          ChangeField.UPDATED);
-
-  @Deprecated static final Schema<ChangeData> V40 = schema(V39, ChangeField.PRIVATE);
-  @Deprecated static final Schema<ChangeData> V41 = schema(V40, ChangeField.REVIEWER_BY_EMAIL);
-  @Deprecated static final Schema<ChangeData> V42 = schema(V41, ChangeField.WIP);
-
-  @Deprecated
-  static final Schema<ChangeData> V43 =
-      schema(V42, ChangeField.EXACT_AUTHOR, ChangeField.EXACT_COMMITTER);
-
-  @Deprecated
-  static final Schema<ChangeData> V44 =
-      schema(
-          V43,
-          ChangeField.STARTED,
-          ChangeField.PENDING_REVIEWER,
-          ChangeField.PENDING_REVIEWER_BY_EMAIL);
-
-  @Deprecated static final Schema<ChangeData> V45 = schema(V44, ChangeField.REVERT_OF);
-
-  @Deprecated static final Schema<ChangeData> V46 = schema(V45);
-
-  // Removal of draft change workflow requires reindexing
-  @Deprecated static final Schema<ChangeData> V47 = schema(V46);
-
-  // Rename of star label 'mute' to 'reviewed' requires reindexing
-  @Deprecated static final Schema<ChangeData> V48 = schema(V47);
-
-  @Deprecated static final Schema<ChangeData> V49 = schema(V48);
-
-  // Bump Lucene version requires reindexing
-  @Deprecated static final Schema<ChangeData> V50 = schema(V49);
-
-  @Deprecated static final Schema<ChangeData> V51 = schema(V50, ChangeField.TOTAL_COMMENT_COUNT);
-
-  @Deprecated static final Schema<ChangeData> V52 = schema(V51, ChangeField.EXTENSION);
-
-  @Deprecated static final Schema<ChangeData> V53 = schema(V52, ChangeField.ONLY_EXTENSIONS);
-
-  @Deprecated static final Schema<ChangeData> V54 = schema(V53, ChangeField.FOOTER);
-
-  @Deprecated static final Schema<ChangeData> V55 = schema(V54, ChangeField.DIRECTORY);
+          ChangeField.UPDATED,
+          ChangeField.WIP);
 
   // The computation of the 'extension' field is changed, hence reindexing is required.
-  static final Schema<ChangeData> V56 = schema(V55);
+  @Deprecated static final Schema<ChangeData> V56 = schema(V55);
+
+  // New numeric types: use dimensional points using the k-d tree geo-spatial data structure
+  // to offer fast single- and multi-dimensional numeric range. As the consequense, integer
+  // document id type is replaced with string document id type.
+  static final Schema<ChangeData> V57 =
+      new Schema.Builder<ChangeData>()
+          .add(V56)
+          .remove(ChangeField.LEGACY_ID)
+          .add(ChangeField.LEGACY_ID_STR)
+          .legacyNumericFields(false)
+          .build();
 
   public static final String NAME = "changes";
   public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
diff --git a/java/com/google/gerrit/server/index/change/DummyChangeIndex.java b/java/com/google/gerrit/server/index/change/DummyChangeIndex.java
index 9be93f7..ae3b729 100644
--- a/java/com/google/gerrit/server/index/change/DummyChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/DummyChangeIndex.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.index.change;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
 
diff --git a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index ed09eed..57a2091 100644
--- a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.query.DataSource;
@@ -31,7 +32,6 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.ResultSet;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
 import java.util.HashMap;
diff --git a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index fd12345..f6d3b6f 100644
--- a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -17,37 +17,30 @@
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 import static com.google.gerrit.server.query.change.ChangeData.asChanges;
 
-import com.google.common.base.Objects;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.QueueProvider.QueueType;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.index.account.AccountIndexer;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import java.io.IOException;
-import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Future;
 import org.eclipse.jgit.lib.Config;
 
@@ -58,15 +51,12 @@
   private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeIndexer.Factory indexerFactory;
   private final ChangeIndexCollection indexes;
-  private final ChangeNotes.Factory notesFactory;
   private final AllUsersName allUsersName;
   private final AccountCache accountCache;
   private final Provider<AccountIndexer> indexer;
   private final ListeningExecutorService executor;
   private final boolean enabled;
 
-  private final Set<Index> queuedIndexTasks = Collections.newSetFromMap(new ConcurrentHashMap<>());
-
   @Inject
   ReindexAfterRefUpdate(
       @GerritServerConfig Config cfg,
@@ -74,7 +64,6 @@
       Provider<InternalChangeQuery> queryProvider,
       ChangeIndexer.Factory indexerFactory,
       ChangeIndexCollection indexes,
-      ChangeNotes.Factory notesFactory,
       AllUsersName allUsersName,
       AccountCache accountCache,
       Provider<AccountIndexer> indexer,
@@ -83,7 +72,6 @@
     this.queryProvider = queryProvider;
     this.indexerFactory = indexerFactory;
     this.indexes = indexes;
-    this.notesFactory = notesFactory;
     this.allUsersName = allUsersName;
     this.accountCache = accountCache;
     this.indexer = indexer;
@@ -113,12 +101,9 @@
           @Override
           public void onSuccess(List<Change> changes) {
             for (Change c : changes) {
-              Index task = new Index(event, c.getId());
-              if (queuedIndexTasks.add(task)) {
-                // Don't retry indefinitely; if this fails changes may be stale.
-                @SuppressWarnings("unused")
-                Future<?> possiblyIgnoredError = executor.submit(task);
-              }
+              @SuppressWarnings("unused")
+              Future<?> possiblyIgnoredError =
+                  indexerFactory.create(executor, indexes).indexAsync(c.getProject(), c.getId());
             }
           }
 
@@ -160,11 +145,11 @@
     @Override
     protected List<Change> impl(RequestContext ctx) {
       String ref = event.getRefName();
-      Project.NameKey project = new Project.NameKey(event.getProjectName());
+      Project.NameKey project = Project.nameKey(event.getProjectName());
       if (ref.equals(RefNames.REFS_CONFIG)) {
         return asChanges(queryProvider.get().byProjectOpen(project));
       }
-      return asChanges(queryProvider.get().byBranchNew(new Branch.NameKey(project, ref)));
+      return asChanges(queryProvider.get().byBranchNew(BranchNameKey.create(project, ref)));
     }
 
     @Override
@@ -178,51 +163,4 @@
     @Override
     protected void remove() {}
   }
-
-  private class Index extends Task<Void> {
-    private final Change.Id id;
-
-    Index(Event event, Change.Id id) {
-      super(event);
-      this.id = id;
-    }
-
-    @Override
-    protected Void impl(RequestContext ctx) throws IOException {
-      // Reload change, as some time may have passed since GetChanges.
-      remove();
-      try {
-        Change c =
-            notesFactory.createChecked(new Project.NameKey(event.getProjectName()), id).getChange();
-        indexerFactory.create(executor, indexes).index(c);
-      } catch (NoSuchChangeException e) {
-        indexerFactory.create(executor, indexes).delete(id);
-      }
-      return null;
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hashCode(Index.class, id.get());
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-      if (!(obj instanceof Index)) {
-        return false;
-      }
-      Index other = (Index) obj;
-      return id.get() == other.id.get();
-    }
-
-    @Override
-    public String toString() {
-      return "Index change " + id.get() + " of project " + event.getProjectName();
-    }
-
-    @Override
-    protected void remove() {
-      queuedIndexTasks.remove(this);
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/index/change/StalenessChecker.java b/java/com/google/gerrit/server/index/change/StalenessChecker.java
index 338cf3d..47fd7ba 100644
--- a/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -29,11 +29,11 @@
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.RefState;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
@@ -131,8 +131,7 @@
       String s = new String(b, UTF_8);
       List<String> parts = Splitter.on(':').splitToList(s);
       RefStatePattern.check(parts.size() == 2, s);
-      result.put(
-          new Project.NameKey(Url.decode(parts.get(0))), RefStatePattern.create(parts.get(1)));
+      result.put(Project.nameKey(Url.decode(parts.get(0))), RefStatePattern.create(parts.get(1)));
     }
     return result;
   }
diff --git a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
index 3474934..0dbbbc5 100644
--- a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
+++ b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -23,8 +23,8 @@
 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.index.SiteIndexer;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.db.Groups;
diff --git a/java/com/google/gerrit/server/index/group/GroupField.java b/java/com/google/gerrit/server/index/group/GroupField.java
index 29e3867..a3d913d 100644
--- a/java/com/google/gerrit/server/index/group/GroupField.java
+++ b/java/com/google/gerrit/server/index/group/GroupField.java
@@ -23,13 +23,13 @@
 import static com.google.gerrit.index.FieldDef.timestamp;
 
 import com.google.common.base.MoreObjects;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.SchemaUtil;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import java.sql.Timestamp;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 
 /** Secondary index schemas for groups. */
@@ -82,7 +82,7 @@
       storedOnly("ref_state")
           .build(
               g -> {
-                byte[] a = new byte[Constants.OBJECT_ID_STRING_LENGTH];
+                byte[] a = new byte[ObjectIds.STR_LEN];
                 MoreObjects.firstNonNull(g.getRefState(), ObjectId.zeroId()).copyTo(a, 0);
                 return a;
               });
diff --git a/java/com/google/gerrit/server/index/group/GroupIndex.java b/java/com/google/gerrit/server/index/group/GroupIndex.java
index 6a430f8..daa2131 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndex.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndex.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.index.group;
 
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.query.group.GroupPredicates;
 
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexCollection.java b/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
index 531c446..9d74b7d 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.index.group;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.index.IndexCollection;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Singleton;
 
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java b/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java
index d117dfd..e403752 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.index.group;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.index.IndexDefinition;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Inject;
 
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexer.java b/java/com/google/gerrit/server/index/group/GroupIndexer.java
index 5d9232e..25d5840 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexer.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexer.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.index.group;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 
 public interface GroupIndexer {
 
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index 5982de7..790066d 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -17,12 +17,13 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.GroupIndexedListener;
 import com.google.gerrit.index.Index;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -90,13 +91,21 @@
       if (internalGroup.isPresent()) {
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
-                "Replacing group %s in index version %d", uuid.get(), i.getSchema().getVersion())) {
+                "Replacing group",
+                Metadata.builder()
+                    .groupUuid(uuid.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.replace(internalGroup.get());
         }
       } else {
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
-                "Deleting group %s in index version %d", uuid.get(), i.getSchema().getVersion())) {
+                "Deleting group",
+                Metadata.builder()
+                    .groupUuid(uuid.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.delete(uuid);
         }
       }
diff --git a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
index 6d0f3b6..e64e2eb 100644
--- a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -43,7 +43,11 @@
   @Deprecated static final Schema<InternalGroup> V6 = schema(V5);
 
   // Lucene index was changed to add an additional field for sorting.
-  static final Schema<InternalGroup> V7 = schema(V6);
+  @Deprecated static final Schema<InternalGroup> V7 = schema(V6);
+
+  // New numeric types: use dimensional points using the k-d tree geo-spatial data structure
+  // to offer fast single- and multi-dimensional numeric range.
+  static final Schema<InternalGroup> V8 = schema(V7, false);
 
   public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions();
 
diff --git a/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java b/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
index 32393b06..dfdf3ca 100644
--- a/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
+++ b/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.index.group;
 
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
@@ -21,7 +22,6 @@
 import com.google.gerrit.index.query.IndexedQuery;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import java.util.HashSet;
 import java.util.Set;
diff --git a/java/com/google/gerrit/server/index/group/StalenessChecker.java b/java/com/google/gerrit/server/index/group/StalenessChecker.java
index 7900287..3a721c3 100644
--- a/java/com/google/gerrit/server/index/group/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/group/StalenessChecker.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.index.group;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.query.FieldBundle;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java b/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java
index 305cd25..b760fd7 100644
--- a/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java
+++ b/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java
@@ -21,10 +21,10 @@
 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.entities.Project;
 import com.google.gerrit.index.SiteIndexer;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectIndex;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/index/project/ProjectIndexDefinition.java b/java/com/google/gerrit/server/index/project/ProjectIndexDefinition.java
index ce2b634..6a844b5 100644
--- a/java/com/google/gerrit/server/index/project/ProjectIndexDefinition.java
+++ b/java/com/google/gerrit/server/index/project/ProjectIndexDefinition.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.index.project;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectIndex;
 import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.index.project.ProjectSchemaDefinitions;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 
 public class ProjectIndexDefinition
diff --git a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
index 6d6f78d..4de83be 100644
--- a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
@@ -17,12 +17,13 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.events.ProjectIndexedListener;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectIndex;
 import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.index.project.ProjectIndexer;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -78,8 +79,11 @@
       for (ProjectIndex i : getWriteIndexes()) {
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
-                "Replacing project %s in index version %d",
-                nameKey.get(), i.getSchema().getVersion())) {
+                "Replacing project",
+                Metadata.builder()
+                    .projectName(nameKey.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.replace(projectData);
         }
       }
@@ -89,8 +93,11 @@
       for (ProjectIndex i : getWriteIndexes()) {
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
-                "Deleting project %s in index version %d",
-                nameKey.get(), i.getSchema().getVersion())) {
+                "Deleting project",
+                Metadata.builder()
+                    .projectName(nameKey.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.delete(nameKey);
         }
       }
diff --git a/java/com/google/gerrit/server/index/project/StalenessChecker.java b/java/com/google/gerrit/server/index/project/StalenessChecker.java
index dc5ebc6..e4c1a7d 100644
--- a/java/com/google/gerrit/server/index/project/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/project/StalenessChecker.java
@@ -17,6 +17,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.RefState;
@@ -25,8 +27,6 @@
 import com.google.gerrit.index.project.ProjectIndex;
 import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.index.query.FieldBundle;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import java.util.Optional;
diff --git a/java/com/google/gerrit/server/ioutil/BUILD b/java/com/google/gerrit/server/ioutil/BUILD
index ed58d5b..fd0c4f1 100644
--- a/java/com/google/gerrit/server/ioutil/BUILD
+++ b/java/com/google/gerrit/server/ioutil/BUILD
@@ -5,10 +5,10 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//lib:automaton",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
+        "//lib:jgit-archive",
     ],
 )
diff --git a/java/com/google/gerrit/server/ioutil/BasicSerialization.java b/java/com/google/gerrit/server/ioutil/BasicSerialization.java
index c7f2ecd..296cf22 100644
--- a/java/com/google/gerrit/server/ioutil/BasicSerialization.java
+++ b/java/com/google/gerrit/server/ioutil/BasicSerialization.java
@@ -31,7 +31,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.gerrit.reviewdb.client.CodedEnum;
+import com.google.gerrit.entities.CodedEnum;
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
diff --git a/java/com/google/gerrit/server/logging/BUILD b/java/com/google/gerrit/server/logging/BUILD
index 672cb75..7af34f7 100644
--- a/java/com/google/gerrit/server/logging/BUILD
+++ b/java/com/google/gerrit/server/logging/BUILD
@@ -8,12 +8,15 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/server/util/time",
         "//lib:gson",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/flogger:api",
+        "//lib/guice",
         "//lib/log:log4j",
     ],
 )
diff --git a/java/com/google/gerrit/server/logging/CallerFinder.java b/java/com/google/gerrit/server/logging/CallerFinder.java
index c27dbbb..bd7e608 100644
--- a/java/com/google/gerrit/server/logging/CallerFinder.java
+++ b/java/com/google/gerrit/server/logging/CallerFinder.java
@@ -142,13 +142,27 @@
 
   /**
    * The minimum number of calls known to have occurred between the first call to the target class
-   * and the call of {@link #findCaller()}. If in doubt, specify zero here to avoid accidentally
+   * and the call of {@link #findCallerLazy()}. If in doubt, specify zero here to avoid accidentally
    * skipping past the caller.
    *
    * @return the number of stack elements to skip when computing the caller
    */
   public abstract int skip();
 
+  /**
+   * Packages that should be ignored and not be considered as caller once a target has been found.
+   *
+   * @return the ignored packages
+   */
+  public abstract ImmutableList<String> ignoredPackages();
+
+  /**
+   * Classes that should be ignored and not be considered as caller once a target has been found.
+   *
+   * @return the qualified names of the ignored classes
+   */
+  public abstract ImmutableList<String> ignoredClasses();
+
   @AutoValue.Builder
   public abstract static class Builder {
     abstract ImmutableList.Builder<Class<?>> targetsBuilder();
@@ -164,10 +178,24 @@
 
     public abstract Builder skip(int skip);
 
+    abstract ImmutableList.Builder<String> ignoredPackagesBuilder();
+
+    public Builder addIgnoredPackage(String ignoredPackage) {
+      ignoredPackagesBuilder().add(ignoredPackage);
+      return this;
+    }
+
+    abstract ImmutableList.Builder<String> ignoredClassesBuilder();
+
+    public Builder addIgnoredClass(Class<?> ignoredClass) {
+      ignoredClassesBuilder().add(ignoredClass.getName());
+      return this;
+    }
+
     public abstract CallerFinder build();
   }
 
-  public LazyArg<String> findCaller() {
+  public LazyArg<String> findCallerLazy() {
     return lazy(
         () ->
             targets().stream()
@@ -194,7 +222,9 @@
         StackTraceElement element = stack[index];
         if (isCaller(target, element.getClassName(), matchSubClasses())) {
           foundCaller = true;
-        } else if (foundCaller) {
+        } else if (foundCaller
+            && !ignoredPackages().contains(getPackageName(element))
+            && !ignoredClasses().contains(element.getClassName())) {
           return Optional.of(element.toString());
         }
       }
@@ -206,6 +236,11 @@
     }
   }
 
+  private static String getPackageName(StackTraceElement element) {
+    String className = element.getClassName();
+    return className.substring(0, className.lastIndexOf("."));
+  }
+
   private boolean isCaller(Class<?> target, String className, boolean matchSubClasses)
       throws ClassNotFoundException {
     if (matchSubClasses) {
diff --git a/java/com/google/gerrit/server/logging/LoggingContext.java b/java/com/google/gerrit/server/logging/LoggingContext.java
index cb7d01e..8e786fc 100644
--- a/java/com/google/gerrit/server/logging/LoggingContext.java
+++ b/java/com/google/gerrit/server/logging/LoggingContext.java
@@ -14,8 +14,14 @@
 
 package com.google.gerrit.server.logging;
 
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.flogger.backend.Tags;
+import com.google.inject.Provider;
+import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.logging.Level;
 
@@ -35,6 +41,9 @@
 
   private static final ThreadLocal<MutableTags> tags = new ThreadLocal<>();
   private static final ThreadLocal<Boolean> forceLogging = new ThreadLocal<>();
+  private static final ThreadLocal<Boolean> performanceLogging = new ThreadLocal<>();
+  private static final ThreadLocal<MutablePerformanceLogRecords> performanceLogRecords =
+      new ThreadLocal<>();
 
   private LoggingContext() {}
 
@@ -47,14 +56,39 @@
     if (runnable instanceof LoggingContextAwareRunnable) {
       return runnable;
     }
-    return new LoggingContextAwareRunnable(runnable);
+
+    // Pass the MutablePerformanceLogRecords instance into the LoggingContextAwareRunnable
+    // constructor so that performance log records that are created in the wrapped runnable are
+    // added to this MutablePerformanceLogRecords instance. This is important since performance
+    // log records are processed only at the end of the request and performance log records that
+    // are created in another thread should not get lost.
+    return new LoggingContextAwareRunnable(
+        runnable, getInstance().getMutablePerformanceLogRecords());
   }
 
   public static <T> Callable<T> copy(Callable<T> callable) {
     if (callable instanceof LoggingContextAwareCallable) {
       return callable;
     }
-    return new LoggingContextAwareCallable<>(callable);
+
+    // Pass the MutablePerformanceLogRecords instance into the LoggingContextAwareCallable
+    // constructor so that performance log records that are created in the wrapped runnable are
+    // added to this MutablePerformanceLogRecords instance. This is important since performance
+    // log records are processed only at the end of the request and performance log records that
+    // are created in another thread should not get lost.
+    return new LoggingContextAwareCallable<>(
+        callable, getInstance().getMutablePerformanceLogRecords());
+  }
+
+  public boolean isEmpty() {
+    return tags.get() == null && forceLogging.get() == null && performanceLogging.get() == null;
+  }
+
+  public void clear() {
+    tags.remove();
+    forceLogging.remove();
+    performanceLogging.remove();
+    performanceLogRecords.remove();
   }
 
   @Override
@@ -119,4 +153,113 @@
     }
     return Boolean.TRUE.equals(oldValue);
   }
+
+  boolean isPerformanceLogging() {
+    Boolean isPerformanceLogging = performanceLogging.get();
+    return isPerformanceLogging != null ? isPerformanceLogging : false;
+  }
+
+  /**
+   * Enables performance logging.
+   *
+   * <p>It's important to enable performance logging only in a context that ensures to consume the
+   * captured performance log records. Otherwise captured performance log records might leak into
+   * other requests that are executed by the same thread (if a thread pool is used to process
+   * requests).
+   *
+   * @param enable whether performance logging should be enabled.
+   * @return whether performance logging was be enabled before invoking this method (old value).
+   */
+  boolean performanceLogging(boolean enable) {
+    Boolean oldValue = performanceLogging.get();
+    if (enable) {
+      performanceLogging.set(true);
+    } else {
+      performanceLogging.remove();
+    }
+    return oldValue != null ? oldValue : false;
+  }
+
+  /**
+   * Adds a performance log record, if performance logging is enabled.
+   *
+   * @param recordProvider Provider for the performance log record. This provider is only invoked if
+   *     performance logging is enabled. This means if performance logging is disabled, we avoid the
+   *     creation of a {@link PerformanceLogRecord}.
+   */
+  public void addPerformanceLogRecord(Provider<PerformanceLogRecord> recordProvider) {
+    if (!isPerformanceLogging()) {
+      // return early and avoid the creation of a PerformanceLogRecord
+      return;
+    }
+
+    getMutablePerformanceLogRecords().add(recordProvider.get());
+  }
+
+  ImmutableList<PerformanceLogRecord> getPerformanceLogRecords() {
+    MutablePerformanceLogRecords records = performanceLogRecords.get();
+    if (records != null) {
+      return records.list();
+    }
+    return ImmutableList.of();
+  }
+
+  void clearPerformanceLogEntries() {
+    performanceLogRecords.remove();
+  }
+
+  /**
+   * Set the performance log records in this logging context. Existing log records are overwritten.
+   *
+   * <p>This method makes a defensive copy of the passed in list.
+   *
+   * @param newPerformanceLogRecords performance log records that should be set
+   */
+  void setPerformanceLogRecords(List<PerformanceLogRecord> newPerformanceLogRecords) {
+    if (newPerformanceLogRecords.isEmpty()) {
+      performanceLogRecords.remove();
+      return;
+    }
+
+    getMutablePerformanceLogRecords().set(newPerformanceLogRecords);
+  }
+
+  /**
+   * Sets a {@link MutablePerformanceLogRecords} instance for storing performance log records.
+   *
+   * <p><strong>Attention:</strong> The passed in {@link MutablePerformanceLogRecords} instance is
+   * directly stored in the logging context.
+   *
+   * <p>This method is intended to be only used when the logging context is copied to a new thread
+   * to ensure that the performance log records that are added in the new thread are added to the
+   * same {@link MutablePerformanceLogRecords} instance (see {@link LoggingContextAwareRunnable} and
+   * {@link LoggingContextAwareCallable}). This is important since performance log records are
+   * processed only at the end of the request and performance log records that are created in
+   * another thread should not get lost.
+   *
+   * @param mutablePerformanceLogRecords the {@link MutablePerformanceLogRecords} instance in which
+   *     performance log records should be stored
+   */
+  void setMutablePerformanceLogRecords(MutablePerformanceLogRecords mutablePerformanceLogRecords) {
+    performanceLogRecords.set(requireNonNull(mutablePerformanceLogRecords));
+  }
+
+  private MutablePerformanceLogRecords getMutablePerformanceLogRecords() {
+    MutablePerformanceLogRecords records = performanceLogRecords.get();
+    if (records == null) {
+      records = new MutablePerformanceLogRecords();
+      performanceLogRecords.set(records);
+    }
+    return records;
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("tags", tags.get())
+        .add("forceLogging", forceLogging.get())
+        .add("performanceLogging", performanceLogging.get())
+        .add("performanceLogRecords", performanceLogRecords.get())
+        .toString();
+  }
 }
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java b/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java
index 6aff5c4..d2701d7 100644
--- a/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java
+++ b/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.logging;
 
 import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.flogger.FluentLogger;
 import java.util.concurrent.Callable;
 
 /**
@@ -31,16 +32,30 @@
  * @see LoggingContextAwareRunnable
  */
 class LoggingContextAwareCallable<T> implements Callable<T> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final Callable<T> callable;
   private final Thread callingThread;
   private final ImmutableSetMultimap<String, String> tags;
   private final boolean forceLogging;
+  private final boolean performanceLogging;
+  private final MutablePerformanceLogRecords mutablePerformanceLogRecords;
 
-  LoggingContextAwareCallable(Callable<T> callable) {
+  /**
+   * Creates a LoggingContextAwareCallable that wraps the given {@link Callable}.
+   *
+   * @param callable Callable that should be wrapped.
+   * @param mutablePerformanceLogRecords instance of {@link MutablePerformanceLogRecords} to which
+   *     performance log records that are created from the runnable are added
+   */
+  LoggingContextAwareCallable(
+      Callable<T> callable, MutablePerformanceLogRecords mutablePerformanceLogRecords) {
     this.callable = callable;
     this.callingThread = Thread.currentThread();
     this.tags = LoggingContext.getInstance().getTagsAsMap();
     this.forceLogging = LoggingContext.getInstance().isLoggingForced();
+    this.performanceLogging = LoggingContext.getInstance().isPerformanceLogging();
+    this.mutablePerformanceLogRecords = mutablePerformanceLogRecords;
   }
 
   @Override
@@ -50,17 +65,29 @@
       return callable.call();
     }
 
-    // propagate logging context
     LoggingContext loggingCtx = LoggingContext.getInstance();
-    ImmutableSetMultimap<String, String> oldTags = loggingCtx.getTagsAsMap();
-    boolean oldForceLogging = loggingCtx.isLoggingForced();
+
+    if (!loggingCtx.isEmpty()) {
+      logger.atWarning().log("Logging context is not empty: %s", loggingCtx);
+    }
+
+    // propagate logging context
     loggingCtx.setTags(tags);
     loggingCtx.forceLogging(forceLogging);
+    loggingCtx.performanceLogging(performanceLogging);
+
+    // For the performance log records use the {@link MutablePerformanceLogRecords} instance from
+    // the logging context of the calling thread in the logging context of the new thread. This way
+    // performance log records that are created from the new thread are available from the logging
+    // context of the calling thread. This is important since performance log records are processed
+    // only at the end of the request and performance log records that are created in another thread
+    // should not get lost.
+    loggingCtx.setMutablePerformanceLogRecords(mutablePerformanceLogRecords);
     try {
       return callable.call();
     } finally {
-      loggingCtx.setTags(oldTags);
-      loggingCtx.forceLogging(oldForceLogging);
+      // Cleanup logging context. This is important if the thread is pooled and reused.
+      loggingCtx.clear();
     }
   }
 }
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java b/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
index 0bd7d00..23162b1 100644
--- a/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
+++ b/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.logging;
 
 import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.flogger.FluentLogger;
 
 /**
  * Wrapper for a {@link Runnable} that copies the {@link LoggingContext} from the current thread to
@@ -49,16 +50,30 @@
  * @see LoggingContextAwareCallable
  */
 public class LoggingContextAwareRunnable implements Runnable {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final Runnable runnable;
   private final Thread callingThread;
   private final ImmutableSetMultimap<String, String> tags;
   private final boolean forceLogging;
+  private final boolean performanceLogging;
+  private final MutablePerformanceLogRecords mutablePerformanceLogRecords;
 
-  LoggingContextAwareRunnable(Runnable runnable) {
+  /**
+   * Creates a LoggingContextAwareRunnable that wraps the given {@link Runnable}.
+   *
+   * @param runnable Runnable that should be wrapped.
+   * @param mutablePerformanceLogRecords instance of {@link MutablePerformanceLogRecords} to which
+   *     performance log records that are created from the runnable are added
+   */
+  LoggingContextAwareRunnable(
+      Runnable runnable, MutablePerformanceLogRecords mutablePerformanceLogRecords) {
     this.runnable = runnable;
     this.callingThread = Thread.currentThread();
     this.tags = LoggingContext.getInstance().getTagsAsMap();
     this.forceLogging = LoggingContext.getInstance().isLoggingForced();
+    this.performanceLogging = LoggingContext.getInstance().isPerformanceLogging();
+    this.mutablePerformanceLogRecords = mutablePerformanceLogRecords;
   }
 
   public Runnable unwrap() {
@@ -73,17 +88,29 @@
       return;
     }
 
-    // propagate logging context
     LoggingContext loggingCtx = LoggingContext.getInstance();
-    ImmutableSetMultimap<String, String> oldTags = loggingCtx.getTagsAsMap();
-    boolean oldForceLogging = loggingCtx.isLoggingForced();
+
+    if (!loggingCtx.isEmpty()) {
+      logger.atWarning().log("Logging context is not empty: %s", loggingCtx);
+    }
+
+    // propagate logging context
     loggingCtx.setTags(tags);
     loggingCtx.forceLogging(forceLogging);
+    loggingCtx.performanceLogging(performanceLogging);
+
+    // For the performance log records use the {@link MutablePerformanceLogRecords} instance from
+    // the logging context of the calling thread in the logging context of the new thread. This way
+    // performance log records that are created from the new thread are available from the logging
+    // context of the calling thread. This is important since performance log records are processed
+    // only at the end of the request and performance log records that are created in another thread
+    // should not get lost.
+    loggingCtx.setMutablePerformanceLogRecords(mutablePerformanceLogRecords);
     try {
       runnable.run();
     } finally {
-      loggingCtx.setTags(oldTags);
-      loggingCtx.forceLogging(oldForceLogging);
+      // Cleanup logging context. This is important if the thread is pooled and reused.
+      loggingCtx.clear();
     }
   }
 }
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
new file mode 100644
index 0000000..7af204e
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -0,0 +1,339 @@
+// 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.logging;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.LazyArg;
+import com.google.common.flogger.LazyArgs;
+import com.google.gerrit.common.Nullable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Optional;
+
+/** Metadata that is provided to {@link PerformanceLogger}s as context for performance records. */
+@AutoValue
+public abstract class Metadata {
+  // The numeric ID of an account.
+  public abstract Optional<Integer> accountId();
+
+  // The type of an action (ACCOUNT_UPDATE, CHANGE_UPDATE, GROUP_UPDATE, INDEX_QUERY,
+  // PLUGIN_UPDATE).
+  public abstract Optional<String> actionType();
+
+  // An authentication domain name.
+  public abstract Optional<String> authDomainName();
+
+  // The name of a branch.
+  public abstract Optional<String> branchName();
+
+  // Key of an entity in a cache.
+  public abstract Optional<String> cacheKey();
+
+  // The name of a cache.
+  public abstract Optional<String> cacheName();
+
+  // The name of the implementation class.
+  public abstract Optional<String> className();
+
+  // The numeric ID of a change.
+  public abstract Optional<Integer> changeId();
+
+  // The type of change ID which the user used to identify a change (e.g. numeric ID, triplet etc.).
+  public abstract Optional<String> changeIdType();
+
+  // The type of an event.
+  public abstract Optional<String> eventType();
+
+  // The value of the @Export annotation which was used to register a plugin extension.
+  public abstract Optional<String> exportValue();
+
+  // Path of a file in a repository.
+  public abstract Optional<String> filePath();
+
+  // Garbage collector name.
+  public abstract Optional<String> garbageCollectorName();
+
+  // Git operation (CLONE, FETCH).
+  public abstract Optional<String> gitOperation();
+
+  // The numeric ID of an internal group.
+  public abstract Optional<Integer> groupId();
+
+  // The name of a group.
+  public abstract Optional<String> groupName();
+
+  // The UUID of a group.
+  public abstract Optional<String> groupUuid();
+
+  // HTTP status response code.
+  public abstract Optional<Integer> httpStatus();
+
+  // The name of a secondary index.
+  public abstract Optional<String> indexName();
+
+  // The version of a secondary index.
+  public abstract Optional<Integer> indexVersion();
+
+  // The name of the implementation method.
+  public abstract Optional<String> methodName();
+
+  // One or more resources
+  public abstract Optional<Boolean> multiple();
+
+  // The name of an operation that is performed.
+  public abstract Optional<String> operationName();
+
+  // Partial or full computation
+  public abstract Optional<Boolean> partial();
+
+  // Path of a metadata file in NoteDb.
+  public abstract Optional<String> noteDbFilePath();
+
+  // Name of a metadata ref in NoteDb.
+  public abstract Optional<String> noteDbRefName();
+
+  // Type of a sequence in NoteDb (ACCOUNTS, CHANGES, GROUPS).
+  public abstract Optional<String> noteDbSequenceType();
+
+  // Name of a "table" in NoteDb (if set, always CHANGES).
+  public abstract Optional<String> noteDbTable();
+
+  // The ID of a patch set.
+  public abstract Optional<Integer> patchSetId();
+
+  // Plugin metadata that doesn't fit into any other category.
+  public abstract ImmutableList<PluginMetadata> pluginMetadata();
+
+  // The name of a plugin.
+  public abstract Optional<String> pluginName();
+
+  // The name of a Gerrit project (aka Git repository).
+  public abstract Optional<String> projectName();
+
+  // The type of a Git push to Gerrit (CREATE_REPLACE, NORMAL, AUTOCLOSE).
+  public abstract Optional<String> pushType();
+
+  // The number of resources that is processed.
+  public abstract Optional<Integer> resourceCount();
+
+  // The name of a REST view.
+  public abstract Optional<String> restViewName();
+
+  // The SHA1 of Git commit.
+  public abstract Optional<String> revision();
+
+  // The username of an account.
+  public abstract Optional<String> username();
+
+  /**
+   * Returns a string representation of this instance that is suitable for logging. This is wrapped
+   * in a {@link LazyArg} because it is expensive to evaluate.
+   *
+   * <p>{@link #toString()} formats the {@link Optional} fields as {@code key=Optional[value]} or
+   * {@code key=Optional.empty}. Since this class has many optional fields from which usually only a
+   * few are populated this leads to long string representations such as
+   *
+   * <pre>
+   * Metadata{accountId=Optional.empty, actionType=Optional.empty, authDomainName=Optional.empty,
+   * branchName=Optional.empty, cacheKey=Optional.empty, cacheName=Optional.empty,
+   * className=Optional.empty, changeId=Optional[9212550], changeIdType=Optional.empty,
+   * eventType=Optional.empty, exportValue=Optional.empty, filePath=Optional.empty,
+   * garbageCollectorName=Optional.empty, gitOperation=Optional.empty, groupId=Optional.empty,
+   * groupName=Optional.empty, groupUuid=Optional.empty, httpStatus=Optional.empty,
+   * indexName=Optional.empty, indexVersion=Optional[0], methodName=Optional.empty,
+   * multiple=Optional.empty, operationName=Optional.empty, partial=Optional.empty,
+   * noteDbFilePath=Optional.empty, noteDbRefName=Optional.empty,
+   * noteDbSequenceType=Optional.empty, noteDbTable=Optional.empty, patchSetId=Optional.empty,
+   * pluginMetadata=[], pluginName=Optional.empty, projectName=Optional.empty,
+   * pushType=Optional.empty, resourceCount=Optional.empty, restViewName=Optional.empty,
+   * revision=Optional.empty, username=Optional.empty}
+   * </pre>
+   *
+   * <p>That's hard to read in logs. This is why this method
+   *
+   * <ul>
+   *   <li>drops fields which have {@code Optional.empty} as value and
+   *   <li>reformats values that are {@code Optional[value]} to {@code value}.
+   * </ul>
+   *
+   * <p>For the example given above the formatted string would look like this:
+   *
+   * <pre>
+   * Metadata{changeId=9212550, indexVersion=0, pluginMetadata=[]}
+   * </pre>
+   *
+   * @return string representation of this instance that is suitable for logging
+   */
+  LazyArg<String> toStringForLoggingLazy() {
+    // Don't use a lambda because different compilers generate different method names for lambdas,
+    // e.g. "lambda$myFunction$0" vs. just "lambda$0" in Eclipse. We need to identify the method
+    // by name to skip it and avoid infinite recursion.
+    return LazyArgs.lazy(this::toStringForLoggingImpl);
+  }
+
+  private String toStringForLoggingImpl() {
+    // Append class name.
+    String className = getClass().getSimpleName();
+    if (className.startsWith("AutoValue_")) {
+      className = className.substring(10);
+    }
+    ToStringHelper stringHelper = MoreObjects.toStringHelper(className);
+
+    // Append key-value pairs for field which are set.
+    Method[] methods = Metadata.class.getDeclaredMethods();
+    Arrays.sort(methods, Comparator.comparing(Method::getName));
+    for (Method method : methods) {
+      if (Modifier.isStatic(method.getModifiers())) {
+        // skip static method
+        continue;
+      }
+
+      if (method.getName().equals("toStringForLoggingLazy")
+          || method.getName().equals("toStringForLoggingImpl")) {
+        // Don't call myself in infinite recursion.
+        continue;
+      }
+
+      if (method.getReturnType().equals(Void.TYPE) || method.getParameterCount() > 0) {
+        // skip method since it's not a getter
+        continue;
+      }
+
+      method.setAccessible(true);
+
+      Object returnValue;
+      try {
+        returnValue = method.invoke(this);
+      } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
+        // should never happen
+        throw new IllegalStateException(e);
+      }
+
+      if (returnValue instanceof Optional) {
+        Optional<?> fieldValueOptional = (Optional<?>) returnValue;
+        if (!fieldValueOptional.isPresent()) {
+          // drop this key-value pair
+          continue;
+        }
+
+        // format as 'key=value' instead of 'key=Optional[value]'
+        stringHelper.add(method.getName(), fieldValueOptional.get());
+      } else {
+        // not an Optional value, keep as is
+        stringHelper.add(method.getName(), returnValue);
+      }
+    }
+
+    return stringHelper.toString();
+  }
+
+  public static Metadata.Builder builder() {
+    return new AutoValue_Metadata.Builder();
+  }
+
+  public static Metadata empty() {
+    return builder().build();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder accountId(int accountId);
+
+    public abstract Builder actionType(@Nullable String actionType);
+
+    public abstract Builder authDomainName(@Nullable String authDomainName);
+
+    public abstract Builder branchName(@Nullable String branchName);
+
+    public abstract Builder cacheKey(@Nullable String cacheKey);
+
+    public abstract Builder cacheName(@Nullable String cacheName);
+
+    public abstract Builder className(@Nullable String className);
+
+    public abstract Builder changeId(int changeId);
+
+    public abstract Builder changeIdType(@Nullable String changeIdType);
+
+    public abstract Builder eventType(@Nullable String eventType);
+
+    public abstract Builder exportValue(@Nullable String exportValue);
+
+    public abstract Builder filePath(@Nullable String filePath);
+
+    public abstract Builder garbageCollectorName(@Nullable String garbageCollectorName);
+
+    public abstract Builder gitOperation(@Nullable String gitOperation);
+
+    public abstract Builder groupId(int groupId);
+
+    public abstract Builder groupName(@Nullable String groupName);
+
+    public abstract Builder groupUuid(@Nullable String groupUuid);
+
+    public abstract Builder httpStatus(int httpStatus);
+
+    public abstract Builder indexName(@Nullable String indexName);
+
+    public abstract Builder indexVersion(int indexVersion);
+
+    public abstract Builder methodName(@Nullable String methodName);
+
+    public abstract Builder multiple(boolean multiple);
+
+    public abstract Builder operationName(String operationName);
+
+    public abstract Builder partial(boolean partial);
+
+    public abstract Builder noteDbFilePath(@Nullable String noteDbFilePath);
+
+    public abstract Builder noteDbRefName(@Nullable String noteDbRefName);
+
+    public abstract Builder noteDbSequenceType(@Nullable String noteDbSequenceType);
+
+    public abstract Builder noteDbTable(@Nullable String noteDbTable);
+
+    public abstract Builder patchSetId(int patchSetId);
+
+    abstract ImmutableList.Builder<PluginMetadata> pluginMetadataBuilder();
+
+    public Builder addPluginMetadata(PluginMetadata pluginMetadata) {
+      pluginMetadataBuilder().add(pluginMetadata);
+      return this;
+    }
+
+    public abstract Builder pluginName(@Nullable String pluginName);
+
+    public abstract Builder projectName(@Nullable String projectName);
+
+    public abstract Builder pushType(@Nullable String pushType);
+
+    public abstract Builder resourceCount(int resourceCount);
+
+    public abstract Builder restViewName(@Nullable String restViewName);
+
+    public abstract Builder revision(@Nullable String revision);
+
+    public abstract Builder username(@Nullable String username);
+
+    public abstract Metadata build();
+  }
+}
diff --git a/java/com/google/gerrit/server/logging/MutablePerformanceLogRecords.java b/java/com/google/gerrit/server/logging/MutablePerformanceLogRecords.java
new file mode 100644
index 0000000..4ee70d7
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/MutablePerformanceLogRecords.java
@@ -0,0 +1,55 @@
+// 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.
+
+package com.google.gerrit.server.logging;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Thread-safe store for performance log records.
+ *
+ * <p>This class is intended to keep track of performance log records in {@link LoggingContext}. It
+ * needs to be thread-safe because it gets shared between threads when the logging context is copied
+ * to another thread (see {@link LoggingContextAwareRunnable} and {@link
+ * LoggingContextAwareCallable}. In this case the logging contexts of both threads share the same
+ * instance of this class. This is important since performance log records are processed only at the
+ * end of a request and performance log records that are created in another thread should not get
+ * lost.
+ */
+public class MutablePerformanceLogRecords {
+  private final ArrayList<PerformanceLogRecord> performanceLogRecords = new ArrayList<>();
+
+  public synchronized void add(PerformanceLogRecord record) {
+    performanceLogRecords.add(record);
+  }
+
+  public synchronized void set(List<PerformanceLogRecord> records) {
+    performanceLogRecords.clear();
+    performanceLogRecords.addAll(records);
+  }
+
+  public synchronized ImmutableList<PerformanceLogRecord> list() {
+    return ImmutableList.copyOf(performanceLogRecords);
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("performanceLogRecords", performanceLogRecords)
+        .toString();
+  }
+}
diff --git a/java/com/google/gerrit/server/logging/MutableTags.java b/java/com/google/gerrit/server/logging/MutableTags.java
index f70a8db..83009a6 100644
--- a/java/com/google/gerrit/server/logging/MutableTags.java
+++ b/java/com/google/gerrit/server/logging/MutableTags.java
@@ -16,6 +16,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
@@ -110,4 +111,10 @@
     tagMap.forEach(tagsBuilder::addTag);
     tags = tagsBuilder.build();
   }
+
+  @Override
+  public String toString() {
+    buildTags();
+    return MoreObjects.toStringHelper(this).add("tags", tags).toString();
+  }
 }
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogContext.java b/java/com/google/gerrit/server/logging/PerformanceLogContext.java
new file mode 100644
index 0000000..b6dafdc
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/PerformanceLogContext.java
@@ -0,0 +1,117 @@
+// 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.logging;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.Extension;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Context for capturing performance log records. When the context is closed the performance log
+ * records are handed over to the registered {@link PerformanceLogger}s.
+ *
+ * <p>Capturing performance log records is disabled if there are no {@link PerformanceLogger}
+ * registered (in this case the captured performance log records would never be used).
+ *
+ * <p>It's important to enable capturing of performance log records in a context that ensures to
+ * consume the captured performance log records. Otherwise captured performance log records might
+ * leak into other requests that are executed by the same thread (if a thread pool is used to
+ * process requests).
+ */
+public class PerformanceLogContext implements AutoCloseable {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  // Do not use PluginSetContext. PluginSetContext traces the plugin latency with a timer metric
+  // which would result in a performance log and we don't want to log the performance of writing
+  // a performance log in the performance log (endless loop).
+  private final DynamicSet<PerformanceLogger> performanceLoggers;
+
+  private final boolean oldPerformanceLogging;
+  private final ImmutableList<PerformanceLogRecord> oldPerformanceLogRecords;
+
+  public PerformanceLogContext(
+      Config gerritConfig, DynamicSet<PerformanceLogger> performanceLoggers) {
+    this.performanceLoggers = performanceLoggers;
+
+    // Just in case remember the old state and reset performance log entries.
+    this.oldPerformanceLogging = LoggingContext.getInstance().isPerformanceLogging();
+    this.oldPerformanceLogRecords = LoggingContext.getInstance().getPerformanceLogRecords();
+    LoggingContext.getInstance().clearPerformanceLogEntries();
+
+    // Do not create performance log entries if performance logging is disabled or if no
+    // PerformanceLogger is registered.
+    boolean enablePerformanceLogging =
+        gerritConfig.getBoolean("tracing", "performanceLogging", true);
+    LoggingContext.getInstance()
+        .performanceLogging(
+            enablePerformanceLogging && !Iterables.isEmpty(performanceLoggers.entries()));
+  }
+
+  @Override
+  public void close() {
+    if (LoggingContext.getInstance().isPerformanceLogging()) {
+      runEach(performanceLoggers, LoggingContext.getInstance().getPerformanceLogRecords());
+    }
+
+    // Restore old state. Required to support nesting of PerformanceLogContext's.
+    LoggingContext.getInstance().performanceLogging(oldPerformanceLogging);
+    LoggingContext.getInstance().setPerformanceLogRecords(oldPerformanceLogRecords);
+  }
+
+  /**
+   * Invokes all performance loggers.
+   *
+   * <p>Similar to how {@code com.google.gerrit.server.plugincontext.PluginContext} invokes plugins
+   * but without recording metrics for invoking {@link PerformanceLogger}s.
+   *
+   * @param performanceLoggers the performance loggers that should be invoked
+   * @param performanceLogRecords the performance log records that should be handed over to the
+   *     performance loggers
+   */
+  private static void runEach(
+      DynamicSet<PerformanceLogger> performanceLoggers,
+      ImmutableList<PerformanceLogRecord> performanceLogRecords) {
+    performanceLoggers
+        .entries()
+        .forEach(
+            p -> {
+              try (TraceContext traceContext = newPluginTrace(p)) {
+                performanceLogRecords.forEach(r -> r.writeTo(p.get()));
+              } catch (Throwable e) {
+                logger.atWarning().withCause(e).log(
+                    "Failure in %s of plugin %s", p.get().getClass(), p.getPluginName());
+              }
+            });
+  }
+
+  /**
+   * Opens a trace context for a plugin that implements {@link PerformanceLogger}.
+   *
+   * <p>Basically the same as {@code
+   * com.google.gerrit.server.plugincontext.PluginContext#newTrace(Extension<T>)}. We have this
+   * method here to avoid a dependency on PluginContext which lives in
+   * "//java/com/google/gerrit/server". This package ("//java/com/google/gerrit/server/logging")
+   * should have as few dependencies as possible.
+   *
+   * @param extension performance logger extension
+   * @return the trace context
+   */
+  private static TraceContext newPluginTrace(Extension<PerformanceLogger> extension) {
+    return TraceContext.open().addPluginTag(extension.getPluginName());
+  }
+}
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogRecord.java b/java/com/google/gerrit/server/logging/PerformanceLogRecord.java
new file mode 100644
index 0000000..046eeb3
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/PerformanceLogRecord.java
@@ -0,0 +1,64 @@
+// 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.logging;
+
+import com.google.auto.value.AutoValue;
+import java.util.Optional;
+
+/**
+ * The record of an operation for which the execution time was measured.
+ *
+ * <p>Metadata to provide additional context can be included by providing a {@link Metadata}
+ * instance.
+ */
+@AutoValue
+public abstract class PerformanceLogRecord {
+  /**
+   * Creates a performance log record without meta data.
+   *
+   * @param operation the name of operation the is was performed
+   * @param durationMs the execution time in milliseconds
+   * @return the performance log record
+   */
+  public static PerformanceLogRecord create(String operation, long durationMs) {
+    return new AutoValue_PerformanceLogRecord(operation, durationMs, Optional.empty());
+  }
+
+  /**
+   * Creates a performance log record with meta data.
+   *
+   * @param operation the name of operation the is was performed
+   * @param durationMs the execution time in milliseconds
+   * @param metadata metadata
+   * @return the performance log record
+   */
+  public static PerformanceLogRecord create(String operation, long durationMs, Metadata metadata) {
+    return new AutoValue_PerformanceLogRecord(operation, durationMs, Optional.of(metadata));
+  }
+
+  public abstract String operation();
+
+  public abstract long durationMs();
+
+  public abstract Optional<Metadata> metadata();
+
+  void writeTo(PerformanceLogger performanceLogger) {
+    if (metadata().isPresent()) {
+      performanceLogger.log(operation(), durationMs(), metadata().get());
+    } else {
+      performanceLogger.log(operation(), durationMs());
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogger.java b/java/com/google/gerrit/server/logging/PerformanceLogger.java
new file mode 100644
index 0000000..74a1684
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/PerformanceLogger.java
@@ -0,0 +1,50 @@
+// 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.logging;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Extension point for logging performance records.
+ *
+ * <p>This extension point is invoked for all operations for which the execution time is measured.
+ * The invocation of the extension point does not happen immediately, but only at the end of a
+ * request (REST call, SSH call, git push). Implementors can write the execution times into a
+ * performance log for further analysis.
+ *
+ * <p>For optimal performance implementors should overwrite the default <code>log</code> methods to
+ * avoid an unneeded instantiation of Metadata.
+ */
+@ExtensionPoint
+public interface PerformanceLogger {
+  /**
+   * Record the execution time of an operation in a performance log.
+   *
+   * @param operation operation that was performed
+   * @param durationMs time that the execution of the operation took (in milliseconds)
+   */
+  default void log(String operation, long durationMs) {
+    log(operation, durationMs, Metadata.empty());
+  }
+
+  /**
+   * Record the execution time of an operation in a performance log.
+   *
+   * @param operation operation that was performed
+   * @param durationMs time that the execution of the operation took (in milliseconds)
+   * @param metadata metadata
+   */
+  void log(String operation, long durationMs, Metadata metadata);
+}
diff --git a/java/com/google/gerrit/server/logging/PluginMetadata.java b/java/com/google/gerrit/server/logging/PluginMetadata.java
new file mode 100644
index 0000000..21f7359
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/PluginMetadata.java
@@ -0,0 +1,39 @@
+// 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.logging;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
+
+/**
+ * Key-value pair for custom metadata that is provided by plugins.
+ *
+ * <p>PluginMetadata allows plugins to include custom metadata into the {@link Metadata} instances
+ * that are provided as context for performance tracing.
+ *
+ * <p>Plugins should use PluginMetadata only for metadata kinds that are not known to Gerrit core
+ * (metadata for which {@link Metadata} doesn't have a dedicated field).
+ */
+@AutoValue
+public abstract class PluginMetadata {
+  public static PluginMetadata create(String key, @Nullable String value) {
+    return new AutoValue_PluginMetadata(key, Optional.ofNullable(value));
+  }
+
+  public abstract String key();
+
+  public abstract Optional<String> value();
+}
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
index 5c0406d..21a4ce6 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -19,6 +19,7 @@
 import com.google.common.base.Stopwatch;
 import com.google.common.base.Strings;
 import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Table;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -155,116 +156,66 @@
   /**
    * Opens a new timer that logs the time for an operation if request tracing is enabled.
    *
-   * <p>If request tracing is not enabled this is a no-op.
-   *
-   * @param message the message
+   * @param operation the name of operation the is being performed
    * @return the trace timer
    */
-  public static TraceTimer newTimer(String message) {
-    return new TraceTimer(message);
+  public static TraceTimer newTimer(String operation) {
+    return new TraceTimer(requireNonNull(operation, "operation is required"));
   }
 
   /**
    * Opens a new timer that logs the time for an operation if request tracing is enabled.
    *
-   * <p>If request tracing is not enabled this is a no-op.
-   *
-   * @param format the message format string
-   * @param arg argument for the message
+   * @param operation the name of operation the is being performed
+   * @param metadata metadata
    * @return the trace timer
    */
-  public static TraceTimer newTimer(String format, Object arg) {
-    return new TraceTimer(format, arg);
-  }
-
-  /**
-   * Opens a new timer that logs the time for an operation if request tracing is enabled.
-   *
-   * <p>If request tracing is not enabled this is a no-op.
-   *
-   * @param format the message format string
-   * @param arg1 first argument for the message
-   * @param arg2 second argument for the message
-   * @return the trace timer
-   */
-  public static TraceTimer newTimer(String format, Object arg1, Object arg2) {
-    return new TraceTimer(format, arg1, arg2);
-  }
-
-  /**
-   * Opens a new timer that logs the time for an operation if request tracing is enabled.
-   *
-   * <p>If request tracing is not enabled this is a no-op.
-   *
-   * @param format the message format string
-   * @param arg1 first argument for the message
-   * @param arg2 second argument for the message
-   * @param arg3 third argument for the message
-   * @return the trace timer
-   */
-  public static TraceTimer newTimer(String format, Object arg1, Object arg2, Object arg3) {
-    return new TraceTimer(format, arg1, arg2, arg3);
-  }
-
-  /**
-   * Opens a new timer that logs the time for an operation if request tracing is enabled.
-   *
-   * <p>If request tracing is not enabled this is a no-op.
-   *
-   * @param format the message format string
-   * @param arg1 first argument for the message
-   * @param arg2 second argument for the message
-   * @param arg3 third argument for the message
-   * @param arg4 fourth argument for the message
-   * @return the trace timer
-   */
-  public static TraceTimer newTimer(
-      String format, Object arg1, Object arg2, Object arg3, Object arg4) {
-    return new TraceTimer(format, arg1, arg2, arg3, arg4);
+  public static TraceTimer newTimer(String operation, Metadata metadata) {
+    return new TraceTimer(
+        requireNonNull(operation, "operation is required"),
+        requireNonNull(metadata, "metadata is required"));
   }
 
   public static class TraceTimer implements AutoCloseable {
     private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-    private final Consumer<Long> logFn;
+    private final Consumer<Long> doneLogFn;
     private final Stopwatch stopwatch;
 
-    private TraceTimer(String message) {
-      this(elapsedMs -> logger.atFine().log(message + " (%d ms)", elapsedMs));
-    }
-
-    private TraceTimer(String format, @Nullable Object arg) {
-      this(elapsedMs -> logger.atFine().log(format + " (%d ms)", arg, elapsedMs));
-    }
-
-    private TraceTimer(String format, @Nullable Object arg1, @Nullable Object arg2) {
-      this(elapsedMs -> logger.atFine().log(format + " (%d ms)", arg1, arg2, elapsedMs));
-    }
-
-    private TraceTimer(
-        String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
-      this(elapsedMs -> logger.atFine().log(format + " (%d ms)", arg1, arg2, arg3, elapsedMs));
-    }
-
-    private TraceTimer(
-        String format,
-        @Nullable Object arg1,
-        @Nullable Object arg2,
-        @Nullable Object arg3,
-        @Nullable Object arg4) {
+    private TraceTimer(String operation) {
       this(
-          elapsedMs -> logger.atFine().log(format + " (%d ms)", arg1, arg2, arg3, arg4, elapsedMs));
+          () -> logger.atFine().log("Starting timer for %s", operation),
+          elapsedMs -> {
+            LoggingContext.getInstance()
+                .addPerformanceLogRecord(() -> PerformanceLogRecord.create(operation, elapsedMs));
+            logger.atFine().log("%s done (%d ms)", operation, elapsedMs);
+          });
     }
 
-    private TraceTimer(Consumer<Long> logFn) {
-      this.logFn = logFn;
+    private TraceTimer(String operation, Metadata metadata) {
+      this(
+          () ->
+              logger.atFine().log(
+                  "Starting timer for %s (%s)", operation, metadata.toStringForLoggingLazy()),
+          elapsedMs -> {
+            LoggingContext.getInstance()
+                .addPerformanceLogRecord(
+                    () -> PerformanceLogRecord.create(operation, elapsedMs, metadata));
+            logger.atFine().log(
+                "%s (%s) done (%d ms)", operation, metadata.toStringForLoggingLazy(), elapsedMs);
+          });
+    }
+
+    private TraceTimer(Runnable startLogFn, Consumer<Long> doneLogFn) {
+      startLogFn.run();
+      this.doneLogFn = doneLogFn;
       this.stopwatch = Stopwatch.createStarted();
     }
 
     @Override
     public void close() {
       stopwatch.stop();
-      logFn.accept(stopwatch.elapsed(TimeUnit.MILLISECONDS));
+      doneLogFn.accept(stopwatch.elapsed(TimeUnit.MILLISECONDS));
     }
   }
 
@@ -286,6 +237,12 @@
     return this;
   }
 
+  public ImmutableMap<String, String> getTags() {
+    ImmutableMap.Builder<String, String> tagMap = ImmutableMap.builder();
+    tags.cellSet().forEach(c -> tagMap.put(c.getRowKey(), c.getColumnKey()));
+    return tagMap.build();
+  }
+
   public TraceContext addPluginTag(String pluginName) {
     return addTag(PLUGIN_TAG, pluginName);
   }
@@ -299,6 +256,15 @@
     return this;
   }
 
+  public boolean isTracing() {
+    return LoggingContext.getInstance().isLoggingForced();
+  }
+
+  public Optional<String> getTraceId() {
+    return LoggingContext.getInstance().getTagsAsMap().get(RequestId.Type.TRACE_ID.name()).stream()
+        .findFirst();
+  }
+
   @Override
   public void close() {
     for (Table.Cell<String, String, Boolean> cell : tags.cellSet()) {
diff --git a/java/com/google/gerrit/server/mail/EmailTokenVerifier.java b/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
index 5b5f33d..2ff5fc3 100644
--- a/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
+++ b/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
 
diff --git a/java/com/google/gerrit/server/mail/MailUtil.java b/java/com/google/gerrit/server/mail/MailUtil.java
index 4afcc7b..1040a55 100644
--- a/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/java/com/google/gerrit/server/mail/MailUtil.java
@@ -18,8 +18,8 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 
 import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.account.AccountResolver;
 import java.io.IOException;
@@ -62,7 +62,7 @@
   @SuppressWarnings("deprecation")
   private static Account.Id toAccountId(AccountResolver accountResolver, String nameOrEmail)
       throws UnprocessableEntityException, IOException, ConfigInvalidException {
-    return accountResolver.resolveByNameOrEmail(nameOrEmail).asUnique().getAccount().getId();
+    return accountResolver.resolveByNameOrEmail(nameOrEmail).asUnique().account().id();
   }
 
   private static boolean isReviewer(FooterLine candidateFooterLine) {
diff --git a/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
index 897a5f2..77be665 100644
--- a/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
+++ b/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -18,7 +18,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.io.BaseEncoding;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
 import com.google.inject.AbstractModule;
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index b1acab9..158db1c 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -18,7 +18,14 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -26,23 +33,20 @@
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentValidationFailure;
+import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.mail.HtmlParser;
 import com.google.gerrit.mail.MailComment;
 import com.google.gerrit.mail.MailHeaderParser;
 import com.google.gerrit.mail.MailMessage;
 import com.google.gerrit.mail.MailMetadata;
 import com.google.gerrit.mail.TextParser;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.PublishCommentUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Emails;
@@ -54,6 +58,7 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -83,6 +88,15 @@
 public class MailProcessor {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private static final ImmutableMap<MailComment.CommentType, CommentForValidation.CommentType>
+      MAIL_COMMENT_TYPE_TO_VALIDATION_TYPE =
+          ImmutableMap.of(
+              MailComment.CommentType.CHANGE_MESSAGE,
+                  CommentForValidation.CommentType.CHANGE_MESSAGE,
+              MailComment.CommentType.FILE_COMMENT, CommentForValidation.CommentType.FILE_COMMENT,
+              MailComment.CommentType.INLINE_COMMENT,
+                  CommentForValidation.CommentType.INLINE_COMMENT);
+
   private final Emails emails;
   private final InboundEmailRejectionSender.Factory emailRejectionSender;
   private final RetryHelper retryHelper;
@@ -98,6 +112,7 @@
   private final ApprovalsUtil approvalsUtil;
   private final AccountCache accountCache;
   private final DynamicItem<UrlFormatter> urlFormatter;
+  private final PluginSetContext<CommentValidator> commentValidators;
 
   @Inject
   public MailProcessor(
@@ -115,7 +130,8 @@
       ApprovalsUtil approvalsUtil,
       CommentAdded commentAdded,
       AccountCache accountCache,
-      DynamicItem<UrlFormatter> urlFormatter) {
+      DynamicItem<UrlFormatter> urlFormatter,
+      PluginSetContext<CommentValidator> commentValidators) {
     this.emails = emails;
     this.emailRejectionSender = emailRejectionSender;
     this.retryHelper = retryHelper;
@@ -131,6 +147,7 @@
     this.approvalsUtil = approvalsUtil;
     this.accountCache = accountCache;
     this.urlFormatter = urlFormatter;
+    this.commentValidators = commentValidators;
   }
 
   /**
@@ -187,7 +204,7 @@
       logger.atWarning().log("Mail: Account %s doesn't exist. Will delete message.", accountId);
       return;
     }
-    if (!accountState.get().getAccount().isActive()) {
+    if (!accountState.get().account().isActive()) {
       logger.atWarning().log("Mail: Account %s is inactive. Will delete message.", accountId);
       sendRejectionEmail(message, InboundEmailRejectionSender.Error.INACTIVE_ACCOUNT);
       return;
@@ -211,7 +228,7 @@
       throws UpdateException, RestApiException {
     try (ManualRequestContext ctx = oneOffRequestContext.openAs(sender)) {
       List<ChangeData> changeDataList =
-          queryProvider.get().byLegacyChangeId(new Change.Id(metadata.changeNumber));
+          queryProvider.get().byLegacyChangeId(Change.id(metadata.changeNumber));
       if (changeDataList.size() != 1) {
         logger.atSevere().log(
             "Message %s references unique change %s,"
@@ -259,7 +276,22 @@
         return;
       }
 
-      Op o = new Op(new PatchSet.Id(cd.getId(), metadata.patchSet), parsedComments, message.id());
+      ImmutableList<CommentForValidation> parsedCommentsForValidation =
+          parsedComments.stream()
+              .map(
+                  comment ->
+                      CommentForValidation.create(
+                          MAIL_COMMENT_TYPE_TO_VALIDATION_TYPE.get(comment.getType()),
+                          comment.getMessage()))
+              .collect(ImmutableList.toImmutableList());
+      ImmutableList<CommentValidationFailure> commentValidationFailures =
+          PublishCommentUtil.findInvalidComments(commentValidators, parsedCommentsForValidation);
+      if (!commentValidationFailures.isEmpty()) {
+        sendRejectionEmail(message, InboundEmailRejectionSender.Error.COMMENT_REJECTED);
+        return;
+      }
+
+      Op o = new Op(PatchSet.id(cd.getId(), metadata.patchSet), parsedComments, message.id());
       BatchUpdate batchUpdate = buf.create(project, ctx.getUser(), TimeUtil.nowTs());
       batchUpdate.addOp(cd.getId(), o);
       batchUpdate.execute();
@@ -302,7 +334,7 @@
             persistentCommentFromMailComment(ctx, c, targetPatchSetForComment(ctx, c, patchSet)));
       }
       commentsUtil.putComments(
-          ctx.getUpdate(ctx.getChange().currentPatchSetId()), Status.PUBLISHED, comments);
+          ctx.getUpdate(ctx.getChange().currentPatchSetId()), Comment.Status.PUBLISHED, comments);
 
       return true;
     }
@@ -330,7 +362,7 @@
       approvalsUtil
           .byPatchSetUser(
               notes, psId, ctx.getAccountId(), ctx.getRevWalk(), ctx.getRepoView().getConfig())
-          .forEach(a -> approvals.put(a.getLabel(), a.getValue()));
+          .forEach(a -> approvals.put(a.label(), a.value()));
       // Fire Gerrit event. Note that approvals can't be granted via email, so old and new approvals
       // are always the same here.
       commentAdded.fire(
@@ -362,7 +394,7 @@
       if (mailComment.getInReplyTo() != null) {
         return psUtil.get(
             ctx.getNotes(),
-            new PatchSet.Id(ctx.getChange().getId(), mailComment.getInReplyTo().key.patchSetId));
+            PatchSet.id(ctx.getChange().getId(), mailComment.getInReplyTo().key.patchSetId));
       }
       return current;
     }
@@ -386,7 +418,7 @@
           commentsUtil.newComment(
               ctx,
               fileName,
-              patchSetForComment.getId(),
+              patchSetForComment.id(),
               (short) side.ordinal(),
               mailComment.getMessage(),
               false,
@@ -399,7 +431,7 @@
         comment.range = mailComment.getInReplyTo().range;
         comment.unresolved = mailComment.getInReplyTo().unresolved;
       }
-      CommentsUtil.setCommentRevId(comment, patchListCache, ctx.getChange(), patchSetForComment);
+      CommentsUtil.setCommentCommitId(comment, patchListCache, ctx.getChange(), patchSetForComment);
       return comment;
     }
   }
diff --git a/java/com/google/gerrit/server/mail/send/AbandonedSender.java b/java/com/google/gerrit/server/mail/send/AbandonedSender.java
index 1017965..a6fb4de 100644
--- a/java/com/google/gerrit/server/mail/send/AbandonedSender.java
+++ b/java/com/google/gerrit/server/mail/send/AbandonedSender.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -25,13 +25,13 @@
 public class AbandonedSender extends ReplyToChangeSender {
   public interface Factory extends ReplyToChangeSender.Factory<AbandonedSender> {
     @Override
-    AbandonedSender create(Project.NameKey project, Change.Id change);
+    AbandonedSender create(Project.NameKey project, Change.Id changeId);
   }
 
   @Inject
   public AbandonedSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
-    super(ea, "abandon", ChangeEmail.newChangeData(ea, project, id));
+      EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+    super(args, "abandon", ChangeEmail.newChangeData(args, project, changeId));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index 6a4918c..8b3d3f7 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -37,8 +37,8 @@
 
   @AssistedInject
   public AddKeySender(
-      EmailArguments ea, @Assisted IdentifiedUser user, @Assisted AccountSshKey sshKey) {
-    super(ea, "addkey");
+      EmailArguments args, @Assisted IdentifiedUser user, @Assisted AccountSshKey sshKey) {
+    super(args, "addkey");
     this.user = user;
     this.sshKey = sshKey;
     this.gpgKeys = null;
@@ -46,8 +46,8 @@
 
   @AssistedInject
   public AddKeySender(
-      EmailArguments ea, @Assisted IdentifiedUser user, @Assisted List<String> gpgKeys) {
-    super(ea, "addkey");
+      EmailArguments args, @Assisted IdentifiedUser user, @Assisted List<String> gpgKeys) {
+    super(args, "addkey");
     this.user = user;
     this.sshKey = null;
     this.gpgKeys = gpgKeys;
@@ -79,7 +79,7 @@
   }
 
   public String getEmail() {
-    return user.getAccount().getPreferredEmail();
+    return user.getAccount().preferredEmail();
   }
 
   public String getUserNameEmail() {
diff --git a/java/com/google/gerrit/server/mail/send/AddReviewerSender.java b/java/com/google/gerrit/server/mail/send/AddReviewerSender.java
index 22abd9c..96d9483 100644
--- a/java/com/google/gerrit/server/mail/send/AddReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/AddReviewerSender.java
@@ -14,22 +14,22 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 /** Asks a user to review a change. */
 public class AddReviewerSender extends NewChangeSender {
   public interface Factory {
-    AddReviewerSender create(Project.NameKey project, Change.Id id);
+    AddReviewerSender create(Project.NameKey project, Change.Id changeId);
   }
 
   @Inject
   public AddReviewerSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
-    super(ea, newChangeData(ea, project, id));
+      EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+    super(args, newChangeData(args, project, changeId));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index beb179c..0ff6072 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -20,18 +20,18 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetInfo;
+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.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.mail.MailHeader;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
@@ -84,11 +84,11 @@
   protected Set<Account.Id> authors;
   protected boolean emailOnlyAuthors;
 
-  protected ChangeEmail(EmailArguments ea, String mc, ChangeData cd) {
-    super(ea, mc, cd);
-    changeData = cd;
-    change = cd.change();
-    emailOnlyAuthors = false;
+  protected ChangeEmail(EmailArguments args, String messageClass, ChangeData changeData) {
+    super(args, messageClass, changeData);
+    this.changeData = changeData;
+    this.change = changeData.change();
+    this.emailOnlyAuthors = false;
   }
 
   @Override
@@ -156,10 +156,10 @@
     }
 
     if (patchSet != null) {
-      setHeader(MailHeader.PATCH_SET.fieldName(), patchSet.getPatchSetId() + "");
+      setHeader(MailHeader.PATCH_SET.fieldName(), patchSet.number() + "");
       if (patchSetInfo == null) {
         try {
-          patchSetInfo = args.patchSetInfoFactory.get(changeData.notes(), patchSet.getId());
+          patchSetInfo = args.patchSetInfoFactory.get(changeData.notes(), patchSet.id());
         } catch (PatchSetInfoNotAvailableException | StorageException err) {
           patchSetInfo = null;
         }
@@ -205,11 +205,8 @@
   }
 
   private void setCommitIdHeader() {
-    if (patchSet != null
-        && patchSet.getRevision() != null
-        && patchSet.getRevision().get() != null
-        && patchSet.getRevision().get().length() > 0) {
-      setHeader(MailHeader.COMMIT.fieldName(), patchSet.getRevision().get());
+    if (patchSet != null) {
+      setHeader(MailHeader.COMMIT.fieldName(), patchSet.commitId().name());
     }
   }
 
@@ -290,11 +287,11 @@
   /** Get the patch list corresponding to patch set patchSetId of this change. */
   protected PatchList getPatchList(int patchSetId) throws PatchListNotAvailableException {
     PatchSet ps;
-    if (patchSetId == patchSet.getPatchSetId()) {
+    if (patchSetId == patchSet.number()) {
       ps = patchSet;
     } else {
       try {
-        ps = args.patchSetUtil.get(changeData.notes(), new PatchSet.Id(change.getId(), patchSetId));
+        ps = args.patchSetUtil.get(changeData.notes(), PatchSet.id(change.getId(), patchSetId));
       } catch (StorageException e) {
         throw new PatchListNotAvailableException("Failed to get patchSet", e);
       }
@@ -338,7 +335,7 @@
   protected void removeUsersThatIgnoredTheChange() {
     for (Map.Entry<Account.Id, Collection<String>> e : stars.asMap().entrySet()) {
       if (e.getValue().contains(StarredChangesUtil.IGNORE_LABEL)) {
-        args.accountCache.get(e.getKey()).ifPresent(a -> removeUser(a.getAccount()));
+        args.accountCache.get(e.getKey()).ifPresent(a -> removeUser(a.account()));
       }
     }
   }
@@ -349,7 +346,7 @@
       return new Watchers();
     }
 
-    ProjectWatch watch = new ProjectWatch(args, branch.getParentKey(), projectState, changeData);
+    ProjectWatch watch = new ProjectWatch(args, branch.project(), projectState, changeData);
     return watch.getWatchers(type, includeWatchersFromNotifyConfig);
   }
 
@@ -415,7 +412,7 @@
       case ALL:
       default:
         if (patchSet != null) {
-          authors.add(patchSet.getUploader());
+          authors.add(patchSet.uploader());
         }
         if (patchSetInfo != null) {
           if (patchSetInfo.getAuthor().getAccount() != null) {
@@ -465,8 +462,8 @@
     soyContext.put("change", changeData);
 
     Map<String, Object> patchSetData = new HashMap<>();
-    patchSetData.put("patchSetId", patchSet.getPatchSetId());
-    patchSetData.put("refName", patchSet.getRefName());
+    patchSetData.put("patchSetId", patchSet.number());
+    patchSetData.put("refName", patchSet.refName());
     soyContext.put("patchSet", patchSetData);
 
     Map<String, Object> patchSetInfoData = new HashMap<>();
@@ -476,7 +473,7 @@
 
     footers.add(MailHeader.CHANGE_ID.withDelimiter() + change.getKey().get());
     footers.add(MailHeader.CHANGE_NUMBER.withDelimiter() + Integer.toString(change.getChangeId()));
-    footers.add(MailHeader.PATCH_SET.withDelimiter() + patchSet.getPatchSetId());
+    footers.add(MailHeader.PATCH_SET.withDelimiter() + patchSet.number());
     footers.add(MailHeader.OWNER.withDelimiter() + getNameEmailFor(change.getOwner()));
     if (change.getAssignee() != null) {
       footers.add(MailHeader.ASSIGNEE.withDelimiter() + getNameEmailFor(change.getAssignee()));
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 7b11ce6..48d342e 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -17,21 +17,21 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.Ordering;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.FilenameComparator;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.KeyUtil;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.exceptions.NoSuchEntityException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.mail.MailProcessingUtil;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.util.LabelVote;
-import com.google.gwtorm.client.KeyUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -55,7 +54,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
 import org.apache.james.mime4j.dom.field.FieldName;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
@@ -65,7 +63,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
-    CommentSender create(Project.NameKey project, Change.Id id);
+    CommentSender create(Project.NameKey project, Change.Id changeId);
   }
 
   private class FileCommentGroup {
@@ -113,12 +111,12 @@
 
   @Inject
   public CommentSender(
-      EmailArguments ea,
+      EmailArguments args,
       CommentsUtil commentsUtil,
       @GerritServerConfig Config cfg,
       @Assisted Project.NameKey project,
-      @Assisted Change.Id id) {
-    super(ea, "comment", newChangeData(ea, project, id));
+      @Assisted Change.Id changeId) {
+    super(args, "comment", newChangeData(args, project, changeId));
     this.commentsUtil = commentsUtil;
     this.incomingEmailEnabled =
         cfg.getEnum("receiveemail", null, "protocol", Protocol.NONE).ordinal()
@@ -128,14 +126,6 @@
 
   public void setComments(List<Comment> comments) {
     inlineComments = comments;
-
-    Set<String> paths = new HashSet<>();
-    for (Comment c : comments) {
-      if (!Patch.isMagic(c.key.filename)) {
-        paths.add(c.key.filename);
-      }
-    }
-    changeData.setCurrentFilePaths(Ordering.natural().sortedCopy(paths));
   }
 
   public void setPatchSetComment(String comment) {
diff --git a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
index 9895e07..1f58abb 100644
--- a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -34,18 +34,18 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
-    CreateChangeSender create(Project.NameKey project, Change.Id id);
+    CreateChangeSender create(Project.NameKey project, Change.Id changeId);
   }
 
   private final PermissionBackend permissionBackend;
 
   @Inject
   public CreateChangeSender(
-      EmailArguments ea,
+      EmailArguments args,
       PermissionBackend permissionBackend,
       @Assisted Project.NameKey project,
-      @Assisted Change.Id id) {
-    super(ea, newChangeData(ea, project, id));
+      @Assisted Change.Id changeId) {
+    super(args, newChangeData(args, project, changeId));
     this.permissionBackend = permissionBackend;
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
index 39e21a5..c9bb1e4 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -38,8 +38,8 @@
 
   @AssistedInject
   public DeleteKeySender(
-      EmailArguments ea, @Assisted IdentifiedUser user, @Assisted AccountSshKey sshKey) {
-    super(ea, "deletekey");
+      EmailArguments args, @Assisted IdentifiedUser user, @Assisted AccountSshKey sshKey) {
+    super(args, "deletekey");
     this.user = user;
     this.gpgKeyFingerprints = Collections.emptyList();
     this.sshKey = sshKey;
@@ -47,8 +47,10 @@
 
   @AssistedInject
   public DeleteKeySender(
-      EmailArguments ea, @Assisted IdentifiedUser user, @Assisted List<String> gpgKeyFingerprints) {
-    super(ea, "deletekey");
+      EmailArguments args,
+      @Assisted IdentifiedUser user,
+      @Assisted List<String> gpgKeyFingerprints) {
+    super(args, "deletekey");
     this.user = user;
     this.gpgKeyFingerprints = gpgKeyFingerprints;
     this.sshKey = null;
@@ -75,7 +77,7 @@
   }
 
   public String getEmail() {
-    return user.getAccount().getPreferredEmail();
+    return user.getAccount().preferredEmail();
   }
 
   public String getUserNameEmail() {
diff --git a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
index f941acc..4f42679 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -36,13 +36,13 @@
 
   public interface Factory extends ReplyToChangeSender.Factory<DeleteReviewerSender> {
     @Override
-    DeleteReviewerSender create(Project.NameKey project, Change.Id change);
+    DeleteReviewerSender create(Project.NameKey project, Change.Id changeId);
   }
 
   @Inject
   public DeleteReviewerSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
-    super(ea, "deleteReviewer", newChangeData(ea, project, id));
+      EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+    super(args, "deleteReviewer", newChangeData(args, project, changeId));
   }
 
   public void addReviewers(Collection<Account.Id> cc) {
diff --git a/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java b/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
index 195d53d..76f9b81 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -25,13 +25,13 @@
 public class DeleteVoteSender extends ReplyToChangeSender {
   public interface Factory extends ReplyToChangeSender.Factory<DeleteVoteSender> {
     @Override
-    DeleteVoteSender create(Project.NameKey project, Change.Id change);
+    DeleteVoteSender create(Project.NameKey project, Change.Id changeId);
   }
 
   @Inject
   protected DeleteVoteSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
-    super(ea, "deleteVote", newChangeData(ea, project, id));
+      EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+    super(args, "deleteVote", newChangeData(args, project, changeId));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/EmailArguments.java b/java/com/google/gerrit/server/mail/send/EmailArguments.java
index fe2f74b..ede5765 100644
--- a/java/com/google/gerrit/server/mail/send/EmailArguments.java
+++ b/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -45,7 +45,7 @@
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.template.soy.tofu.SoyTofu;
+import com.google.template.soy.jbcsrc.api.SoySauce;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -75,7 +75,7 @@
 
   final ChangeQueryBuilder queryBuilder;
   final ChangeData.Factory changeDataFactory;
-  final SoyTofu soyTofu;
+  final SoySauce soySauce;
   final EmailSettings settings;
   final DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners;
   final Provider<InternalAccountQuery> accountQueryProvider;
@@ -105,7 +105,7 @@
       AllProjectsName allProjectsName,
       ChangeQueryBuilder queryBuilder,
       ChangeData.Factory changeDataFactory,
-      @MailTemplates SoyTofu soyTofu,
+      @MailTemplates SoySauce soySauce,
       EmailSettings settings,
       @SshAdvertisedAddresses List<String> sshAddresses,
       SitePaths site,
@@ -134,7 +134,7 @@
     this.allProjectsName = allProjectsName;
     this.queryBuilder = queryBuilder;
     this.changeDataFactory = changeDataFactory;
-    this.soyTofu = soyTofu;
+    this.soySauce = soySauce;
     this.settings = settings;
     this.sshAddresses = sshAddresses;
     this.site = site;
diff --git a/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java b/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
index 5baabe9..61fa50d 100644
--- a/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
+++ b/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
 
 /** 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 b77909e..1bf1ff1 100644
--- a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
+++ b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
@@ -17,8 +17,8 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
@@ -123,9 +123,9 @@
     public Address from(Account.Id fromId) {
       String senderName;
       if (fromId != null) {
-        Optional<Account> a = accountCache.get(fromId).map(AccountState::getAccount);
-        String fullName = a.map(Account::getFullName).orElse(null);
-        String userEmail = a.map(Account::getPreferredEmail).orElse(null);
+        Optional<Account> a = accountCache.get(fromId).map(AccountState::account);
+        String fullName = a.map(Account::fullName).orElse(null);
+        String userEmail = a.map(Account::preferredEmail).orElse(null);
         if (canRelay(userEmail)) {
           return new Address(fullName, userEmail);
         }
@@ -208,8 +208,7 @@
       final String senderName;
 
       if (fromId != null) {
-        String fullName =
-            accountCache.get(fromId).map(a -> a.getAccount().getFullName()).orElse(null);
+        String fullName = accountCache.get(fromId).map(a -> a.account().fullName()).orElse(null);
         if (fullName == null || "".equals(fullName)) {
           fullName = anonymousCowardName;
         }
@@ -235,7 +234,7 @@
       byte[] bytes = hash.digest(data.getBytes(UTF_8));
       return Base64.encodeBase64URLSafeString(bytes);
     } catch (NoSuchAlgorithmException e) {
-      throw new RuntimeException("No MD5 available", e);
+      throw new IllegalStateException("No MD5 available", e);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
index ca332ff..2db2d6d 100644
--- a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
+++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -31,8 +31,8 @@
 
   @AssistedInject
   public HttpPasswordUpdateSender(
-      EmailArguments ea, @Assisted IdentifiedUser user, @Assisted String operation) {
-    super(ea, "HttpPasswordUpdate");
+      EmailArguments args, @Assisted IdentifiedUser user, @Assisted String operation) {
+    super(args, "HttpPasswordUpdate");
     this.user = user;
     this.operation = operation;
   }
@@ -59,7 +59,7 @@
   }
 
   public String getEmail() {
-    return user.getAccount().getPreferredEmail();
+    return user.getAccount().preferredEmail();
   }
 
   public String getUserNameEmail() {
diff --git a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
index b5d384d..110f26a 100644
--- a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
+++ b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
@@ -32,7 +32,8 @@
     PARSING_ERROR,
     INACTIVE_ACCOUNT,
     UNKNOWN_ACCOUNT,
-    INTERNAL_EXCEPTION;
+    INTERNAL_EXCEPTION,
+    COMMENT_REJECTED
   }
 
   public interface Factory {
@@ -45,8 +46,11 @@
 
   @Inject
   public InboundEmailRejectionSender(
-      EmailArguments ea, @Assisted Address to, @Assisted String threadId, @Assisted Error reason) {
-    super(ea, "error");
+      EmailArguments args,
+      @Assisted Address to,
+      @Assisted String threadId,
+      @Assisted Error reason) {
+    super(args, "error");
     this.to = requireNonNull(to);
     this.threadId = requireNonNull(threadId);
     this.reason = requireNonNull(reason);
diff --git a/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java b/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
similarity index 75%
rename from java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
rename to java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
index 3bb44c7..92220eb 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
@@ -17,22 +17,23 @@
 import com.google.common.io.CharStreams;
 import com.google.common.io.Resources;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.google.template.soy.SoyFileSet;
+import com.google.template.soy.jbcsrc.api.SoySauce;
 import com.google.template.soy.shared.SoyAstCache;
-import com.google.template.soy.tofu.SoyTofu;
 import java.io.IOException;
 import java.io.Reader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
-/** Configures Soy Tofu object for rendering email templates. */
+/** Configures Soy Sauce object for rendering email templates. */
 @Singleton
-public class MailSoyTofuProvider implements Provider<SoyTofu> {
+public class MailSoySauceProvider implements Provider<SoySauce> {
 
   // Note: will fail to construct the tofu object if this array is empty.
   private static final String[] TEMPLATES = {
@@ -80,24 +81,32 @@
 
   private final SitePaths site;
   private final SoyAstCache cache;
+  private final PluginSetContext<MailSoyTemplateProvider> templateProviders;
 
   @Inject
-  MailSoyTofuProvider(SitePaths site, SoyAstCache cache) {
+  MailSoySauceProvider(
+      SitePaths site,
+      SoyAstCache cache,
+      PluginSetContext<MailSoyTemplateProvider> templateProviders) {
     this.site = site;
     this.cache = cache;
+    this.templateProviders = templateProviders;
   }
 
   @Override
-  public SoyTofu get() throws ProvisionException {
+  public SoySauce get() throws ProvisionException {
     SoyFileSet.Builder builder = SoyFileSet.builder();
     builder.setSoyAstCache(cache);
     for (String name : TEMPLATES) {
-      addTemplate(builder, name);
+      addTemplate(builder, "com/google/gerrit/server/mail/", name);
     }
-    return builder.build().compileToTofu();
+    templateProviders.runEach(
+        e -> e.getFileNames().forEach(p -> addTemplate(builder, e.getPath(), p)));
+    return builder.build().compileTemplates();
   }
 
-  private void addTemplate(SoyFileSet.Builder builder, String name) throws ProvisionException {
+  private void addTemplate(SoyFileSet.Builder builder, String resourcePath, String name)
+      throws ProvisionException {
     // Load as a file in the mail templates directory if present.
     Path tmpl = site.mail_dir.resolve(name);
     if (Files.isRegularFile(tmpl)) {
@@ -115,7 +124,9 @@
     }
 
     // Otherwise load the template as a resource.
-    String resourcePath = "com/google/gerrit/server/mail/" + name;
-    builder.add(Resources.getResource(resourcePath));
+    if (!resourcePath.endsWith("/")) {
+      resourcePath += "/";
+    }
+    builder.add(Resources.getResource(resourcePath + name));
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/MailSoyTemplateProvider.java b/java/com/google/gerrit/server/mail/send/MailSoyTemplateProvider.java
new file mode 100644
index 0000000..3a6ff64
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/MailSoyTemplateProvider.java
@@ -0,0 +1,43 @@
+// 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.mail.send;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.util.Set;
+
+/**
+ * Extension point to provide soy templates that should be registered so that they can be used for
+ * sending emails from a plugin.
+ */
+@ExtensionPoint
+public interface MailSoyTemplateProvider {
+  /**
+   * Return the name of the resource path that contains the soy template files that are returned by
+   * {@link #getFileNames()}.
+   *
+   * @return resource path of the templates
+   */
+  String getPath();
+
+  /**
+   * Return the names of the soy template files.
+   *
+   * <p>These files are expected to exist in the resource path that is returned by {@link
+   * #getPath()}.
+   *
+   * @return names of the template files, including the {@code .soy} file extension
+   */
+  Set<String> getFileNames();
+}
diff --git a/java/com/google/gerrit/server/mail/send/MergedSender.java b/java/com/google/gerrit/server/mail/send/MergedSender.java
index cbc4117..b28a4dc 100644
--- a/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -19,12 +19,12 @@
 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.PatchSetApproval;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -32,15 +32,15 @@
 /** Send notice about a change successfully merged. */
 public class MergedSender extends ReplyToChangeSender {
   public interface Factory {
-    MergedSender create(Project.NameKey project, Change.Id id);
+    MergedSender create(Project.NameKey project, Change.Id changeId);
   }
 
   private final LabelTypes labelTypes;
 
   @Inject
   public MergedSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
-    super(ea, "merged", newChangeData(ea, project, id));
+      EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+    super(args, "merged", newChangeData(args, project, changeId));
     labelTypes = changeData.getLabelTypes();
   }
 
@@ -69,15 +69,15 @@
       Table<Account.Id, String, PatchSetApproval> pos = HashBasedTable.create();
       Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create();
       for (PatchSetApproval ca :
-          args.approvalsUtil.byPatchSet(changeData.notes(), patchSet.getId(), null, null)) {
-        LabelType lt = labelTypes.byLabel(ca.getLabelId());
+          args.approvalsUtil.byPatchSet(changeData.notes(), patchSet.id(), null, null)) {
+        LabelType lt = labelTypes.byLabel(ca.labelId());
         if (lt == null) {
           continue;
         }
-        if (ca.getValue() > 0) {
-          pos.put(ca.getAccountId(), lt.getName(), ca);
-        } else if (ca.getValue() < 0) {
-          neg.put(ca.getAccountId(), lt.getName(), ca);
+        if (ca.value() > 0) {
+          pos.put(ca.accountId(), lt.getName(), ca);
+        } else if (ca.value() < 0) {
+          neg.put(ca.accountId(), lt.getName(), ca);
         }
       }
 
@@ -117,7 +117,7 @@
         } else {
           txt.append(lt.getName());
           txt.append('=');
-          txt.append(LabelValue.formatValue(ca.getValue()));
+          txt.append(LabelValue.formatValue(ca.value()));
         }
       }
       txt.append('\n');
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index a31596b..83c3a94 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.query.change.ChangeData;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -32,8 +32,8 @@
   private final Set<Account.Id> extraCC = new HashSet<>();
   private final Set<Address> extraCCByEmail = new HashSet<>();
 
-  protected NewChangeSender(EmailArguments ea, ChangeData cd) {
-    super(ea, "newchange", cd);
+  protected NewChangeSender(EmailArguments args, ChangeData changeData) {
+    super(args, "newchange", changeData);
   }
 
   public void addReviewers(Collection<Account.Id> cc) {
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 9ef8d872e..1883875 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -17,13 +17,13 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
 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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow;
@@ -36,10 +36,10 @@
 public abstract class NotificationEmail extends OutgoingEmail {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  protected Branch.NameKey branch;
+  protected BranchNameKey branch;
 
-  protected NotificationEmail(EmailArguments ea, String mc, ChangeData cd) {
-    super(ea, mc);
+  protected NotificationEmail(EmailArguments args, String messageClass, ChangeData cd) {
+    super(args, messageClass);
     try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) {
       this.branch = cd.change().getDest();
     }
@@ -55,7 +55,7 @@
     // Set a reasonable list id so that filters can be used to sort messages
     setHeader(
         "List-Id",
-        "<gerrit-" + branch.getParentKey().get().replace('/', '-') + "." + getGerritHost() + ">");
+        "<gerrit-" + branch.project().get().replace('/', '-') + "." + getGerritHost() + ">");
     if (getSettingsUrl() != null) {
       setHeader("List-Unsubscribe", "<" + getSettingsUrl() + ">");
     }
@@ -109,7 +109,7 @@
   protected void setupSoyContext() {
     super.setupSoyContext();
 
-    String projectName = branch.getParentKey().get();
+    String projectName = branch.project().get();
     soyContext.put("projectName", projectName);
     // shortProjectName is the project name with the path abbreviated.
     soyContext.put("shortProjectName", getShortProjectName(projectName));
@@ -123,11 +123,11 @@
     soyContextEmailData.put("sshHost", getSshHost());
 
     Map<String, String> branchData = new HashMap<>();
-    branchData.put("shortName", branch.getShortName());
+    branchData.put("shortName", branch.shortName());
     soyContext.put("branch", branchData);
 
-    footers.add(MailHeader.PROJECT.withDelimiter() + branch.getParentKey().get());
-    footers.add(MailHeader.BRANCH.withDelimiter() + branch.getShortName());
+    footers.add(MailHeader.PROJECT.withDelimiter() + branch.project().get());
+    footers.add(MailHeader.BRANCH.withDelimiter() + branch.shortName());
   }
 
   @VisibleForTesting
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 949348d..0448688 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -21,6 +21,8 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.UserIdentity;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -29,8 +31,6 @@
 import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.mail.EmailHeader.AddressList;
 import com.google.gerrit.mail.MailHeader;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow;
@@ -38,7 +38,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
-import com.google.template.soy.data.SanitizedContent;
+import com.google.template.soy.jbcsrc.api.SoySauce;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
@@ -58,6 +58,7 @@
 
 /** Sends an email to one or more interested parties. */
 public abstract class OutgoingEmail {
+  private static final String SOY_TEMPLATE_NAMESPACE = "com.google.gerrit.server.mail.template.";
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected String messageClass;
@@ -74,10 +75,10 @@
   protected Account.Id fromId;
   protected NotifyResolver.Result notify = NotifyResolver.Result.all();
 
-  protected OutgoingEmail(EmailArguments ea, String mc) {
-    args = ea;
-    messageClass = mc;
-    headers = new LinkedHashMap<>();
+  protected OutgoingEmail(EmailArguments args, String messageClass) {
+    this.args = args;
+    this.messageClass = messageClass;
+    this.headers = new LinkedHashMap<>();
   }
 
   public void setFrom(Account.Id id) {
@@ -123,7 +124,7 @@
         if (fromId != null) {
           Optional<AccountState> fromUser = args.accountCache.get(fromId);
           if (fromUser.isPresent()) {
-            GeneralPreferencesInfo senderPrefs = fromUser.get().getGeneralPreferences();
+            GeneralPreferencesInfo senderPrefs = fromUser.get().generalPreferences();
             if (senderPrefs != null && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) {
               // If we are impersonating a user, make sure they receive a CC of
               // this message so they can always review and audit what we sent
@@ -134,7 +135,7 @@
               // If they don't want a copy, but we queued one up anyway,
               // drop them from the recipient lists.
               //
-              removeUser(fromUser.get().getAccount());
+              removeUser(fromUser.get().account());
             }
           }
         }
@@ -144,14 +145,14 @@
         for (Account.Id id : rcptTo) {
           Optional<AccountState> thisUser = args.accountCache.get(id);
           if (thisUser.isPresent()) {
-            Account thisUserAccount = thisUser.get().getAccount();
-            GeneralPreferencesInfo prefs = thisUser.get().getGeneralPreferences();
+            Account thisUserAccount = thisUser.get().account();
+            GeneralPreferencesInfo prefs = thisUser.get().generalPreferences();
             if (prefs == null || prefs.getEmailStrategy() == DISABLED) {
               removeUser(thisUserAccount);
             } else if (useHtml() && prefs.getEmailFormat() == EmailFormat.PLAINTEXT) {
               removeUser(thisUserAccount);
               smtpRcptToPlaintextOnly.add(
-                  new Address(thisUserAccount.getFullName(), thisUserAccount.getPreferredEmail()));
+                  new Address(thisUserAccount.fullName(), thisUserAccount.preferredEmail()));
             }
           }
           if (smtpRcptTo.isEmpty() && smtpRcptToPlaintextOnly.isEmpty()) {
@@ -268,10 +269,10 @@
 
   protected String getFromLine() {
     StringBuilder f = new StringBuilder();
-    Optional<Account> account = args.accountCache.get(fromId).map(AccountState::getAccount);
+    Optional<Account> account = args.accountCache.get(fromId).map(AccountState::account);
     if (account.isPresent()) {
-      String name = account.get().getFullName();
-      String email = account.get().getPreferredEmail();
+      String name = account.get().fullName();
+      String email = account.get().preferredEmail();
       if ((name != null && !name.isEmpty()) || (email != null && !email.isEmpty())) {
         f.append("From");
         if (name != null && !name.isEmpty()) {
@@ -344,12 +345,12 @@
       return args.gerritPersonIdent.getName();
     }
 
-    Optional<Account> account = args.accountCache.get(accountId).map(AccountState::getAccount);
+    Optional<Account> account = args.accountCache.get(accountId).map(AccountState::account);
     String name = null;
     if (account.isPresent()) {
-      name = account.get().getFullName();
+      name = account.get().fullName();
       if (name == null) {
-        name = account.get().getPreferredEmail();
+        name = account.get().preferredEmail();
       }
     }
     if (name == null) {
@@ -373,10 +374,10 @@
           + ">";
     }
 
-    Optional<Account> account = args.accountCache.get(accountId).map(AccountState::getAccount);
+    Optional<Account> account = args.accountCache.get(accountId).map(AccountState::account);
     if (account.isPresent()) {
-      String name = account.get().getFullName();
-      String email = account.get().getPreferredEmail();
+      String name = account.get().fullName();
+      String email = account.get().preferredEmail();
       if (name != null && email != null) {
         return name + " <" + email + ">";
       } else if (name != null) {
@@ -405,9 +406,9 @@
       return null;
     }
 
-    Account account = accountState.get().getAccount();
-    String name = account.getFullName();
-    String email = account.getPreferredEmail();
+    Account account = accountState.get().account();
+    String name = account.fullName();
+    String email = account.preferredEmail();
     if (name != null && email != null) {
       return name + " <" + email + ">";
     } else if (email != null) {
@@ -415,7 +416,7 @@
     } else if (name != null) {
       return name;
     }
-    return accountState.get().getUserName().orElse(null);
+    return accountState.get().userName().orElse(null);
   }
 
   protected boolean shouldSendMessage() {
@@ -537,17 +538,17 @@
   }
 
   private Address toAddress(Account.Id id) {
-    Optional<Account> accountState = args.accountCache.get(id).map(AccountState::getAccount);
+    Optional<Account> accountState = args.accountCache.get(id).map(AccountState::account);
     if (!accountState.isPresent()) {
       return null;
     }
 
     Account account = accountState.get();
-    String e = account.getPreferredEmail();
+    String e = account.preferredEmail();
     if (!account.isActive() || e == null) {
       return null;
     }
-    return new Address(account.getFullName(), e);
+    return new Address(account.fullName(), e);
   }
 
   protected void setupSoyContext() {
@@ -569,24 +570,23 @@
     return args.instanceNameProvider.get();
   }
 
-  private String soyTemplate(String name, SanitizedContent.ContentKind kind) {
-    return args.soyTofu
-        .newRenderer("com.google.gerrit.server.mail.template." + name)
-        .setContentKind(kind)
-        .setData(soyContext)
-        .render();
-  }
-
+  /** Renders a soy template of kind="text". */
   protected String textTemplate(String name) {
-    return soyTemplate(name, SanitizedContent.ContentKind.TEXT);
+    return configureRenderer(name).renderText().get();
   }
 
+  /** Renders a soy template of kind="html". */
   protected String soyHtmlTemplate(String name) {
-    return soyTemplate(name, SanitizedContent.ContentKind.HTML);
+    return configureRenderer(name).renderHtml().get().toString();
+  }
+
+  /** Configures a soy renderer for the given template name and rendering data map. */
+  private SoySauce.Renderer configureRenderer(String templateName) {
+    return args.soySauce.renderTemplate(SOY_TEMPLATE_NAMESPACE + templateName).setData(soyContext);
   }
 
   protected void removeUser(Account user) {
-    String fromEmail = user.getPreferredEmail();
+    String fromEmail = user.preferredEmail();
     for (Iterator<Address> j = smtpRcptTo.iterator(); j.hasNext(); ) {
       if (j.next().getEmail().equals(fromEmail)) {
         j.remove();
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 0a468b4..2530d7e 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -19,12 +19,12 @@
 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.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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
@@ -66,9 +66,8 @@
     Set<Account.Id> projectWatchers = new HashSet<>();
 
     for (AccountState a : args.accountQueryProvider.get().byWatchedProject(project)) {
-      Account.Id accountId = a.getAccount().getId();
-      for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e :
-          a.getProjectWatches().entrySet()) {
+      Account.Id accountId = a.account().id();
+      for (Map.Entry<ProjectWatchKey, ImmutableSet<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
@@ -78,10 +77,9 @@
     }
 
     for (AccountState a : args.accountQueryProvider.get().byWatchedProject(args.allProjectsName)) {
-      for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e :
-          a.getProjectWatches().entrySet()) {
+      for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e : a.projectWatches().entrySet()) {
         if (args.allProjectsName.equals(e.getKey().project())) {
-          Account.Id accountId = a.getAccount().getId();
+          Account.Id accountId = a.account().id();
           if (!projectWatchers.contains(accountId)) {
             add(matching, accountId, e.getKey(), e.getValue(), type);
           }
@@ -97,9 +95,9 @@
       for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
         if (nc.isNotify(type)) {
           try {
-            add(matching, nc);
+            add(matching, state.getNameKey(), nc);
           } catch (QueryParseException e) {
-            logger.atWarning().log(
+            logger.atInfo().log(
                 "Project %s has invalid notify %s filter \"%s\": %s",
                 state.getName(), nc.getName(), nc.getFilter(), e.getMessage());
           }
@@ -146,17 +144,27 @@
     }
   }
 
-  private void add(Watchers matching, NotifyConfig nc) throws QueryParseException {
-    for (GroupReference ref : nc.getGroups()) {
-      CurrentUser user = new SingleGroupUser(ref.getUUID());
+  private void add(Watchers matching, Project.NameKey projectName, NotifyConfig nc)
+      throws QueryParseException {
+    logger.atFine().log("Checking watchers for notify config %s from project %s", nc, projectName);
+    for (GroupReference groupRef : nc.getGroups()) {
+      CurrentUser user = new SingleGroupUser(groupRef.getUUID());
       if (filterMatch(user, nc.getFilter())) {
-        deliverToMembers(matching.list(nc.getHeader()), ref.getUUID());
+        deliverToMembers(matching.list(nc.getHeader()), groupRef.getUUID());
+        logger.atFine().log("Added watchers for group %s", groupRef);
+      } else {
+        logger.atFine().log("The filter did not match for group %s; skip notification", groupRef);
       }
     }
 
     if (!nc.getAddresses().isEmpty()) {
       if (filterMatch(null, nc.getFilter())) {
         matching.list(nc.getHeader()).emails.addAll(nc.getAddresses());
+        logger.atFine().log("Added watchers for these addresses: %s", nc.getAddresses());
+      } else {
+        logger.atFine().log(
+            "The filter did not match; skip notification for these addresses: %s",
+            nc.getAddresses());
       }
     }
   }
@@ -172,19 +180,24 @@
       AccountGroup.UUID uuid = q.remove(q.size() - 1);
       GroupDescription.Basic group = args.groupBackend.get(uuid);
       if (group == null) {
+        logger.atFine().log("group %s not found, skip notification", uuid);
         continue;
       }
       if (!Strings.isNullOrEmpty(group.getEmailAddress())) {
         // If the group has an email address, do not expand membership.
         matching.emails.add(new Address(group.getEmailAddress()));
+        logger.atFine().log(
+            "notify group email address %s; skip expanding to members", group.getEmailAddress());
         continue;
       }
 
       if (!(group instanceof GroupDescription.Internal)) {
         // Non-internal groups cannot be expanded by the server.
+        logger.atFine().log("group %s is not an internal group, skip notification", uuid);
         continue;
       }
 
+      logger.atFine().log("adding the members of group %s as watchers", uuid);
       GroupDescription.Internal ig = (GroupDescription.Internal) group;
       matching.accounts.addAll(ig.getMembers());
       for (AccountGroup.UUID m : ig.getSubgroups()) {
@@ -201,8 +214,9 @@
       ProjectWatchKey key,
       Set<NotifyType> watchedTypes,
       NotifyType type) {
-    IdentifiedUser user = args.identifiedUserFactory.create(accountId);
+    logger.atFine().log("Checking project watch %s of account %s", key, accountId);
 
+    IdentifiedUser user = args.identifiedUserFactory.create(accountId);
     try {
       if (filterMatch(user, key.filter())) {
         // If we are set to notify on this type, add the user.
@@ -210,10 +224,14 @@
         if (watchedTypes.contains(type)) {
           matching.bcc.accounts.add(accountId);
         }
+        logger.atFine().log("Added account %s as watcher", accountId);
         return true;
       }
+      logger.atFine().log("The filter did not match for account %s; skip notification", accountId);
     } catch (QueryParseException e) {
       // Ignore broken filter expressions.
+      logger.atInfo().log(
+          "Account %s has invalid filter in project watch %s: %s", accountId, key, e.getMessage());
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
index 436736b..91d8e81 100644
--- a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
@@ -36,14 +36,14 @@
 
   @Inject
   public RegisterNewEmailSender(
-      EmailArguments ea,
-      EmailTokenVerifier etv,
+      EmailArguments args,
+      EmailTokenVerifier tokenVerifier,
       IdentifiedUser callingUser,
       @Assisted final String address) {
-    super(ea, "registernewemail");
-    tokenVerifier = etv;
-    user = callingUser;
-    addr = address;
+    super(args, "registernewemail");
+    this.tokenVerifier = tokenVerifier;
+    this.user = callingUser;
+    this.addr = address;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
index 30bcdeb..909c52a 100644
--- a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -32,7 +32,7 @@
 /** Send notice of new patch sets for reviewers. */
 public class ReplacePatchSetSender extends ReplyToChangeSender {
   public interface Factory {
-    ReplacePatchSetSender create(Project.NameKey project, Change.Id id);
+    ReplacePatchSetSender create(Project.NameKey project, Change.Id changeId);
   }
 
   private final Set<Account.Id> reviewers = new HashSet<>();
@@ -40,8 +40,8 @@
 
   @Inject
   public ReplacePatchSetSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
-    super(ea, "newpatchset", newChangeData(ea, project, id));
+      EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+    super(args, "newpatchset", newChangeData(args, project, changeId));
   }
 
   public void addReviewers(Collection<Account.Id> cc) {
diff --git a/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java b/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
index 960c3a8..c765430 100644
--- a/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.query.change.ChangeData;
 
 /** Alert a user to a reply to a change, usually commentary made during review. */
@@ -26,8 +26,8 @@
     T create(Project.NameKey project, Change.Id id);
   }
 
-  protected ReplyToChangeSender(EmailArguments ea, String mc, ChangeData cd) {
-    super(ea, mc, cd);
+  protected ReplyToChangeSender(EmailArguments args, String messageClass, ChangeData changeData) {
+    super(args, messageClass, changeData);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/RestoredSender.java b/java/com/google/gerrit/server/mail/send/RestoredSender.java
index 0d998aa..2a4c556 100644
--- a/java/com/google/gerrit/server/mail/send/RestoredSender.java
+++ b/java/com/google/gerrit/server/mail/send/RestoredSender.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -25,13 +25,13 @@
 public class RestoredSender extends ReplyToChangeSender {
   public interface Factory extends ReplyToChangeSender.Factory<RestoredSender> {
     @Override
-    RestoredSender create(Project.NameKey project, Change.Id id);
+    RestoredSender create(Project.NameKey project, Change.Id changeId);
   }
 
   @Inject
   public RestoredSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
-    super(ea, "restore", ChangeEmail.newChangeData(ea, project, id));
+      EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+    super(args, "restore", ChangeEmail.newChangeData(args, project, changeId));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/RevertedSender.java b/java/com/google/gerrit/server/mail/send/RevertedSender.java
index 48b5d99..dadd0d2 100644
--- a/java/com/google/gerrit/server/mail/send/RevertedSender.java
+++ b/java/com/google/gerrit/server/mail/send/RevertedSender.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -24,13 +24,13 @@
 /** Send notice about a change being reverted. */
 public class RevertedSender extends ReplyToChangeSender {
   public interface Factory {
-    RevertedSender create(Project.NameKey project, Change.Id id);
+    RevertedSender create(Project.NameKey project, Change.Id changeId);
   }
 
   @Inject
   public RevertedSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
-    super(ea, "revert", ChangeEmail.newChangeData(ea, project, id));
+      EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+    super(args, "revert", ChangeEmail.newChangeData(args, project, changeId));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
index a120769..850f775 100644
--- a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
+++ b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
@@ -14,28 +14,28 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 public class SetAssigneeSender extends ChangeEmail {
   public interface Factory {
-    SetAssigneeSender create(Project.NameKey project, Change.Id id, Account.Id assignee);
+    SetAssigneeSender create(Project.NameKey project, Change.Id changeId, Account.Id assignee);
   }
 
   private final Account.Id assignee;
 
   @Inject
   public SetAssigneeSender(
-      EmailArguments ea,
+      EmailArguments args,
       @Assisted Project.NameKey project,
-      @Assisted Change.Id id,
+      @Assisted Change.Id changeId,
       @Assisted Account.Id assignee) {
-    super(ea, "setassignee", newChangeData(ea, project, id));
+    super(args, "setassignee", newChangeData(args, project, changeId));
     this.assignee = assignee;
   }
 
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 1c6057d..0acf20e 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -20,11 +20,12 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.metrics.Timer1;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -49,8 +50,8 @@
     public final ChangeNoteJson changeNoteJson;
     public final GitRepositoryManager repoManager;
     public final AllUsersName allUsers;
-    public final LegacyChangeNoteRead legacyChangeNoteRead;
     public final NoteDbMetrics metrics;
+    public final String serverId;
 
     // Providers required to avoid dependency cycles.
 
@@ -62,16 +63,16 @@
         GitRepositoryManager repoManager,
         AllUsersName allUsers,
         ChangeNoteJson changeNoteJson,
-        LegacyChangeNoteRead legacyChangeNoteRead,
         NoteDbMetrics metrics,
-        Provider<ChangeNotesCache> cache) {
+        Provider<ChangeNotesCache> cache,
+        @GerritServerId String serverId) {
       this.failOnLoadForTest = new AtomicBoolean();
       this.repoManager = repoManager;
       this.allUsers = allUsers;
-      this.legacyChangeNoteRead = legacyChangeNoteRead;
       this.changeNoteJson = changeNoteJson;
       this.metrics = metrics;
       this.cache = cache;
+      this.serverId = serverId;
     }
   }
 
@@ -139,7 +140,7 @@
     if (args.failOnLoadForTest.get()) {
       throw new StorageException("Reading from NoteDb is disabled");
     }
-    try (Timer1.Context timer = args.metrics.readLatency.start(CHANGES);
+    try (Timer1.Context<NoteDbTable> timer = args.metrics.readLatency.start(CHANGES);
         Repository repo = args.repoManager.openRepository(getProjectName());
         // Call openHandle even if reading is disabled, to trigger
         // auto-rebuilding before this object may get passed to a ChangeUpdate.
@@ -153,6 +154,7 @@
     return self();
   }
 
+  @Nullable
   protected ObjectId readRef(Repository repo) throws IOException {
     Ref ref = repo.getRefDatabase().exactRef(getRefName());
     return ref != null ? ref.getObjectId() : null;
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index daef7e7..ee3ccd6 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -19,11 +19,11 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
@@ -147,7 +147,7 @@
   }
 
   public void setPatchSetId(PatchSet.Id psId) {
-    checkArgument(psId == null || psId.getParentKey().equals(getId()));
+    checkArgument(psId == null || psId.changeId().equals(getId()));
     this.psId = psId;
   }
 
@@ -193,6 +193,14 @@
   }
 
   /**
+   * Whether to allow bypassing the check that an update does not exceed the max update count on an
+   * object.
+   */
+  protected boolean bypassMaxUpdates() {
+    return false;
+  }
+
+  /**
    * Apply this update to the given inserter.
    *
    * @param rw walk for reading back any objects needed for the update.
@@ -267,7 +275,7 @@
   }
 
   protected void verifyComment(Comment c) {
-    checkArgument(c.revId != null, "RevId required for comment: %s", c);
+    checkArgument(c.getCommitId() != null, "commit ID required for comment: %s", c);
     checkArgument(
         c.author.getId().equals(getAccountId()),
         "The author for the following comment does not match the author of this %s (%s): %s",
diff --git a/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java b/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java
new file mode 100644
index 0000000..112f687
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java
@@ -0,0 +1,116 @@
+// 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.notedb;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.git.RefUpdateUtil;
+import com.google.gerrit.server.FanOutExecutor;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.transport.PushCertificate;
+
+/**
+ * Performs an update on {@code All-Users} asynchronously if required. No-op in case no updates were
+ * scheduled for asynchronous execution.
+ */
+public class AllUsersAsyncUpdate {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final ExecutorService executor;
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager repoManager;
+  private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
+
+  private PersonIdent serverIdent;
+
+  @Inject
+  AllUsersAsyncUpdate(
+      @FanOutExecutor ExecutorService executor,
+      AllUsersName allUsersName,
+      GitRepositoryManager repoManager) {
+    this.executor = executor;
+    this.allUsersName = allUsersName;
+    this.repoManager = repoManager;
+    this.draftUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
+  }
+
+  void setDraftUpdates(ListMultimap<String, ChangeDraftUpdate> draftUpdates) {
+    checkState(isEmpty(), "attempted to set draft comment updates for async execution twice");
+    boolean allPublishOnly =
+        draftUpdates.values().stream().allMatch(ChangeDraftUpdate::canRunAsync);
+    checkState(allPublishOnly, "not all updates can be run asynchronously");
+    // Add deep copies to avoid any threading issues.
+    for (Map.Entry<String, ChangeDraftUpdate> entry : draftUpdates.entries()) {
+      this.draftUpdates.put(entry.getKey(), entry.getValue().copy());
+    }
+    if (draftUpdates.size() > 0) {
+      // Save the PersonIdent for later so that we get consistent time stamps in the commit and ref
+      // log.
+      serverIdent = Iterables.get(draftUpdates.entries(), 0).getValue().serverIdent;
+    }
+  }
+
+  /** Returns true if no operations should be performed on the repo. */
+  boolean isEmpty() {
+    return draftUpdates.isEmpty();
+  }
+
+  /** Executes repository update asynchronously. No-op in case no updates were scheduled. */
+  void execute(PersonIdent refLogIdent, String refLogMessage, PushCertificate pushCert) {
+    if (isEmpty()) {
+      return;
+    }
+
+    @SuppressWarnings("unused")
+    Future<?> possiblyIgnoredError =
+        executor.submit(
+            () -> {
+              try (OpenRepo allUsersRepo = OpenRepo.open(repoManager, allUsersName)) {
+                allUsersRepo.addUpdates(draftUpdates);
+                allUsersRepo.flush();
+                BatchRefUpdate bru = allUsersRepo.repo.getRefDatabase().newBatchUpdate();
+                bru.setPushCertificate(pushCert);
+                if (refLogMessage != null) {
+                  bru.setRefLogMessage(refLogMessage, false);
+                } else {
+                  bru.setRefLogMessage(
+                      firstNonNull(NoteDbUtil.guessRestApiHandler(), "Update NoteDb refs async"),
+                      false);
+                }
+                bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent);
+                bru.setAtomic(true);
+                allUsersRepo.cmds.addTo(bru);
+                bru.setAllowNonFastForwards(true);
+                RefUpdateUtil.executeChecked(bru, allUsersRepo.rw);
+              } catch (IOException e) {
+                logger.atSevere().withCause(e).log(
+                    "Failed to delete draft comments asynchronously after publishing them");
+              }
+            });
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 8773d9b..05fdee9 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -15,18 +15,17 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkState;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.Sets;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.inject.assistedinject.Assisted;
@@ -35,7 +34,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
-import java.util.HashSet;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -74,19 +73,25 @@
 
   @AutoValue
   abstract static class Key {
-    abstract String revId();
+    abstract ObjectId commitId();
 
     abstract Comment.Key key();
   }
 
+  enum DeleteReason {
+    DELETED,
+    PUBLISHED,
+    FIXED
+  }
+
   private static Key key(Comment c) {
-    return new AutoValue_ChangeDraftUpdate_Key(c.revId, c.key);
+    return new AutoValue_ChangeDraftUpdate_Key(c.getCommitId(), c.key);
   }
 
   private final AllUsersName draftsProject;
 
   private List<Comment> put = new ArrayList<>();
-  private Set<Key> delete = new HashSet<>();
+  private Map<Key, DeleteReason> delete = new HashMap<>();
 
   @AssistedInject
   private ChangeDraftUpdate(
@@ -117,40 +122,92 @@
   }
 
   public void putComment(Comment c) {
+    checkState(!put.contains(c), "comment already added");
     verifyComment(c);
     put.add(c);
   }
 
-  public void deleteComment(Comment c) {
+  /**
+   * Marks a comment for deletion. Called when the comment is deleted because the user published it.
+   */
+  public void markCommentPublished(Comment c) {
+    checkState(!delete.containsKey(key(c)), "comment already marked for deletion");
     verifyComment(c);
-    delete.add(key(c));
+    delete.put(key(c), DeleteReason.PUBLISHED);
   }
 
-  public void deleteComment(String revId, Comment.Key key) {
-    delete.add(new AutoValue_ChangeDraftUpdate_Key(revId, key));
+  /**
+   * Marks a comment for deletion. Called when the comment is deleted because the user removed it.
+   */
+  public void deleteComment(Comment c) {
+    checkState(!delete.containsKey(key(c)), "comment already marked for deletion");
+    verifyComment(c);
+    delete.put(key(c), DeleteReason.DELETED);
+  }
+
+  /**
+   * Marks a comment for deletion. Called when the comment should have been deleted previously, but
+   * wasn't, so we're fixing it up.
+   */
+  public void deleteComment(ObjectId commitId, Comment.Key key) {
+    Key commentKey = new AutoValue_ChangeDraftUpdate_Key(commitId, key);
+    checkState(!delete.containsKey(commentKey), "comment already marked for deletion");
+    delete.put(commentKey, DeleteReason.FIXED);
+  }
+
+  /**
+   * Returns true if all we do in this operations is deletes caused by publishing or fixing up
+   * comments.
+   */
+  public boolean canRunAsync() {
+    return put.isEmpty()
+        && delete.values().stream()
+            .allMatch(r -> r == DeleteReason.PUBLISHED || r == DeleteReason.FIXED);
+  }
+
+  /**
+   * Returns a copy of the current {@link ChangeDraftUpdate} that contains references to all
+   * deletions. Copying of {@link ChangeDraftUpdate} is only allowed if it contains no new comments.
+   */
+  ChangeDraftUpdate copy() {
+    checkState(
+        put.isEmpty(),
+        "copying ChangeDraftUpdate is allowed only if it doesn't contain new comments");
+    ChangeDraftUpdate clonedUpdate =
+        new ChangeDraftUpdate(
+            authorIdent,
+            draftsProject,
+            noteUtil,
+            new Change(getChange()),
+            accountId,
+            realAccountId,
+            authorIdent,
+            when);
+    clonedUpdate.delete.putAll(delete);
+    return clonedUpdate;
   }
 
   private CommitBuilder storeCommentsInNotes(
       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
       throws ConfigInvalidException, IOException {
     RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
-    Set<RevId> updatedRevs = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
+    Set<ObjectId> updatedCommits = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
 
     for (Comment c : put) {
-      if (!delete.contains(key(c))) {
-        cache.get(new RevId(c.revId)).putComment(c);
+      if (!delete.keySet().contains(key(c))) {
+        cache.get(c.getCommitId()).putComment(c);
       }
     }
-    for (Key k : delete) {
-      cache.get(new RevId(k.revId())).deleteComment(k.key());
+    for (Key k : delete.keySet()) {
+      cache.get(k.commitId()).deleteComment(k.key());
     }
 
-    Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders();
+    Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
     boolean touchedAnyRevs = false;
-    for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
-      updatedRevs.add(e.getKey());
-      ObjectId id = ObjectId.fromString(e.getKey().get());
+    for (Map.Entry<ObjectId, RevisionNoteBuilder> e : builders.entrySet()) {
+      updatedCommits.add(e.getKey());
+      ObjectId id = e.getKey();
       byte[] data = e.getValue().build(noteUtil.getChangeNoteJson());
       if (!Arrays.equals(data, e.getValue().baseRaw)) {
         touchedAnyRevs = true;
@@ -204,12 +261,7 @@
     // Even though reading from changes might not be enabled, we need to
     // parse any existing revision notes so we can merge them.
     return RevisionNoteMap.parse(
-        noteUtil.getChangeNoteJson(),
-        noteUtil.getLegacyChangeNoteRead(),
-        getId(),
-        rw.getObjectReader(),
-        noteMap,
-        PatchLineComment.Status.DRAFT);
+        noteUtil.getChangeNoteJson(), rw.getObjectReader(), noteMap, Comment.Status.DRAFT);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 628dfd2..b221ef5 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -16,7 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.inject.Inject;
 import java.util.Date;
@@ -63,22 +63,13 @@
   static final String UNRESOLVED = "Unresolved";
   static final String TAG = FOOTER_TAG.getName();
 
-  private final LegacyChangeNoteRead legacyChangeNoteRead;
   private final ChangeNoteJson changeNoteJson;
   private final String serverId;
 
   @Inject
-  public ChangeNoteUtil(
-      ChangeNoteJson changeNoteJson,
-      LegacyChangeNoteRead legacyChangeNoteRead,
-      @GerritServerId String serverId) {
+  public ChangeNoteUtil(ChangeNoteJson changeNoteJson, @GerritServerId String serverId) {
     this.serverId = serverId;
     this.changeNoteJson = changeNoteJson;
-    this.legacyChangeNoteRead = legacyChangeNoteRead;
-  }
-
-  public LegacyChangeNoteRead getLegacyChangeNoteRead() {
-    return legacyChangeNoteRead;
   }
 
   public ChangeNoteJson getChangeNoteJson() {
@@ -96,8 +87,8 @@
   @VisibleForTesting
   public PersonIdent newIdent(Account author, Date when, PersonIdent serverIdent) {
     return new PersonIdent(
-        "Gerrit User " + author.getId(),
-        author.getId().get() + "@" + serverId,
+        "Gerrit User " + author.id(),
+        author.id().get() + "@" + serverId,
         when,
         serverIdent.getTimeZone());
   }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index cda394c..d6a32d3 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -16,18 +16,21 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.entities.RefNames.changeMetaRef;
 import static java.util.Comparator.comparing;
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Ordering;
@@ -36,18 +39,18 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+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.entities.RobotComment;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
@@ -64,7 +67,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Predicate;
@@ -79,7 +81,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static final Ordering<PatchSetApproval> PSA_BY_TIME =
-      Ordering.from(comparing(PatchSetApproval::getGranted));
+      Ordering.from(comparing(PatchSetApproval::granted));
 
   public static final Ordering<ChangeMessage> MESSAGE_BY_TIME =
       Ordering.from(comparing(ChangeMessage::getWrittenOn));
@@ -128,7 +130,7 @@
 
     public static Change newChange(Project.NameKey project, Change.Id changeId) {
       return new Change(
-          null, changeId, null, new Branch.NameKey(project, "INVALID_NOTE_DB_ONLY"), null);
+          null, changeId, null, BranchNameKey.create(project, "INVALID_NOTE_DB_ONLY"), null);
     }
 
     public ChangeNotes create(Project.NameKey project, Change.Id changeId) {
@@ -219,6 +221,7 @@
       return idStream.map(id -> scanOneChange(project, sr, id)).filter(Objects::nonNull);
     }
 
+    @Nullable
     private ChangeNotesResult scanOneChange(Project.NameKey project, ScanResult sr, Change.Id id) {
       if (!sr.fromMetaRefs().contains(id)) {
         // Stray patch set refs can happen due to normal error conditions, e.g. failed
@@ -227,10 +230,15 @@
       }
 
       // TODO(dborowitz): See discussion in BatchUpdate#newChangeContext.
-      Change change = ChangeNotes.Factory.newChange(project, id);
-
-      logger.atFine().log("adding change %s found in project %s", id, project);
-      return toResult(change);
+      try {
+        Change change = ChangeNotes.Factory.newChange(project, id);
+        logger.atFine().log("adding change %s found in project %s", id, project);
+        return toResult(change);
+      } catch (InvalidServerIdException ise) {
+        logger.atWarning().withCause(ise).log(
+            "skipping change %d in project %s because of an invalid server id", id.get(), project);
+        return null;
+      }
     }
 
     @Nullable
@@ -339,9 +347,7 @@
     if (patchSets == null) {
       ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b =
           ImmutableSortedMap.orderedBy(comparing(PatchSet.Id::get));
-      for (Map.Entry<PatchSet.Id, PatchSet> e : state.patchSets()) {
-        b.put(e.getKey(), new PatchSet(e.getValue()));
-      }
+      b.putAll(state.patchSets());
       patchSets = b.build();
     }
     return patchSets;
@@ -349,12 +355,7 @@
 
   public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovals() {
     if (approvals == null) {
-      ImmutableListMultimap.Builder<PatchSet.Id, PatchSetApproval> b =
-          ImmutableListMultimap.builder();
-      for (Map.Entry<PatchSet.Id, PatchSetApproval> e : state.approvals()) {
-        b.put(e.getKey(), new PatchSetApproval(e.getValue()));
-      }
-      approvals = b.build();
+      approvals = ImmutableListMultimap.copyOf(state.approvals());
     }
     return approvals;
   }
@@ -382,9 +383,24 @@
     return state.reviewerUpdates();
   }
 
-  /** @return an ImmutableSet of Account.Ids of all users that have been assigned to this change. */
+  /**
+   * @return an ImmutableSet of Account.Ids of all users that have been assigned to this change. The
+   *     order of the set is the order in which they were assigned.
+   */
   public ImmutableSet<Account.Id> getPastAssignees() {
-    return state.pastAssignees();
+    return Lists.reverse(state.assigneeUpdates()).stream()
+        .map(AssigneeStatusUpdate::currentAssignee)
+        .filter(Optional::isPresent)
+        .map(Optional::get)
+        .collect(toImmutableSet());
+  }
+
+  /**
+   * @return an ImmutableList of AssigneeStatusUpdate of all the updates to the assignee field to
+   *     this change. The order of the list is from most recent updates to least recent.
+   */
+  public ImmutableList<AssigneeStatusUpdate> getAssigneeUpdates() {
+    return state.assigneeUpdates();
   }
 
   /** @return a ImmutableSet of all hashtags for this change sorted in alphabetical order. */
@@ -411,7 +427,7 @@
   }
 
   /** @return inline comments on each revision. */
-  public ImmutableListMultimap<RevId, Comment> getComments() {
+  public ImmutableListMultimap<ObjectId, Comment> getComments() {
     return state.publishedComments();
   }
 
@@ -426,11 +442,15 @@
     return commentKeys;
   }
 
-  public ImmutableListMultimap<RevId, Comment> getDraftComments(Account.Id author) {
+  public int getUpdateCount() {
+    return state.updateCount();
+  }
+
+  public ImmutableListMultimap<ObjectId, Comment> getDraftComments(Account.Id author) {
     return getDraftComments(author, null);
   }
 
-  public ImmutableListMultimap<RevId, Comment> getDraftComments(
+  public ImmutableListMultimap<ObjectId, Comment> getDraftComments(
       Account.Id author, @Nullable Ref ref) {
     loadDraftComments(author, ref);
     // Filter out any zombie draft comments. These are drafts that are also in
@@ -441,7 +461,7 @@
             draftCommentNotes.getComments(), e -> !getCommentKeys().contains(e.getValue().key)));
   }
 
-  public ImmutableListMultimap<RevId, RobotComment> getRobotComments() {
+  public ImmutableListMultimap<ObjectId, RobotComment> getRobotComments() {
     loadRobotComments();
     return robotCommentNotes.getComments();
   }
@@ -516,6 +536,19 @@
     ChangeNotesCache.Value v =
         args.cache.get().get(getProjectName(), getChangeId(), rev, handle::walk);
     state = v.state();
+
+    String stateServerId = state.serverId();
+    /**
+     * In earlier Gerrit versions serverId wasn't part of the change notes cache. That's why the
+     * earlier cached entries don't have the serverId attribute. That's fine because in earlier
+     * gerrit version serverId was already validated. Another approach to simplify the check would
+     * be to bump the cache version, but that would invalidate all persistent cache entries, what we
+     * rather try to avoid.
+     */
+    if (!Strings.isNullOrEmpty(stateServerId) && !args.serverId.equals(stateServerId)) {
+      throw new InvalidServerIdException(args.serverId, stateServerId);
+    }
+
     state.copyColumnsTo(change);
     revisionNoteMap = v.revisionNoteMap();
   }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index e2af855..7fde297 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -20,10 +20,10 @@
 import com.google.common.collect.Table;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.proto.Protos;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.cache.CacheModule;
@@ -61,7 +61,7 @@
             .weigher(Weigher.class)
             .maximumWeight(10 << 20)
             .diskLimit(-1)
-            .version(1)
+            .version(2)
             .keySerializer(Key.Serializer.INSTANCE)
             .valueSerializer(ChangeNotesState.Serializer.INSTANCE);
       }
@@ -98,8 +98,8 @@
       public Key deserialize(byte[] in) {
         ChangeNotesKeyProto proto = Protos.parseUnchecked(ChangeNotesKeyProto.parser(), in);
         return Key.create(
-            new Project.NameKey(proto.getProject()),
-            new Change.Id(proto.getChangeId()),
+            Project.nameKey(proto.getProject()),
+            Change.id(proto.getChangeId()),
             ObjectIdConverter.create().fromByteString(proto.getId()));
       }
     }
@@ -112,8 +112,11 @@
     // Single pointer overhead.
     private static final int P = 8;
 
+    // Single int overhead.
+    private static final int I = 4;
+
     // Single IntKey overhead.
-    private static final int K = O + 4;
+    private static final int K = O + I;
 
     // Single Timestamp overhead.
     private static final int T = O + 8;
@@ -145,11 +148,8 @@
           + str(state.columns().originalSubject())
           + P
           + str(state.columns().submissionId())
-          + ptr(state.columns().assignee(), K) // assignee
           + P // status
           + P
-          + set(state.pastAssignees(), K)
-          + P
           + set(state.hashtags(), str(10))
           + P
           + list(state.patchSets(), patchSet())
@@ -166,6 +166,8 @@
           + P
           + list(state.reviewerUpdates(), 4 * O + K + K + P)
           + P
+          + list(state.assigneeUpdates(), 4 * O + K + K)
+          + P
           + list(state.submitRecords(), P + list(2, str(4) + P + K) + P)
           + P
           + list(state.changeMessages(), changeMessage())
@@ -173,11 +175,8 @@
           + map(state.publishedComments().asMap(), comment())
           + 1 // isPrivate
           + 1 // workInProgress
-          + 1; // reviewStarted
-    }
-
-    private static int ptr(Object o, int size) {
-      return o != null ? P + size : P;
+          + 1 // reviewStarted
+          + I; // updateCount
     }
 
     private static int str(String s) {
@@ -351,12 +350,7 @@
           "Load change notes for change %s of project %s", key.changeId(), key.project());
       ChangeNotesParser parser =
           new ChangeNotesParser(
-              key.changeId(),
-              key.id(),
-              walkSupplier.get(),
-              args.changeNoteJson,
-              args.legacyChangeNoteRead,
-              args.metrics);
+              key.changeId(), key.id(), walkSupplier.get(), args.changeNoteJson, args.metrics);
       ChangeNotesState result = parser.parseAll();
       // This assignment only happens if call() was actually called, which only
       // happens when Cache#get(K, Callable<V>) incurs a cache miss.
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index d3b34b9..60162cd 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -40,7 +40,6 @@
 import static java.util.Comparator.comparing;
 import static java.util.stream.Collectors.joining;
 
-import com.google.auto.value.AutoValue;
 import com.google.common.base.Enums;
 import com.google.common.base.Splitter;
 import com.google.common.collect.HashBasedTable;
@@ -48,6 +47,7 @@
 import com.google.common.collect.ImmutableTable;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Table;
@@ -56,18 +56,17 @@
 import com.google.common.primitives.Ints;
 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.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.LabelId;
+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.Timer1;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
@@ -101,26 +100,8 @@
 class ChangeNotesParser {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  // Sentinel RevId indicating a mutable field on a patch set was parsed, but
-  // the parser does not yet know its commit SHA-1.
-  private static final RevId PARTIAL_PATCH_SET = new RevId("INVALID PARTIAL PATCH SET");
-
-  @AutoValue
-  abstract static class ApprovalKey {
-    abstract PatchSet.Id psId();
-
-    abstract Account.Id accountId();
-
-    abstract String label();
-
-    private static ApprovalKey create(PatchSet.Id psId, Account.Id accountId, String label) {
-      return new AutoValue_ChangeNotesParser_ApprovalKey(psId, accountId, label);
-    }
-  }
-
   // Private final members initialized in the constructor.
   private final ChangeNoteJson changeNoteJson;
-  private final LegacyChangeNoteRead legacyChangeNoteRead;
 
   private final NoteDbMetrics metrics;
   private final Change.Id id;
@@ -133,26 +114,26 @@
   private final Table<Address, ReviewerStateInternal, Timestamp> reviewersByEmail;
   private final List<Account.Id> allPastReviewers;
   private final List<ReviewerStatusUpdate> reviewerUpdates;
+  private final List<AssigneeStatusUpdate> assigneeUpdates;
   private final List<SubmitRecord> submitRecords;
-  private final ListMultimap<RevId, Comment> comments;
-  private final Map<PatchSet.Id, PatchSet> patchSets;
+  private final ListMultimap<ObjectId, Comment> comments;
+  private final Map<PatchSet.Id, PatchSet.Builder> patchSets;
   private final Set<PatchSet.Id> deletedPatchSets;
   private final Map<PatchSet.Id, PatchSetState> patchSetStates;
   private final List<PatchSet.Id> currentPatchSets;
-  private final Map<ApprovalKey, PatchSetApproval> approvals;
-  private final List<PatchSetApproval> bufferedApprovals;
+  private final Map<PatchSetApproval.Key, PatchSetApproval.Builder> approvals;
+  private final List<PatchSetApproval.Builder> bufferedApprovals;
   private final List<ChangeMessage> allChangeMessages;
 
   // Non-final private members filled in during the parsing process.
   private String branch;
   private Change.Status status;
   private String topic;
-  private Optional<Account.Id> assignee;
-  private List<Account.Id> pastAssignees;
   private Set<String> hashtags;
   private Timestamp createdOn;
   private Timestamp lastUpdatedOn;
   private Account.Id ownerId;
+  private String serverId;
   private String changeId;
   private String subject;
   private String originalSubject;
@@ -166,19 +147,18 @@
   private ReviewerSet pendingReviewers;
   private ReviewerByEmailSet pendingReviewersByEmail;
   private Change.Id revertOf;
+  private int updateCount;
 
   ChangeNotesParser(
       Change.Id changeId,
       ObjectId tip,
       ChangeNotesRevWalk walk,
       ChangeNoteJson changeNoteJson,
-      LegacyChangeNoteRead legacyChangeNoteRead,
       NoteDbMetrics metrics) {
     this.id = changeId;
     this.tip = tip;
     this.walk = walk;
     this.changeNoteJson = changeNoteJson;
-    this.legacyChangeNoteRead = legacyChangeNoteRead;
     this.metrics = metrics;
     approvals = new LinkedHashMap<>();
     bufferedApprovals = new ArrayList<>();
@@ -188,6 +168,7 @@
     pendingReviewersByEmail = ReviewerByEmailSet.empty();
     allPastReviewers = new ArrayList<>();
     reviewerUpdates = new ArrayList<>();
+    assigneeUpdates = new ArrayList<>();
     submitRecords = Lists.newArrayListWithExpectedSize(1);
     allChangeMessages = new ArrayList<>();
     comments = MultimapBuilder.hashKeys().arrayListValues().build();
@@ -204,7 +185,7 @@
     walk.reset();
     walk.markStart(walk.parseCommit(tip));
 
-    try (Timer1.Context timer = metrics.parseLatency.start(CHANGES)) {
+    try (Timer1.Context<NoteDbTable> timer = metrics.parseLatency.start(CHANGES)) {
       ChangeNotesCommit commit;
       while ((commit = walk.next()) != null) {
         parse(commit);
@@ -232,25 +213,24 @@
     return revisionNoteMap;
   }
 
-  private ChangeNotesState buildState() {
+  private ChangeNotesState buildState() throws ConfigInvalidException {
     return ChangeNotesState.create(
         tip.copy(),
         id,
-        new Change.Key(changeId),
+        Change.key(changeId),
         createdOn,
         lastUpdatedOn,
         ownerId,
+        serverId,
         branch,
         buildCurrentPatchSetId(),
         subject,
         topic,
         originalSubject,
         submissionId,
-        assignee != null ? assignee.orElse(null) : null,
         status,
-        Sets.newLinkedHashSet(Lists.reverse(pastAssignees)),
         firstNonNull(hashtags, ImmutableSet.of()),
-        patchSets,
+        buildPatchSets(),
         buildApprovals(),
         ReviewerSet.fromTable(Tables.transpose(reviewers)),
         ReviewerByEmailSet.fromTable(Tables.transpose(reviewersByEmail)),
@@ -258,20 +238,37 @@
         pendingReviewersByEmail,
         allPastReviewers,
         buildReviewerUpdates(),
+        assigneeUpdates,
         submitRecords,
         buildAllMessages(),
         comments,
         firstNonNull(isPrivate, false),
         firstNonNull(workInProgress, false),
         firstNonNull(hasReviewStarted, true),
-        revertOf);
+        revertOf,
+        updateCount);
+  }
+
+  private Map<PatchSet.Id, PatchSet> buildPatchSets() throws ConfigInvalidException {
+    Map<PatchSet.Id, PatchSet> result = Maps.newHashMapWithExpectedSize(patchSets.size());
+    for (Map.Entry<PatchSet.Id, PatchSet.Builder> e : patchSets.entrySet()) {
+      try {
+        PatchSet ps = e.getValue().build();
+        result.put(ps.id(), ps);
+      } catch (Exception ex) {
+        ConfigInvalidException cie = parseException("Error building patch set %s", e.getKey());
+        cie.initCause(ex);
+        throw cie;
+      }
+    }
+    return result;
   }
 
   private PatchSet.Id buildCurrentPatchSetId() {
     // currentPatchSets are in parse order, i.e. newest first. Pick the first
     // patch set that was marked as current, excluding deleted patch sets.
     for (PatchSet.Id psId : currentPatchSets) {
-      if (patchSets.containsKey(psId)) {
+      if (patchSetCommitParsed(psId)) {
         return psId;
       }
     }
@@ -281,14 +278,14 @@
   private ListMultimap<PatchSet.Id, PatchSetApproval> buildApprovals() {
     ListMultimap<PatchSet.Id, PatchSetApproval> result =
         MultimapBuilder.hashKeys().arrayListValues().build();
-    for (PatchSetApproval a : approvals.values()) {
-      if (!patchSets.containsKey(a.getPatchSetId())) {
+    for (PatchSetApproval.Builder a : approvals.values()) {
+      if (!patchSetCommitParsed(a.key().patchSetId())) {
         continue; // Patch set deleted or missing.
-      } else if (allPastReviewers.contains(a.getAccountId())
-          && !reviewers.containsRow(a.getAccountId())) {
+      } else if (allPastReviewers.contains(a.key().accountId())
+          && !reviewers.containsRow(a.key().accountId())) {
         continue; // Reviewer was explicitly removed.
       }
-      result.put(a.getPatchSetId(), a);
+      result.put(a.key().patchSetId(), a.build());
     }
     result.keySet().forEach(k -> result.get(k).sort(ChangeNotes.PSA_BY_TIME));
     return result;
@@ -311,6 +308,7 @@
   }
 
   private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
+    updateCount++;
     Timestamp ts = new Timestamp(commit.getCommitterIdent().getWhen().getTime());
 
     createdOn = ts;
@@ -334,6 +332,10 @@
     Account.Id accountId = parseIdent(commit);
     if (accountId != null) {
       ownerId = accountId;
+      PersonIdent personIdent = commit.getAuthorIdent();
+      serverId = NoteDbUtil.extractHostPartFromPersonIdent(personIdent);
+    } else {
+      serverId = "UNKNOWN_SERVER_ID";
     }
     Account.Id realAccountId = parseRealAccountId(commit, accountId);
 
@@ -355,17 +357,29 @@
     }
 
     parseHashtags(commit);
-    parseAssignee(commit);
+    parseAssigneeUpdates(ts, commit);
 
     if (submissionId == null) {
       submissionId = parseSubmissionId(commit);
     }
 
+    if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) {
+      lastUpdatedOn = ts;
+    }
+
+    if (deletedPatchSets.contains(psId)) {
+      // Do not update PS details as PS was deleted and this meta data is of no relevance.
+      return;
+    }
+
+    // Parse mutable patch set fields first so they can be recorded in the PendingPatchSetFields.
+    parseDescription(psId, commit);
+    parseGroups(psId, commit);
+
     ObjectId currRev = parseRevision(commit);
     if (currRev != null) {
       parsePatchSet(psId, currRev, accountId, ts);
     }
-    parseGroups(psId, commit);
     parseCurrentPatchSet(psId, commit);
 
     if (submitRecords.isEmpty()) {
@@ -405,12 +419,6 @@
 
     previousWorkInProgressFooter = null;
     parseWorkInProgress(commit);
-
-    if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) {
-      lastUpdatedOn = ts;
-    }
-
-    parseDescription(psId, commit);
   }
 
   private String parseSubmissionId(ChangeNotesCommit commit) throws ConfigInvalidException {
@@ -437,7 +445,7 @@
       return effectiveAccountId;
     }
     PersonIdent ident = RawParseUtils.parsePersonIdent(realUser);
-    return legacyChangeNoteRead.parseIdent(ident, id);
+    return parseIdent(ident);
   }
 
   private String parseTopic(ChangeNotesCommit commit) throws ConfigInvalidException {
@@ -483,24 +491,23 @@
     if (accountId == null) {
       throw parseException("patch set %s requires an identified user as uploader", psId.get());
     }
-    PatchSet ps = patchSets.get(psId);
-    if (ps == null) {
-      ps = new PatchSet(psId);
-      patchSets.put(psId, ps);
-    } else if (!ps.getRevision().equals(PARTIAL_PATCH_SET)) {
-      if (deletedPatchSets.contains(psId)) {
-        // Do not update PS details as PS was deleted and this meta data is of
-        // no relevance
-        return;
-      }
+    if (patchSetCommitParsed(psId)) {
+      ObjectId commitId = patchSets.get(psId).commitId().orElseThrow(IllegalStateException::new);
       throw new ConfigInvalidException(
           String.format(
               "Multiple revisions parsed for patch set %s: %s and %s",
-              psId.get(), patchSets.get(psId).getRevision(), rev.name()));
+              psId.get(), commitId.name(), rev.name()));
     }
-    ps.setRevision(new RevId(rev.name()));
-    ps.setUploader(accountId);
-    ps.setCreatedOn(ts);
+    patchSets
+        .computeIfAbsent(psId, id -> PatchSet.builder())
+        .id(psId)
+        .commitId(rev)
+        .uploader(accountId)
+        .createdOn(ts);
+    // Fields not set here:
+    // * Groups, parsed earlier in parseGroups.
+    // * Description, parsed earlier in parseDescription.
+    // * Push certificate, parsed later in parseNotes.
   }
 
   private void parseGroups(PatchSet.Id psId, ChangeNotesCommit commit)
@@ -509,15 +516,11 @@
     if (groupsStr == null) {
       return;
     }
-    PatchSet ps = patchSets.get(psId);
-    if (ps == null) {
-      ps = new PatchSet(psId);
-      ps.setRevision(PARTIAL_PATCH_SET);
-      patchSets.put(psId, ps);
-    } else if (!ps.getGroups().isEmpty()) {
-      return;
+    checkPatchSetCommitNotParsed(psId, FOOTER_GROUPS);
+    PatchSet.Builder pending = patchSets.computeIfAbsent(psId, id -> PatchSet.builder());
+    if (pending.groups().isEmpty()) {
+      pending.groups(PatchSet.splitGroups(groupsStr));
     }
-    ps.setGroups(PatchSet.splitGroups(groupsStr));
   }
 
   private void parseCurrentPatchSet(PatchSet.Id psId, ChangeNotesCommit commit)
@@ -560,10 +563,8 @@
     }
   }
 
-  private void parseAssignee(ChangeNotesCommit commit) throws ConfigInvalidException {
-    if (pastAssignees == null) {
-      pastAssignees = Lists.newArrayList();
-    }
+  private void parseAssigneeUpdates(Timestamp ts, ChangeNotesCommit commit)
+      throws ConfigInvalidException {
     String assigneeValue = parseOneFooter(commit, FOOTER_ASSIGNEE);
     if (assigneeValue != null) {
       Optional<Account.Id> parsedAssignee;
@@ -572,14 +573,9 @@
         parsedAssignee = Optional.empty();
       } else {
         PersonIdent ident = RawParseUtils.parsePersonIdent(assigneeValue);
-        parsedAssignee = Optional.ofNullable(legacyChangeNoteRead.parseIdent(ident, id));
+        parsedAssignee = Optional.ofNullable(parseIdent(ident));
       }
-      if (assignee == null) {
-        assignee = parsedAssignee;
-      }
-      if (parsedAssignee.isPresent()) {
-        pastAssignees.add(parsedAssignee.get());
-      }
+      assigneeUpdates.add(AssigneeStatusUpdate.create(ts, ownerId, parsedAssignee));
     }
   }
 
@@ -612,9 +608,9 @@
     // exception is the legacy SUBM approval, which is never considered post-submit, but might end
     // up sorted after the submit during rebuilding.
     if (status == Change.Status.MERGED) {
-      for (PatchSetApproval psa : bufferedApprovals) {
-        if (!psa.isLegacySubmit()) {
-          psa.setPostSubmit(true);
+      for (PatchSetApproval.Builder psa : bufferedApprovals) {
+        if (!psa.key().isLegacySubmit()) {
+          psa.postSubmit(true);
         }
       }
     }
@@ -630,7 +626,7 @@
     if (psId == null) {
       throw invalidFooter(FOOTER_PATCH_SET, psIdStr);
     }
-    return new PatchSet.Id(id, psId);
+    return PatchSet.id(id, psId);
   }
 
   private PatchSetState parsePatchSetState(ChangeNotesCommit commit) throws ConfigInvalidException {
@@ -658,16 +654,14 @@
     List<String> descLines = commit.getFooterLineValues(FOOTER_PATCH_SET_DESCRIPTION);
     if (descLines.isEmpty()) {
       return;
-    } else if (descLines.size() == 1) {
+    }
+
+    checkPatchSetCommitNotParsed(psId, FOOTER_PATCH_SET_DESCRIPTION);
+    if (descLines.size() == 1) {
       String desc = descLines.get(0).trim();
-      PatchSet ps = patchSets.get(psId);
-      if (ps == null) {
-        ps = new PatchSet(psId);
-        ps.setRevision(PARTIAL_PATCH_SET);
-        patchSets.put(psId, ps);
-      }
-      if (ps.getDescription() == null) {
-        ps.setDescription(desc);
+      PatchSet.Builder pending = patchSets.computeIfAbsent(psId, p -> PatchSet.builder());
+      if (!pending.description().isPresent()) {
+        pending.description(Optional.of(desc));
       }
     } else {
       throw expectedOneFooter(FOOTER_PATCH_SET_DESCRIPTION, descLines);
@@ -686,8 +680,7 @@
     }
 
     ChangeMessage changeMessage =
-        new ChangeMessage(
-            new ChangeMessage.Key(psId.getParentKey(), commit.name()), accountId, ts, psId);
+        new ChangeMessage(ChangeMessage.key(psId.changeId(), commit.name()), accountId, ts, psId);
     changeMessage.setMessage(changeMsgString.get());
     changeMessage.setTag(tag);
     changeMessage.setRealAuthor(realAccountId);
@@ -713,24 +706,24 @@
     ChangeNotesCommit tipCommit = walk.parseCommit(tip);
     revisionNoteMap =
         RevisionNoteMap.parse(
-            changeNoteJson,
-            legacyChangeNoteRead,
-            id,
-            reader,
-            NoteMap.read(reader, tipCommit),
-            PatchLineComment.Status.PUBLISHED);
-    Map<RevId, ChangeRevisionNote> rns = revisionNoteMap.revisionNotes;
+            changeNoteJson, reader, NoteMap.read(reader, tipCommit), Comment.Status.PUBLISHED);
+    Map<ObjectId, ChangeRevisionNote> rns = revisionNoteMap.revisionNotes;
 
-    for (Map.Entry<RevId, ChangeRevisionNote> e : rns.entrySet()) {
+    for (Map.Entry<ObjectId, ChangeRevisionNote> e : rns.entrySet()) {
       for (Comment c : e.getValue().getEntities()) {
         comments.put(e.getKey(), c);
       }
     }
 
-    for (PatchSet ps : patchSets.values()) {
-      ChangeRevisionNote rn = rns.get(ps.getRevision());
+    for (PatchSet.Builder b : patchSets.values()) {
+      ObjectId commitId =
+          b.commitId()
+              .orElseThrow(
+                  () ->
+                      new IllegalStateException("never parsed commit ID for patch set " + b.id()));
+      ChangeRevisionNote rn = rns.get(commitId);
       if (rn != null && rn.getPushCert() != null) {
-        ps.setPushCertificate(rn.getPushCert());
+        b.pushCertificate(Optional.of(rn.getPushCert()));
       }
     }
   }
@@ -741,7 +734,7 @@
     if (accountId == null) {
       throw parseException("patch set %s requires an identified user as uploader", psId.get());
     }
-    PatchSetApproval psa;
+    PatchSetApproval.Builder psa;
     if (line.startsWith("-")) {
       psa = parseRemoveApproval(psId, accountId, realAccountId, ts, line);
     } else {
@@ -750,7 +743,7 @@
     bufferedApprovals.add(psa);
   }
 
-  private PatchSetApproval parseAddApproval(
+  private PatchSetApproval.Builder parseAddApproval(
       PatchSet.Id psId, Account.Id committerId, Account.Id realAccountId, Timestamp ts, String line)
       throws ConfigInvalidException {
     // There are potentially 3 accounts involved here:
@@ -772,7 +765,7 @@
       labelVoteStr = line.substring(0, s);
       PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
       checkFooter(ident != null, FOOTER_LABEL, line);
-      effectiveAccountId = legacyChangeNoteRead.parseIdent(ident, id);
+      effectiveAccountId = parseIdent(ident);
     } else {
       labelVoteStr = line;
       effectiveAccountId = committerId;
@@ -787,23 +780,20 @@
       throw pe;
     }
 
-    PatchSetApproval psa =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(psId, effectiveAccountId, new LabelId(l.label())),
-            l.value(),
-            ts);
-    psa.setTag(tag);
+    PatchSetApproval.Builder psa =
+        PatchSetApproval.builder()
+            .key(PatchSetApproval.key(psId, effectiveAccountId, LabelId.create(l.label())))
+            .value(l.value())
+            .granted(ts)
+            .tag(Optional.ofNullable(tag));
     if (!Objects.equals(realAccountId, committerId)) {
-      psa.setRealAccountId(realAccountId);
+      psa.realAccountId(realAccountId);
     }
-    ApprovalKey k = ApprovalKey.create(psId, effectiveAccountId, l.label());
-    if (!approvals.containsKey(k)) {
-      approvals.put(k, psa);
-    }
+    approvals.putIfAbsent(psa.key(), psa);
     return psa;
   }
 
-  private PatchSetApproval parseRemoveApproval(
+  private PatchSetApproval.Builder parseRemoveApproval(
       PatchSet.Id psId, Account.Id committerId, Account.Id realAccountId, Timestamp ts, String line)
       throws ConfigInvalidException {
     // See comments in parseAddApproval about the various users involved.
@@ -814,7 +804,7 @@
       label = line.substring(1, s);
       PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
       checkFooter(ident != null, FOOTER_LABEL, line);
-      effectiveAccountId = legacyChangeNoteRead.parseIdent(ident, id);
+      effectiveAccountId = parseIdent(ident);
     } else {
       label = line.substring(1);
       effectiveAccountId = committerId;
@@ -830,16 +820,15 @@
 
     // Store an actual 0-vote approval in the map for a removed approval, because ApprovalCopier
     // needs an actual approval in order to block copying an earlier approval over a later delete.
-    PatchSetApproval remove =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(psId, effectiveAccountId, new LabelId(label)), (short) 0, ts);
+    PatchSetApproval.Builder remove =
+        PatchSetApproval.builder()
+            .key(PatchSetApproval.key(psId, effectiveAccountId, LabelId.create(label)))
+            .value(0)
+            .granted(ts);
     if (!Objects.equals(realAccountId, committerId)) {
-      remove.setRealAccountId(realAccountId);
+      remove.realAccountId(realAccountId);
     }
-    ApprovalKey k = ApprovalKey.create(psId, effectiveAccountId, label);
-    if (!approvals.containsKey(k)) {
-      approvals.put(k, remove);
-    }
+    approvals.putIfAbsent(remove.key(), remove);
     return remove;
   }
 
@@ -874,7 +863,7 @@
           label.label = line.substring(c + 2, c2);
           PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(c2 + 2));
           checkFooter(ident != null, FOOTER_SUBMITTED_WITH, line);
-          label.appliedBy = legacyChangeNoteRead.parseIdent(ident, id);
+          label.appliedBy = parseIdent(ident);
         } else {
           label.label = line.substring(c + 2);
         }
@@ -890,7 +879,7 @@
     if (a.getName().equals(c.getName()) && a.getEmailAddress().equals(c.getEmailAddress())) {
       return null;
     }
-    return legacyChangeNoteRead.parseIdent(commit.getAuthorIdent(), id);
+    return parseIdent(commit.getAuthorIdent());
   }
 
   private void parseReviewer(Timestamp ts, ReviewerStateInternal state, String line)
@@ -899,7 +888,7 @@
     if (ident == null) {
       throw invalidFooter(state.getFooterKey(), line);
     }
-    Account.Id accountId = legacyChangeNoteRead.parseIdent(ident, id);
+    Account.Id accountId = parseIdent(ident);
     reviewerUpdates.add(ReviewerStatusUpdate.create(ts, ownerId, accountId, state));
     if (!reviewers.containsRow(accountId)) {
       reviewers.put(accountId, state, ts);
@@ -976,7 +965,7 @@
     if (revertOf == null) {
       throw invalidFooter(FOOTER_REVERT_OF, footer);
     }
-    return new Change.Id(revertOf);
+    return Change.id(revertOf);
   }
 
   private void pruneReviewers() {
@@ -1003,13 +992,8 @@
 
   private void updatePatchSetStates() {
     Set<PatchSet.Id> missing = new TreeSet<>(comparing(PatchSet.Id::get));
-    for (Iterator<PatchSet> it = patchSets.values().iterator(); it.hasNext(); ) {
-      PatchSet ps = it.next();
-      if (ps.getRevision().equals(PARTIAL_PATCH_SET)) {
-        missing.add(ps.getId());
-        it.remove();
-      }
-    }
+    patchSets.keySet().stream().filter(p -> !patchSetCommitParsed(p)).forEach(p -> missing.add(p));
+
     for (Map.Entry<PatchSet.Id, PatchSetState> e : patchSetStates.entrySet()) {
       switch (e.getValue()) {
         case PUBLISHED:
@@ -1030,10 +1014,10 @@
         pruneEntitiesForMissingPatchSets(allChangeMessages, ChangeMessage::getPatchSetId, missing);
     pruned +=
         pruneEntitiesForMissingPatchSets(
-            comments.values(), c -> new PatchSet.Id(id, c.key.patchSetId), missing);
+            comments.values(), c -> PatchSet.id(id, c.key.patchSetId), missing);
     pruned +=
         pruneEntitiesForMissingPatchSets(
-            approvals.values(), PatchSetApproval::getPatchSetId, missing);
+            approvals.values(), psa -> psa.key().patchSetId(), missing);
 
     if (!missing.isEmpty()) {
       logger.atWarning().log(
@@ -1046,7 +1030,7 @@
     int pruned = 0;
     for (Iterator<T> it = ents.iterator(); it.hasNext(); ) {
       PatchSet.Id psId = psIdFunc.apply(it.next());
-      if (!patchSets.containsKey(psId)) {
+      if (!patchSetCommitParsed(psId)) {
         pruned++;
         missing.add(psId);
         it.remove();
@@ -1089,7 +1073,27 @@
     }
   }
 
+  private void checkPatchSetCommitNotParsed(PatchSet.Id psId, FooterKey footer)
+      throws ConfigInvalidException {
+    if (patchSetCommitParsed(psId)) {
+      throw parseException(
+          "%s field found for patch set %s before patch set was originally defined",
+          footer.getName(), psId.get());
+    }
+  }
+
+  private boolean patchSetCommitParsed(PatchSet.Id psId) {
+    PatchSet.Builder pending = patchSets.get(psId);
+    return pending != null && pending.commitId().isPresent();
+  }
+
   private ConfigInvalidException parseException(String fmt, Object... args) {
     return ChangeNotes.parseException(id, fmt, args);
   }
+
+  private Account.Id parseIdent(PersonIdent ident) throws ConfigInvalidException {
+    return NoteDbUtil.parseIdent(ident)
+        .orElseThrow(
+            () -> parseException("cannot retrieve account id: %s", ident.getEmailAddress()));
+  }
 }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index fd260e7..896cca3 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -18,7 +18,6 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
@@ -35,26 +34,27 @@
 import com.google.common.collect.Table;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.Project;
+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.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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.converter.ChangeMessageProtoConverter;
-import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
-import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
-import com.google.gerrit.reviewdb.converter.ProtoConverter;
+import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AssigneeStatusUpdateProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
@@ -68,6 +68,7 @@
 import java.sql.Timestamp;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -97,15 +98,14 @@
       Timestamp createdOn,
       Timestamp lastUpdatedOn,
       Account.Id owner,
+      String serverId,
       String branch,
       @Nullable PatchSet.Id currentPatchSetId,
       String subject,
       @Nullable String topic,
       @Nullable String originalSubject,
       @Nullable String submissionId,
-      @Nullable Account.Id assignee,
       @Nullable Change.Status status,
-      Set<Account.Id> pastAssignees,
       Set<String> hashtags,
       Map<PatchSet.Id, PatchSet> patchSets,
       ListMultimap<PatchSet.Id, PatchSetApproval> approvals,
@@ -115,13 +115,15 @@
       ReviewerByEmailSet pendingReviewersByEmail,
       List<Account.Id> allPastReviewers,
       List<ReviewerStatusUpdate> reviewerUpdates,
+      List<AssigneeStatusUpdate> assigneeUpdates,
       List<SubmitRecord> submitRecords,
       List<ChangeMessage> changeMessages,
-      ListMultimap<RevId, Comment> publishedComments,
+      ListMultimap<ObjectId, Comment> publishedComments,
       boolean isPrivate,
       boolean workInProgress,
       boolean reviewStarted,
-      @Nullable Change.Id revertOf) {
+      @Nullable Change.Id revertOf,
+      int updateCount) {
     requireNonNull(
         metaId,
         () ->
@@ -146,14 +148,13 @@
                 .topic(topic)
                 .originalSubject(originalSubject)
                 .submissionId(submissionId)
-                .assignee(assignee)
                 .isPrivate(isPrivate)
                 .workInProgress(workInProgress)
                 .reviewStarted(reviewStarted)
                 .revertOf(revertOf)
                 .build())
-        .pastAssignees(pastAssignees)
         .hashtags(hashtags)
+        .serverId(serverId)
         .patchSets(patchSets.entrySet())
         .approvals(approvals.entries())
         .reviewers(reviewers)
@@ -162,9 +163,11 @@
         .pendingReviewersByEmail(pendingReviewersByEmail)
         .allPastReviewers(allPastReviewers)
         .reviewerUpdates(reviewerUpdates)
+        .assigneeUpdates(assigneeUpdates)
         .submitRecords(submitRecords)
         .changeMessages(changeMessages)
         .publishedComments(publishedComments)
+        .updateCount(updateCount)
         .build();
   }
 
@@ -208,9 +211,6 @@
     @Nullable
     abstract String submissionId();
 
-    @Nullable
-    abstract Account.Id assignee();
-
     abstract boolean isPrivate();
 
     abstract boolean workInProgress();
@@ -244,8 +244,6 @@
 
       abstract Builder submissionId(@Nullable String submissionId);
 
-      abstract Builder assignee(@Nullable Account.Id assignee);
-
       abstract Builder status(@Nullable Change.Status status);
 
       abstract Builder isPrivate(boolean isPrivate);
@@ -271,10 +269,11 @@
   abstract ChangeColumns columns();
 
   // Other related to this Change.
-  abstract ImmutableSet<Account.Id> pastAssignees();
-
   abstract ImmutableSet<String> hashtags();
 
+  @Nullable
+  abstract String serverId();
+
   abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSet>> patchSets();
 
   abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSetApproval>> approvals();
@@ -291,11 +290,15 @@
 
   abstract ImmutableList<ReviewerStatusUpdate> reviewerUpdates();
 
+  abstract ImmutableList<AssigneeStatusUpdate> assigneeUpdates();
+
   abstract ImmutableList<SubmitRecord> submitRecords();
 
   abstract ImmutableList<ChangeMessage> changeMessages();
 
-  abstract ImmutableListMultimap<RevId, Comment> publishedComments();
+  abstract ImmutableListMultimap<ObjectId, Comment> publishedComments();
+
+  abstract int updateCount();
 
   Change newChange(Project.NameKey project) {
     ChangeColumns c = requireNonNull(columns(), "columns are required");
@@ -304,7 +307,7 @@
             c.changeKey(),
             changeId(),
             c.owner(),
-            new Branch.NameKey(project, c.branch()),
+            BranchNameKey.create(project, c.branch()),
             c.createdOn());
     copyNonConstructorColumnsTo(change);
     return change;
@@ -318,7 +321,7 @@
         this);
     change.setKey(c.changeKey());
     change.setOwner(c.owner());
-    change.setDest(new Branch.NameKey(change.getProject(), c.branch()));
+    change.setDest(BranchNameKey.create(change.getProject(), c.branch()));
     change.setCreatedOn(c.createdOn());
     copyNonConstructorColumnsTo(change);
   }
@@ -331,7 +334,9 @@
     change.setTopic(Strings.emptyToNull(c.topic()));
     change.setLastUpdatedOn(c.lastUpdatedOn());
     change.setSubmissionId(c.submissionId());
-    change.setAssignee(c.assignee());
+    if (!assigneeUpdates().isEmpty()) {
+      change.setAssignee(assigneeUpdates().get(0).currentAssignee().orElse(null));
+    }
     change.setPrivate(c.isPrivate());
     change.setWorkInProgress(c.workInProgress());
     change.setReviewStarted(c.reviewStarted());
@@ -351,7 +356,6 @@
     static Builder empty(Change.Id changeId) {
       return new AutoValue_ChangeNotesState.Builder()
           .changeId(changeId)
-          .pastAssignees(ImmutableSet.of())
           .hashtags(ImmutableSet.of())
           .patchSets(ImmutableList.of())
           .approvals(ImmutableList.of())
@@ -361,9 +365,11 @@
           .pendingReviewersByEmail(ReviewerByEmailSet.empty())
           .allPastReviewers(ImmutableList.of())
           .reviewerUpdates(ImmutableList.of())
+          .assigneeUpdates(ImmutableList.of())
           .submitRecords(ImmutableList.of())
           .changeMessages(ImmutableList.of())
-          .publishedComments(ImmutableListMultimap.of());
+          .publishedComments(ImmutableListMultimap.of())
+          .updateCount(0);
     }
 
     abstract Builder metaId(ObjectId metaId);
@@ -372,7 +378,7 @@
 
     abstract Builder columns(ChangeColumns columns);
 
-    abstract Builder pastAssignees(Set<Account.Id> pastAssignees);
+    abstract Builder serverId(String serverId);
 
     abstract Builder hashtags(Iterable<String> hashtags);
 
@@ -392,11 +398,15 @@
 
     abstract Builder reviewerUpdates(List<ReviewerStatusUpdate> reviewerUpdates);
 
+    abstract Builder assigneeUpdates(List<AssigneeStatusUpdate> assigneeUpdates);
+
     abstract Builder submitRecords(List<SubmitRecord> submitRecords);
 
     abstract Builder changeMessages(List<ChangeMessage> changeMessages);
 
-    abstract Builder publishedComments(ListMultimap<RevId, Comment> publishedComments);
+    abstract Builder publishedComments(ListMultimap<ObjectId, Comment> publishedComments);
+
+    abstract Builder updateCount(int updateCount);
 
     abstract ChangeNotesState build();
   }
@@ -421,7 +431,10 @@
           .setChangeId(object.changeId().get())
           .setColumns(toChangeColumnsProto(object.columns()));
 
-      object.pastAssignees().forEach(a -> b.addPastAssignee(a.get()));
+      if (object.serverId() != null) {
+        b.setServerId(object.serverId());
+        b.setHasServerId(true);
+      }
       object.hashtags().forEach(b::addHashtag);
       object
           .patchSets()
@@ -452,6 +465,7 @@
 
       object.allPastReviewers().forEach(a -> b.addPastReviewer(a.get()));
       object.reviewerUpdates().forEach(u -> b.addReviewerUpdate(toReviewerStatusUpdateProto(u)));
+      object.assigneeUpdates().forEach(u -> b.addAssigneeUpdate(toAssigneeStatusUpdateProto(u)));
       object
           .submitRecords()
           .forEach(r -> b.addSubmitRecord(GSON.toJson(new StoredSubmitRecord(r))));
@@ -459,6 +473,7 @@
           .changeMessages()
           .forEach(m -> b.addChangeMessage(toByteString(m, ChangeMessageProtoConverter.INSTANCE)));
       object.publishedComments().values().forEach(c -> b.addPublishedComment(GSON.toJson(c)));
+      b.setUpdateCount(object.updateCount());
 
       return Protos.toByteArray(b.build());
     }
@@ -490,9 +505,6 @@
       if (cols.submissionId() != null) {
         b.setSubmissionId(cols.submissionId()).setHasSubmissionId(true);
       }
-      if (cols.assignee() != null) {
-        b.setAssignee(cols.assignee().get()).setHasAssignee(true);
-      }
       if (cols.status() != null) {
         b.setStatus(STATUS_CONVERTER.reverse().convert(cols.status())).setHasStatus(true);
       }
@@ -532,40 +544,47 @@
           .build();
     }
 
+    private static AssigneeStatusUpdateProto toAssigneeStatusUpdateProto(AssigneeStatusUpdate u) {
+      AssigneeStatusUpdateProto.Builder builder =
+          AssigneeStatusUpdateProto.newBuilder()
+              .setDate(u.date().getTime())
+              .setUpdatedBy(u.updatedBy().get())
+              .setHasCurrentAssignee(u.currentAssignee().isPresent());
+
+      u.currentAssignee().ifPresent(assignee -> builder.setCurrentAssignee(assignee.get()));
+      return builder.build();
+    }
+
     @Override
     public ChangeNotesState deserialize(byte[] in) {
       ChangeNotesStateProto proto = Protos.parseUnchecked(ChangeNotesStateProto.parser(), in);
-      Change.Id changeId = new Change.Id(proto.getChangeId());
+      Change.Id changeId = Change.id(proto.getChangeId());
 
       ChangeNotesState.Builder b =
           builder()
               .metaId(ObjectIdConverter.create().fromByteString(proto.getMetaId()))
               .changeId(changeId)
               .columns(toChangeColumns(changeId, proto.getColumns()))
-              .pastAssignees(
-                  proto.getPastAssigneeList().stream()
-                      .map(Account.Id::new)
-                      .collect(toImmutableSet()))
+              .serverId(proto.getHasServerId() ? proto.getServerId() : null)
               .hashtags(proto.getHashtagList())
               .patchSets(
                   proto.getPatchSetList().stream()
                       .map(bytes -> parseProtoFrom(PatchSetProtoConverter.INSTANCE, bytes))
-                      .map(ps -> Maps.immutableEntry(ps.getId(), ps))
+                      .map(ps -> Maps.immutableEntry(ps.id(), ps))
                       .collect(toImmutableList()))
               .approvals(
                   proto.getApprovalList().stream()
                       .map(bytes -> parseProtoFrom(PatchSetApprovalProtoConverter.INSTANCE, bytes))
-                      .map(a -> Maps.immutableEntry(a.getPatchSetId(), a))
+                      .map(a -> Maps.immutableEntry(a.patchSetId(), a))
                       .collect(toImmutableList()))
               .reviewers(toReviewerSet(proto.getReviewerList()))
               .reviewersByEmail(toReviewerByEmailSet(proto.getReviewerByEmailList()))
               .pendingReviewers(toReviewerSet(proto.getPendingReviewerList()))
               .pendingReviewersByEmail(toReviewerByEmailSet(proto.getPendingReviewerByEmailList()))
               .allPastReviewers(
-                  proto.getPastReviewerList().stream()
-                      .map(Account.Id::new)
-                      .collect(toImmutableList()))
+                  proto.getPastReviewerList().stream().map(Account::id).collect(toImmutableList()))
               .reviewerUpdates(toReviewerStatusUpdateList(proto.getReviewerUpdateList()))
+              .assigneeUpdates(toAssigneeStatusUpdateList(proto.getAssigneeUpdateList()))
               .submitRecords(
                   proto.getSubmitRecordList().stream()
                       .map(r -> GSON.fromJson(r, StoredSubmitRecord.class).toSubmitRecord())
@@ -577,7 +596,8 @@
               .publishedComments(
                   proto.getPublishedCommentList().stream()
                       .map(r -> GSON.fromJson(r, Comment.class))
-                      .collect(toImmutableListMultimap(c -> new RevId(c.revId), c -> c)));
+                      .collect(toImmutableListMultimap(Comment::getCommitId, c -> c)))
+              .updateCount(proto.getUpdateCount());
       return b.build();
     }
 
@@ -590,13 +610,13 @@
     private static ChangeColumns toChangeColumns(Change.Id changeId, ChangeColumnsProto proto) {
       ChangeColumns.Builder b =
           ChangeColumns.builder()
-              .changeKey(new Change.Key(proto.getChangeKey()))
+              .changeKey(Change.key(proto.getChangeKey()))
               .createdOn(new Timestamp(proto.getCreatedOn()))
               .lastUpdatedOn(new Timestamp(proto.getLastUpdatedOn()))
-              .owner(new Account.Id(proto.getOwner()))
+              .owner(Account.id(proto.getOwner()))
               .branch(proto.getBranch());
       if (proto.getHasCurrentPatchSetId()) {
-        b.currentPatchSetId(new PatchSet.Id(changeId, proto.getCurrentPatchSetId()));
+        b.currentPatchSetId(PatchSet.id(changeId, proto.getCurrentPatchSetId()));
       }
       b.subject(proto.getSubject());
       if (proto.getHasTopic()) {
@@ -608,9 +628,6 @@
       if (proto.getHasSubmissionId()) {
         b.submissionId(proto.getSubmissionId());
       }
-      if (proto.getHasAssignee()) {
-        b.assignee(new Account.Id(proto.getAssignee()));
-      }
       if (proto.getHasStatus()) {
         b.status(STATUS_CONVERTER.convert(proto.getStatus()));
       }
@@ -618,7 +635,7 @@
           .workInProgress(proto.getWorkInProgress())
           .reviewStarted(proto.getReviewStarted());
       if (proto.getHasRevertOf()) {
-        b.revertOf(new Change.Id(proto.getRevertOf()));
+        b.revertOf(Change.id(proto.getRevertOf()));
       }
       return b.build();
     }
@@ -629,7 +646,7 @@
       for (ReviewerSetEntryProto e : protos) {
         b.put(
             REVIEWER_STATE_CONVERTER.convert(e.getState()),
-            new Account.Id(e.getAccountId()),
+            Account.id(e.getAccountId()),
             new Timestamp(e.getTimestamp()));
       }
       return ReviewerSet.fromTable(b.build());
@@ -655,11 +672,26 @@
         b.add(
             ReviewerStatusUpdate.create(
                 new Timestamp(proto.getDate()),
-                new Account.Id(proto.getUpdatedBy()),
-                new Account.Id(proto.getReviewer()),
+                Account.id(proto.getUpdatedBy()),
+                Account.id(proto.getReviewer()),
                 REVIEWER_STATE_CONVERTER.convert(proto.getState())));
       }
       return b.build();
     }
+
+    private static ImmutableList<AssigneeStatusUpdate> toAssigneeStatusUpdateList(
+        List<AssigneeStatusUpdateProto> protos) {
+      ImmutableList.Builder<AssigneeStatusUpdate> b = ImmutableList.builder();
+      for (AssigneeStatusUpdateProto proto : protos) {
+        b.add(
+            AssigneeStatusUpdate.create(
+                new Timestamp(proto.getDate()),
+                Account.id(proto.getUpdatedBy()),
+                proto.getHasCurrentAssignee()
+                    ? Optional.of(Account.id(proto.getCurrentAssignee()))
+                    : Optional.empty()));
+      }
+      return b.build();
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java b/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
index 66dd5e8..b6443f1 100644
--- a/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
+++ b/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
@@ -16,10 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.primitives.Bytes;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.entities.Comment;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -30,30 +27,16 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.util.MutableInteger;
-import org.eclipse.jgit.util.RawParseUtils;
 
 class ChangeRevisionNote extends RevisionNote<Comment> {
-  private static final byte[] CERT_HEADER = "certificate version ".getBytes(UTF_8);
-  // See org.eclipse.jgit.transport.PushCertificateParser.END_SIGNATURE
-  private static final byte[] END_SIGNATURE = "-----END PGP SIGNATURE-----\n".getBytes(UTF_8);
-
   private final ChangeNoteJson noteJson;
-  private final LegacyChangeNoteRead legacyChangeNoteRead;
-  private final Change.Id changeId;
-  private final PatchLineComment.Status status;
+  private final Comment.Status status;
   private String pushCert;
 
   ChangeRevisionNote(
-      ChangeNoteJson noteJson,
-      LegacyChangeNoteRead legacyChangeNoteRead,
-      Change.Id changeId,
-      ObjectReader reader,
-      ObjectId noteId,
-      PatchLineComment.Status status) {
+      ChangeNoteJson noteJson, ObjectReader reader, ObjectId noteId, Comment.Status status) {
     super(reader, noteId);
-    this.legacyChangeNoteRead = legacyChangeNoteRead;
     this.noteJson = noteJson;
-    this.changeId = changeId;
     this.status = status;
   }
 
@@ -67,29 +50,13 @@
     MutableInteger p = new MutableInteger();
     p.value = offset;
 
-    if (isJson(raw, p.value)) {
-      RevisionNoteData data = parseJson(noteJson, raw, p.value);
-      if (status == PatchLineComment.Status.PUBLISHED) {
-        pushCert = data.pushCert;
-      } else {
-        pushCert = null;
-      }
-      return data.comments;
-    }
-
-    if (status == PatchLineComment.Status.PUBLISHED) {
-      pushCert = parsePushCert(changeId, raw, p);
-      trimLeadingEmptyLines(raw, p);
+    RevisionNoteData data = parseJson(noteJson, raw, p.value);
+    if (status == Comment.Status.PUBLISHED) {
+      pushCert = data.pushCert;
     } else {
       pushCert = null;
     }
-    List<Comment> comments = legacyChangeNoteRead.parseNote(raw, p, changeId);
-    comments.forEach(c -> c.legacyFormat = true);
-    return comments;
-  }
-
-  static boolean isJson(byte[] raw, int offset) {
-    return raw[offset] == '{' || raw[offset] == '[';
+    return data.comments;
   }
 
   private RevisionNoteData parseJson(ChangeNoteJson noteUtil, byte[] raw, int offset)
@@ -99,18 +66,4 @@
       return noteUtil.getGson().fromJson(r, RevisionNoteData.class);
     }
   }
-
-  private static String parsePushCert(Change.Id changeId, byte[] bytes, MutableInteger p)
-      throws ConfigInvalidException {
-    if (RawParseUtils.match(bytes, p.value, CERT_HEADER) < 0) {
-      return null;
-    }
-    int end = Bytes.indexOf(bytes, END_SIGNATURE);
-    if (end < 0) {
-      throw ChangeNotes.parseException(changeId, "invalid push certificate in note");
-    }
-    int start = p.value;
-    p.value = end + END_SIGNATURE.length;
-    return new String(bytes, start, p.value, UTF_8);
-  }
 }
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 0cde363..b2f85fc 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -17,7 +17,7 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.gerrit.entities.RefNames.changeMetaRef;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
@@ -39,7 +39,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_WORK_IN_PROGRESS;
 import static com.google.gerrit.server.notedb.NoteDbUtil.sanitizeFooter;
-import static java.util.Comparator.comparing;
+import static java.util.Comparator.naturalOrder;
 import static java.util.Objects.requireNonNull;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
@@ -50,21 +50,18 @@
 import com.google.common.collect.Table;
 import com.google.common.collect.TreeBasedTable;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
-import com.google.gwtorm.client.IntKey;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
@@ -173,7 +170,7 @@
 
   private static Table<String, Account.Id, Optional<Short>> approvals(
       Comparator<String> nameComparator) {
-    return TreeBasedTable.create(nameComparator, comparing(IntKey::get));
+    return TreeBasedTable.create(nameComparator, naturalOrder());
   }
 
   @AssistedInject
@@ -250,11 +247,6 @@
     checkArgument(!this.submitRecords.isEmpty(), "no submit records specified at submit time");
   }
 
-  @Deprecated // Only until we improve ChangeRebuilder to call merge().
-  public void setSubmissionId(String submissionId) {
-    this.submissionId = submissionId;
-  }
-
   public void setSubjectForCommit(String commitSubject) {
     this.commitSubject = commitSubject;
   }
@@ -280,18 +272,14 @@
     this.psDescription = psDescription;
   }
 
-  public void putComment(PatchLineComment.Status status, Comment c) {
+  public void putComment(Comment.Status status, Comment c) {
     verifyComment(c);
     createDraftUpdateIfNull();
-    if (status == PatchLineComment.Status.DRAFT) {
+    if (status == Comment.Status.DRAFT) {
       draftUpdate.putComment(c);
     } else {
       comments.add(c);
-      // Always delete the corresponding comment from drafts. Published comments
-      // are immutable, meaning in normal operation we only hit this path when
-      // publishing a comment. It's exactly in that case that we have to delete
-      // the draft.
-      draftUpdate.deleteComment(c);
+      draftUpdate.markCommentPublished(c);
     }
   }
 
@@ -421,7 +409,7 @@
   }
 
   public void setRevertOf(int revertOf) {
-    int ownId = getChange().getId().get();
+    int ownId = getId().get();
     checkArgument(ownId != revertOf, "A change cannot revert itself");
     this.revertOf = revertOf;
     rootOnly = true;
@@ -438,18 +426,18 @@
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
     for (Comment c : comments) {
       c.tag = tag;
-      cache.get(new RevId(c.revId)).putComment(c);
+      cache.get(c.getCommitId()).putComment(c);
     }
     if (pushCert != null) {
       checkState(commit != null);
-      cache.get(new RevId(commit)).setPushCertificate(pushCert);
+      cache.get(ObjectId.fromString(commit)).setPushCertificate(pushCert);
     }
-    Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders();
+    Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
     checkComments(rnm.revisionNotes, builders);
 
-    for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
+    for (Map.Entry<ObjectId, RevisionNoteBuilder> e : builders.entrySet()) {
       ObjectId data = inserter.insert(OBJ_BLOB, e.getValue().build(noteUtil.getChangeNoteJson()));
-      rnm.noteMap.set(ObjectId.fromString(e.getKey().get()), data);
+      rnm.noteMap.set(e.getKey(), data);
     }
 
     return rnm.noteMap.writeTree(inserter);
@@ -473,16 +461,12 @@
     // Even though reading from changes might not be enabled, we need to
     // parse any existing revision notes so we can merge them.
     return RevisionNoteMap.parse(
-        noteUtil.getChangeNoteJson(),
-        noteUtil.getLegacyChangeNoteRead(),
-        getId(),
-        rw.getObjectReader(),
-        noteMap,
-        PatchLineComment.Status.PUBLISHED);
+        noteUtil.getChangeNoteJson(), rw.getObjectReader(), noteMap, Comment.Status.PUBLISHED);
   }
 
   private void checkComments(
-      Map<RevId, ChangeRevisionNote> existingNotes, Map<RevId, RevisionNoteBuilder> toUpdate) {
+      Map<ObjectId, ChangeRevisionNote> existingNotes,
+      Map<ObjectId, RevisionNoteBuilder> toUpdate) {
     // Prohibit various kinds of illegal operations on comments.
     Set<Comment.Key> existing = new HashSet<>();
     for (ChangeRevisionNote rn : existingNotes.values()) {
@@ -504,7 +488,7 @@
           // separate commit. But note that we don't care much about the commit
           // graph of the draft ref, particularly because the ref is completely
           // deleted when all drafts are gone.
-          draftUpdate.deleteComment(c.revId, c.key);
+          draftUpdate.deleteComment(c.getCommitId(), c.key);
         }
       }
     }
@@ -524,6 +508,12 @@
   }
 
   @Override
+  protected boolean bypassMaxUpdates() {
+    // Allow abandoning or submitting a change even if it would exceed the max update count.
+    return status != null && status.isClosed();
+  }
+
+  @Override
   protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
       throws IOException {
     checkState(
diff --git a/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java b/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
index 8fb28e1..b555fdb 100644
--- a/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
+import static java.util.Objects.requireNonNull;
 import static org.eclipse.jgit.util.RawParseUtils.decode;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.util.Optional;
@@ -46,7 +46,7 @@
 
   DeleteChangeMessageRewriter(Change.Id changeId, String targetMessageId, String newChangeMessage) {
     this.changeId = changeId;
-    this.targetMessageId = checkNotNull(targetMessageId);
+    this.targetMessageId = requireNonNull(targetMessageId);
     this.newChangeMessage = newChangeMessage;
   }
 
diff --git a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
index c100550..9c8b369 100644
--- a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -15,16 +15,15 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED;
+import static com.google.gerrit.entities.Comment.Status;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toMap;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.RefNames;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -96,13 +95,13 @@
     ObjectReader reader = revWalk.getObjectReader();
     RevCommit newTipCommit = revWalk.next(); // The first commit will not be rewritten.
     Map<String, Comment> parentComments =
-        getPublishedComments(noteUtil, changeId, reader, NoteMap.read(reader, newTipCommit));
+        getPublishedComments(noteUtil, reader, NoteMap.read(reader, newTipCommit));
 
     boolean rewrite = false;
     RevCommit originalCommit;
     while ((originalCommit = revWalk.next()) != null) {
       NoteMap noteMap = NoteMap.read(reader, originalCommit);
-      Map<String, Comment> currComments = getPublishedComments(noteUtil, changeId, reader, noteMap);
+      Map<String, Comment> currComments = getPublishedComments(noteUtil, reader, noteMap);
 
       if (!rewrite && currComments.containsKey(uuid)) {
         rewrite = true;
@@ -132,28 +131,18 @@
    */
   @VisibleForTesting
   public static Map<String, Comment> getPublishedComments(
-      ChangeNoteJson changeNoteJson,
-      LegacyChangeNoteRead legacyChangeNoteRead,
-      Change.Id changeId,
-      ObjectReader reader,
-      NoteMap noteMap)
+      ChangeNoteJson changeNoteJson, ObjectReader reader, NoteMap noteMap)
       throws IOException, ConfigInvalidException {
-    return RevisionNoteMap.parse(
-            changeNoteJson, legacyChangeNoteRead, changeId, reader, noteMap, PUBLISHED)
-        .revisionNotes.values().stream()
+    return RevisionNoteMap.parse(changeNoteJson, reader, noteMap, Status.PUBLISHED).revisionNotes
+        .values().stream()
         .flatMap(n -> n.getEntities().stream())
         .collect(toMap(c -> c.key.uuid, Function.identity()));
   }
 
   public static Map<String, Comment> getPublishedComments(
-      ChangeNoteUtil noteUtil, Change.Id changeId, ObjectReader reader, NoteMap noteMap)
+      ChangeNoteUtil noteUtil, ObjectReader reader, NoteMap noteMap)
       throws IOException, ConfigInvalidException {
-    return getPublishedComments(
-        noteUtil.getChangeNoteJson(),
-        noteUtil.getLegacyChangeNoteRead(),
-        changeId,
-        reader,
-        noteMap);
+    return getPublishedComments(noteUtil.getChangeNoteJson(), reader, noteMap);
   }
   /**
    * Gets the comments put in by the current commit. The message of the target comment will be
@@ -216,24 +205,22 @@
     RevisionNoteMap<ChangeRevisionNote> revNotesMap =
         RevisionNoteMap.parse(
             noteUtil.getChangeNoteJson(),
-            noteUtil.getLegacyChangeNoteRead(),
-            changeId,
             reader,
             NoteMap.read(reader, parentCommit),
-            PUBLISHED);
+            Status.PUBLISHED);
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(revNotesMap);
 
     for (Comment c : putInComments) {
-      cache.get(new RevId(c.revId)).putComment(c);
+      cache.get(c.getCommitId()).putComment(c);
     }
 
     for (Comment c : deletedComments) {
-      cache.get(new RevId(c.revId)).deleteComment(c.key);
+      cache.get(c.getCommitId()).deleteComment(c.key);
     }
 
-    Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders();
-    for (Map.Entry<RevId, RevisionNoteBuilder> entry : builders.entrySet()) {
-      ObjectId objectId = ObjectId.fromString(entry.getKey().get());
+    Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
+    for (Map.Entry<ObjectId, RevisionNoteBuilder> entry : builders.entrySet()) {
+      ObjectId objectId = entry.getKey();
       byte[] data = entry.getValue().build(noteUtil.getChangeNoteJson());
       if (data.length == 0) {
         revNotesMap.noteMap.remove(objectId);
diff --git a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
index 495ac65..128e185 100644
--- a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
+++ b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
@@ -18,8 +18,8 @@
 
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 213613e..3966396 100644
--- a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
+import static com.google.gerrit.entities.RefNames.refsDraftComments;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -24,12 +24,10 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Project;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
@@ -52,7 +50,7 @@
   private final Account.Id author;
   private final Ref ref;
 
-  private ImmutableListMultimap<RevId, Comment> comments;
+  private ImmutableListMultimap<ObjectId, Comment> comments;
   private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
 
   @AssistedInject
@@ -82,7 +80,7 @@
     return author;
   }
 
-  public ImmutableListMultimap<RevId, Comment> getComments() {
+  public ImmutableListMultimap<ObjectId, Comment> getComments() {
     return comments;
   }
 
@@ -122,16 +120,11 @@
     ObjectReader reader = handle.walk().getObjectReader();
     revisionNoteMap =
         RevisionNoteMap.parse(
-            args.changeNoteJson,
-            args.legacyChangeNoteRead,
-            getChangeId(),
-            reader,
-            NoteMap.read(reader, tipCommit),
-            PatchLineComment.Status.DRAFT);
-    ListMultimap<RevId, Comment> cs = MultimapBuilder.hashKeys().arrayListValues().build();
+            args.changeNoteJson, reader, NoteMap.read(reader, tipCommit), Comment.Status.DRAFT);
+    ListMultimap<ObjectId, Comment> cs = MultimapBuilder.hashKeys().arrayListValues().build();
     for (ChangeRevisionNote rn : revisionNoteMap.revisionNotes.values()) {
       for (Comment c : rn.getEntities()) {
-        cs.put(new RevId(c.revId), c);
+        cs.put(c.getCommitId(), c);
       }
     }
     comments = ImmutableListMultimap.copyOf(cs);
diff --git a/java/com/google/gerrit/server/notedb/IntBlob.java b/java/com/google/gerrit/server/notedb/IntBlob.java
index cbc933c..5efc2b0 100644
--- a/java/com/google/gerrit/server/notedb/IntBlob.java
+++ b/java/com/google/gerrit/server/notedb/IntBlob.java
@@ -23,9 +23,9 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import java.io.IOException;
 import java.util.Optional;
diff --git a/java/com/google/gerrit/server/notedb/InvalidServerIdException.java b/java/com/google/gerrit/server/notedb/InvalidServerIdException.java
new file mode 100644
index 0000000..f79e07c
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/InvalidServerIdException.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.notedb;
+
+public class InvalidServerIdException extends IllegalStateException {
+  private static final long serialVersionUID = 5302751510361680907L;
+
+  public InvalidServerIdException(String expectedServerId, String actualServerId) {
+    super(
+        String.format(
+            "invalid server id, expected %s: actual: %s", expectedServerId, actualServerId));
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java b/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java
deleted file mode 100644
index 916cc16..0000000
--- a/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java
+++ /dev/null
@@ -1,401 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.config.GerritServerId;
-import com.google.inject.Inject;
-import java.sql.Timestamp;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.util.GitDateParser;
-import org.eclipse.jgit.util.MutableInteger;
-import org.eclipse.jgit.util.QuotedString;
-import org.eclipse.jgit.util.RawParseUtils;
-
-public class LegacyChangeNoteRead {
-  private final String serverId;
-
-  @Inject
-  public LegacyChangeNoteRead(@GerritServerId String serverId) {
-    this.serverId = serverId;
-  }
-
-  public Account.Id parseIdent(PersonIdent ident, Change.Id changeId)
-      throws ConfigInvalidException {
-    return NoteDbUtil.parseIdent(ident, serverId)
-        .orElseThrow(
-            () ->
-                parseException(
-                    changeId,
-                    "invalid identity, expected <id>@%s: %s",
-                    serverId,
-                    ident.getEmailAddress()));
-  }
-
-  private static boolean match(byte[] note, MutableInteger p, byte[] expected) {
-    int m = RawParseUtils.match(note, p.value, expected);
-    return m == p.value + expected.length;
-  }
-
-  public List<Comment> parseNote(byte[] note, MutableInteger p, Change.Id changeId)
-      throws ConfigInvalidException {
-    if (p.value >= note.length) {
-      return ImmutableList.of();
-    }
-    Set<Comment.Key> seen = new HashSet<>();
-    List<Comment> result = new ArrayList<>();
-    int sizeOfNote = note.length;
-    byte[] psb = ChangeNoteUtil.PATCH_SET.getBytes(UTF_8);
-    byte[] bpsb = ChangeNoteUtil.BASE_PATCH_SET.getBytes(UTF_8);
-    byte[] bpn = ChangeNoteUtil.PARENT_NUMBER.getBytes(UTF_8);
-
-    RevId revId = new RevId(parseStringField(note, p, changeId, ChangeNoteUtil.REVISION));
-    String fileName = null;
-    PatchSet.Id psId = null;
-    boolean isForBase = false;
-    Integer parentNumber = null;
-
-    while (p.value < sizeOfNote) {
-      boolean matchPs = match(note, p, psb);
-      boolean matchBase = match(note, p, bpsb);
-      if (matchPs) {
-        fileName = null;
-        psId = parsePsId(note, p, changeId, ChangeNoteUtil.PATCH_SET);
-        isForBase = false;
-      } else if (matchBase) {
-        fileName = null;
-        psId = parsePsId(note, p, changeId, ChangeNoteUtil.BASE_PATCH_SET);
-        isForBase = true;
-        if (match(note, p, bpn)) {
-          parentNumber = parseParentNumber(note, p, changeId);
-        }
-      } else if (psId == null) {
-        throw parseException(
-            changeId,
-            "missing %s or %s header",
-            ChangeNoteUtil.PATCH_SET,
-            ChangeNoteUtil.BASE_PATCH_SET);
-      }
-
-      Comment c = parseComment(note, p, fileName, psId, revId, isForBase, parentNumber);
-      fileName = c.key.filename;
-      if (!seen.add(c.key)) {
-        throw parseException(changeId, "multiple comments for %s in note", c.key);
-      }
-      result.add(c);
-    }
-    return result;
-  }
-
-  private Comment parseComment(
-      byte[] note,
-      MutableInteger curr,
-      String currentFileName,
-      PatchSet.Id psId,
-      RevId revId,
-      boolean isForBase,
-      Integer parentNumber)
-      throws ConfigInvalidException {
-    Change.Id changeId = psId.getParentKey();
-
-    // Check if there is a new file.
-    boolean newFile =
-        (RawParseUtils.match(note, curr.value, ChangeNoteUtil.FILE.getBytes(UTF_8))) != -1;
-    if (newFile) {
-      // If so, parse the new file name.
-      currentFileName = parseFilename(note, curr, changeId);
-    } else if (currentFileName == null) {
-      throw parseException(changeId, "could not parse %s", ChangeNoteUtil.FILE);
-    }
-
-    CommentRange range = parseCommentRange(note, curr);
-    if (range == null) {
-      throw parseException(changeId, "could not parse %s", ChangeNoteUtil.COMMENT_RANGE);
-    }
-
-    Timestamp commentTime = parseTimestamp(note, curr, changeId);
-    Account.Id aId = parseAuthor(note, curr, changeId, ChangeNoteUtil.AUTHOR);
-    boolean hasRealAuthor =
-        (RawParseUtils.match(note, curr.value, ChangeNoteUtil.REAL_AUTHOR.getBytes(UTF_8))) != -1;
-    Account.Id raId = null;
-    if (hasRealAuthor) {
-      raId = parseAuthor(note, curr, changeId, ChangeNoteUtil.REAL_AUTHOR);
-    }
-
-    boolean hasParent =
-        (RawParseUtils.match(note, curr.value, ChangeNoteUtil.PARENT.getBytes(UTF_8))) != -1;
-    String parentUUID = null;
-    boolean unresolved = false;
-    if (hasParent) {
-      parentUUID = parseStringField(note, curr, changeId, ChangeNoteUtil.PARENT);
-    }
-    boolean hasUnresolved =
-        (RawParseUtils.match(note, curr.value, ChangeNoteUtil.UNRESOLVED.getBytes(UTF_8))) != -1;
-    if (hasUnresolved) {
-      unresolved = parseBooleanField(note, curr, changeId, ChangeNoteUtil.UNRESOLVED);
-    }
-
-    String uuid = parseStringField(note, curr, changeId, ChangeNoteUtil.UUID);
-
-    boolean hasTag =
-        (RawParseUtils.match(note, curr.value, ChangeNoteUtil.TAG.getBytes(UTF_8))) != -1;
-    String tag = null;
-    if (hasTag) {
-      tag = parseStringField(note, curr, changeId, ChangeNoteUtil.TAG);
-    }
-
-    int commentLength = parseCommentLength(note, curr, changeId);
-
-    String message = RawParseUtils.decode(UTF_8, note, curr.value, curr.value + commentLength);
-    checkResult(message, "message contents", changeId);
-
-    Comment c =
-        new Comment(
-            new Comment.Key(uuid, currentFileName, psId.get()),
-            aId,
-            commentTime,
-            isForBase ? (short) (parentNumber == null ? 0 : -parentNumber) : (short) 1,
-            message,
-            serverId,
-            unresolved);
-    c.lineNbr = range.getEndLine();
-    c.parentUuid = parentUUID;
-    c.tag = tag;
-    c.setRevId(revId);
-    if (raId != null) {
-      c.setRealAuthor(raId);
-    }
-
-    if (range.getStartCharacter() != -1) {
-      c.setRange(range);
-    }
-
-    curr.value = RawParseUtils.nextLF(note, curr.value + commentLength);
-    curr.value = RawParseUtils.nextLF(note, curr.value);
-    return c;
-  }
-
-  private static String parseStringField(
-      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
-      throws ConfigInvalidException {
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    checkHeaderLineFormat(note, curr, fieldName, changeId);
-    int startOfField = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
-    curr.value = endOfLine;
-    return RawParseUtils.decode(UTF_8, note, startOfField, endOfLine - 1);
-  }
-
-  /**
-   * @return a comment range. If the comment range line in the note only has one number, we return a
-   *     CommentRange with that one number as the end line and the other fields as -1. If the
-   *     comment range line in the note contains a whole comment range, then we return a
-   *     CommentRange with all fields set. If the line is not correctly formatted, return null.
-   */
-  private static CommentRange parseCommentRange(byte[] note, MutableInteger ptr) {
-    CommentRange range = new CommentRange(-1, -1, -1, -1);
-
-    int last = ptr.value;
-    int startLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (ptr.value == last) {
-      return null;
-    } else if (note[ptr.value] == '\n') {
-      range.setEndLine(startLine);
-      ptr.value += 1;
-      return range;
-    } else if (note[ptr.value] == ':') {
-      range.setStartLine(startLine);
-      ptr.value += 1;
-    } else {
-      return null;
-    }
-
-    last = ptr.value;
-    int startChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (ptr.value == last) {
-      return null;
-    } else if (note[ptr.value] == '-') {
-      range.setStartCharacter(startChar);
-      ptr.value += 1;
-    } else {
-      return null;
-    }
-
-    last = ptr.value;
-    int endLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (ptr.value == last) {
-      return null;
-    } else if (note[ptr.value] == ':') {
-      range.setEndLine(endLine);
-      ptr.value += 1;
-    } else {
-      return null;
-    }
-
-    last = ptr.value;
-    int endChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (ptr.value == last) {
-      return null;
-    } else if (note[ptr.value] == '\n') {
-      range.setEndCharacter(endChar);
-      ptr.value += 1;
-    } else {
-      return null;
-    }
-    return range;
-  }
-
-  private static PatchSet.Id parsePsId(
-      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
-      throws ConfigInvalidException {
-    checkHeaderLineFormat(note, curr, fieldName, changeId);
-    int startOfPsId = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
-    MutableInteger i = new MutableInteger();
-    int patchSetId = RawParseUtils.parseBase10(note, startOfPsId, i);
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    if (i.value != endOfLine - 1) {
-      throw parseException(changeId, "could not parse %s", fieldName);
-    }
-    checkResult(patchSetId, "patchset id", changeId);
-    curr.value = endOfLine;
-    return new PatchSet.Id(changeId, patchSetId);
-  }
-
-  private static Integer parseParentNumber(byte[] note, MutableInteger curr, Change.Id changeId)
-      throws ConfigInvalidException {
-    checkHeaderLineFormat(note, curr, ChangeNoteUtil.PARENT_NUMBER, changeId);
-
-    int start = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
-    MutableInteger i = new MutableInteger();
-    int parentNumber = RawParseUtils.parseBase10(note, start, i);
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    if (i.value != endOfLine - 1) {
-      throw parseException(changeId, "could not parse %s", ChangeNoteUtil.PARENT_NUMBER);
-    }
-    checkResult(parentNumber, "parent number", changeId);
-    curr.value = endOfLine;
-    return Integer.valueOf(parentNumber);
-  }
-
-  private static String parseFilename(byte[] note, MutableInteger curr, Change.Id changeId)
-      throws ConfigInvalidException {
-    checkHeaderLineFormat(note, curr, ChangeNoteUtil.FILE, changeId);
-    int startOfFileName = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    curr.value = endOfLine;
-    curr.value = RawParseUtils.nextLF(note, curr.value);
-    return QuotedString.GIT_PATH.dequote(
-        RawParseUtils.decode(UTF_8, note, startOfFileName, endOfLine - 1));
-  }
-
-  private static Timestamp parseTimestamp(byte[] note, MutableInteger curr, Change.Id changeId)
-      throws ConfigInvalidException {
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    Timestamp commentTime;
-    String dateString = RawParseUtils.decode(UTF_8, note, curr.value, endOfLine - 1);
-    try {
-      commentTime = new Timestamp(GitDateParser.parse(dateString, null, Locale.US).getTime());
-    } catch (ParseException e) {
-      throw new ConfigInvalidException("could not parse comment timestamp", e);
-    }
-    curr.value = endOfLine;
-    return checkResult(commentTime, "comment timestamp", changeId);
-  }
-
-  private Account.Id parseAuthor(
-      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
-      throws ConfigInvalidException {
-    checkHeaderLineFormat(note, curr, fieldName, changeId);
-    int startOfAccountId = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
-    PersonIdent ident = RawParseUtils.parsePersonIdent(note, startOfAccountId);
-    Account.Id aId = parseIdent(ident, changeId);
-    curr.value = RawParseUtils.nextLF(note, curr.value);
-    return checkResult(aId, fieldName, changeId);
-  }
-
-  private static int parseCommentLength(byte[] note, MutableInteger curr, Change.Id changeId)
-      throws ConfigInvalidException {
-    checkHeaderLineFormat(note, curr, ChangeNoteUtil.LENGTH, changeId);
-    int startOfLength = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
-    MutableInteger i = new MutableInteger();
-    i.value = startOfLength;
-    int commentLength = RawParseUtils.parseBase10(note, startOfLength, i);
-    if (i.value == startOfLength) {
-      throw parseException(changeId, "could not parse %s", ChangeNoteUtil.LENGTH);
-    }
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    if (i.value != endOfLine - 1) {
-      throw parseException(changeId, "could not parse %s", ChangeNoteUtil.LENGTH);
-    }
-    curr.value = endOfLine;
-    return checkResult(commentLength, "comment length", changeId);
-  }
-
-  private boolean parseBooleanField(
-      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
-      throws ConfigInvalidException {
-    String str = parseStringField(note, curr, changeId, fieldName);
-    if ("true".equalsIgnoreCase(str)) {
-      return true;
-    } else if ("false".equalsIgnoreCase(str)) {
-      return false;
-    }
-    throw parseException(changeId, "invalid boolean for %s: %s", fieldName, str);
-  }
-
-  private static <T> T checkResult(T o, String fieldName, Change.Id changeId)
-      throws ConfigInvalidException {
-    if (o == null) {
-      throw parseException(changeId, "could not parse %s", fieldName);
-    }
-    return o;
-  }
-
-  private static int checkResult(int i, String fieldName, Change.Id changeId)
-      throws ConfigInvalidException {
-    if (i <= 0) {
-      throw parseException(changeId, "could not parse %s", fieldName);
-    }
-    return i;
-  }
-
-  private static void checkHeaderLineFormat(
-      byte[] note, MutableInteger curr, String fieldName, Change.Id changeId)
-      throws ConfigInvalidException {
-    boolean correct = RawParseUtils.match(note, curr.value, fieldName.getBytes(UTF_8)) != -1;
-    int p = curr.value + fieldName.length();
-    correct &= (p < note.length && note[p] == ':');
-    p++;
-    correct &= (p < note.length && note[p] == ' ');
-    if (!correct) {
-      throw parseException(changeId, "could not parse %s", fieldName);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java b/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java
deleted file mode 100644
index 2d5293f..0000000
--- a/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java
+++ /dev/null
@@ -1,189 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ListMultimap;
-import com.google.gerrit.common.UsedAt;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.GerritServerId;
-import com.google.inject.Inject;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.sql.Timestamp;
-import java.util.Date;
-import java.util.List;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.util.QuotedString;
-
-public class LegacyChangeNoteWrite {
-
-  private final PersonIdent serverIdent;
-  private final String serverId;
-
-  @Inject
-  public LegacyChangeNoteWrite(
-      @GerritPersonIdent PersonIdent serverIdent, @GerritServerId String serverId) {
-    this.serverIdent = serverIdent;
-    this.serverId = serverId;
-  }
-
-  public PersonIdent newIdent(Account.Id authorId, Date when, PersonIdent serverIdent) {
-    return new PersonIdent(
-        authorId.toString(), authorId.get() + "@" + serverId, when, serverIdent.getTimeZone());
-  }
-
-  public String getServerId() {
-    return serverId;
-  }
-
-  private void appendHeaderField(PrintWriter writer, String field, String value) {
-    writer.print(field);
-    writer.print(": ");
-    writer.print(value);
-    writer.print('\n');
-  }
-
-  /**
-   * Build a note that contains the metadata for and the contents of all of the comments in the
-   * given comments.
-   *
-   * @param comments Comments to be written to the output stream, keyed by patch set ID; multiple
-   *     patch sets are allowed since base revisions may be shared across patch sets. All of the
-   *     comments must share the same RevId, and all the comments for a given patch set must have
-   *     the same side.
-   * @param out output stream to write to.
-   */
-  @UsedAt(UsedAt.Project.GOOGLE)
-  public void buildNote(ListMultimap<Integer, Comment> comments, OutputStream out) {
-    if (comments.isEmpty()) {
-      return;
-    }
-
-    ImmutableList<Integer> psIds = comments.keySet().stream().sorted().collect(toImmutableList());
-
-    OutputStreamWriter streamWriter = new OutputStreamWriter(out, UTF_8);
-    try (PrintWriter writer = new PrintWriter(streamWriter)) {
-      String revId = comments.values().iterator().next().revId;
-      appendHeaderField(writer, ChangeNoteUtil.REVISION, revId);
-
-      for (int psId : psIds) {
-        List<Comment> psComments = COMMENT_ORDER.sortedCopy(comments.get(psId));
-        Comment first = psComments.get(0);
-
-        short side = first.side;
-        appendHeaderField(
-            writer,
-            side <= 0 ? ChangeNoteUtil.BASE_PATCH_SET : ChangeNoteUtil.PATCH_SET,
-            Integer.toString(psId));
-        if (side < 0) {
-          appendHeaderField(writer, ChangeNoteUtil.PARENT_NUMBER, Integer.toString(-side));
-        }
-
-        String currentFilename = null;
-
-        for (Comment c : psComments) {
-          checkArgument(
-              revId.equals(c.revId),
-              "All comments being added must have all the same RevId. The "
-                  + "comment below does not have the same RevId as the others "
-                  + "(%s).\n%s",
-              revId,
-              c);
-          checkArgument(
-              side == c.side,
-              "All comments being added must all have the same side. The "
-                  + "comment below does not have the same side as the others "
-                  + "(%s).\n%s",
-              side,
-              c);
-          String commentFilename = QuotedString.GIT_PATH.quote(c.key.filename);
-
-          if (!commentFilename.equals(currentFilename)) {
-            currentFilename = commentFilename;
-            writer.print("File: ");
-            writer.print(commentFilename);
-            writer.print("\n\n");
-          }
-
-          appendOneComment(writer, c);
-        }
-      }
-    }
-  }
-
-  private void appendOneComment(PrintWriter writer, Comment c) {
-    // The CommentRange field for a comment is allowed to be null. If it is
-    // null, then in the first line, we simply use the line number field for a
-    // comment instead. If it isn't null, we write the comment range itself.
-    Comment.Range range = c.range;
-    if (range != null) {
-      writer.print(range.startLine);
-      writer.print(':');
-      writer.print(range.startChar);
-      writer.print('-');
-      writer.print(range.endLine);
-      writer.print(':');
-      writer.print(range.endChar);
-    } else {
-      writer.print(c.lineNbr);
-    }
-    writer.print("\n");
-
-    writer.print(NoteDbUtil.formatTime(serverIdent, c.writtenOn));
-    writer.print("\n");
-
-    appendIdent(writer, ChangeNoteUtil.AUTHOR, c.author.getId(), c.writtenOn);
-    if (!c.getRealAuthor().equals(c.author)) {
-      appendIdent(writer, ChangeNoteUtil.REAL_AUTHOR, c.getRealAuthor().getId(), c.writtenOn);
-    }
-
-    String parent = c.parentUuid;
-    if (parent != null) {
-      appendHeaderField(writer, ChangeNoteUtil.PARENT, parent);
-    }
-
-    appendHeaderField(writer, ChangeNoteUtil.UNRESOLVED, Boolean.toString(c.unresolved));
-    appendHeaderField(writer, ChangeNoteUtil.UUID, c.key.uuid);
-
-    if (c.tag != null) {
-      appendHeaderField(writer, ChangeNoteUtil.TAG, c.tag);
-    }
-
-    byte[] messageBytes = c.message.getBytes(UTF_8);
-    appendHeaderField(writer, ChangeNoteUtil.LENGTH, Integer.toString(messageBytes.length));
-
-    writer.print(c.message);
-    writer.print("\n\n");
-  }
-
-  private void appendIdent(PrintWriter writer, String header, Account.Id id, Timestamp ts) {
-    PersonIdent ident = newIdent(id, ts, serverIdent);
-    StringBuilder name = new StringBuilder();
-    PersonIdent.appendSanitized(name, ident.getName());
-    name.append(" <");
-    PersonIdent.appendSanitized(name, ident.getEmailAddress());
-    name.append('>');
-    appendHeaderField(writer, header, name.toString());
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbMetrics.java b/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
index 61f475f..18ffd17 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -45,7 +46,8 @@
 
   @Inject
   NoteDbMetrics(MetricMaker metrics) {
-    Field<NoteDbTable> view = Field.ofEnum(NoteDbTable.class, "table");
+    Field<NoteDbTable> tableField =
+        Field.ofEnum(NoteDbTable.class, "table", Metadata.Builder::noteDbTable).build();
 
     updateLatency =
         metrics.newTimer(
@@ -53,7 +55,7 @@
             new Description("NoteDb update latency by table")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            view);
+            tableField);
 
     stageUpdateLatency =
         metrics.newTimer(
@@ -61,7 +63,7 @@
             new Description("Latency for staging updates to NoteDb by table")
                 .setCumulative()
                 .setUnit(Units.MICROSECONDS),
-            view);
+            tableField);
 
     readLatency =
         metrics.newTimer(
@@ -69,7 +71,7 @@
             new Description("NoteDb read latency by table")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            view);
+            tableField);
 
     parseLatency =
         metrics.newTimer(
@@ -77,6 +79,6 @@
             new Description("NoteDb parse latency by table")
                 .setCumulative()
                 .setUnit(Units.MICROSECONDS),
-            view);
+            tableField);
   }
 }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index ea42a9d..7022cdc 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -18,27 +18,21 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
-import static java.util.Objects.requireNonNull;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.metrics.Timer1;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.InMemoryInserter;
-import com.google.gerrit.server.git.InsertedObject;
 import com.google.gerrit.server.update.ChainedReceiveCommands;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -50,9 +44,9 @@
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -70,85 +64,16 @@
  * {@link #stage()}.
  */
 public class NoteDbUpdateManager implements AutoCloseable {
-  private static final ImmutableList<String> PACKAGE_PREFIXES =
-      ImmutableList.of("com.google.gerrit.server.", "com.google.gerrit.");
-  private static final ImmutableSet<String> SERVLET_NAMES =
-      ImmutableSet.of(
-          "com.google.gerrit.httpd.restapi.RestApiServlet", RetryingRestModifyView.class.getName());
-
   public interface Factory {
     NoteDbUpdateManager create(Project.NameKey projectName);
   }
 
-  public static class OpenRepo implements AutoCloseable {
-    public final Repository repo;
-    public final RevWalk rw;
-    public final ChainedReceiveCommands cmds;
-
-    private final InMemoryInserter inMemIns;
-    private final ObjectInserter tempIns;
-    @Nullable private final ObjectInserter finalIns;
-
-    private final boolean close;
-
-    private OpenRepo(
-        Repository repo,
-        RevWalk rw,
-        @Nullable ObjectInserter ins,
-        ChainedReceiveCommands cmds,
-        boolean close) {
-      ObjectReader reader = rw.getObjectReader();
-      checkArgument(
-          ins == null || reader.getCreatedFromInserter() == ins,
-          "expected reader to be created from %s, but was %s",
-          ins,
-          reader.getCreatedFromInserter());
-      this.repo = requireNonNull(repo);
-
-      this.inMemIns = new InMemoryInserter(rw.getObjectReader());
-      this.tempIns = inMemIns;
-
-      this.rw = new RevWalk(tempIns.newReader());
-      this.finalIns = ins;
-      this.cmds = requireNonNull(cmds);
-      this.close = close;
-    }
-
-    public Optional<ObjectId> getObjectId(String refName) throws IOException {
-      return cmds.get(refName);
-    }
-
-    void flush() throws IOException {
-      flushToFinalInserter();
-      finalIns.flush();
-    }
-
-    void flushToFinalInserter() throws IOException {
-      checkState(finalIns != null);
-      for (InsertedObject obj : inMemIns.getInsertedObjects()) {
-        finalIns.insert(obj.type(), obj.data().toByteArray());
-      }
-      inMemIns.clear();
-    }
-
-    @Override
-    public void close() {
-      rw.getObjectReader().close();
-      rw.close();
-      if (close) {
-        if (finalIns != null) {
-          finalIns.close();
-        }
-        repo.close();
-      }
-    }
-  }
-
   private final Provider<PersonIdent> serverIdent;
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
   private final NoteDbMetrics metrics;
   private final Project.NameKey projectName;
+  private final int maxUpdates;
   private final ListMultimap<String, ChangeUpdate> changeUpdates;
   private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
   private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates;
@@ -157,6 +82,7 @@
 
   private OpenRepo changeRepo;
   private OpenRepo allUsersRepo;
+  private AllUsersAsyncUpdate updateAllUsersAsync;
   private boolean executed;
   private String refLogMessage;
   private PersonIdent refLogIdent;
@@ -164,16 +90,20 @@
 
   @Inject
   NoteDbUpdateManager(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       GitRepositoryManager repoManager,
       AllUsersName allUsersName,
       NoteDbMetrics metrics,
+      AllUsersAsyncUpdate updateAllUsersAsync,
       @Assisted Project.NameKey projectName) {
     this.serverIdent = serverIdent;
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
     this.metrics = metrics;
+    this.updateAllUsersAsync = updateAllUsersAsync;
     this.projectName = projectName;
+    maxUpdates = cfg.getInt("change", null, "maxUpdates", 1000);
     changeUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     draftUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     robotCommentUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
@@ -236,28 +166,13 @@
 
   private void initChangeRepo() throws IOException {
     if (changeRepo == null) {
-      changeRepo = openRepo(projectName);
+      changeRepo = OpenRepo.open(repoManager, projectName);
     }
   }
 
   private void initAllUsersRepo() throws IOException {
     if (allUsersRepo == null) {
-      allUsersRepo = openRepo(allUsersName);
-    }
-  }
-
-  private OpenRepo openRepo(Project.NameKey p) throws IOException {
-    Repository repo = repoManager.openRepository(p); // Closed by OpenRepo#close.
-    ObjectInserter ins = repo.newObjectInserter(); // Closed by OpenRepo#close.
-    ObjectReader reader = ins.newReader(); // Not closed by OpenRepo#close.
-    try (RevWalk rw = new RevWalk(reader)) { // Doesn't escape OpenRepo constructor.
-      return new OpenRepo(repo, rw, ins, new ChainedReceiveCommands(repo), true) {
-        @Override
-        public void close() {
-          reader.close();
-          super.close();
-        }
-      };
+      allUsersRepo = OpenRepo.open(repoManager, allUsersName);
     }
   }
 
@@ -268,7 +183,8 @@
         && rewriters.isEmpty()
         && toDelete.isEmpty()
         && !hasCommands(changeRepo)
-        && !hasCommands(allUsersRepo);
+        && !hasCommands(allUsersRepo)
+        && updateAllUsersAsync.isEmpty();
   }
 
   private static boolean hasCommands(@Nullable OpenRepo or) {
@@ -351,7 +267,7 @@
    * @throws IOException if a storage layer error occurs.
    */
   private void stage() throws IOException {
-    try (Timer1.Context timer = metrics.stageUpdateLatency.start(CHANGES)) {
+    try (Timer1.Context<NoteDbTable> timer = metrics.stageUpdateLatency.start(CHANGES)) {
       if (isEmpty()) {
         return;
       }
@@ -364,16 +280,6 @@
     }
   }
 
-  public void flush() throws IOException {
-    checkNotExecuted();
-    if (changeRepo != null) {
-      changeRepo.flush();
-    }
-    if (allUsersRepo != null) {
-      allUsersRepo.flush();
-    }
-  }
-
   @Nullable
   public BatchRefUpdate execute() throws IOException {
     return execute(false);
@@ -386,7 +292,7 @@
       executed = true;
       return null;
     }
-    try (Timer1.Context timer = metrics.updateLatency.start(CHANGES)) {
+    try (Timer1.Context<NoteDbTable> timer = metrics.updateLatency.start(CHANGES)) {
       stage();
       // ChangeUpdates must execute before ChangeDraftUpdates.
       //
@@ -398,6 +304,13 @@
       // comments can only go from DRAFT to PUBLISHED, not vice versa.
       BatchRefUpdate result = execute(changeRepo, dryrun, pushCert);
       execute(allUsersRepo, dryrun, null);
+      if (!dryrun) {
+        // Only execute the asynchronous operation if we are not in dry-run mode: The dry run would
+        // have to run synchronous to be of any value at all. For the removal of draft comments from
+        // All-Users we don't care much of the operation succeeds, so we are skipping the dry run
+        // altogether.
+        updateAllUsersAsync.execute(refLogIdent, refLogMessage, pushCert);
+      }
       executed = true;
       return result;
     } finally {
@@ -423,7 +336,8 @@
     if (refLogMessage != null) {
       bru.setRefLogMessage(refLogMessage, false);
     } else {
-      bru.setRefLogMessage(firstNonNull(guessRestApiHandler(), "Update NoteDb refs"), false);
+      bru.setRefLogMessage(
+          firstNonNull(NoteDbUtil.guessRestApiHandler(), "Update NoteDb refs"), false);
     }
     bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent.get());
     bru.setAtomic(true);
@@ -436,59 +350,18 @@
     return bru;
   }
 
-  private static String guessRestApiHandler() {
-    StackTraceElement[] trace = Thread.currentThread().getStackTrace();
-    int i = findRestApiServlet(trace);
-    if (i < 0) {
-      return null;
-    }
-    try {
-      for (i--; i >= 0; i--) {
-        String cn = trace[i].getClassName();
-        Class<?> cls = Class.forName(cn);
-        if (RestModifyView.class.isAssignableFrom(cls) && cls != RetryingRestModifyView.class) {
-          return viewName(cn);
-        }
-      }
-      return null;
-    } catch (ClassNotFoundException e) {
-      return null;
-    }
-  }
-
-  private static String viewName(String cn) {
-    String impl = cn.replace('$', '.');
-    for (String p : PACKAGE_PREFIXES) {
-      if (impl.startsWith(p)) {
-        return impl.substring(p.length());
-      }
-    }
-    return impl;
-  }
-
-  private static int findRestApiServlet(StackTraceElement[] trace) {
-    for (int i = 0; i < trace.length; i++) {
-      if (SERVLET_NAMES.contains(trace[i].getClassName())) {
-        return i;
-      }
-    }
-    return -1;
-  }
-
   private void addCommands() throws IOException {
-    if (isEmpty()) {
-      return;
-    }
-    checkState(changeRepo != null, "must set change repo");
+    changeRepo.addUpdates(changeUpdates, Optional.of(maxUpdates));
     if (!draftUpdates.isEmpty()) {
-      checkState(allUsersRepo != null, "must set all users repo");
-    }
-    addUpdates(changeUpdates, changeRepo);
-    if (!draftUpdates.isEmpty()) {
-      addUpdates(draftUpdates, allUsersRepo);
+      boolean publishOnly = draftUpdates.values().stream().allMatch(ChangeDraftUpdate::canRunAsync);
+      if (publishOnly) {
+        updateAllUsersAsync.setDraftUpdates(draftUpdates);
+      } else {
+        allUsersRepo.addUpdates(draftUpdates);
+      }
     }
     if (!robotCommentUpdates.isEmpty()) {
-      addUpdates(robotCommentUpdates, changeRepo);
+      changeRepo.addUpdates(robotCommentUpdates);
     }
     if (!rewriters.isEmpty()) {
       addRewrites(rewriters, changeRepo);
@@ -520,36 +393,6 @@
     checkState(!executed, "update has already been executed");
   }
 
-  private static <U extends AbstractChangeUpdate> void addUpdates(
-      ListMultimap<String, U> all, OpenRepo or) throws IOException {
-    for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) {
-      String refName = e.getKey();
-      Collection<U> updates = e.getValue();
-      ObjectId old = or.cmds.get(refName).orElse(ObjectId.zeroId());
-      // Only actually write to the ref if one of the updates explicitly allows
-      // us to do so, i.e. it is known to represent a new change. This avoids
-      // writing partial change meta if the change hasn't been backfilled yet.
-      if (!allowWrite(updates, old)) {
-        continue;
-      }
-
-      ObjectId curr = old;
-      for (U u : updates) {
-        if (u.isRootOnly() && !old.equals(ObjectId.zeroId())) {
-          throw new StorageException("Given ChangeUpdate is only allowed on initial commit");
-        }
-        ObjectId next = u.apply(or.rw, or.tempIns, curr);
-        if (next == null) {
-          continue;
-        }
-        curr = next;
-      }
-      if (!old.equals(curr)) {
-        or.cmds.add(new ReceiveCommand(old, curr, refName));
-      }
-    }
-  }
-
   private static void addRewrites(ListMultimap<String, NoteDbRewriter> rewriters, OpenRepo openRepo)
       throws IOException {
     for (Map.Entry<String, Collection<NoteDbRewriter>> entry : rewriters.asMap().entrySet()) {
@@ -578,12 +421,4 @@
       }
     }
   }
-
-  private static <U extends AbstractChangeUpdate> boolean allowWrite(
-      Collection<U> updates, ObjectId old) {
-    if (!old.equals(ObjectId.zeroId())) {
-      return true;
-    }
-    return updates.iterator().next().allowWriteToNewRef();
-  }
 }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUtil.java b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
index 667ceab..58a33c8 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUtil.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
@@ -15,8 +15,12 @@
 package com.google.gerrit.server.notedb;
 
 import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import java.sql.Timestamp;
 import java.util.Optional;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -25,26 +29,35 @@
 
 public class NoteDbUtil {
 
-  /**
-   * Returns an AccountId for the given email address. Returns empty if the address isn't on this
-   * server.
-   */
-  public static Optional<Account.Id> parseIdent(PersonIdent ident, String serverId) {
+  private static final CharMatcher INVALID_FOOTER_CHARS = CharMatcher.anyOf("\r\n\0");
+
+  private static final ImmutableList<String> PACKAGE_PREFIXES =
+      ImmutableList.of("com.google.gerrit.server.", "com.google.gerrit.");
+  private static final ImmutableSet<String> SERVLET_NAMES =
+      ImmutableSet.of(
+          "com.google.gerrit.httpd.restapi.RestApiServlet", RetryingRestModifyView.class.getName());
+
+  /** Returns an AccountId for the given email address. */
+  public static Optional<Account.Id> parseIdent(PersonIdent ident) {
     String email = ident.getEmailAddress();
     int at = email.indexOf('@');
     if (at >= 0) {
-      String host = email.substring(at + 1);
-      if (host.equals(serverId)) {
-        Integer id = Ints.tryParse(email.substring(0, at));
-        if (id != null) {
-          return Optional.of(new Account.Id(id));
-        }
+      Integer id = Ints.tryParse(email.substring(0, at));
+      if (id != null) {
+        return Optional.of(Account.id(id));
       }
     }
     return Optional.empty();
   }
 
-  private NoteDbUtil() {}
+  public static String extractHostPartFromPersonIdent(PersonIdent ident) {
+    String email = ident.getEmailAddress();
+    int at = email.indexOf('@');
+    if (at >= 0) {
+      return email.substring(at + 1);
+    }
+    throw new IllegalArgumentException("No host part found: " + email);
+  }
 
   public static String formatTime(PersonIdent ident, Timestamp t) {
     GitDateFormatter dateFormatter = new GitDateFormatter(Format.DEFAULT);
@@ -53,7 +66,29 @@
     return dateFormatter.formatDate(newIdent);
   }
 
-  private static final CharMatcher INVALID_FOOTER_CHARS = CharMatcher.anyOf("\r\n\0");
+  /**
+   * Returns the name of the REST API handler that is in the stack trace of the caller of this
+   * method.
+   */
+  static String guessRestApiHandler() {
+    StackTraceElement[] trace = Thread.currentThread().getStackTrace();
+    int i = findRestApiServlet(trace);
+    if (i < 0) {
+      return null;
+    }
+    try {
+      for (i--; i >= 0; i--) {
+        String cn = trace[i].getClassName();
+        Class<?> cls = Class.forName(cn);
+        if (RestModifyView.class.isAssignableFrom(cls) && cls != RetryingRestModifyView.class) {
+          return viewName(cn);
+        }
+      }
+      return null;
+    } catch (ClassNotFoundException e) {
+      return null;
+    }
+  }
 
   static String sanitizeFooter(String value) {
     // Remove characters that would confuse JGit's footer parser if they were
@@ -65,4 +100,25 @@
     // empty paragraph for the purposes of footer parsing.
     return INVALID_FOOTER_CHARS.trimAndCollapseFrom(value, ' ');
   }
+
+  private static int findRestApiServlet(StackTraceElement[] trace) {
+    for (int i = 0; i < trace.length; i++) {
+      if (SERVLET_NAMES.contains(trace[i].getClassName())) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  private static String viewName(String cn) {
+    String impl = cn.replace('$', '.');
+    for (String p : PACKAGE_PREFIXES) {
+      if (impl.startsWith(p)) {
+        return impl.substring(p.length());
+      }
+    }
+    return impl;
+  }
+
+  private NoteDbUtil() {}
 }
diff --git a/java/com/google/gerrit/server/notedb/OpenRepo.java b/java/com/google/gerrit/server/notedb/OpenRepo.java
new file mode 100644
index 0000000..de88684
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/OpenRepo.java
@@ -0,0 +1,176 @@
+// 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.notedb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.InMemoryInserter;
+import com.google.gerrit.server.git.InsertedObject;
+import com.google.gerrit.server.update.ChainedReceiveCommands;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * Wrapper around {@link Repository} that keeps track of related {@link ObjectInserter}s and other
+ * objects that are jointly closed when invoking {@link #close}.
+ */
+class OpenRepo implements AutoCloseable {
+  /** Returns a {@link OpenRepo} wrapping around an open {@link Repository}. */
+  static OpenRepo open(GitRepositoryManager repoManager, Project.NameKey project)
+      throws IOException {
+    Repository repo = repoManager.openRepository(project); // Closed by OpenRepo#close.
+    ObjectInserter ins = repo.newObjectInserter(); // Closed by OpenRepo#close.
+    ObjectReader reader = ins.newReader(); // Not closed by OpenRepo#close.
+    try (RevWalk rw = new RevWalk(reader)) { // Doesn't escape OpenRepo constructor.
+      return new OpenRepo(repo, rw, ins, new ChainedReceiveCommands(repo), true) {
+        @Override
+        public void close() {
+          reader.close();
+          super.close();
+        }
+      };
+    }
+  }
+
+  final Repository repo;
+  final RevWalk rw;
+  final ChainedReceiveCommands cmds;
+  final ObjectInserter tempIns;
+
+  private final InMemoryInserter inMemIns;
+  @Nullable private final ObjectInserter finalIns;
+  private final boolean close;
+
+  OpenRepo(
+      Repository repo,
+      RevWalk rw,
+      @Nullable ObjectInserter ins,
+      ChainedReceiveCommands cmds,
+      boolean close) {
+    ObjectReader reader = rw.getObjectReader();
+    checkArgument(
+        ins == null || reader.getCreatedFromInserter() == ins,
+        "expected reader to be created from %s, but was %s",
+        ins,
+        reader.getCreatedFromInserter());
+    this.repo = requireNonNull(repo);
+
+    this.inMemIns = new InMemoryInserter(rw.getObjectReader());
+    this.tempIns = inMemIns;
+
+    this.rw = new RevWalk(tempIns.newReader());
+    this.finalIns = ins;
+    this.cmds = requireNonNull(cmds);
+    this.close = close;
+  }
+
+  @Override
+  public void close() {
+    rw.getObjectReader().close();
+    rw.close();
+    if (close) {
+      if (finalIns != null) {
+        finalIns.close();
+      }
+      repo.close();
+    }
+  }
+
+  void flush() throws IOException {
+    flushToFinalInserter();
+    finalIns.flush();
+  }
+
+  void flushToFinalInserter() throws IOException {
+    checkState(finalIns != null);
+    for (InsertedObject obj : inMemIns.getInsertedObjects()) {
+      finalIns.insert(obj.type(), obj.data().toByteArray());
+    }
+    inMemIns.clear();
+  }
+
+  private static <U extends AbstractChangeUpdate> boolean allowWrite(
+      Collection<U> updates, ObjectId old) {
+    if (!old.equals(ObjectId.zeroId())) {
+      return true;
+    }
+    return updates.iterator().next().allowWriteToNewRef();
+  }
+
+  <U extends AbstractChangeUpdate> void addUpdates(ListMultimap<String, U> all) throws IOException {
+    addUpdates(all, Optional.empty());
+  }
+
+  <U extends AbstractChangeUpdate> void addUpdates(
+      ListMultimap<String, U> all, Optional<Integer> maxUpdates) throws IOException {
+    for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) {
+      String refName = e.getKey();
+      Collection<U> updates = e.getValue();
+      ObjectId old = cmds.get(refName).orElse(ObjectId.zeroId());
+      // Only actually write to the ref if one of the updates explicitly allows
+      // us to do so, i.e. it is known to represent a new change. This avoids
+      // writing partial change meta if the change hasn't been backfilled yet.
+      if (!allowWrite(updates, old)) {
+        continue;
+      }
+
+      int updateCount;
+      U first = updates.iterator().next();
+      if (maxUpdates.isPresent()) {
+        checkState(first.getNotes() != null, "expected ChangeNotes on %s", first);
+        updateCount = first.getNotes().getUpdateCount();
+      } else {
+        updateCount = 0;
+      }
+
+      ObjectId curr = old;
+      for (U u : updates) {
+        if (u.isRootOnly() && !old.equals(ObjectId.zeroId())) {
+          throw new StorageException("Given ChangeUpdate is only allowed on initial commit");
+        }
+        ObjectId next = u.apply(rw, tempIns, curr);
+        if (next == null) {
+          continue;
+        }
+        if (maxUpdates.isPresent()
+            && !Objects.equals(next, curr)
+            && ++updateCount > maxUpdates.get()
+            && !u.bypassMaxUpdates()) {
+          throw new TooManyUpdatesException(u.getId(), maxUpdates.get());
+        }
+        curr = next;
+      }
+      if (!old.equals(curr)) {
+        cmds.add(new ReceiveCommand(old, curr, refName));
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index 68b2b95..e52b84c 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_SEQUENCES;
+import static com.google.gerrit.entities.RefNames.REFS;
+import static com.google.gerrit.entities.RefNames.REFS_SEQUENCES;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -29,19 +29,20 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Runnables;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Lock;
@@ -72,9 +73,12 @@
   }
 
   @VisibleForTesting
-  static RetryerBuilder<RefUpdate> retryerBuilder() {
-    return RetryerBuilder.<RefUpdate>newBuilder()
-        .retryIfResult(ru -> ru != null && RefUpdate.Result.LOCK_FAILURE.equals(ru.getResult()))
+  static RetryerBuilder<ImmutableList<Integer>> retryerBuilder() {
+    return RetryerBuilder.<ImmutableList<Integer>>newBuilder()
+        .retryIfException(
+            t ->
+                t instanceof StorageException
+                    && ((StorageException) t).getCause() instanceof LockFailureException)
         .withWaitStrategy(
             WaitStrategies.join(
                 WaitStrategies.exponentialWait(5, TimeUnit.SECONDS),
@@ -82,7 +86,7 @@
         .withStopStrategy(StopStrategies.stopAfterDelay(30, TimeUnit.SECONDS));
   }
 
-  private static final Retryer<RefUpdate> RETRYER = retryerBuilder().build();
+  private static final Retryer<ImmutableList<Integer>> RETRYER = retryerBuilder().build();
 
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
@@ -92,7 +96,7 @@
   private final int floor;
   private final int batchSize;
   private final Runnable afterReadRef;
-  private final Retryer<RefUpdate> retryer;
+  private final Retryer<ImmutableList<Integer>> retryer;
 
   // Protects all non-final fields.
   private final Lock counterLock;
@@ -150,7 +154,7 @@
       Seed seed,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate> retryer) {
+      Retryer<ImmutableList<Integer>> retryer) {
     this(repoManager, gitRefUpdated, projectName, name, seed, batchSize, afterReadRef, retryer, 0);
   }
 
@@ -162,7 +166,7 @@
       Seed seed,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate> retryer,
+      Retryer<ImmutableList<Integer>> retryer,
       int floor) {
     this.repoManager = requireNonNull(repoManager, "repoManager");
     this.gitRefUpdated = requireNonNull(gitRefUpdated, "gitRefUpdated");
@@ -188,79 +192,86 @@
     counterLock = new ReentrantLock(true);
   }
 
+  /**
+   * Retrieves the next available sequence number.
+   *
+   * <p>This method is thread-safe.
+   *
+   * @return the next available sequence number
+   */
   public int next() {
-    counterLock.lock();
-    try {
-      if (counter >= limit) {
-        acquire(batchSize);
-      }
-      return counter++;
-    } finally {
-      counterLock.unlock();
-    }
+    return Iterables.getOnlyElement(next(1));
   }
 
+  /**
+   * Retrieves the next N available sequence number.
+   *
+   * <p>This method is thread-safe.
+   *
+   * @param count the number of sequence numbers which should be returned
+   * @return the next N available sequence numbers
+   */
   public ImmutableList<Integer> next(int count) {
     if (count == 0) {
       return ImmutableList.of();
     }
     checkArgument(count > 0, "count is negative: %s", count);
-    counterLock.lock();
-    try {
-      List<Integer> ids = new ArrayList<>(count);
-      while (counter < limit) {
-        ids.add(counter++);
-        if (ids.size() == count) {
-          return ImmutableList.copyOf(ids);
-        }
-      }
-      acquire(Math.max(count - ids.size(), batchSize));
-      while (ids.size() < count) {
-        ids.add(counter++);
-      }
-      return ImmutableList.copyOf(ids);
-    } finally {
-      counterLock.unlock();
-    }
-  }
 
-  private void acquire(int count) {
-    try (Repository repo = repoManager.openRepository(projectName);
-        RevWalk rw = new RevWalk(repo)) {
-      logger.atFine().log("acquire %d ids on %s in %s", count, refName, projectName);
-      TryAcquire attempt = new TryAcquire(repo, rw, count);
-      RefUpdateUtil.checkResult(retryer.call(attempt));
-      counter = attempt.next;
-      limit = counter + count;
-      acquireCount++;
+    try {
+      return retryer.call(
+          () -> {
+            counterLock.lock();
+            try {
+              if (count == 1) {
+                if (counter >= limit) {
+                  acquire(batchSize);
+                }
+                return ImmutableList.of(counter++);
+              }
+
+              List<Integer> ids = new ArrayList<>(count);
+              while (counter < limit) {
+                ids.add(counter++);
+                if (ids.size() == count) {
+                  return ImmutableList.copyOf(ids);
+                }
+              }
+              acquire(Math.max(count - ids.size(), batchSize));
+              while (ids.size() < count) {
+                ids.add(counter++);
+              }
+              return ImmutableList.copyOf(ids);
+            } finally {
+              counterLock.unlock();
+            }
+          });
     } catch (ExecutionException | RetryException e) {
       if (e.getCause() != null) {
         Throwables.throwIfInstanceOf(e.getCause(), StorageException.class);
       }
       throw new StorageException(e);
-    } catch (IOException e) {
-      throw new StorageException(e);
     }
   }
 
-  private class TryAcquire implements Callable<RefUpdate> {
-    private final Repository repo;
-    private final RevWalk rw;
-    private final int count;
-
-    private int next;
-
-    private TryAcquire(Repository repo, RevWalk rw, int count) {
-      this.repo = repo;
-      this.rw = rw;
-      this.count = count;
-    }
-
-    @Override
-    public RefUpdate call() throws Exception {
+  /**
+   * Updates the next available sequence number in NoteDb in order to have a batch of sequence
+   * numbers available that can be handed out. {@link #counter} stores the next sequence number that
+   * can be handed out. When {@link #limit} is reached a new batch of sequence numbers needs to be
+   * retrieved by calling this method.
+   *
+   * <p><strong>Note:</strong> Callers are required to acquire the {@link #counterLock} before
+   * calling this method.
+   *
+   * @param count the number of sequence numbers which should be retrieved
+   */
+  private void acquire(int count) {
+    try (Repository repo = repoManager.openRepository(projectName);
+        RevWalk rw = new RevWalk(repo)) {
+      logger.atFine().log("acquire %d ids on %s in %s", count, refName, projectName);
       Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw);
       afterReadRef.run();
       ObjectId oldId;
+      int next;
       if (!blob.isPresent()) {
         oldId = ObjectId.zeroId();
         next = seed.get();
@@ -269,7 +280,14 @@
         next = blob.get().value();
       }
       next = Math.max(floor, next);
-      return IntBlob.tryStore(repo, rw, projectName, refName, oldId, next + count, gitRefUpdated);
+      RefUpdate refUpdate =
+          IntBlob.tryStore(repo, rw, projectName, refName, oldId, next + count, gitRefUpdated);
+      RefUpdateUtil.checkResult(refUpdate);
+      counter = next;
+      limit = counter + count;
+      acquireCount++;
+    } catch (IOException e) {
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java b/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
index fad9832..d5a7259 100644
--- a/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
+++ b/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
@@ -21,13 +21,13 @@
 /** State of a reviewer on a change. */
 public enum ReviewerStateInternal {
   /** The user has contributed at least one nonzero vote on the change. */
-  REVIEWER(new FooterKey("Reviewer"), ReviewerState.REVIEWER),
+  REVIEWER("Reviewer", ReviewerState.REVIEWER),
 
   /** The reviewer was added to the change, but has not voted. */
-  CC(new FooterKey("CC"), ReviewerState.CC),
+  CC("CC", ReviewerState.CC),
 
   /** The user was previously a reviewer on the change, but was removed. */
-  REMOVED(new FooterKey("Removed"), ReviewerState.REMOVED);
+  REMOVED("Removed", ReviewerState.REMOVED);
 
   public static ReviewerStateInternal fromReviewerState(ReviewerState state) {
     return ReviewerStateInternal.values()[state.ordinal()];
@@ -50,20 +50,20 @@
     }
   }
 
-  private final FooterKey footerKey;
+  private final String footer;
   private final ReviewerState state;
 
-  ReviewerStateInternal(FooterKey footerKey, ReviewerState state) {
-    this.footerKey = footerKey;
+  ReviewerStateInternal(String footer, ReviewerState state) {
+    this.footer = footer;
     this.state = state;
   }
 
   FooterKey getFooterKey() {
-    return footerKey;
+    return new FooterKey(footer);
   }
 
   FooterKey getByEmailFooterKey() {
-    return new FooterKey(footerKey.getName() + "-email");
+    return new FooterKey(footer + "-email");
   }
 
   public ReviewerState asReviewerState() {
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
index ac7a89d..e63737c 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
@@ -21,8 +21,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Maps;
 import com.google.common.collect.MultimapBuilder;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.entities.Comment;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -33,27 +32,29 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
 
 class RevisionNoteBuilder {
   static class Cache {
     private final RevisionNoteMap<? extends RevisionNote<? extends Comment>> revisionNoteMap;
-    private final Map<RevId, RevisionNoteBuilder> builders;
+    private final Map<ObjectId, RevisionNoteBuilder> builders;
 
     Cache(RevisionNoteMap<? extends RevisionNote<? extends Comment>> revisionNoteMap) {
       this.revisionNoteMap = revisionNoteMap;
       this.builders = new HashMap<>();
     }
 
-    RevisionNoteBuilder get(RevId revId) {
-      RevisionNoteBuilder b = builders.get(revId);
+    RevisionNoteBuilder get(AnyObjectId commitId) {
+      RevisionNoteBuilder b = builders.get(commitId);
       if (b == null) {
-        b = new RevisionNoteBuilder(revisionNoteMap.revisionNotes.get(revId));
-        builders.put(revId, b);
+        b = new RevisionNoteBuilder(revisionNoteMap.revisionNotes.get(commitId));
+        builders.put(commitId.copy(), b);
       }
       return b;
     }
 
-    Map<RevId, RevisionNoteBuilder> getBuilders() {
+    Map<ObjectId, RevisionNoteBuilder> getBuilders() {
       return Collections.unmodifiableMap(builders);
     }
   }
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteData.java b/java/com/google/gerrit/server/notedb/RevisionNoteData.java
index 1e16b22..c0e09ed 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteData.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteData.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.notedb;
 
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.entities.Comment;
 import java.util.List;
 
 /**
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
index da790e2..3e1bad1 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
@@ -15,37 +15,28 @@
 package com.google.gerrit.server.notedb;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.entities.Comment;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.notes.Note;
 import org.eclipse.jgit.notes.NoteMap;
 
 class RevisionNoteMap<T extends RevisionNote<? extends Comment>> {
   final NoteMap noteMap;
-  final ImmutableMap<RevId, T> revisionNotes;
+  final ImmutableMap<ObjectId, T> revisionNotes;
 
   static RevisionNoteMap<ChangeRevisionNote> parse(
-      ChangeNoteJson noteJson,
-      LegacyChangeNoteRead legacyChangeNoteRead,
-      Change.Id changeId,
-      ObjectReader reader,
-      NoteMap noteMap,
-      PatchLineComment.Status status)
+      ChangeNoteJson noteJson, ObjectReader reader, NoteMap noteMap, Comment.Status status)
       throws ConfigInvalidException, IOException {
-    Map<RevId, ChangeRevisionNote> result = new HashMap<>();
+    Map<ObjectId, ChangeRevisionNote> result = new HashMap<>();
     for (Note note : noteMap) {
-      ChangeRevisionNote rn =
-          new ChangeRevisionNote(
-              noteJson, legacyChangeNoteRead, changeId, reader, note.getData(), status);
+      ChangeRevisionNote rn = new ChangeRevisionNote(noteJson, reader, note.getData(), status);
       rn.parse();
-      result.put(new RevId(note.name()), rn);
+      result.put(note.copy(), rn);
     }
     return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result));
   }
@@ -53,12 +44,12 @@
   static RevisionNoteMap<RobotCommentsRevisionNote> parseRobotComments(
       ChangeNoteJson changeNoteJson, ObjectReader reader, NoteMap noteMap)
       throws ConfigInvalidException, IOException {
-    Map<RevId, RobotCommentsRevisionNote> result = new HashMap<>();
+    Map<ObjectId, RobotCommentsRevisionNote> result = new HashMap<>();
     for (Note note : noteMap) {
       RobotCommentsRevisionNote rn =
           new RobotCommentsRevisionNote(changeNoteJson, reader, note.getData());
       rn.parse();
-      result.put(new RevId(note.name()), rn);
+      result.put(note.copy(), rn);
     }
     return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result));
   }
@@ -67,7 +58,7 @@
     return new RevisionNoteMap<>(NoteMap.newEmptyMap(), ImmutableMap.of());
   }
 
-  private RevisionNoteMap(NoteMap noteMap, ImmutableMap<RevId, T> revisionNotes) {
+  private RevisionNoteMap(NoteMap noteMap, ImmutableMap<ObjectId, T> revisionNotes) {
     this.noteMap = noteMap;
     this.revisionNotes = revisionNotes;
   }
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
index 92dd7d8..fe05643 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -19,11 +19,10 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.RobotComment;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -42,7 +41,7 @@
 
   private final Change change;
 
-  private ImmutableListMultimap<RevId, RobotComment> comments;
+  private ImmutableListMultimap<ObjectId, RobotComment> comments;
   private RevisionNoteMap<RobotCommentsRevisionNote> revisionNoteMap;
   private ObjectId metaId;
 
@@ -56,7 +55,7 @@
     return revisionNoteMap;
   }
 
-  public ImmutableListMultimap<RevId, RobotComment> getComments() {
+  public ImmutableListMultimap<ObjectId, RobotComment> getComments() {
     return comments;
   }
 
@@ -95,10 +94,10 @@
     revisionNoteMap =
         RevisionNoteMap.parseRobotComments(
             args.changeNoteJson, reader, NoteMap.read(reader, tipCommit));
-    ListMultimap<RevId, RobotComment> cs = MultimapBuilder.hashKeys().arrayListValues().build();
+    ListMultimap<ObjectId, RobotComment> cs = MultimapBuilder.hashKeys().arrayListValues().build();
     for (RobotCommentsRevisionNote rn : revisionNoteMap.revisionNotes.values()) {
       for (RobotComment c : rn.getEntities()) {
-        cs.put(new RevId(c.revId), c);
+        cs.put(c.getCommitId(), c);
       }
     }
     comments = ImmutableListMultimap.copyOf(cs);
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
index 0304ab8..895f378 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -15,16 +15,15 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.gerrit.reviewdb.client.RefNames.robotCommentsRef;
+import static com.google.gerrit.entities.RefNames.robotCommentsRef;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.collect.Sets;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -103,19 +102,19 @@
       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
       throws ConfigInvalidException, IOException {
     RevisionNoteMap<RobotCommentsRevisionNote> rnm = getRevisionNoteMap(rw, curr);
-    Set<RevId> updatedRevs = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
+    Set<ObjectId> updatedRevs = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
 
     for (RobotComment c : put) {
-      cache.get(new RevId(c.revId)).putComment(c);
+      cache.get(c.getCommitId()).putComment(c);
     }
 
-    Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders();
+    Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
     boolean touchedAnyRevs = false;
     boolean hasComments = false;
-    for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
-      updatedRevs.add(e.getKey());
-      ObjectId id = ObjectId.fromString(e.getKey().get());
+    for (Map.Entry<ObjectId, RevisionNoteBuilder> e : builders.entrySet()) {
+      ObjectId id = e.getKey();
+      updatedRevs.add(id);
       byte[] data = e.getValue().build(noteUtil);
       if (!Arrays.equals(data, e.getValue().baseRaw)) {
         touchedAnyRevs = true;
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java b/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
index 6c3cc86..97a8ad4 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
@@ -16,7 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.entities.RobotComment;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java b/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java
index 116b30e..3619c43 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.notedb;
 
-import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.entities.RobotComment;
 import java.util.List;
 
 public class RobotCommentsRevisionNoteData {
diff --git a/java/com/google/gerrit/server/notedb/Sequences.java b/java/com/google/gerrit/server/notedb/Sequences.java
index d4144ce..9f19725 100644
--- a/java/com/google/gerrit/server/notedb/Sequences.java
+++ b/java/com/google/gerrit/server/notedb/Sequences.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import org.eclipse.jgit.lib.Config;
@@ -110,30 +111,35 @@
             new Description("Latency of requesting IDs from repo sequences")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            Field.ofEnum(SequenceType.class, "sequence"),
-            Field.ofBoolean("multiple"));
+            Field.ofEnum(SequenceType.class, "sequence", Metadata.Builder::noteDbSequenceType)
+                .build(),
+            Field.ofBoolean("multiple", Metadata.Builder::multiple).build());
   }
 
   public int nextAccountId() {
-    try (Timer2.Context timer = nextIdLatency.start(SequenceType.ACCOUNTS, false)) {
+    try (Timer2.Context<SequenceType, Boolean> timer =
+        nextIdLatency.start(SequenceType.ACCOUNTS, false)) {
       return accountSeq.next();
     }
   }
 
   public int nextChangeId() {
-    try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, false)) {
+    try (Timer2.Context<SequenceType, Boolean> timer =
+        nextIdLatency.start(SequenceType.CHANGES, false)) {
       return changeSeq.next();
     }
   }
 
   public ImmutableList<Integer> nextChangeIds(int count) {
-    try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, count > 1)) {
+    try (Timer2.Context<SequenceType, Boolean> timer =
+        nextIdLatency.start(SequenceType.CHANGES, count > 1)) {
       return changeSeq.next(count);
     }
   }
 
   public int nextGroupId() {
-    try (Timer2.Context timer = nextIdLatency.start(SequenceType.GROUPS, false)) {
+    try (Timer2.Context<SequenceType, Boolean> timer =
+        nextIdLatency.start(SequenceType.GROUPS, false)) {
       return groupSeq.next();
     }
   }
diff --git a/java/com/google/gerrit/server/notedb/TooManyUpdatesException.java b/java/com/google/gerrit/server/notedb/TooManyUpdatesException.java
new file mode 100644
index 0000000..9c6faaf
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/TooManyUpdatesException.java
@@ -0,0 +1,41 @@
+// 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.notedb;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.exceptions.StorageException;
+
+/**
+ * Exception indicating that the change has received too many updates. Further actions apart from
+ * {@code abandon} or {@code submit} are blocked.
+ */
+public class TooManyUpdatesException extends StorageException {
+  @VisibleForTesting
+  public static String message(Change.Id id, int maxUpdates) {
+    return "Change "
+        + id
+        + " may not exceed "
+        + maxUpdates
+        + " updates. It may still be abandoned or submitted. To continue working on this "
+        + "change, recreate it with a new Change-Id, then abandon this one.";
+  }
+
+  private static final long serialVersionUID = 1L;
+
+  TooManyUpdatesException(Change.Id id, int maxUpdates) {
+    super(message(id, maxUpdates));
+  }
+}
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index 18aa8b9..a9cc9b5 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -19,7 +19,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.InMemoryInserter;
diff --git a/java/com/google/gerrit/server/patch/DiffSummaryLoader.java b/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
index 9153638..b61e0c7 100644
--- a/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
+++ b/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Project;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.ArrayList;
diff --git a/java/com/google/gerrit/server/patch/IntraLineDiff.java b/java/com/google/gerrit/server/patch/IntraLineDiff.java
index 1c3d78a..fb13207 100644
--- a/java/com/google/gerrit/server/patch/IntraLineDiff.java
+++ b/java/com/google/gerrit/server/patch/IntraLineDiff.java
@@ -21,8 +21,8 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.CodedEnum;
 import com.google.gerrit.jgit.diff.ReplaceEdit;
-import com.google.gerrit.reviewdb.client.CodedEnum;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.ObjectInputStream;
diff --git a/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java b/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java
index 4661485..2ebae02 100644
--- a/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java
+++ b/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java
@@ -17,7 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import java.util.List;
 import java.util.Set;
 import org.eclipse.jgit.diff.Edit;
diff --git a/java/com/google/gerrit/server/patch/PatchFile.java b/java/com/google/gerrit/server/patch/PatchFile.java
index 0e5bcc1..ca5223d 100644
--- a/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/java/com/google/gerrit/server/patch/PatchFile.java
@@ -16,8 +16,8 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.exceptions.NoSuchEntityException;
-import com.google.gerrit.reviewdb.client.Patch;
 import java.io.IOException;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -39,6 +39,15 @@
   private final RevTree aTree;
   private final RevTree bTree;
 
+  // Full text of both sides of the file. For standard files, these are not directly reconstructable
+  // from the PatchListEntry, which comes from the PatchListCache and only contains the diff between
+  // the two blobs. This is intentional, to avoid storing entire large blobs in the cache. For
+  // regular files, the full text is initialized from the repo lazily only when necessary, e.g. in
+  // getLine. Although it's a safe assumption that any caller constructing a PatchSet will want to
+  // read some content, we don't know in advance which side they are interested in.
+  //
+  // For special files like COMMIT_MSG, the full text is loaded eagerly during the constructor.
+  // TODO(dborowitz): I see why the logic is different, but I don't see why it needs to be eager.
   private Text a;
   private Text b;
 
diff --git a/java/com/google/gerrit/server/patch/PatchList.java b/java/com/google/gerrit/server/patch/PatchList.java
index dd717ba..fee9088 100644
--- a/java/com/google/gerrit/server/patch/PatchList.java
+++ b/java/com/google/gerrit/server/patch/PatchList.java
@@ -25,8 +25,9 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.git.ObjectIds;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -78,7 +79,7 @@
       boolean isMerge,
       ComparisonType comparisonType,
       PatchListEntry[] patches) {
-    this.oldId = oldId != null ? oldId.copy() : null;
+    this.oldId = ObjectIds.copyOrNull(oldId);
     this.newId = newId.copy();
     this.isMerge = isMerge;
     this.comparisonType = comparisonType;
diff --git a/java/com/google/gerrit/server/patch/PatchListCache.java b/java/com/google/gerrit/server/patch/PatchListCache.java
index 728d227..63cac0e 100644
--- a/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import org.eclipse.jgit.lib.ObjectId;
 
 /** Provides a cached list of {@link PatchListEntry}. */
diff --git a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 20f0f72..15fa0f4 100644
--- a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -18,10 +18,10 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.Cache;
 import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.CacheBackend;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -137,10 +137,7 @@
   private PatchList get(Change change, PatchSet patchSet, Integer parentNum)
       throws PatchListNotAvailableException {
     Project.NameKey project = change.getProject();
-    if (patchSet.getRevision() == null) {
-      throw new PatchListNotAvailableException("revision is null for " + patchSet.getId());
-    }
-    ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
+    ObjectId b = patchSet.commitId();
     Whitespace ws = Whitespace.IGNORE_NONE;
     if (parentNum != null) {
       return get(PatchListKey.againstParentNum(parentNum, b, ws), project);
diff --git a/java/com/google/gerrit/server/patch/PatchListEntry.java b/java/com/google/gerrit/server/patch/PatchListEntry.java
index 6b1a153..625f56c 100644
--- a/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -30,10 +30,10 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gerrit.reviewdb.client.Patch.PatchType;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Patch.ChangeType;
+import com.google.gerrit.entities.Patch.PatchType;
+import com.google.gerrit.entities.PatchSet;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -229,7 +229,7 @@
   }
 
   Patch toPatch(PatchSet.Id setId) {
-    final Patch p = new Patch(new Patch.Key(setId, getNewName()));
+    final Patch p = new Patch(Patch.key(setId, getNewName()));
     p.setChangeType(getChangeType());
     p.setPatchType(getPatchType());
     p.setSourceFileName(getOldName());
diff --git a/java/com/google/gerrit/server/patch/PatchListKey.java b/java/com/google/gerrit/server/patch/PatchListKey.java
index 2df6d66..bf38029 100644
--- a/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableBiMap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.git.ObjectIds;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
@@ -82,7 +83,7 @@
   private transient Whitespace whitespace;
 
   private PatchListKey(AnyObjectId a, AnyObjectId b, Whitespace ws) {
-    oldId = a != null ? a.copy() : null;
+    oldId = ObjectIds.copyOrNull(a);
     newId = b.copy();
     whitespace = ws;
   }
diff --git a/java/com/google/gerrit/server/patch/PatchListLoader.java b/java/com/google/gerrit/server/patch/PatchListLoader.java
index 08de537..c3d9a1d 100644
--- a/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -28,9 +28,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -314,12 +314,12 @@
   }
 
   private static boolean areParentChild(RevCommit commitA, RevCommit commitB) {
-    return ObjectId.equals(commitA.getParent(0), commitB)
-        || ObjectId.equals(commitB.getParent(0), commitA);
+    return ObjectId.isEqual(commitA.getParent(0), commitB)
+        || ObjectId.isEqual(commitB.getParent(0), commitA);
   }
 
   private static boolean haveCommonParent(RevCommit commitA, RevCommit commitB) {
-    return ObjectId.equals(commitA.getParent(0), commitB.getParent(0));
+    return ObjectId.isEqual(commitA.getParent(0), commitB.getParent(0));
   }
 
   private static Set<String> getTouchedFilePaths(PatchListEntry patchListEntry) {
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index c2caa01..1c1c639 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -21,14 +21,14 @@
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.mime.FileTypeRegistry;
 import com.google.inject.Inject;
 import eu.medsea.mimeutil.MimeType;
@@ -37,6 +37,7 @@
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.errors.CorruptObjectException;
@@ -120,7 +121,6 @@
 
   private PatchScript build(PatchListEntry content, CommentDetail comments, List<Patch> history)
       throws IOException {
-    boolean intralineDifferenceIsPossible = true;
     boolean intralineFailure = false;
     boolean intralineTimeout = false;
 
@@ -146,21 +146,17 @@
             break;
 
           case DISABLED:
-            intralineDifferenceIsPossible = false;
             break;
 
           case ERROR:
-            intralineDifferenceIsPossible = false;
             intralineFailure = true;
             break;
 
           case TIMEOUT:
-            intralineDifferenceIsPossible = false;
             intralineTimeout = true;
             break;
         }
       } else {
-        intralineDifferenceIsPossible = false;
         intralineFailure = true;
       }
     }
@@ -220,7 +216,6 @@
         comments,
         history,
         hugeFile,
-        intralineDifferenceIsPossible,
         intralineFailure,
         intralineTimeout,
         content.getPatchType() == Patch.PatchType.BINARY,
@@ -514,8 +509,7 @@
       try {
         final boolean reuse;
         if (Patch.COMMIT_MSG.equals(path)) {
-          if (comparisonType.isAgainstParentOrAutoMerge()
-              && (aId == within || within.equals(aId))) {
+          if (comparisonType.isAgainstParentOrAutoMerge() && Objects.equals(aId, within)) {
             id = ObjectId.zeroId();
             src = Text.EMPTY;
             srcContent = Text.NO_BYTES;
@@ -534,8 +528,7 @@
           }
           reuse = false;
         } else if (Patch.MERGE_LIST.equals(path)) {
-          if (comparisonType.isAgainstParentOrAutoMerge()
-              && (aId == within || within.equals(aId))) {
+          if (comparisonType.isAgainstParentOrAutoMerge() && Objects.equals(aId, within)) {
             id = ObjectId.zeroId();
             src = Text.EMPTY;
             srcContent = Text.NO_BYTES;
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index a7e7738..319a5bc 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -15,20 +15,21 @@
 package com.google.gerrit.server.patch;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Patch.ChangeType;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -136,7 +137,7 @@
     this.psb = patchSetB;
     this.diffPrefs = diffPrefs;
 
-    changeId = patchSetB.getParentKey();
+    changeId = patchSetB.changeId();
   }
 
   @AssistedInject
@@ -172,7 +173,7 @@
     this.psb = patchSetB;
     this.diffPrefs = diffPrefs;
 
-    changeId = patchSetB.getParentKey();
+    changeId = patchSetB.changeId();
     checkArgument(parentNum >= 0, "parentNum must be >= 0");
   }
 
@@ -188,19 +189,28 @@
   public PatchScript call()
       throws LargeObjectException, AuthException, InvalidChangeOperationException, IOException,
           PermissionBackendException {
-    if (parentNum < 0) {
-      validatePatchSetId(psa);
-    }
+    validatePatchSetId(psa);
     validatePatchSetId(psb);
 
-    PatchSet psEntityA = psa != null ? psUtil.get(notes, psa) : null;
-    PatchSet psEntityB = psb.get() == 0 ? new PatchSet(psb) : psUtil.get(notes, psb);
-    if (psEntityA != null || psEntityB != null) {
-      try {
-        permissionBackend.currentUser().change(notes).check(ChangePermission.READ);
-      } catch (AuthException e) {
-        throw new NoSuchChangeException(changeId, e);
-      }
+    if (psa != null) {
+      checkState(parentNum < 0, "expected no parentNum when psa is present");
+      checkArgument(psa.get() != 0, "edit not supported for left side");
+      aId = getCommitId(psa);
+    } else {
+      aId = null;
+    }
+
+    if (psb.get() != 0) {
+      bId = getCommitId(psb);
+    } else {
+      // Change edit: create synthetic PatchSet corresponding to the edit.
+      bId = getEditRev();
+    }
+
+    try {
+      permissionBackend.currentUser().change(notes).check(ChangePermission.READ);
+    } catch (AuthException e) {
+      throw new NoSuchChangeException(changeId, e);
     }
 
     if (!projectCache.checkedGet(notes.getProjectName()).statePermitsRead()) {
@@ -208,11 +218,6 @@
     }
 
     try (Repository git = repoManager.openRepository(notes.getProjectName())) {
-      bId = toObjectId(psEntityB);
-      if (parentNum < 0) {
-        aId = psEntityA != null ? toObjectId(psEntityA) : null;
-      }
-
       try {
         final PatchList list = listFor(keyFor(diffPrefs.ignoreWhitespace));
         final PatchScriptBuilder b = newBuilder(list, git);
@@ -258,20 +263,12 @@
     return b;
   }
 
-  private ObjectId toObjectId(PatchSet ps) throws AuthException, IOException {
-    if (ps.getId().get() == 0) {
-      return getEditRev();
+  private ObjectId getCommitId(PatchSet.Id psId) {
+    PatchSet ps = psUtil.get(notes, psId);
+    if (ps == null) {
+      throw new NoSuchChangeException(psId.changeId());
     }
-    if (ps.getRevision() == null || ps.getRevision().get() == null) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    try {
-      return ObjectId.fromString(ps.getRevision().get());
-    } catch (IllegalArgumentException e) {
-      logger.atSevere().log("Patch set %s has invalid revision", ps.getId());
-      throw new NoSuchChangeException(changeId, e);
-    }
+    return ps.commitId();
   }
 
   private ObjectId getEditRev() throws AuthException, IOException {
@@ -284,7 +281,7 @@
 
   private void validatePatchSetId(PatchSet.Id psId) throws NoSuchChangeException {
     if (psId == null) { // OK, means use base;
-    } else if (changeId.equals(psId.getParentKey())) { // OK, same change;
+    } else if (changeId.equals(psId.changeId())) { // OK, same change;
     } else {
       throw new NoSuchChangeException(changeId);
     }
@@ -306,7 +303,7 @@
           switch (changeType) {
             case COPIED:
             case RENAMED:
-              if (ps.getId().equals(psa)) {
+              if (ps.id().equals(psa)) {
                 name = oldName;
               }
               break;
@@ -319,12 +316,12 @@
           }
         }
 
-        Patch p = new Patch(new Patch.Key(ps.getId(), name));
+        Patch p = new Patch(Patch.key(ps.id(), name));
         history.add(p);
         byKey.put(p.getKey(), p);
       }
       if (edit != null && edit.isPresent()) {
-        Patch p = new Patch(new Patch.Key(new PatchSet.Id(psb.getParentKey(), 0), fileName));
+        Patch p = new Patch(Patch.key(PatchSet.id(psb.changeId(), 0), fileName));
         history.add(p);
         byKey.put(p.getKey(), p);
       }
@@ -385,8 +382,8 @@
   private void loadPublished(Map<Patch.Key, Patch> byKey, String file) {
     for (Comment c : commentsUtil.publishedByChangeFile(notes, file)) {
       comments.include(notes.getChangeId(), c);
-      PatchSet.Id psId = new PatchSet.Id(notes.getChangeId(), c.key.patchSetId);
-      Patch.Key pKey = new Patch.Key(psId, c.key.filename);
+      PatchSet.Id psId = PatchSet.id(notes.getChangeId(), c.key.patchSetId);
+      Patch.Key pKey = Patch.key(psId, c.key.filename);
       Patch p = byKey.get(pKey);
       if (p != null) {
         p.setCommentCount(p.getCommentCount() + 1);
@@ -397,8 +394,8 @@
   private void loadDrafts(Map<Patch.Key, Patch> byKey, Account.Id me, String file) {
     for (Comment c : commentsUtil.draftByChangeFileAuthor(notes, file, me)) {
       comments.include(notes.getChangeId(), c);
-      PatchSet.Id psId = new PatchSet.Id(notes.getChangeId(), c.key.patchSetId);
-      Patch.Key pKey = new Patch.Key(psId, c.key.filename);
+      PatchSet.Id psId = PatchSet.id(notes.getChangeId(), c.key.patchSetId);
+      Patch.Key pKey = Patch.key(psId, c.key.filename);
       Patch p = byKey.get(pKey);
       if (p != null) {
         p.setDraftCount(p.getDraftCount() + 1);
diff --git a/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index 0eb5588..019fe15 100644
--- a/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -14,13 +14,10 @@
 
 package com.google.gerrit.server.patch;
 
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetInfo;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -28,13 +25,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -58,9 +51,9 @@
     PatchSetInfo info = new PatchSetInfo(psi);
     info.setSubject(src.getShortMessage());
     info.setMessage(src.getFullMessage());
-    info.setAuthor(toUserIdentity(src.getAuthorIdent()));
-    info.setCommitter(toUserIdentity(src.getCommitterIdent()));
-    info.setRevId(src.getName());
+    info.setAuthor(emails.toUserIdentity(src.getAuthorIdent()));
+    info.setCommitter(emails.toUserIdentity(src.getCommitterIdent()));
+    info.setCommitId(src);
     return info;
   }
 
@@ -78,8 +71,8 @@
       throws PatchSetInfoNotAvailableException {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
-      final RevCommit src = rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
-      PatchSetInfo info = get(rw, src, patchSet.getId());
+      RevCommit src = rw.parseCommit(patchSet.commitId());
+      PatchSetInfo info = get(rw, src, patchSet.id());
       info.setParents(toParentInfos(src.getParents(), rw));
       return info;
     } catch (IOException | StorageException e) {
@@ -87,33 +80,13 @@
     }
   }
 
-  // TODO: The same method exists in EventFactory, find a common place for it
-  private UserIdentity toUserIdentity(PersonIdent who) throws IOException {
-    final UserIdentity u = new UserIdentity();
-    u.setName(who.getName());
-    u.setEmail(who.getEmailAddress());
-    u.setDate(new Timestamp(who.getWhen().getTime()));
-    u.setTimeZone(who.getTimeZoneOffset());
-
-    // If only one account has access to this email address, select it
-    // as the identity of the user.
-    //
-    Set<Account.Id> a = emails.getAccountFor(u.getEmail());
-    if (a.size() == 1) {
-      u.setAccount(a.iterator().next());
-    }
-
-    return u;
-  }
-
   private List<PatchSetInfo.ParentInfo> toParentInfos(RevCommit[] parents, RevWalk walk)
       throws IOException, MissingObjectException {
     List<PatchSetInfo.ParentInfo> pInfos = new ArrayList<>(parents.length);
     for (RevCommit parent : parents) {
       walk.parseBody(parent);
-      RevId rev = new RevId(parent.getId().name());
       String msg = parent.getShortMessage();
-      pInfos.add(new PatchSetInfo.ParentInfo(rev, msg));
+      pInfos.add(new PatchSetInfo.ParentInfo(parent, msg));
     }
     return pInfos;
   }
diff --git a/java/com/google/gerrit/server/patch/Text.java b/java/com/google/gerrit/server/patch/Text.java
index 011185d..cc0a5e4 100644
--- a/java/com/google/gerrit/server/patch/Text.java
+++ b/java/com/google/gerrit/server/patch/Text.java
@@ -18,6 +18,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.git.ObjectIds;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.charset.IllegalCharsetNameException;
@@ -62,7 +63,7 @@
             RevCommit p = c.getParent(0);
             rw.parseBody(p);
             b.append("Parent:     ");
-            b.append(reader.abbreviate(p, 8).name());
+            b.append(abbreviateName(p, reader));
             b.append(" (");
             b.append(p.getShortMessage());
             b.append(")\n");
@@ -73,7 +74,7 @@
             RevCommit p = c.getParent(i);
             rw.parseBody(p);
             b.append(i == 0 ? "Merge Of:   " : "            ");
-            b.append(reader.abbreviate(p, 8).name());
+            b.append(abbreviateName(p, reader));
             b.append(" (");
             b.append(p.getShortMessage());
             b.append(")\n");
@@ -106,7 +107,7 @@
           b.append("Merge List:\n\n");
           for (RevCommit commit : MergeListBuilder.build(rw, c, uniterestingParent)) {
             b.append("* ");
-            b.append(reader.abbreviate(commit, 8).name());
+            b.append(abbreviateName(commit, reader));
             b.append(" ");
             b.append(commit.getShortMessage());
             b.append("\n");
@@ -116,6 +117,10 @@
     }
   }
 
+  private static String abbreviateName(RevCommit p, ObjectReader reader) throws IOException {
+    return ObjectIds.abbreviateName(p, 8, reader);
+  }
+
   private static void appendPersonIdent(StringBuilder b, String field, PersonIdent person) {
     if (person != null) {
       b.append(field).append(":    ");
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index c3be7f2..cecdb95 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -22,12 +22,12 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
@@ -79,6 +79,13 @@
     return new ForChangeImpl();
   }
 
+  ChangeControl setChangeData(@Nullable ChangeData cd) {
+    if (cd != null) {
+      this.cd = cd;
+    }
+    return this;
+  }
+
   private CurrentUser getUser() {
     return refControl.getUser();
   }
@@ -98,13 +105,6 @@
     return cd;
   }
 
-  ChangeControl setChangeData(@Nullable ChangeData cd) {
-    if (cd != null) {
-      this.cd = cd;
-    }
-    return this;
-  }
-
   /** Can this user see this change? */
   boolean isVisible() {
     if (getChange().isPrivate() && !isPrivateVisible(changeData())) {
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index b23c85f..6142bc0 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -22,13 +22,13 @@
 import com.google.common.flogger.FluentLogger;
 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.Project;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.api.access.PluginPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 2d1f6fc..f887f61 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -16,8 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS_SELF;
+import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
 import static java.util.stream.Collectors.toMap;
 
 import com.google.auto.value.AutoValue;
@@ -28,20 +27,22 @@
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TagMatcher;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
@@ -59,7 +60,6 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.SymbolicRef;
 
 class DefaultRefFilter {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -81,7 +81,7 @@
   private final Counter0 skipFilterCount;
   private final boolean skipFullRefEvaluationIfAllRefsAreVisible;
 
-  private Map<Change.Id, Branch.NameKey> visibleChanges;
+  private Map<Change.Id, BranchNameKey> visibleChanges;
 
   @Inject
   DefaultRefFilter(
@@ -122,69 +122,100 @@
   /** Filters given refs and tags by visibility. */
   Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
       throws PermissionBackendException {
+    logger.atFinest().log(
+        "Filter refs for repository %s by visibility (options = %s, refs = %s)",
+        projectState.getNameKey(), opts, refs);
+    logger.atFinest().log("Calling user: %s", user.getLoggableName());
+    logger.atFinest().log("Groups: %s", user.getEffectiveGroups().getKnownGroups());
+    logger.atFinest().log(
+        "auth.skipFullRefEvaluationIfAllRefsAreVisible = %s",
+        skipFullRefEvaluationIfAllRefsAreVisible);
+    logger.atFinest().log(
+        "Project state %s permits read = %s",
+        projectState.getProject().getState(), projectState.statePermitsRead());
+
     // See if we can get away with a single, cheap ref evaluation.
     if (refs.size() == 1) {
       String refName = Iterables.getOnlyElement(refs.values()).getName();
       if (opts.filterMeta() && isMetadata(refName)) {
+        logger.atFinest().log("Filter out metadata ref %s", refName);
         return ImmutableMap.of();
       }
       if (RefNames.isRefsChanges(refName)) {
-        return canSeeSingleChangeRef(refName) ? refs : ImmutableMap.of();
+        boolean isChangeRefVisisble = canSeeSingleChangeRef(refName);
+        if (isChangeRefVisisble) {
+          logger.atFinest().log("Change ref %s is visible", refName);
+          return refs;
+        }
+        logger.atFinest().log("Filter out non-visible change ref %s", refName);
+        return ImmutableMap.of();
       }
     }
 
     // Perform an initial ref filtering with all the refs the caller asked for. If we find tags that
-    // we have to investigate (deferred tags) separately then perform a reachability check starting
+    // we have to investigate separately (deferred tags) then perform a reachability check starting
     // from all visible branches (refs/heads/*).
     Result initialRefFilter = filterRefs(refs, repo, opts);
     Map<String, Ref> visibleRefs = initialRefFilter.visibleRefs();
     if (!initialRefFilter.deferredTags().isEmpty()) {
-      Result allVisibleBranches = filterRefs(getTaggableRefsMap(repo), repo, opts);
-      checkState(
-          allVisibleBranches.deferredTags().isEmpty(),
-          "unexpected tags found when filtering refs/heads/* " + allVisibleBranches.deferredTags());
+      try (TraceTimer traceTimer = TraceContext.newTimer("Check visibility of deferred tags")) {
+        Result allVisibleBranches = filterRefs(getTaggableRefsMap(repo), repo, opts);
+        checkState(
+            allVisibleBranches.deferredTags().isEmpty(),
+            "unexpected tags found when filtering refs/heads/* "
+                + allVisibleBranches.deferredTags());
 
-      TagMatcher tags =
-          tagCache
-              .get(projectState.getNameKey())
-              .matcher(tagCache, repo, allVisibleBranches.visibleRefs().values());
-      for (Ref tag : initialRefFilter.deferredTags()) {
-        try {
-          if (tags.isReachable(tag)) {
-            visibleRefs.put(tag.getName(), tag);
+        TagMatcher tags =
+            tagCache
+                .get(projectState.getNameKey())
+                .matcher(tagCache, repo, allVisibleBranches.visibleRefs().values());
+        for (Ref tag : initialRefFilter.deferredTags()) {
+          try {
+            if (tags.isReachable(tag)) {
+              logger.atFinest().log("Include reachable tag %s", tag.getName());
+              visibleRefs.put(tag.getName(), tag);
+            } else {
+              logger.atFinest().log("Filter out non-reachable tag %s", tag.getName());
+            }
+          } catch (IOException e) {
+            throw new PermissionBackendException(e);
           }
-        } catch (IOException e) {
-          throw new PermissionBackendException(e);
         }
       }
     }
+
+    logger.atFinest().log("visible refs = %s", visibleRefs);
     return visibleRefs;
   }
 
   /**
    * Filters refs by visibility. Returns tags where visibility can't be trivially computed
-   * separately for later ref-walk-based visibility computation. Tags where visibility is trivial to
+   * separately for later rev-walk-based visibility computation. Tags where visibility is trivial to
    * compute will be returned as part of {@link Result#visibleRefs()}.
    */
   Result filterRefs(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
       throws PermissionBackendException {
-    if (projectState.isAllUsers()) {
-      refs = addUsersSelfSymref(refs);
-    }
+    logger.atFinest().log("Filter refs (refs = %s)", refs);
 
     // TODO(hiesel): Remove when optimization is done.
     boolean hasReadOnRefsStar =
         checkProjectPermission(permissionBackendForProject, ProjectPermission.READ);
+    logger.atFinest().log("User has READ on refs/* = %s", hasReadOnRefsStar);
     if (skipFullRefEvaluationIfAllRefsAreVisible && !projectState.isAllUsers()) {
       if (projectState.statePermitsRead() && hasReadOnRefsStar) {
         skipFilterCount.increment();
+        logger.atFinest().log(
+            "Fast path, all refs are visible because user has READ on refs/*: %s", refs);
         return new AutoValue_DefaultRefFilter_Result(refs, ImmutableList.of());
       } else if (projectControl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
         skipFilterCount.increment();
-        return new AutoValue_DefaultRefFilter_Result(
-            fastHideRefsMetaConfig(refs), ImmutableList.of());
+        refs = fastHideRefsMetaConfig(refs);
+        logger.atFinest().log(
+            "Fast path, all refs except %s are visible: %s", RefNames.REFS_CONFIG, refs);
+        return new AutoValue_DefaultRefFilter_Result(refs, ImmutableList.of());
       }
     }
+    logger.atFinest().log("Doing full ref filtering");
     fullFilterCount.increment();
 
     boolean hasAccessDatabase =
@@ -239,7 +270,9 @@
         resultRefs.put(refName, ref);
       }
     }
-    return new AutoValue_DefaultRefFilter_Result(resultRefs, deferredTags);
+    Result result = new AutoValue_DefaultRefFilter_Result(resultRefs, deferredTags);
+    logger.atFinest().log("Result of ref filtering = %s", result);
+    return result;
   }
 
   /**
@@ -272,18 +305,6 @@
     return refs;
   }
 
-  private Map<String, Ref> addUsersSelfSymref(Map<String, Ref> refs) {
-    if (user.isIdentifiedUser()) {
-      Ref r = refs.get(RefNames.refsUsers(user.getAccountId()));
-      if (r != null) {
-        SymbolicRef s = new SymbolicRef(REFS_USERS_SELF, r);
-        refs = new HashMap<>(refs);
-        refs.put(s.getName(), s);
-      }
-    }
-    return refs;
-  }
-
   private boolean visible(Repository repo, Change.Id changeId) throws PermissionBackendException {
     if (visibleChanges == null) {
       if (changeCache == null) {
@@ -291,44 +312,51 @@
       } else {
         visibleChanges = visibleChangesBySearch();
       }
+      logger.atFinest().log("Visible changes: %s", visibleChanges.keySet());
     }
     return visibleChanges.containsKey(changeId);
   }
 
   private boolean visibleEdit(Repository repo, String name) throws PermissionBackendException {
     Change.Id id = Change.Id.fromEditRefPart(name);
-    // Initialize if it wasn't yet
-    if (visibleChanges == null) {
-      visible(repo, id);
-    }
     if (id == null) {
+      logger.atWarning().log("Couldn't extract change ID from edit ref %s", name);
       return false;
     }
 
     if (user.isIdentifiedUser()
         && name.startsWith(RefNames.refsEditPrefix(user.asIdentifiedUser().getAccountId()))
         && visible(repo, id)) {
+      logger.atFinest().log("Own change edit ref is visible: %s", name);
       return true;
     }
+
+    // Initialize visibleChanges if it wasn't initialized yet.
+    if (visibleChanges == null) {
+      visible(repo, id);
+    }
     if (visibleChanges.containsKey(id)) {
       try {
         // Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
         permissionBackendForProject
-            .ref(visibleChanges.get(id).get())
+            .ref(visibleChanges.get(id).branch())
             .check(RefPermission.READ_PRIVATE_CHANGES);
+        logger.atFinest().log("Foreign change edit ref is visible: %s", name);
         return true;
       } catch (AuthException e) {
+        logger.atFinest().log("Foreign change edit ref is not visible: %s", name);
         return false;
       }
     }
+
+    logger.atFinest().log("Change %d of change edit ref %s is not visible", id.get(), name);
     return false;
   }
 
-  private Map<Change.Id, Branch.NameKey> visibleChangesBySearch()
-      throws PermissionBackendException {
+  private Map<Change.Id, BranchNameKey> visibleChangesBySearch() throws PermissionBackendException {
     Project.NameKey project = projectState.getNameKey();
     try {
-      Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>();
+      Map<Change.Id, BranchNameKey> visibleChanges = new HashMap<>();
       for (ChangeData cd : changeCache.getChangeData(project)) {
         ChangeNotes notes = changeNotesFactory.createFromIndexedChange(cd.change());
         if (!projectState.statePermitsRead()) {
@@ -349,7 +377,7 @@
     }
   }
 
-  private Map<Change.Id, Branch.NameKey> visibleChangesByScan(Repository repo)
+  private Map<Change.Id, BranchNameKey> visibleChangesByScan(Repository repo)
       throws PermissionBackendException {
     Project.NameKey p = projectState.getNameKey();
     ImmutableList<ChangeNotesResult> changes;
@@ -361,7 +389,7 @@
       return Collections.emptyMap();
     }
 
-    Map<Change.Id, Branch.NameKey> result = Maps.newHashMapWithExpectedSize(changes.size());
+    Map<Change.Id, BranchNameKey> result = Maps.newHashMapWithExpectedSize(changes.size());
     for (ChangeNotesResult notesResult : changes) {
       ChangeNotes notes = toNotes(notesResult);
       if (notes != null) {
@@ -393,7 +421,9 @@
   }
 
   private boolean isMetadata(String name) {
-    return RefNames.isRefsChanges(name) || RefNames.isRefsEdit(name);
+    boolean isMetaData = RefNames.isRefsChanges(name) || RefNames.isRefsEdit(name);
+    logger.atFinest().log("ref %s is " + (isMetaData ? "" : "not ") + "a metadata ref", name);
+    return isMetaData;
   }
 
   private static boolean isTag(Ref ref) {
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 5c7ee0d..0800d6b 100644
--- a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.permissions;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.access.CoreOrPluginProjectPermission;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
 import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 7960c65..e0c5927 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -23,15 +23,15 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.access.CoreOrPluginProjectPermission;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -157,8 +157,8 @@
     public abstract ForProject project(Project.NameKey project);
 
     /** Returns an instance scoped for the {@code ref}, and its parent project. */
-    public ForRef ref(Branch.NameKey ref) {
-      return project(ref.getParentKey()).ref(ref.get());
+    public ForRef ref(BranchNameKey ref) {
+      return project(ref.project()).ref(ref.branch());
     }
 
     /** Returns an instance scoped for the change, and its destination ref and project. */
@@ -280,7 +280,7 @@
     /** Returns an instance scoped for the change, and its destination ref and project. */
     public ForChange change(ChangeData cd) {
       try {
-        return ref(cd.change().getDest().get()).change(cd);
+        return ref(cd.change().getDest().branch()).change(cd);
       } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
@@ -288,7 +288,7 @@
 
     /** Returns an instance scoped for the change, and its destination ref and project. */
     public ForChange change(ChangeNotes notes) {
-      return ref(notes.getChange().getDest().get()).change(notes);
+      return ref(notes.getChange().getDest().branch()).change(notes);
     }
 
     /**
@@ -297,7 +297,7 @@
      * stale data from the index is acceptable.
      */
     public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
-      return ref(notes.getChange().getDest().get()).indexedChange(cd, notes);
+      return ref(notes.getChange().getDest().branch()).indexedChange(cd, notes);
     }
 
     /** Verify scoped user can {@code perm}, throwing if denied. */
diff --git a/java/com/google/gerrit/server/permissions/PermissionCollection.java b/java/com/google/gerrit/server/permissions/PermissionCollection.java
index 8ecb274..1f0370b 100644
--- a/java/com/google/gerrit/server/permissions/PermissionCollection.java
+++ b/java/com/google/gerrit/server/permissions/PermissionCollection.java
@@ -27,12 +27,12 @@
 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.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.project.RefPatternMatcher.ExpandParameters;
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 20eeaa6..c0fbf36 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -17,23 +17,23 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.common.data.AccessSection.ALL;
 import static com.google.gerrit.common.data.AccessSection.REGEX_PREFIX;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_TAGS;
+import static com.google.gerrit.entities.RefNames.REFS_TAGS;
 import static com.google.gerrit.server.util.MagicBranch.NEW_CHANGE;
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.AccessSection;
 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.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.access.CoreOrPluginProjectPermission;
 import com.google.gerrit.extensions.api.access.PluginProjectPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.config.AllUsersName;
@@ -122,8 +122,8 @@
     return changeControlFactory.create(controlForRef(notes.getChange().getDest()), notes);
   }
 
-  RefControl controlForRef(Branch.NameKey ref) {
-    return controlForRef(ref.get());
+  RefControl controlForRef(BranchNameKey ref) {
+    return controlForRef(ref.branch());
   }
 
   public RefControl controlForRef(String refName) {
diff --git a/java/com/google/gerrit/server/permissions/ProjectPermission.java b/java/com/google/gerrit/server/permissions/ProjectPermission.java
index 653303a..fc31e96 100644
--- a/java/com/google/gerrit/server/permissions/ProjectPermission.java
+++ b/java/com/google/gerrit/server/permissions/ProjectPermission.java
@@ -16,9 +16,9 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.access.CoreOrPluginProjectPermission;
 import com.google.gerrit.extensions.api.access.GerritPermission;
-import com.google.gerrit.reviewdb.client.RefNames;
 
 public enum ProjectPermission implements CoreOrPluginProjectPermission {
   /**
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 10ded3c..945ae06 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -22,12 +22,12 @@
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.logging.CallerFinder;
@@ -426,7 +426,7 @@
           withForce,
           projectControl.getProject().getName(),
           refName,
-          callerFinder.findCaller());
+          callerFinder.findCallerLazy());
       return false;
     }
 
@@ -439,7 +439,7 @@
             withForce,
             projectControl.getProject().getName(),
             refName,
-            callerFinder.findCaller());
+            callerFinder.findCallerLazy());
         return true;
       }
     }
@@ -451,7 +451,7 @@
         withForce,
         projectControl.getProject().getName(),
         refName,
-        callerFinder.findCaller());
+        callerFinder.findCallerLazy());
     return false;
   }
 
diff --git a/java/com/google/gerrit/server/permissions/RefVisibilityControl.java b/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
index 6db5051..23a7db1 100644
--- a/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
+++ b/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
@@ -15,16 +15,17 @@
 package com.google.gerrit.server.permissions;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.entities.RefNames.REFS_CACHE_AUTOMERGE;
 
 import com.google.common.base.Throwables;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -71,7 +72,7 @@
       return projectControl.controlForRef(refName).hasReadPermissionOnRef(false);
     }
 
-    if (refName.startsWith(RefNames.REFS_CACHE_AUTOMERGE)) {
+    if (refName.startsWith(REFS_CACHE_AUTOMERGE)) {
       // Internal cache state that is accessible to no one.
       return false;
     }
diff --git a/java/com/google/gerrit/server/plugincontext/PluginContext.java b/java/com/google/gerrit/server/plugincontext/PluginContext.java
index 70b23e3..90d56c8 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginContext.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer3;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -118,25 +119,32 @@
 
     @Inject
     PluginMetrics(MetricMaker metricMaker) {
+      Field<String> pluginNameField =
+          Field.ofString("plugin_name", Metadata.Builder::pluginName).build();
+      Field<String> classNameField =
+          Field.ofString("class_name", Metadata.Builder::className).build();
+      Field<String> exportValueField =
+          Field.ofString("export_value", Metadata.Builder::exportValue).build();
+
       this.latency =
           metricMaker.newTimer(
               "plugin/latency",
               new Description("Latency for plugin invocation")
                   .setCumulative()
                   .setUnit(Units.MILLISECONDS),
-              Field.ofString("plugin_name"),
-              Field.ofString("class_name"),
-              Field.ofString("export_name"));
+              pluginNameField,
+              classNameField,
+              exportValueField);
       this.errorCount =
           metricMaker.newCounter(
               "plugin/error_count",
               new Description("Number of plugin errors").setCumulative().setUnit("errors"),
-              Field.ofString("plugin_name"),
-              Field.ofString("class_name"),
-              Field.ofString("export_name"));
+              pluginNameField,
+              classNameField,
+              exportValueField);
     }
 
-    Timer3.Context startLatency(Extension<?> extension) {
+    Timer3.Context<String, String, String> startLatency(Extension<?> extension) {
       return latency.start(
           extension.getPluginName(),
           extension.get().getClass().getName(),
@@ -194,7 +202,7 @@
       return;
     }
     try (TraceContext traceContext = newTrace(extension);
-        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
+        Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       extensionImplConsumer.run(extensionImpl);
     } catch (Throwable e) {
       pluginMetrics.incrementErrorCount(extension);
@@ -223,7 +231,7 @@
     }
 
     try (TraceContext traceContext = newTrace(extension);
-        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
+        Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       extensionConsumer.run(extension);
     } catch (Throwable e) {
       pluginMetrics.incrementErrorCount(extension);
@@ -257,14 +265,14 @@
     }
 
     try (TraceContext traceContext = newTrace(extension);
-        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
+        Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       extensionImplConsumer.run(extensionImpl);
     } catch (Throwable e) {
       Throwables.throwIfInstanceOf(e, exceptionClass);
       Throwables.throwIfUnchecked(e);
       pluginMetrics.incrementErrorCount(extension);
       logger.atWarning().withCause(e).log(
-          "Failure in %s of plugin invoke%s", extensionImpl.getClass(), extension.getPluginName());
+          "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
     }
   }
 
@@ -294,7 +302,7 @@
     }
 
     try (TraceContext traceContext = newTrace(extension);
-        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
+        Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       extensionConsumer.run(extension);
     } catch (Throwable e) {
       Throwables.throwIfInstanceOf(e, exceptionClass);
@@ -320,7 +328,7 @@
       Extension<T> extension,
       ExtensionImplFunction<T, R> extensionImplFunction) {
     try (TraceContext traceContext = newTrace(extension);
-        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
+        Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       return extensionImplFunction.call(extension.get());
     }
   }
@@ -345,7 +353,7 @@
       Class<X> exceptionClass)
       throws X {
     try (TraceContext traceContext = newTrace(extension);
-        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
+        Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       try {
         return checkedExtensionImplFunction.call(extension.get());
       } catch (Exception e) {
@@ -374,7 +382,7 @@
       Extension<T> extension,
       ExtensionFunction<Extension<T>, R> extensionFunction) {
     try (TraceContext traceContext = newTrace(extension);
-        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
+        Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       return extensionFunction.call(extension);
     }
   }
@@ -400,7 +408,7 @@
       Class<X> exceptionClass)
       throws X {
     try (TraceContext traceContext = newTrace(extension);
-        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
+        Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       try {
         return checkedExtensionFunction.call(extension);
       } catch (Exception e) {
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/DisablePlugin.java b/java/com/google/gerrit/server/plugins/DisablePlugin.java
index 62eb993..8adae52 100644
--- a/java/com/google/gerrit/server/plugins/DisablePlugin.java
+++ b/java/com/google/gerrit/server/plugins/DisablePlugin.java
@@ -17,6 +17,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.common.PluginInfo;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+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.permissions.GlobalPermission;
@@ -30,15 +32,20 @@
 
   private final PluginLoader loader;
   private final PermissionBackend permissionBackend;
+  private final MandatoryPluginsCollection mandatoryPluginsCollection;
 
   @Inject
-  DisablePlugin(PluginLoader loader, PermissionBackend permissionBackend) {
+  DisablePlugin(
+      PluginLoader loader,
+      PermissionBackend permissionBackend,
+      MandatoryPluginsCollection mandatoryPluginsCollection) {
     this.loader = loader;
     this.permissionBackend = permissionBackend;
+    this.mandatoryPluginsCollection = mandatoryPluginsCollection;
   }
 
   @Override
-  public PluginInfo apply(PluginResource resource, Input input) throws RestApiException {
+  public Response<PluginInfo> apply(PluginResource resource, Input input) throws RestApiException {
     try {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     } catch (PermissionBackendException e) {
@@ -46,7 +53,10 @@
     }
     loader.checkRemoteAdminEnabled();
     String name = resource.getName();
+    if (mandatoryPluginsCollection.contains(name)) {
+      throw new MethodNotAllowedException("Plugin " + name + " is mandatory");
+    }
     loader.disablePlugins(ImmutableSet.of(name));
-    return ListPlugins.toPluginInfo(loader.get(name));
+    return Response.ok(ListPlugins.toPluginInfo(loader.get(name)));
   }
 }
diff --git a/java/com/google/gerrit/server/plugins/EnablePlugin.java b/java/com/google/gerrit/server/plugins/EnablePlugin.java
index 569bc39..b45aaf1f 100644
--- a/java/com/google/gerrit/server/plugins/EnablePlugin.java
+++ b/java/com/google/gerrit/server/plugins/EnablePlugin.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.common.PluginInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.inject.Inject;
@@ -39,7 +40,7 @@
   }
 
   @Override
-  public PluginInfo apply(PluginResource resource, Input input) throws RestApiException {
+  public Response<PluginInfo> apply(PluginResource resource, Input input) throws RestApiException {
     loader.checkRemoteAdminEnabled();
     String name = resource.getName();
     try {
@@ -52,6 +53,6 @@
       pw.flush();
       throw new ResourceConflictException(buf.toString());
     }
-    return ListPlugins.toPluginInfo(loader.get(name));
+    return Response.ok(ListPlugins.toPluginInfo(loader.get(name)));
   }
 }
diff --git a/java/com/google/gerrit/server/plugins/GetStatus.java b/java/com/google/gerrit/server/plugins/GetStatus.java
index cbd864a..5fcc96a 100644
--- a/java/com/google/gerrit/server/plugins/GetStatus.java
+++ b/java/com/google/gerrit/server/plugins/GetStatus.java
@@ -15,13 +15,14 @@
 package com.google.gerrit.server.plugins;
 
 import com.google.gerrit.extensions.common.PluginInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GetStatus implements RestReadView<PluginResource> {
   @Override
-  public PluginInfo apply(PluginResource resource) {
-    return ListPlugins.toPluginInfo(resource.getPlugin());
+  public Response<PluginInfo> apply(PluginResource resource) {
+    return Response.ok(ListPlugins.toPluginInfo(resource.getPlugin()));
   }
 }
diff --git a/java/com/google/gerrit/server/plugins/ListPlugins.java b/java/com/google/gerrit/server/plugins/ListPlugins.java
index 438a0c4..0408efc 100644
--- a/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.api.plugins.Plugins;
 import com.google.gerrit.extensions.common.PluginInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
@@ -111,7 +112,8 @@
   }
 
   @Override
-  public SortedMap<String, PluginInfo> apply(TopLevelResource resource) throws BadRequestException {
+  public Response<SortedMap<String, PluginInfo>> apply(TopLevelResource resource)
+      throws BadRequestException {
     Stream<Plugin> s = Streams.stream(pluginLoader.getPlugins(all));
     if (matchPrefix != null) {
       checkMatchOptions(matchSubstring == null && matchRegex == null);
@@ -132,7 +134,7 @@
     if (limit > 0) {
       s = s.limit(limit);
     }
-    return new TreeMap<>(s.collect(toMap(Plugin::getName, ListPlugins::toPluginInfo)));
+    return Response.ok(new TreeMap<>(s.collect(toMap(Plugin::getName, ListPlugins::toPluginInfo))));
   }
 
   private void checkMatchOptions(boolean cond) throws BadRequestException {
diff --git a/java/com/google/gerrit/server/plugins/MandatoryPluginsCollection.java b/java/com/google/gerrit/server/plugins/MandatoryPluginsCollection.java
new file mode 100644
index 0000000..70a0fff
--- /dev/null
+++ b/java/com/google/gerrit/server/plugins/MandatoryPluginsCollection.java
@@ -0,0 +1,50 @@
+// 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.plugins;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class MandatoryPluginsCollection {
+  private final CopyOnWriteArraySet<String> members;
+
+  @Inject
+  MandatoryPluginsCollection(@GerritServerConfig Config cfg) {
+    members = Sets.newCopyOnWriteArraySet();
+    members.addAll(Arrays.asList(cfg.getStringList("plugins", null, "mandatory")));
+  }
+
+  public boolean contains(String name) {
+    return members.contains(name);
+  }
+
+  public Set<String> asSet() {
+    return ImmutableSet.copyOf(members);
+  }
+
+  @VisibleForTesting
+  public void add(String name) {
+    members.add(name);
+  }
+}
diff --git a/java/com/google/gerrit/server/plugins/MissingMandatoryPluginsException.java b/java/com/google/gerrit/server/plugins/MissingMandatoryPluginsException.java
new file mode 100644
index 0000000..1c23550
--- /dev/null
+++ b/java/com/google/gerrit/server/plugins/MissingMandatoryPluginsException.java
@@ -0,0 +1,30 @@
+// 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.plugins;
+
+import java.util.Collection;
+
+/** Raised when one or more mandatory plugins are missing. */
+public class MissingMandatoryPluginsException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  public MissingMandatoryPluginsException(Collection<String> pluginNames) {
+    super(getMessage(pluginNames));
+  }
+
+  private static String getMessage(Collection<String> pluginNames) {
+    return String.format("Cannot find or load the following mandatory plugins: %s", pluginNames);
+  }
+}
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
index 9279f0fe..c4f4a1f 100644
--- a/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -52,6 +52,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -87,6 +88,7 @@
   private final Provider<String> urlProvider;
   private final PersistentCacheFactory persistentCacheFactory;
   private final boolean remoteAdmin;
+  private final MandatoryPluginsCollection mandatoryPlugins;
   private final UniversalServerPluginProvider serverPluginFactory;
   private final GerritRuntime gerritRuntime;
 
@@ -101,6 +103,7 @@
       @CanonicalWebUrl Provider<String> provider,
       PersistentCacheFactory cacheFactory,
       UniversalServerPluginProvider pluginFactory,
+      MandatoryPluginsCollection mpc,
       GerritRuntime gerritRuntime) {
     pluginsDir = sitePaths.plugins_dir;
     dataDir = sitePaths.data_dir;
@@ -114,6 +117,7 @@
     serverPluginFactory = pluginFactory;
 
     remoteAdmin = cfg.getBoolean("plugins", null, "allowRemoteAdmin", false);
+    mandatoryPlugins = mpc;
     this.gerritRuntime = gerritRuntime;
 
     long checkFrequency =
@@ -226,6 +230,11 @@
           continue;
         }
 
+        if (mandatoryPlugins.contains(name)) {
+          logger.atWarning().log("Mandatory plugin %s cannot be disabled", name);
+          continue;
+        }
+
         logger.atInfo().log("Disabling plugin %s", active.getName());
         Path off =
             active.getSrcFile().resolveSibling(active.getSrcFile().getFileName() + ".disabled");
@@ -381,50 +390,57 @@
   }
 
   public synchronized void rescan() {
+    Set<String> loadedPlugins = new HashSet<>();
     SetMultimap<String, Path> pluginsFiles = prunePlugins(pluginsDir);
-    if (pluginsFiles.isEmpty()) {
-      return;
+
+    if (!pluginsFiles.isEmpty()) {
+      syncDisabledPlugins(pluginsFiles);
+
+      Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
+      for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(activePlugins)) {
+        String name = entry.getKey();
+        Path path = entry.getValue();
+        String fileName = path.getFileName().toString();
+        if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) {
+          logger.atWarning().log(
+              "No Plugin provider was found that handles this file format: %s", fileName);
+          continue;
+        }
+
+        FileSnapshot brokenTime = broken.get(name);
+        if (brokenTime != null && !brokenTime.isModified(path.toFile())) {
+          continue;
+        }
+
+        Plugin active = running.get(name);
+        if (active != null && !active.isModified(path)) {
+          loadedPlugins.add(name);
+          continue;
+        }
+
+        if (active != null) {
+          logger.atInfo().log("Reloading plugin %s", active.getName());
+        }
+
+        try {
+          Plugin loadedPlugin = runPlugin(name, path, active);
+          if (!loadedPlugin.isDisabled()) {
+            loadedPlugins.add(name);
+            logger.atInfo().log(
+                "%s plugin %s, version %s",
+                active == null ? "Loaded" : "Reloaded",
+                loadedPlugin.getName(),
+                loadedPlugin.getVersion());
+          }
+        } catch (PluginInstallException e) {
+          logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name);
+        }
+      }
     }
 
-    syncDisabledPlugins(pluginsFiles);
-
-    Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
-    for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(activePlugins)) {
-      String name = entry.getKey();
-      Path path = entry.getValue();
-      String fileName = path.getFileName().toString();
-      if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) {
-        logger.atWarning().log(
-            "No Plugin provider was found that handles this file format: %s", fileName);
-        continue;
-      }
-
-      FileSnapshot brokenTime = broken.get(name);
-      if (brokenTime != null && !brokenTime.isModified(path.toFile())) {
-        continue;
-      }
-
-      Plugin active = running.get(name);
-      if (active != null && !active.isModified(path)) {
-        continue;
-      }
-
-      if (active != null) {
-        logger.atInfo().log("Reloading plugin %s", active.getName());
-      }
-
-      try {
-        Plugin loadedPlugin = runPlugin(name, path, active);
-        if (!loadedPlugin.isDisabled()) {
-          logger.atInfo().log(
-              "%s plugin %s, version %s",
-              active == null ? "Loaded" : "Reloaded",
-              loadedPlugin.getName(),
-              loadedPlugin.getVersion());
-        }
-      } catch (PluginInstallException e) {
-        logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name);
-      }
+    Set<String> missingMandatory = Sets.difference(mandatoryPlugins.asSet(), loadedPlugins);
+    if (!missingMandatory.isEmpty()) {
+      throw new MissingMandatoryPluginsException(missingMandatory);
     }
 
     cleanInBackground();
@@ -471,6 +487,12 @@
       throws PluginInstallException {
     FileSnapshot snapshot = FileSnapshot.save(plugin.toFile());
     try {
+      boolean restartRequired = oldPlugin != null && !oldPlugin.canReload();
+      if (restartRequired && mandatoryPlugins.contains(name)) {
+        logger.atWarning().log("Restarting mandatory plugin %s not allowed", name);
+        return oldPlugin;
+      }
+
       Plugin newPlugin = loadPlugin(name, plugin, snapshot);
       if (newPlugin.getCleanupHandle() != null) {
         cleanupHandles.put(newPlugin, newPlugin.getCleanupHandle());
diff --git a/java/com/google/gerrit/server/plugins/PluginModule.java b/java/com/google/gerrit/server/plugins/PluginModule.java
index 6bc37bd..71186e5 100644
--- a/java/com/google/gerrit/server/plugins/PluginModule.java
+++ b/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -34,6 +34,7 @@
     bind(PluginLoader.class);
     bind(CopyConfigModule.class);
     listener().to(PluginLoader.class);
+    bind(MandatoryPluginsCollection.class);
 
     DynamicSet.setOf(binder(), ServerPluginProvider.class);
     DynamicSet.bind(binder(), ServerPluginProvider.class).to(JarPluginProvider.class);
diff --git a/java/com/google/gerrit/server/plugins/ReloadPlugin.java b/java/com/google/gerrit/server/plugins/ReloadPlugin.java
index 1134f50..490c4aa 100644
--- a/java/com/google/gerrit/server/plugins/ReloadPlugin.java
+++ b/java/com/google/gerrit/server/plugins/ReloadPlugin.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.common.PluginInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -38,7 +39,8 @@
   }
 
   @Override
-  public PluginInfo apply(PluginResource resource, Input input) throws ResourceConflictException {
+  public Response<PluginInfo> apply(PluginResource resource, Input input)
+      throws ResourceConflictException {
     String name = resource.getName();
     try {
       loader.reload(ImmutableList.of(name));
@@ -52,6 +54,6 @@
       pw.flush();
       throw new ResourceConflictException(buf.toString());
     }
-    return ListPlugins.toPluginInfo(loader.get(name));
+    return Response.ok(ListPlugins.toPluginInfo(loader.get(name)));
   }
 }
diff --git a/java/com/google/gerrit/server/project/AccessControlModule.java b/java/com/google/gerrit/server/project/AccessControlModule.java
index 6d77267..89ab8ee 100644
--- a/java/com/google/gerrit/server/project/AccessControlModule.java
+++ b/java/com/google/gerrit/server/project/AccessControlModule.java
@@ -18,8 +18,8 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.config.AdministrateServerGroups;
 import com.google.gerrit.server.config.AdministrateServerGroupsProvider;
 import com.google.gerrit.server.config.GitReceivePackGroups;
diff --git a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
index dc9cf9c..ae9828a 100644
--- a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
+++ b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
@@ -16,11 +16,11 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
+import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInfo.InheritedBooleanInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import java.util.Arrays;
 import java.util.HashSet;
 
diff --git a/java/com/google/gerrit/server/project/BranchResource.java b/java/com/google/gerrit/server/project/BranchResource.java
index 622b1dd..a8936ac 100644
--- a/java/com/google/gerrit/server/project/BranchResource.java
+++ b/java/com/google/gerrit/server/project/BranchResource.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.CurrentUser;
 import com.google.inject.TypeLiteral;
 import org.eclipse.jgit.lib.Ref;
@@ -33,8 +33,8 @@
     this.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null;
   }
 
-  public Branch.NameKey getBranchKey() {
-    return new Branch.NameKey(getNameKey(), refName);
+  public BranchNameKey getBranchKey() {
+    return BranchNameKey.create(getNameKey(), refName);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/project/ChildProjects.java b/java/com/google/gerrit/server/project/ChildProjects.java
index ce9992e..2069a48 100644
--- a/java/com/google/gerrit/server/project/ChildProjects.java
+++ b/java/com/google/gerrit/server/project/ChildProjects.java
@@ -18,8 +18,8 @@
 
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Multimap;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
index b33fcb5..a2de4ef 100644
--- a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
+++ b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
@@ -20,15 +20,15 @@
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.AccountGroup.UUID;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.UrlFormatter;
@@ -117,7 +117,7 @@
         if ((rule.getAction() == Action.ALLOW)
             && (rule.getGroup() != null)
             && (rule.getGroup().getUUID() != null)) {
-          groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
+          groupIds.add(AccountGroup.uuid(rule.getGroup().getUUID().get()));
         }
       }
     }
diff --git a/java/com/google/gerrit/server/project/CreateProjectArgs.java b/java/com/google/gerrit/server/project/CreateProjectArgs.java
index df31c19..c1b7b86 100644
--- a/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import java.util.List;
 
 public class CreateProjectArgs {
@@ -61,7 +61,7 @@
   }
 
   public void setProjectName(String n) {
-    projectName = n != null ? new Project.NameKey(n) : null;
+    projectName = n != null ? Project.nameKey(n) : null;
   }
 
   public void setProjectName(Project.NameKey n) {
diff --git a/java/com/google/gerrit/server/project/CreateRefControl.java b/java/com/google/gerrit/server/project/CreateRefControl.java
index 7a7598b..abbd9f6 100644
--- a/java/com/google/gerrit/server/project/CreateRefControl.java
+++ b/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -65,15 +65,12 @@
    * @throws ResourceConflictException if the project state does not permit the operation
    */
   public void checkCreateRef(
-      Provider<? extends CurrentUser> user,
-      Repository repo,
-      Branch.NameKey branch,
-      RevObject object)
+      Provider<? extends CurrentUser> user, Repository repo, BranchNameKey branch, RevObject object)
       throws AuthException, PermissionBackendException, NoSuchProjectException, IOException,
           ResourceConflictException {
-    ProjectState ps = projectCache.checkedGet(branch.getParentKey());
+    ProjectState ps = projectCache.checkedGet(branch.project());
     if (ps == null) {
-      throw new NoSuchProjectException(branch.getParentKey());
+      throw new NoSuchProjectException(branch.project());
     }
     ps.checkStatePermitsWrite();
 
@@ -86,8 +83,7 @@
       try (RevWalk rw = new RevWalk(repo)) {
         rw.parseBody(tag);
       } catch (IOException e) {
-        logger.atSevere().withCause(e).log(
-            "RevWalk(%s) parsing %s:", branch.getParentKey(), tag.name());
+        logger.atSevere().withCause(e).log("RevWalk(%s) parsing %s:", branch.project(), tag.name());
         throw e;
       }
 
diff --git a/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java b/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java
index 3fb5d2a..000fb09 100644
--- a/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java
+++ b/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.util.concurrent.Striped;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.AbstractModule;
 import com.google.inject.Singleton;
 import java.util.concurrent.locks.Lock;
diff --git a/java/com/google/gerrit/server/project/GroupList.java b/java/com/google/gerrit/server/project/GroupList.java
index fdb8740..fe59012 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.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.TabFile;
 import java.io.IOException;
@@ -48,7 +48,7 @@
         logger.atWarning().log("null field in group list for %s:\n%s", project, text);
         continue;
       }
-      AccountGroup.UUID uuid = new AccountGroup.UUID(row.left);
+      AccountGroup.UUID uuid = AccountGroup.uuid(row.left);
       String name = row.right;
       GroupReference ref = new GroupReference(uuid, name);
 
diff --git a/java/com/google/gerrit/server/project/NoSuchChangeException.java b/java/com/google/gerrit/server/project/NoSuchChangeException.java
index 6f65659..93cf03a 100644
--- a/java/com/google/gerrit/server/project/NoSuchChangeException.java
+++ b/java/com/google/gerrit/server/project/NoSuchChangeException.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Change;
 
 /** Indicates the change does not exist. */
 public class NoSuchChangeException extends StorageException {
diff --git a/java/com/google/gerrit/server/project/NoSuchProjectException.java b/java/com/google/gerrit/server/project/NoSuchProjectException.java
index 23d8d80..d4a8a5c 100644
--- a/java/com/google/gerrit/server/project/NoSuchProjectException.java
+++ b/java/com/google/gerrit/server/project/NoSuchProjectException.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 
 /** Indicates the project does not exist. */
 public class NoSuchProjectException extends Exception {
diff --git a/java/com/google/gerrit/server/project/ProjectCache.java b/java/com/google/gerrit/server/project/ProjectCache.java
index 17250fa..0baaa11e 100644
--- a/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/java/com/google/gerrit/server/project/ProjectCache.java
@@ -16,8 +16,8 @@
 
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import java.io.IOException;
 import java.util.Set;
 
@@ -35,7 +35,7 @@
    * @param projectName name of the project.
    * @return the cached data; null if no such project exists, projectName is null or an error
    *     occurred.
-   * @see #checkedGet(com.google.gerrit.reviewdb.client.Project.NameKey)
+   * @see #checkedGet(com.google.gerrit.entities.Project.NameKey)
    */
   ProjectState get(@Nullable Project.NameKey projectName);
 
@@ -57,7 +57,7 @@
    *     errors.
    * @return the cached data or null when strict = false
    */
-  public ProjectState checkedGet(Project.NameKey projectName, boolean strict) throws Exception;
+  ProjectState checkedGet(Project.NameKey projectName, boolean strict) throws Exception;
 
   /**
    * Invalidate the cached information about the given project, and triggers reindexing for it
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index bd26a8f..e88bfc6 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -24,18 +24,19 @@
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.project.ProjectIndexer;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.inject.Inject;
@@ -262,8 +263,8 @@
 
   @Override
   public ImmutableSortedSet<Project.NameKey> byName(String pfx) {
-    Project.NameKey start = new Project.NameKey(pfx);
-    Project.NameKey end = new Project.NameKey(pfx + Character.MAX_VALUE);
+    Project.NameKey start = Project.nameKey(pfx);
+    Project.NameKey end = Project.nameKey(pfx + Character.MAX_VALUE);
     try {
       // Right endpoint is exclusive, but U+FFFF is a non-character so no project ends with it.
       return list.get(ListKey.ALL).subSet(start, end);
@@ -293,9 +294,11 @@
 
     @Override
     public ProjectState load(String projectName) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading project %s", projectName)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading project", Metadata.builder().projectName(projectName).build())) {
         long now = clock.read();
-        Project.NameKey key = new Project.NameKey(projectName);
+        Project.NameKey key = Project.nameKey(projectName);
         try (Repository git = mgr.openRepository(key)) {
           ProjectConfig cfg = projectConfigFactory.create(key);
           cfg.load(key, git);
diff --git a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
index 10cf2de..d1f31a3 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
@@ -16,8 +16,8 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 09279fa..e6b8d44 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -17,7 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.common.data.Permission.isPermission;
-import static com.google.gerrit.reviewdb.client.Project.DEFAULT_SUBMIT_TYPE;
+import static com.google.gerrit.entities.Project.DEFAULT_SUBMIT_TYPE;
 import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isValidPluginPermission;
 import static java.util.stream.Collectors.toList;
 
@@ -26,6 +26,7 @@
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.common.primitives.Shorts;
 import com.google.gerrit.common.Nullable;
@@ -42,15 +43,15 @@
 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.BooleanProjectConfig;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 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.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -100,6 +101,7 @@
   public static final String KEY_COPY_MIN_SCORE = "copyMinScore";
   public static final String KEY_ALLOW_POST_SUBMIT = "allowPostSubmit";
   public static final String KEY_IGNORE_SELF_APPROVAL = "ignoreSelfApproval";
+  public static final String KEY_COPY_ANY_SCORE = "copyAnyScore";
   public static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
   public static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE =
       "copyAllScoresOnMergeFirstParentUpdate";
@@ -337,6 +339,10 @@
     return as;
   }
 
+  public ImmutableSet<String> getAccessSectionNames() {
+    return ImmutableSet.copyOf(accessSections.keySet());
+  }
+
   public Collection<AccessSection> getAccessSections() {
     return sort(accessSections.values());
   }
@@ -349,7 +355,7 @@
     return subscribeSections;
   }
 
-  public Collection<SubscribeSection> getSubscribeSections(Branch.NameKey branch) {
+  public Collection<SubscribeSection> getSubscribeSections(BranchNameKey branch) {
     Collection<SubscribeSection> ret = new ArrayList<>();
     for (SubscribeSection s : subscribeSections.values()) {
       if (s.appliesTo(branch)) {
@@ -717,7 +723,7 @@
         if (groupName != null) {
           GroupReference ref = groupsByName.get(groupName);
           if (ref == null) {
-            ref = new GroupReference(null, groupName);
+            ref = new GroupReference(groupName);
             groupsByName.put(ref.getName(), ref);
           }
           if (ref.getUUID() != null) {
@@ -972,6 +978,8 @@
           rc.getBoolean(LABEL, name, KEY_ALLOW_POST_SUBMIT, LabelType.DEF_ALLOW_POST_SUBMIT));
       label.setIgnoreSelfApproval(
           rc.getBoolean(LABEL, name, KEY_IGNORE_SELF_APPROVAL, LabelType.DEF_IGNORE_SELF_APPROVAL));
+      label.setCopyAnyScore(
+          rc.getBoolean(LABEL, name, KEY_COPY_ANY_SCORE, LabelType.DEF_COPY_ANY_SCORE));
       label.setCopyMinScore(
           rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, LabelType.DEF_COPY_MIN_SCORE));
       label.setCopyMaxScore(
@@ -1051,7 +1059,7 @@
     subscribeSections = new HashMap<>();
     try {
       for (String projectName : subsections) {
-        Project.NameKey p = new Project.NameKey(projectName);
+        Project.NameKey p = Project.nameKey(projectName);
         SubscribeSection ss = new SubscribeSection(p);
         for (String s :
             rc.getStringList(SUBSCRIBE_SECTION, projectName, SUBSCRIBE_MULTI_MATCH_REFS)) {
@@ -1203,6 +1211,7 @@
   }
 
   private void saveAccountsSection(Config rc, Set<AccountGroup.UUID> keepGroups) {
+    unsetSection(rc, ACCOUNTS);
     if (accountsSection != null) {
       rc.setStringList(
           ACCOUNTS,
@@ -1213,6 +1222,7 @@
   }
 
   private void saveCommentLinkSections(Config rc) {
+    unsetSection(rc, COMMENTLINK);
     if (commentLinkSections != null) {
       for (CommentLinkInfoImpl cm : commentLinkSections.values()) {
         rc.setString(COMMENTLINK, cm.name, KEY_MATCH, cm.match);
@@ -1230,6 +1240,7 @@
   }
 
   private void saveContributorAgreements(Config rc, Set<AccountGroup.UUID> keepGroups) {
+    unsetSection(rc, CONTRIBUTOR_AGREEMENT);
     for (ContributorAgreement ca : sort(contributorAgreements.values())) {
       set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_DESCRIPTION, ca.getDescription());
       set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AGREEMENT_URL, ca.getAgreementUrl());
@@ -1263,6 +1274,7 @@
   }
 
   private void saveNotifySections(Config rc, Set<AccountGroup.UUID> keepGroups) {
+    unsetSection(rc, NOTIFY);
     for (NotifyConfig nc : sort(notifySections.values())) {
       nc.getGroups().stream()
           .map(GroupReference::getUUID)
@@ -1317,6 +1329,7 @@
   }
 
   private void saveAccessSections(Config rc, Set<AccountGroup.UUID> keepGroups) {
+    unsetSection(rc, CAPABILITY);
     AccessSection capability = accessSections.get(AccessSection.GLOBAL_CAPABILITIES);
     if (capability != null) {
       Set<String> have = new HashSet<>();
@@ -1399,9 +1412,7 @@
     List<String> existing = new ArrayList<>(rc.getSubsections(LABEL));
     if (!new ArrayList<>(labelSections.keySet()).equals(existing)) {
       // Order of sections changed, remove and rewrite them all.
-      for (String name : existing) {
-        rc.unsetSection(LABEL, name);
-      }
+      unsetSection(rc, LABEL);
     }
 
     Set<String> toUnset = new HashSet<>(existing);
@@ -1430,6 +1441,13 @@
           rc,
           LABEL,
           name,
+          KEY_COPY_ANY_SCORE,
+          label.isCopyAnyScore(),
+          LabelType.DEF_COPY_ANY_SCORE);
+      setBooleanConfigKey(
+          rc,
+          LABEL,
+          name,
           KEY_COPY_MIN_SCORE,
           label.isCopyMinScore(),
           LabelType.DEF_COPY_MIN_SCORE);
@@ -1497,11 +1515,7 @@
   }
 
   private void savePluginSections(Config rc, Set<AccountGroup.UUID> keepGroups) {
-    List<String> existing = new ArrayList<>(rc.getSubsections(PLUGIN));
-    for (String name : existing) {
-      rc.unsetSection(PLUGIN, name);
-    }
-
+    unsetSection(rc, PLUGIN);
     for (Map.Entry<String, Config> e : pluginConfigs.entrySet()) {
       String plugin = e.getKey();
       Config pluginConfig = e.getValue();
@@ -1542,6 +1556,13 @@
     }
   }
 
+  private void unsetSection(Config rc, String sectionName) {
+    for (String subSectionName : rc.getSubsections(sectionName)) {
+      rc.unsetSection(sectionName, subSectionName);
+    }
+    rc.unsetSection(sectionName, null);
+  }
+
   private <E extends Enum<?>> E getEnum(
       Config rc, String section, String subsection, String name, E defaultValue) {
     try {
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index 35ecd7c..c9eb73e 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -21,13 +21,13 @@
 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.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
diff --git a/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java b/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
index 27bde72..694c541 100644
--- a/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
+++ b/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import java.util.Iterator;
 import java.util.List;
diff --git a/java/com/google/gerrit/server/project/ProjectJson.java b/java/com/google/gerrit/server/project/ProjectJson.java
index 449f607..fab6c11 100644
--- a/java/com/google/gerrit/server/project/ProjectJson.java
+++ b/java/com/google/gerrit/server/project/ProjectJson.java
@@ -17,20 +17,20 @@
 import static java.util.stream.Collectors.toMap;
 
 import com.google.common.base.Strings;
+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.Project;
 import com.google.gerrit.extensions.common.LabelTypeInfo;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.HashMap;
-import java.util.List;
 
 @Singleton
 public class ProjectJson {
@@ -77,7 +77,7 @@
     info.description = Strings.emptyToNull(p.getDescription());
     info.state = p.getState();
     info.id = Url.encode(info.name);
-    List<WebLinkInfo> links = webLinks.getProjectLinks(p.getName());
+    ImmutableList<WebLinkInfo> links = webLinks.getProjectLinks(p.getName());
     info.webLinks = links.isEmpty() ? null : links;
     return info;
   }
diff --git a/java/com/google/gerrit/server/project/ProjectLevelConfig.java b/java/com/google/gerrit/server/project/ProjectLevelConfig.java
index 961d1fc..4e0261c 100644
--- a/java/com/google/gerrit/server/project/ProjectLevelConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectLevelConfig.java
@@ -18,7 +18,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import java.io.IOException;
 import java.util.Arrays;
diff --git a/java/com/google/gerrit/server/project/ProjectNameLockManager.java b/java/com/google/gerrit/server/project/ProjectNameLockManager.java
index 4666c32..72036a7 100644
--- a/java/com/google/gerrit/server/project/ProjectNameLockManager.java
+++ b/java/com/google/gerrit/server/project/ProjectNameLockManager.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import java.util.concurrent.locks.Lock;
 
 public interface ProjectNameLockManager {
diff --git a/java/com/google/gerrit/server/project/ProjectResource.java b/java/com/google/gerrit/server/project/ProjectResource.java
index 22b7bd9..8802758 100644
--- a/java/com/google/gerrit/server/project/ProjectResource.java
+++ b/java/com/google/gerrit/server/project/ProjectResource.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.inject.TypeLiteral;
 
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 0e3a940..4b879a1 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -28,6 +28,11 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -37,17 +42,13 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 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.logging.Metadata;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -69,7 +70,7 @@
 
 /**
  * Cached information on a project. Must not contain any data derived from parents other than it's
- * immediate parent's {@link com.google.gerrit.reviewdb.client.Project.NameKey}.
+ * immediate parent's {@link com.google.gerrit.entities.Project.NameKey}.
  */
 public class ProjectState {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -135,7 +136,7 @@
             new Description("Latency for access computations in ProjectState")
                 .setCumulative()
                 .setUnit(Units.NANOSECONDS),
-            Field.ofString("method"));
+            Field.ofString("method", Metadata.Builder::methodName).build());
 
     if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {
       localOwners = Collections.emptySet();
@@ -354,14 +355,15 @@
    * cached. Callers should try to cache this result per-request as much as possible.
    */
   public List<SectionMatcher> getAllSections() {
-    try (Timer1.Context ignored = computationLatency.start("getAllSections")) {
+    try (Timer1.Context<String> ignored = computationLatency.start("getAllSections")) {
       if (isAllProjects) {
         return getLocalAccessSections();
       }
 
       List<SectionMatcher> all = new ArrayList<>();
       Iterable<ProjectState> tree = tree();
-      try (Timer1.Context ignored2 = computationLatency.start("getAllSections-parsing-only")) {
+      try (Timer1.Context<String> ignored2 =
+          computationLatency.start("getAllSections-parsing-only")) {
         for (ProjectState s : tree) {
           all.addAll(s.getLocalAccessSections());
         }
@@ -476,7 +478,7 @@
   }
 
   /** All available label types for this branch. */
-  public LabelTypes getLabelTypes(Branch.NameKey destination) {
+  public LabelTypes getLabelTypes(BranchNameKey destination) {
     List<LabelType> all = getLabelTypes().getLabelTypes();
 
     List<LabelType> r = Lists.newArrayListWithCapacity(all.size());
@@ -537,7 +539,7 @@
     return null;
   }
 
-  public Collection<SubscribeSection> getSubscribeSections(Branch.NameKey branch) {
+  public Collection<SubscribeSection> getSubscribeSections(BranchNameKey branch) {
     Collection<SubscribeSection> ret = new ArrayList<>();
     for (ProjectState s : tree()) {
       ret.addAll(s.getConfig().getSubscribeSections(branch));
@@ -584,7 +586,7 @@
     return project;
   }
 
-  private boolean match(Branch.NameKey destination, String refPattern) {
-    return RefPatternMatcher.getMatcher(refPattern).match(destination.get(), null);
+  private boolean match(BranchNameKey destination, String refPattern) {
+    return RefPatternMatcher.getMatcher(refPattern).match(destination.branch(), null);
   }
 }
diff --git a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
index 17fd69c..a3b4126 100644
--- a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -24,6 +24,10 @@
 import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.api.projects.CheckProjectInput;
@@ -38,9 +42,6 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeField;
@@ -225,7 +226,7 @@
               // important thing for callers is that auto-closable changes are closed. Which of the
               // commits is used to auto-close a change if there are several candidates is of minor
               // importance and hence can be non-deterministic.
-              Change.Key changeKey = new Change.Key(changeId);
+              Change.Key changeKey = Change.key(changeId);
               if (!changeIdToMergedSha1.containsKey(changeKey)) {
                 changeIdToMergedSha1.put(changeKey, commitId);
               }
@@ -295,7 +296,7 @@
                 // Auto-close by commit
                 for (ObjectId patchSetSha1 :
                     autoCloseableChange.patchSets().stream()
-                        .map(ps -> ObjectId.fromString(ps.getRevision().get()))
+                        .map(PatchSet::commitId)
                         .collect(toSet())) {
                   if (mergedSha1s.contains(patchSetSha1)) {
                     autoCloseableChangesByBranch.add(
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index 93cbf4b..b12f43b 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.permissions.PermissionBackend;
diff --git a/java/com/google/gerrit/server/project/RefFilter.java b/java/com/google/gerrit/server/project/RefFilter.java
index 76bafc0..cdabcbe 100644
--- a/java/com/google/gerrit/server/project/RefFilter.java
+++ b/java/com/google/gerrit/server/project/RefFilter.java
@@ -14,15 +14,17 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.base.Predicate;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.common.base.Strings;
-import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.api.projects.RefInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 import java.util.List;
 import java.util.Locale;
+import java.util.stream.Stream;
 
 public class RefFilter<T extends RefInfo> {
   private final String prefix;
@@ -55,15 +57,17 @@
     return this;
   }
 
-  public List<T> filter(List<T> refs) throws BadRequestException {
+  public ImmutableList<T> filter(List<T> refs) throws BadRequestException {
     if (!Strings.isNullOrEmpty(matchSubstring) && !Strings.isNullOrEmpty(matchRegex)) {
       throw new BadRequestException("specify exactly one of m/r");
     }
-    FluentIterable<T> results = FluentIterable.from(refs);
+    Stream<T> results = refs.stream();
     if (!Strings.isNullOrEmpty(matchSubstring)) {
-      results = results.filter(new SubstringPredicate(matchSubstring));
+      String lowercaseSubstring = matchSubstring.toLowerCase(Locale.US);
+      results = results.filter(refInfo -> matchesSubstring(prefix, lowercaseSubstring, refInfo));
     } else if (!Strings.isNullOrEmpty(matchRegex)) {
-      results = results.filter(new RegexPredicate(matchRegex));
+      RunAutomaton a = parseRegex(matchRegex);
+      results = results.filter(refInfo -> matchesRegex(prefix, a, refInfo));
     }
     if (start > 0) {
       results = results.skip(start);
@@ -71,51 +75,39 @@
     if (limit > 0) {
       results = results.limit(limit);
     }
-    return results.toList();
+    return results.collect(toImmutableList());
   }
 
-  private class SubstringPredicate implements Predicate<T> {
-    private final String substring;
-
-    private SubstringPredicate(String substring) {
-      this.substring = substring.toLowerCase(Locale.US);
+  private static <T extends RefInfo> boolean matchesSubstring(
+      String prefix, String lowercaseSubstring, T refInfo) {
+    String ref = refInfo.ref;
+    if (ref.startsWith(prefix)) {
+      ref = ref.substring(prefix.length());
     }
+    ref = ref.toLowerCase(Locale.US);
+    return ref.contains(lowercaseSubstring);
+  }
 
-    @Override
-    public boolean apply(T in) {
-      String ref = in.ref;
-      if (ref.startsWith(prefix)) {
-        ref = ref.substring(prefix.length());
+  private static RunAutomaton parseRegex(String regex) throws BadRequestException {
+    if (regex.startsWith("^")) {
+      regex = regex.substring(1);
+      if (regex.endsWith("$") && !regex.endsWith("\\$")) {
+        regex = regex.substring(0, regex.length() - 1);
       }
-      ref = ref.toLowerCase(Locale.US);
-      return ref.contains(substring);
+    }
+    try {
+      return new RunAutomaton(new RegExp(regex).toAutomaton());
+    } catch (IllegalArgumentException e) {
+      throw new BadRequestException(e.getMessage());
     }
   }
 
-  private class RegexPredicate implements Predicate<T> {
-    private final RunAutomaton a;
-
-    private RegexPredicate(String regex) throws BadRequestException {
-      if (regex.startsWith("^")) {
-        regex = regex.substring(1);
-        if (regex.endsWith("$") && !regex.endsWith("\\$")) {
-          regex = regex.substring(0, regex.length() - 1);
-        }
-      }
-      try {
-        a = new RunAutomaton(new RegExp(regex).toAutomaton());
-      } catch (IllegalArgumentException e) {
-        throw new BadRequestException(e.getMessage());
-      }
+  private static <T extends RefInfo> boolean matchesRegex(
+      String prefix, RunAutomaton a, T refInfo) {
+    String ref = refInfo.ref;
+    if (ref.startsWith(prefix)) {
+      ref = ref.substring(prefix.length());
     }
-
-    @Override
-    public boolean apply(T in) {
-      String ref = in.ref;
-      if (ref.startsWith(prefix)) {
-        ref = ref.substring(prefix.length());
-      }
-      return a.run(ref);
-    }
+    return a.run(ref);
   }
 }
diff --git a/java/com/google/gerrit/server/project/RefPatternMatcher.java b/java/com/google/gerrit/server/project/RefPatternMatcher.java
index 4032524..b9076b3 100644
--- a/java/com/google/gerrit/server/project/RefPatternMatcher.java
+++ b/java/com/google/gerrit/server/project/RefPatternMatcher.java
@@ -22,8 +22,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.data.ParameterizedString;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import dk.brics.automaton.Automaton;
 import java.util.HashMap;
diff --git a/java/com/google/gerrit/server/project/RefUtil.java b/java/com/google/gerrit/server/project/RefUtil.java
index a9c964d..dc8cdc7 100644
--- a/java/com/google/gerrit/server/project/RefUtil.java
+++ b/java/com/google/gerrit/server/project/RefUtil.java
@@ -20,9 +20,9 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import java.io.IOException;
 import java.util.Collections;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
diff --git a/java/com/google/gerrit/server/project/RefValidationHelper.java b/java/com/google/gerrit/server/project/RefValidationHelper.java
index 0a5980c..9b297f9 100644
--- a/java/com/google/gerrit/server/project/RefValidationHelper.java
+++ b/java/com/google/gerrit/server/project/RefValidationHelper.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.validators.RefOperationValidators;
 import com.google.gerrit.server.validators.ValidationException;
@@ -43,7 +43,7 @@
       throws ResourceConflictException {
     RefOperationValidators refValidators =
         refValidatorsFactory.create(
-            new Project(new Project.NameKey(projectName)),
+            new Project(Project.nameKey(projectName)),
             user,
             RefOperationValidators.getCommand(update, operationType));
     try {
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index eeb2a65..6bf3beb 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -47,7 +47,7 @@
   public void checkRemoveReviewer(
       ChangeNotes notes, CurrentUser currentUser, PatchSetApproval approval)
       throws PermissionBackendException, AuthException {
-    checkRemoveReviewer(notes, currentUser, approval.getAccountId(), approval.getValue());
+    checkRemoveReviewer(notes, currentUser, approval.accountId(), approval.value());
   }
 
   /**
@@ -108,7 +108,7 @@
     // owner and site admin can remove anyone
     PermissionBackend.WithUser withUser = permissionBackend.user(currentUser);
     PermissionBackend.ForProject forProject = withUser.project(change.getProject());
-    if (check(forProject.ref(change.getDest().get()), RefPermission.WRITE_CONFIG)
+    if (check(forProject.ref(change.getDest().branch()), RefPermission.WRITE_CONFIG)
         || check(withUser, GlobalPermission.ADMINISTRATE_SERVER)) {
       return true;
     }
diff --git a/java/com/google/gerrit/server/project/SectionMatcher.java b/java/com/google/gerrit/server/project/SectionMatcher.java
index a8ebd98..6de8eec 100644
--- a/java/com/google/gerrit/server/project/SectionMatcher.java
+++ b/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.project;
 
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
 
 /**
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 1b1869c..cc7591c 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -18,8 +18,12 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.server.index.OnlineReindexMode;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -27,9 +31,9 @@
 import com.google.gerrit.server.rules.SubmitRule;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 /**
@@ -44,6 +48,8 @@
   private final ProjectCache projectCache;
   private final PrologRule prologRule;
   private final PluginSetContext<SubmitRule> submitRules;
+  private final Timer0 submitRuleEvaluationLatency;
+  private final Timer0 submitTypeEvaluationLatency;
   private final SubmitRuleOptions opts;
 
   public interface Factory {
@@ -56,23 +62,36 @@
       ProjectCache projectCache,
       PrologRule prologRule,
       PluginSetContext<SubmitRule> submitRules,
+      MetricMaker metricMaker,
       @Assisted SubmitRuleOptions options) {
     this.projectCache = projectCache;
     this.prologRule = prologRule;
     this.submitRules = submitRules;
+    this.submitRuleEvaluationLatency =
+        metricMaker.newTimer(
+            "change/submit_rule_evaluation",
+            new Description("Latency for evaluating submit rules on a change.")
+                .setCumulative()
+                .setUnit(Units.MILLISECONDS));
+    this.submitTypeEvaluationLatency =
+        metricMaker.newTimer(
+            "change/submit_type_evaluation",
+            new Description("Latency for evaluating the submit type on a change.")
+                .setCumulative()
+                .setUnit(Units.MILLISECONDS));
 
     this.opts = options;
   }
 
-  public static List<SubmitRecord> defaultRuleError() {
+  public static SubmitRecord defaultRuleError() {
     return createRuleError(DEFAULT_MSG);
   }
 
-  public static List<SubmitRecord> createRuleError(String err) {
+  public static SubmitRecord createRuleError(String err) {
     SubmitRecord rec = new SubmitRecord();
     rec.status = SubmitRecord.Status.RULE_ERROR;
     rec.errorMessage = err;
-    return Collections.singletonList(rec);
+    return rec;
   }
 
   public static SubmitTypeRecord defaultTypeError() {
@@ -87,46 +106,42 @@
    * @param cd ChangeData to evaluate
    */
   public List<SubmitRecord> evaluate(ChangeData cd) {
-    Change change;
-    ProjectState projectState;
-    try {
-      change = cd.change();
-      if (change == null) {
-        throw new StorageException("Change not found");
+    try (Timer0.Context ignored = submitRuleEvaluationLatency.start()) {
+      Change change;
+      ProjectState projectState;
+      try {
+        change = cd.change();
+        if (change == null) {
+          throw new StorageException("Change not found");
+        }
+
+        projectState = projectCache.get(cd.project());
+        if (projectState == null) {
+          throw new NoSuchProjectException(cd.project());
+        }
+      } catch (StorageException | NoSuchProjectException e) {
+        return Collections.singletonList(ruleError("Error looking up change " + cd.getId(), e));
       }
 
-      projectState = projectCache.get(cd.project());
-      if (projectState == null) {
-        throw new NoSuchProjectException(cd.project());
+      if ((!opts.allowClosed() || OnlineReindexMode.isActive()) && change.isClosed()) {
+        SubmitRecord rec = new SubmitRecord();
+        rec.status = SubmitRecord.Status.CLOSED;
+        return Collections.singletonList(rec);
       }
-    } catch (StorageException | NoSuchProjectException e) {
-      return ruleError("Error looking up change " + cd.getId(), e);
-    }
 
-    if ((!opts.allowClosed() || OnlineReindexMode.isActive()) && change.isClosed()) {
-      SubmitRecord rec = new SubmitRecord();
-      rec.status = SubmitRecord.Status.CLOSED;
-      return Collections.singletonList(rec);
+      // We evaluate all the plugin-defined evaluators,
+      // and then we collect the results in one list.
+      return Streams.stream(submitRules)
+          .map(c -> c.call(s -> s.evaluate(cd)))
+          .filter(Optional::isPresent)
+          .map(Optional::get)
+          .collect(Collectors.toList());
     }
-
-    // We evaluate all the plugin-defined evaluators,
-    // and then we collect the results in one list.
-    return Streams.stream(submitRules)
-        .map(c -> c.call(s -> s.evaluate(cd, opts)))
-        .flatMap(Collection::stream)
-        .collect(Collectors.toList());
   }
 
-  private List<SubmitRecord> ruleError(String err, Exception e) {
-    if (opts.logErrors()) {
-      if (e == null) {
-        logger.atSevere().log(err);
-      } else {
-        logger.atSevere().withCause(e).log(err);
-      }
-      return defaultRuleError();
-    }
-    return createRuleError(err);
+  private SubmitRecord ruleError(String err, Exception e) {
+    logger.atSevere().withCause(e).log(err);
+    return defaultRuleError();
   }
 
   /**
@@ -136,24 +151,23 @@
    * @param cd
    */
   public SubmitTypeRecord getSubmitType(ChangeData cd) {
-    ProjectState projectState;
-    try {
-      projectState = projectCache.get(cd.project());
-      if (projectState == null) {
-        throw new NoSuchProjectException(cd.project());
+    try (Timer0.Context ignored = submitTypeEvaluationLatency.start()) {
+      ProjectState projectState;
+      try {
+        projectState = projectCache.get(cd.project());
+        if (projectState == null) {
+          throw new NoSuchProjectException(cd.project());
+        }
+      } catch (NoSuchProjectException e) {
+        return typeError("Error looking up change " + cd.getId(), e);
       }
-    } catch (NoSuchProjectException e) {
-      return typeError("Error looking up change " + cd.getId(), e);
-    }
 
-    return prologRule.getSubmitType(cd, opts);
+      return prologRule.getSubmitType(cd);
+    }
   }
 
   private SubmitTypeRecord typeError(String err, Exception e) {
-    if (opts.logErrors()) {
-      logger.atSevere().withCause(e).log(err);
-      return defaultTypeError();
-    }
-    return SubmitTypeRecord.error(err);
+    logger.atSevere().withCause(e).log(err);
+    return defaultTypeError();
   }
 }
diff --git a/java/com/google/gerrit/server/project/SubmitRuleOptions.java b/java/com/google/gerrit/server/project/SubmitRuleOptions.java
index a4340b2..ad077c0 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleOptions.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleOptions.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.project;
 
 import com.google.auto.value.AutoValue;
-import com.google.gerrit.common.Nullable;
 
 /**
  * Stable identifier for options passed to a particular submit rule evaluator.
@@ -26,12 +25,7 @@
 @AutoValue
 public abstract class SubmitRuleOptions {
   private static final SubmitRuleOptions defaults =
-      new AutoValue_SubmitRuleOptions.Builder()
-          .allowClosed(false)
-          .skipFilters(false)
-          .logErrors(true)
-          .rule(null)
-          .build();
+      new AutoValue_SubmitRuleOptions.Builder().allowClosed(false).build();
 
   public static SubmitRuleOptions defaults() {
     return defaults;
@@ -43,25 +37,12 @@
 
   public abstract boolean allowClosed();
 
-  public abstract boolean skipFilters();
-
-  public abstract boolean logErrors();
-
-  @Nullable
-  public abstract String rule();
-
   public abstract Builder toBuilder();
 
   @AutoValue.Builder
   public abstract static class Builder {
     public abstract SubmitRuleOptions.Builder allowClosed(boolean allowClosed);
 
-    public abstract SubmitRuleOptions.Builder skipFilters(boolean skipFilters);
-
-    public abstract SubmitRuleOptions.Builder rule(@Nullable String rule);
-
-    public abstract SubmitRuleOptions.Builder logErrors(boolean logErrors);
-
     public abstract SubmitRuleOptions build();
   }
 }
diff --git a/java/com/google/gerrit/server/project/SuggestParentCandidates.java b/java/com/google/gerrit/server/project/SuggestParentCandidates.java
index d3dfdcd..fdc8b50 100644
--- a/java/com/google/gerrit/server/project/SuggestParentCandidates.java
+++ b/java/com/google/gerrit/server/project/SuggestParentCandidates.java
@@ -16,7 +16,7 @@
 
 import static java.util.stream.Collectors.toList;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/project/testing/BUILD b/java/com/google/gerrit/server/project/testing/BUILD
index 968e3da..988a89f 100644
--- a/java/com/google/gerrit/server/project/testing/BUILD
+++ b/java/com/google/gerrit/server/project/testing/BUILD
@@ -5,9 +5,5 @@
     testonly = True,
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
-    deps = [
-        "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/reviewdb:server",
-        "//java/com/google/gerrit/server",
-    ],
+    deps = ["//java/com/google/gerrit/common:server"],
 )
diff --git a/java/com/google/gerrit/server/project/testing/TestLabels.java b/java/com/google/gerrit/server/project/testing/TestLabels.java
new file mode 100644
index 0000000..6c2ddde
--- /dev/null
+++ b/java/com/google/gerrit/server/project/testing/TestLabels.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project.testing;
+
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import java.util.Arrays;
+
+public class TestLabels {
+  public static LabelType codeReview() {
+    return label(
+        "Code-Review",
+        value(2, "Looks good to me, approved"),
+        value(1, "Looks good to me, but someone else must approve"),
+        value(0, "No score"),
+        value(-1, "I would prefer this is not merged as is"),
+        value(-2, "This shall not be merged"));
+  }
+
+  public static LabelType verified() {
+    return label("Verified", value(1, "Verified"), value(0, "No score"), value(-1, "Fails"));
+  }
+
+  public static LabelType patchSetLock() {
+    LabelType label =
+        label("Patch-Set-Lock", value(1, "Patch Set Locked"), value(0, "Patch Set Unlocked"));
+    label.setFunction(LabelFunction.PATCH_SET_LOCK);
+    return label;
+  }
+
+  public static LabelValue value(int value, String text) {
+    return new LabelValue((short) value, text);
+  }
+
+  public static LabelType label(String name, LabelValue... values) {
+    return new LabelType(name, Arrays.asList(values));
+  }
+
+  private TestLabels() {}
+}
diff --git a/java/com/google/gerrit/server/project/testing/Util.java b/java/com/google/gerrit/server/project/testing/Util.java
deleted file mode 100644
index 204fa7b..0000000
--- a/java/com/google/gerrit/server/project/testing/Util.java
+++ /dev/null
@@ -1,239 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.project.testing;
-
-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.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.PermissionRange;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.project.ProjectConfig;
-import java.util.Arrays;
-
-public class Util {
-  public static final AccountGroup.UUID ADMIN = new AccountGroup.UUID("test.admin");
-  public static final AccountGroup.UUID DEVS = new AccountGroup.UUID("test.devs");
-
-  public static final LabelType codeReview() {
-    return category(
-        "Code-Review",
-        value(2, "Looks good to me, approved"),
-        value(1, "Looks good to me, but someone else must approve"),
-        value(0, "No score"),
-        value(-1, "I would prefer this is not merged as is"),
-        value(-2, "This shall not be merged"));
-  }
-
-  public static final LabelType verified() {
-    return category("Verified", value(1, "Verified"), value(0, "No score"), value(-1, "Fails"));
-  }
-
-  public static final LabelType patchSetLock() {
-    LabelType label =
-        category("Patch-Set-Lock", value(1, "Patch Set Locked"), value(0, "Patch Set Unlocked"));
-    label.setFunction(LabelFunction.PATCH_SET_LOCK);
-    return label;
-  }
-
-  public static LabelValue value(int value, String text) {
-    return new LabelValue((short) value, text);
-  }
-
-  public static LabelType category(String name, LabelValue... values) {
-    return new LabelType(name, Arrays.asList(values));
-  }
-
-  public static PermissionRule newRule(ProjectConfig project, AccountGroup.UUID groupUUID) {
-    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
-    group = project.resolve(group);
-
-    return new PermissionRule(group);
-  }
-
-  public static PermissionRule allow(
-      ProjectConfig project,
-      String permissionName,
-      int min,
-      int max,
-      AccountGroup.UUID group,
-      String ref) {
-    PermissionRule rule = newRule(project, group);
-    rule.setMin(min);
-    rule.setMax(max);
-    return grant(project, permissionName, rule, ref);
-  }
-
-  public static PermissionRule allowExclusive(
-      ProjectConfig project,
-      String permissionName,
-      int min,
-      int max,
-      AccountGroup.UUID group,
-      String ref) {
-    PermissionRule rule = newRule(project, group);
-    rule.setMin(min);
-    rule.setMax(max);
-    return grant(project, permissionName, rule, ref, true);
-  }
-
-  public static PermissionRule block(
-      ProjectConfig project,
-      String permissionName,
-      int min,
-      int max,
-      AccountGroup.UUID group,
-      String ref) {
-    PermissionRule rule = newRule(project, group);
-    rule.setMin(min);
-    rule.setMax(max);
-    PermissionRule r = grant(project, permissionName, rule, ref);
-    r.setBlock();
-    return r;
-  }
-
-  public static PermissionRule allow(
-      ProjectConfig project, String permissionName, AccountGroup.UUID group, String ref) {
-    return grant(project, permissionName, newRule(project, group), ref);
-  }
-
-  public static PermissionRule allow(
-      ProjectConfig project,
-      String permissionName,
-      AccountGroup.UUID group,
-      String ref,
-      boolean exclusive) {
-    return grant(project, permissionName, newRule(project, group), ref, exclusive);
-  }
-
-  public static PermissionRule allow(
-      ProjectConfig project, String capabilityName, AccountGroup.UUID group) {
-    return allow(project, capabilityName, group, (PermissionRange) null);
-  }
-
-  public static PermissionRule allow(
-      ProjectConfig project,
-      String capabilityName,
-      AccountGroup.UUID group,
-      PermissionRange customRange) {
-    PermissionRule rule = newRule(project, group);
-    project
-        .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
-        .getPermission(capabilityName, true)
-        .add(rule);
-    if (GlobalCapability.hasRange(capabilityName)) {
-      if (customRange == null) {
-        PermissionRange.WithDefaults range = GlobalCapability.getRange(capabilityName);
-        if (range != null) {
-          rule.setRange(range.getDefaultMin(), range.getDefaultMax());
-        }
-        return rule;
-      }
-      rule.setRange(customRange.getMin(), customRange.getMax());
-    }
-    return rule;
-  }
-
-  public static PermissionRule remove(
-      ProjectConfig project, String capabilityName, AccountGroup.UUID group) {
-    PermissionRule rule = newRule(project, group);
-    project
-        .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
-        .getPermission(capabilityName, true)
-        .remove(rule);
-    return rule;
-  }
-
-  public static PermissionRule remove(
-      ProjectConfig project, String permissionName, AccountGroup.UUID group, String ref) {
-    PermissionRule rule = newRule(project, group);
-    project.getAccessSection(ref, true).getPermission(permissionName, true).remove(rule);
-    return rule;
-  }
-
-  public static PermissionRule block(
-      ProjectConfig project, String capabilityName, AccountGroup.UUID group) {
-    PermissionRule rule = newRule(project, group);
-    project
-        .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
-        .getPermission(capabilityName, true)
-        .add(rule);
-    return rule;
-  }
-
-  public static PermissionRule block(
-      ProjectConfig project, String permissionName, AccountGroup.UUID group, String ref) {
-    PermissionRule r = grant(project, permissionName, newRule(project, group), ref);
-    r.setBlock();
-    return r;
-  }
-
-  public static PermissionRule blockLabel(
-      ProjectConfig project, String labelName, AccountGroup.UUID group, String ref) {
-    return blockLabel(project, labelName, -1, 1, group, ref);
-  }
-
-  public static PermissionRule blockLabel(
-      ProjectConfig project,
-      String labelName,
-      int min,
-      int max,
-      AccountGroup.UUID group,
-      String ref) {
-    PermissionRule r = grant(project, Permission.LABEL + labelName, newRule(project, group), ref);
-    r.setBlock();
-    r.setRange(min, max);
-    return r;
-  }
-
-  public static PermissionRule deny(
-      ProjectConfig project, String permissionName, AccountGroup.UUID group, String ref) {
-    PermissionRule r = grant(project, permissionName, newRule(project, group), ref);
-    r.setDeny();
-    return r;
-  }
-
-  public static void doNotInherit(ProjectConfig project, String permissionName, String ref) {
-    project
-        .getAccessSection(ref, true) //
-        .getPermission(permissionName, true) //
-        .setExclusiveGroup(true);
-  }
-
-  private static PermissionRule grant(
-      ProjectConfig project, String permissionName, PermissionRule rule, String ref) {
-    return grant(project, permissionName, rule, ref, false);
-  }
-
-  private static PermissionRule grant(
-      ProjectConfig project,
-      String permissionName,
-      PermissionRule rule,
-      String ref,
-      boolean exclusive) {
-    Permission permission = project.getAccessSection(ref, true).getPermission(permissionName, true);
-    if (exclusive) {
-      permission.setExclusiveGroup(exclusive);
-    }
-    permission.add(rule);
-    return rule;
-  }
-
-  private Util() {}
-}
diff --git a/java/com/google/gerrit/server/query/account/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index cb96bc5..1eed7ea 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -16,14 +16,14 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Matchable;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -44,7 +44,7 @@
     List<Predicate<AccountState>> preds = Lists.newArrayListWithCapacity(3);
     Integer id = Ints.tryParse(query);
     if (id != null) {
-      preds.add(id(new Account.Id(id)));
+      preds.add(id(schema, Account.id(id)));
     }
     if (canSeeSecondaryEmails) {
       preds.add(equalsNameIncludingSecondaryEmails(query));
@@ -64,9 +64,11 @@
     return Predicate.or(preds);
   }
 
-  public static Predicate<AccountState> id(Account.Id accountId) {
+  public static Predicate<AccountState> id(Schema<AccountState> schema, Account.Id accountId) {
     return new AccountPredicate(
-        AccountField.ID, AccountQueryBuilder.FIELD_ACCOUNT, accountId.toString());
+        schema.useLegacyNumericFields() ? AccountField.ID : AccountField.ID_STR,
+        AccountQueryBuilder.FIELD_ACCOUNT,
+        accountId.toString());
   }
 
   public static Predicate<AccountState> emailIncludingSecondaryEmails(String email) {
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 38336c1..42a8310 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.NotSignedInException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -26,7 +27,6 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
@@ -204,7 +204,7 @@
 
     if ("self".equalsIgnoreCase(query) || "me".equalsIgnoreCase(query)) {
       try {
-        return Predicate.or(defaultPredicate, AccountPredicates.id(self()));
+        return Predicate.or(defaultPredicate, AccountPredicates.id(args.schema(), self()));
       } catch (QueryParseException e) {
         // Skip.
       }
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java b/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
index 7a7381d..2e29bbd 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
@@ -77,6 +77,6 @@
 
   @Override
   protected String formatForLogging(AccountState accountState) {
-    return accountState.getAccount().getId().toString();
+    return accountState.account().id().toString();
   }
 }
diff --git a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
index 8bbeb24..0252a06 100644
--- a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
+++ b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
@@ -37,7 +37,7 @@
   public boolean match(AccountState accountState) {
     try {
       permissionBackend
-          .absentUser(accountState.getAccount().getId())
+          .absentUser(accountState.account().id())
           .change(changeNotes)
           .check(ChangePermission.READ);
       return true;
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index f2385ec..091edca 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -24,17 +24,16 @@
 import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.InternalQuery;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.inject.Inject;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
 
@@ -77,7 +76,7 @@
       msg.append("Ambiguous external ID ").append(externalId).append(" for accounts: ");
       Joiner.on(", ")
           .appendTo(
-              msg, accountStates.stream().map(AccountState.ACCOUNT_ID_FUNCTION).collect(toList()));
+              msg, accountStates.stream().map(a -> a.account().id().toString()).collect(toList()));
       logger.atWarning().log(msg.toString());
     }
     return null;
@@ -103,7 +102,7 @@
     }
 
     return query(AccountPredicates.preferredEmail(email)).stream()
-        .filter(a -> a.getAccount().getPreferredEmail().equals(email))
+        .filter(a -> a.account().preferredEmail().equals(email))
         .collect(toList());
   }
 
@@ -114,15 +113,13 @@
    * @return multimap of the given emails to accounts that have a preferred email that exactly
    *     matches this email
    */
-  public Multimap<String, AccountState> byPreferredEmail(String... emails) {
-    List<String> emailList = Arrays.asList(emails);
-
+  public Multimap<String, AccountState> byPreferredEmail(List<String> emails) {
     if (hasPreferredEmailExact()) {
       List<List<AccountState>> r =
-          query(emailList.stream().map(AccountPredicates::preferredEmailExact).collect(toList()));
+          query(emails.stream().map(AccountPredicates::preferredEmailExact).collect(toList()));
       Multimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
-      for (int i = 0; i < emailList.size(); i++) {
-        accountsByEmail.putAll(emailList.get(i), r.get(i));
+      for (int i = 0; i < emails.size(); i++) {
+        accountsByEmail.putAll(emails.get(i), r.get(i));
       }
       return accountsByEmail;
     }
@@ -132,13 +129,13 @@
     }
 
     List<List<AccountState>> r =
-        query(emailList.stream().map(AccountPredicates::preferredEmail).collect(toList()));
+        query(emails.stream().map(AccountPredicates::preferredEmail).collect(toList()));
     Multimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
-    for (int i = 0; i < emailList.size(); i++) {
-      String email = emailList.get(i);
+    for (int i = 0; i < emails.size(); i++) {
+      String email = emails.get(i);
       Set<AccountState> matchingAccounts =
           r.get(i).stream()
-              .filter(a -> a.getAccount().getPreferredEmail().equals(email))
+              .filter(a -> a.account().preferredEmail().equals(email))
               .collect(toSet());
       accountsByEmail.putAll(email, matchingAccounts);
     }
diff --git a/java/com/google/gerrit/server/query/change/AgePredicate.java b/java/com/google/gerrit/server/query/change/AgePredicate.java
index 1cf2c2f..36eb5b7 100644
--- a/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -17,7 +17,7 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.util.time.TimeUtil;
diff --git a/java/com/google/gerrit/server/query/change/AssigneePredicate.java b/java/com/google/gerrit/server/query/change/AssigneePredicate.java
index fb19e85..35a91c9 100644
--- a/java/com/google/gerrit/server/query/change/AssigneePredicate.java
+++ b/java/com/google/gerrit/server/query/change/AssigneePredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.index.change.ChangeField;
 
 public class AssigneePredicate extends ChangeIndexPredicate {
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index de58456..8459b9f 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -19,6 +19,7 @@
 import static java.util.stream.Collectors.toMap;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -34,19 +35,19 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+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.entities.RobotComment;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
@@ -74,6 +75,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -222,12 +224,18 @@
    * @return instance for testing.
    */
   public static ChangeData createForTest(
-      Project.NameKey project, Change.Id id, int currentPatchSetId) {
+      Project.NameKey project, Change.Id id, int currentPatchSetId, ObjectId commitId) {
     ChangeData cd =
         new ChangeData(
             null, null, null, null, null, null, null, null, null, null, null, null, null, null,
             null, project, id, null, null);
-    cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
+    cd.currentPatchSet =
+        PatchSet.builder()
+            .id(PatchSet.id(id, currentPatchSetId))
+            .commitId(commitId)
+            .uploader(Account.id(1000))
+            .createdOn(TimeUtil.nowTs())
+            .build();
     return cd;
   }
 
@@ -357,6 +365,7 @@
     return allUsersName;
   }
 
+  @VisibleForTesting
   public void setCurrentFilePaths(List<String> filePaths) {
     PatchSet ps = currentPatchSet();
     if (ps != null) {
@@ -387,7 +396,7 @@
         return Optional.empty();
       }
 
-      ObjectId id = ObjectId.fromString(ps.getRevision().get());
+      ObjectId id = ps.commitId();
       Whitespace ws = Whitespace.IGNORE_NONE;
       PatchListKey pk =
           parentCount > 1
@@ -497,7 +506,7 @@
         return null;
       }
       for (PatchSet p : patchSets()) {
-        if (p.getId().equals(c.currentPatchSetId())) {
+        if (p.id().equals(c.currentPatchSetId())) {
           currentPatchSet = p;
           return p;
         }
@@ -580,10 +589,9 @@
     if (ps == null) {
       return false;
     }
-    String sha1 = ps.getRevision().get();
     try (Repository repo = repoManager.openRepository(project());
         RevWalk walk = new RevWalk(repo)) {
-      RevCommit c = walk.parseCommit(ObjectId.fromString(sha1));
+      RevCommit c = walk.parseCommit(ps.commitId());
       commitMessage = c.getFullMessage();
       commitFooters = c.getFooterLines();
       author = c.getAuthorIdent();
@@ -610,11 +618,11 @@
 
   /** @return patch with the given ID, or null if it does not exist. */
   public PatchSet patchSet(PatchSet.Id psId) {
-    if (currentPatchSet != null && currentPatchSet.getId().equals(psId)) {
+    if (currentPatchSet != null && currentPatchSet.id().equals(psId)) {
       return currentPatchSet;
     }
     for (PatchSet ps : patchSets()) {
-      if (ps.getId().equals(psId)) {
+      if (ps.id().equals(psId)) {
         return ps;
       }
     }
@@ -901,7 +909,7 @@
         }
 
         try (Repository repo = repoManager.openRepository(project())) {
-          Ref ref = repo.getRefDatabase().exactRef(c.getDest().get());
+          Ref ref = repo.getRefDatabase().exactRef(c.getDest().branch());
           SubmitTypeRecord str = submitTypeRecord();
           if (!str.isOk()) {
             // If submit type rules are broken, it's definitely not mergeable.
@@ -911,13 +919,7 @@
           String mergeStrategy =
               mergeUtilFactory.create(projectCache.get(project())).mergeStrategyName();
           mergeable =
-              mergeabilityCache.get(
-                  ObjectId.fromString(ps.getRevision().get()),
-                  ref,
-                  str.type,
-                  mergeStrategy,
-                  c.getDest(),
-                  repo);
+              mergeabilityCache.get(ps.commitId(), ref, str.type, mergeStrategy, c.getDest(), repo);
         } catch (IOException e) {
           throw new StorageException(e);
         }
@@ -995,11 +997,11 @@
 
     PatchSet ps = currentPatchSet();
     if (ps != null) {
-      if (stars.contains(StarredChangesUtil.REVIEWED_LABEL + "/" + ps.getPatchSetId())) {
+      if (stars.contains(StarredChangesUtil.REVIEWED_LABEL + "/" + ps.number())) {
         return true;
       }
 
-      if (stars.contains(StarredChangesUtil.UNREVIEWED_LABEL + "/" + ps.getPatchSetId())) {
+      if (stars.contains(StarredChangesUtil.UNREVIEWED_LABEL + "/" + ps.number())) {
         return false;
       }
     }
diff --git a/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
index 74ad0ef..05cc6ca 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.index.change.ChangeField;
 
 /** Predicate over Change-Id strings (aka Change.Key). */
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 8015a33..819fc2b 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.index.query.IsVisibleToPredicate;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.InternalUser;
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index f998ad3..6f278ab 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
+import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
 import static com.google.gerrit.server.account.AccountResolver.isSelf;
 import static com.google.gerrit.server.query.change.ChangeData.asChanges;
 import static java.util.stream.Collectors.toList;
@@ -29,6 +29,11 @@
 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.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.NotSignedInException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -41,11 +46,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryRequiresAuthException;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -62,10 +62,7 @@
 import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -94,7 +91,6 @@
 import java.util.regex.Pattern;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 
 /** Parses a query string meant to be applied to change objects. */
@@ -189,7 +185,7 @@
   public static final String ARG_ID_USER = "user";
   public static final String ARG_ID_GROUP = "group";
   public static final String ARG_ID_OWNER = "owner";
-  public static final Account.Id OWNER_ACCOUNT_ID = new Account.Id(0);
+  public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0);
 
   private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
       new QueryBuilder.Definition<>(ChangeQueryBuilder.class);
@@ -400,8 +396,6 @@
 
   private final Arguments args;
 
-  private @Inject @GerritServerConfig Config cfg;
-
   @Inject
   ChangeQueryBuilder(Arguments args) {
     this(mydef, args);
@@ -452,13 +446,15 @@
     if (triplet.isPresent()) {
       return Predicate.and(
           project(triplet.get().project().get()),
-          branch(triplet.get().branch().get()),
+          branch(triplet.get().branch().branch()),
           new ChangeIdPredicate(parseChangeId(triplet.get().id().get())));
     }
     if (PAT_LEGACY_ID.matcher(query).matches()) {
       Integer id = Ints.tryParse(query);
       if (id != null) {
-        return new LegacyChangeIdPredicate(new Change.Id(id));
+        return args.getSchema().useLegacyNumericFields()
+            ? new LegacyChangeIdPredicate(Change.id(id))
+            : new LegacyChangeIdStrPredicate(Change.id(id));
       }
     } else if (PAT_CHANGE_ID.matcher(query).matches()) {
       return new ChangeIdPredicate(parseChangeId(query));
@@ -566,15 +562,23 @@
     }
 
     if ("assigned".equalsIgnoreCase(value)) {
-      return Predicate.not(new AssigneePredicate(new Account.Id(ChangeField.NO_ASSIGNEE)));
+      return Predicate.not(new AssigneePredicate(Account.id(ChangeField.NO_ASSIGNEE)));
     }
 
     if ("unassigned".equalsIgnoreCase(value)) {
-      return new AssigneePredicate(new Account.Id(ChangeField.NO_ASSIGNEE));
+      return new AssigneePredicate(Account.id(ChangeField.NO_ASSIGNEE));
     }
 
     if ("submittable".equalsIgnoreCase(value)) {
-      return new SubmittablePredicate(SubmitRecord.Status.OK);
+      // SubmittablePredicate will match if *any* of the submit records are OK,
+      // but we need to check that they're *all* OK, so check that none of the
+      // submit records match any of the negative cases. To avoid checking yet
+      // more negative cases for CLOSED and FORCED, instead make sure at least
+      // one submit record is OK.
+      return Predicate.and(
+          new SubmittablePredicate(SubmitRecord.Status.OK),
+          Predicate.not(new SubmittablePredicate(SubmitRecord.Status.NOT_READY)),
+          Predicate.not(new SubmittablePredicate(SubmitRecord.Status.RULE_ERROR)));
     }
 
     if ("ignored".equalsIgnoreCase(value)) {
@@ -733,10 +737,6 @@
   @Operator
   public Predicate<ChangeData> extension(String ext) throws QueryParseException {
     if (args.getSchema().hasField(ChangeField.EXTENSION)) {
-      if (ext.isEmpty()
-          && IndexModule.getIndexType(cfg).equalsIgnoreCase(IndexType.ELASTICSEARCH.name())) {
-        return new FileWithNoExtensionInElasticPredicate();
-      }
       return new FileExtensionPredicate(ext);
     }
     throw new QueryParseException("'extension' operator is not supported by change index version");
@@ -775,11 +775,6 @@
       if (directory.startsWith("^")) {
         return new RegexDirectoryPredicate(directory);
       }
-
-      if (IndexModule.getIndexType(cfg).equalsIgnoreCase(IndexType.ELASTICSEARCH.name())
-          && (directory.isEmpty() || directory.equals("/"))) {
-        return Predicate.any();
-      }
       return new DirectoryPredicate(directory);
     }
     throw new QueryParseException("'directory' operator is not supported by change index version");
@@ -1184,7 +1179,7 @@
     try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
       VersionedAccountDestinations d = VersionedAccountDestinations.forUser(self());
       d.load(args.allUsersName, git);
-      Set<Branch.NameKey> destinations = d.getDestinationList().getDestinations(name);
+      Set<BranchNameKey> destinations = d.getDestinationList().getDestinations(name);
       if (destinations != null && !destinations.isEmpty()) {
         return new DestinationPredicate(destinations, name);
       }
@@ -1321,7 +1316,7 @@
   private Set<Account.Id> getMembers(AccountGroup.UUID g) throws IOException {
     Set<Account.Id> accounts;
     Set<Account.Id> allMembers =
-        args.groupMembers.listAccounts(g).stream().map(Account::getId).collect(toSet());
+        args.groupMembers.listAccounts(g).stream().map(Account::id).collect(toSet());
     int maxTerms = args.indexConfig.maxTerms();
     if (allMembers.size() > maxTerms) {
       // limit the number of query terms otherwise Gerrit will barf
diff --git a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 66790e7..88e93d9 100644
--- a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -18,9 +18,9 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Change.Status;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.index.change.ChangeField;
 import java.util.ArrayList;
 import java.util.List;
diff --git a/java/com/google/gerrit/server/query/change/CommentByPredicate.java b/java/com/google/gerrit/server/query/change/CommentByPredicate.java
index 0747bb2..bd7981c 100644
--- a/java/com/google/gerrit/server/query/change/CommentByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommentByPredicate.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
 import com.google.gerrit.server.index.change.ChangeField;
 import java.util.Objects;
 
diff --git a/java/com/google/gerrit/server/query/change/CommentPredicate.java b/java/com/google/gerrit/server/query/change/CommentPredicate.java
index d193bb6..4b14f08 100644
--- a/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
@@ -32,9 +33,15 @@
   @Override
   public boolean match(ChangeData object) {
     try {
-      Predicate<ChangeData> p = Predicate.and(new LegacyChangeIdPredicate(object.getId()), this);
+      Change.Id id = object.getId();
+      Predicate<ChangeData> p =
+          Predicate.and(
+              index.getSchema().useLegacyNumericFields()
+                  ? new LegacyChangeIdPredicate(id)
+                  : new LegacyChangeIdStrPredicate(id),
+              this);
       for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
-        if (cData.getId().equals(object.getId())) {
+        if (cData.getId().equals(id)) {
           return true;
         }
       }
diff --git a/java/com/google/gerrit/server/query/change/CommitPredicate.java b/java/com/google/gerrit/server/query/change/CommitPredicate.java
index 567f58d..b54ee64 100644
--- a/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -14,16 +14,17 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.gerrit.git.ObjectIds.matchesAbbreviation;
 import static com.google.gerrit.server.index.change.ChangeField.COMMIT;
 import static com.google.gerrit.server.index.change.ChangeField.EXACT_COMMIT;
-import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
 
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.index.FieldDef;
-import com.google.gerrit.reviewdb.client.PatchSet;
 
 public class CommitPredicate extends ChangeIndexPredicate {
   static FieldDef<ChangeData, ?> commitField(String id) {
-    if (id.length() == OBJECT_ID_STRING_LENGTH) {
+    if (id.length() == ObjectIds.STR_LEN) {
       return EXACT_COMMIT;
     }
     return COMMIT;
@@ -45,9 +46,10 @@
   }
 
   protected boolean equals(PatchSet p, String id) {
-    boolean exact = getField() == EXACT_COMMIT;
-    String rev = p.getRevision() != null ? p.getRevision().get() : null;
-    return (exact && id.equals(rev)) || (!exact && rev != null && rev.startsWith(id));
+    if (getField() == EXACT_COMMIT) {
+      return p.commitId().name().equals(id);
+    }
+    return matchesAbbreviation(p.commitId(), id);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index d415f71..17e4a59 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -20,14 +20,14 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -86,8 +86,12 @@
 
     List<Predicate<ChangeData>> and = new ArrayList<>(5);
     and.add(new ProjectPredicate(c.getProject().get()));
-    and.add(new RefPredicate(c.getDest().get()));
-    and.add(Predicate.not(new LegacyChangeIdPredicate(c.getId())));
+    and.add(new RefPredicate(c.getDest().branch()));
+    and.add(
+        Predicate.not(
+            args.getSchema().useLegacyNumericFields()
+                ? new LegacyChangeIdPredicate(c.getId())
+                : new LegacyChangeIdStrPredicate(c.getId())));
     and.add(Predicate.or(filePredicates));
 
     ChangeDataCache changeDataCache = new ChangeDataCache(cd, args.projectCache);
@@ -97,7 +101,7 @@
 
   private static final class CheckConflict extends PostFilterPredicate<ChangeData> {
     private final Arguments args;
-    private final Branch.NameKey dest;
+    private final BranchNameKey dest;
     private final ChangeDataCache changeDataCache;
 
     CheckConflict(String value, Arguments args, Change c, ChangeDataCache changeDataCache) {
@@ -131,7 +135,7 @@
           return false;
         }
 
-        other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
+        other = object.currentPatchSet().commitId();
         ConflictKey conflictsKey =
             ConflictKey.create(
                 changeDataCache.getTestAgainst(),
@@ -207,7 +211,7 @@
 
     ObjectId getTestAgainst() {
       if (testAgainst == null) {
-        testAgainst = ObjectId.fromString(cd.currentPatchSet().getRevision().get());
+        testAgainst = cd.currentPatchSet().commitId();
       }
       return testAgainst;
     }
diff --git a/java/com/google/gerrit/server/query/change/DestinationPredicate.java b/java/com/google/gerrit/server/query/change/DestinationPredicate.java
index 702745e..3c3d70f 100644
--- a/java/com/google/gerrit/server/query/change/DestinationPredicate.java
+++ b/java/com/google/gerrit/server/query/change/DestinationPredicate.java
@@ -14,15 +14,15 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.query.PostFilterPredicate;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
 import java.util.Set;
 
 public class DestinationPredicate extends PostFilterPredicate<ChangeData> {
-  protected Set<Branch.NameKey> destinations;
+  protected Set<BranchNameKey> destinations;
 
-  public DestinationPredicate(Set<Branch.NameKey> destinations, String value) {
+  public DestinationPredicate(Set<BranchNameKey> destinations, String value) {
     super(ChangeQueryBuilder.FIELD_DESTINATION, value);
     this.destinations = destinations;
   }
diff --git a/java/com/google/gerrit/server/query/change/EditByPredicate.java b/java/com/google/gerrit/server/query/change/EditByPredicate.java
index dfe7310..0fd66f8 100644
--- a/java/com/google/gerrit/server/query/change/EditByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EditByPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.index.change.ChangeField;
 
 public class EditByPredicate extends ChangeIndexPredicate {
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 2e7f34c..6997e02 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -16,11 +16,11 @@
 
 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.AccountGroup;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -60,7 +60,7 @@
       return false;
     }
 
-    ProjectState project = projectCache.get(c.getDest().getParentKey());
+    ProjectState project = projectCache.get(c.getDest().project());
     if (project == null) {
       // The project has disappeared.
       //
@@ -76,7 +76,7 @@
     for (PatchSetApproval p : object.currentApprovals()) {
       if (labelType.matches(p)) {
         hasVote = true;
-        if (match(object, p.getValue(), p.getAccountId())) {
+        if (match(object, p.value(), p.accountId())) {
           return true;
         }
       }
diff --git a/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java b/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
index c6ade75e..6683c91 100644
--- a/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.server.index.change.ChangeField.EXACT_TOPIC;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 
 public class ExactTopicPredicate extends ChangeIndexPredicate {
   public ExactTopicPredicate(String topic) {
diff --git a/java/com/google/gerrit/server/query/change/FileWithNoExtensionInElasticPredicate.java b/java/com/google/gerrit/server/query/change/FileWithNoExtensionInElasticPredicate.java
deleted file mode 100644
index d886baf..0000000
--- a/java/com/google/gerrit/server/query/change/FileWithNoExtensionInElasticPredicate.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// 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.query.change;
-
-import com.google.gerrit.index.query.PostFilterPredicate;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class FileWithNoExtensionInElasticPredicate extends PostFilterPredicate<ChangeData> {
-
-  private static final String NO_EXT = "";
-
-  public FileWithNoExtensionInElasticPredicate() {
-    super(ChangeField.EXTENSION.getName(), NO_EXT);
-  }
-
-  @Override
-  public boolean match(ChangeData cd) {
-    return ChangeField.getExtensions(cd).contains(NO_EXT);
-  }
-
-  @Override
-  public int getCost() {
-    return 1;
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
index 140f26b..d558b0f 100644
--- a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
@@ -17,10 +17,10 @@
 import static com.google.gerrit.server.index.change.ChangeField.FUZZY_TOPIC;
 
 import com.google.common.collect.Iterables;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
 
@@ -43,7 +43,10 @@
       return false;
     }
     try {
-      Predicate<ChangeData> thisId = new LegacyChangeIdPredicate(cd.getId());
+      Predicate<ChangeData> thisId =
+          index.getSchema().useLegacyNumericFields()
+              ? new LegacyChangeIdPredicate(cd.getId())
+              : new LegacyChangeIdStrPredicate(cd.getId());
       Iterable<ChangeData> results =
           index.getSource(and(thisId, this), IndexedChangeQuery.oneResult()).read();
       return !Iterables.isEmpty(results);
diff --git a/java/com/google/gerrit/server/query/change/GroupPredicate.java b/java/com/google/gerrit/server/query/change/GroupPredicate.java
index 7f7bcff..99f37d6 100644
--- a/java/com/google/gerrit/server/query/change/GroupPredicate.java
+++ b/java/com/google/gerrit/server/query/change/GroupPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.server.index.change.ChangeField;
 import java.util.List;
 
@@ -26,7 +26,7 @@
   @Override
   public boolean match(ChangeData cd) {
     for (PatchSet ps : cd.patchSets()) {
-      List<String> groups = ps.getGroups();
+      List<String> groups = ps.groups();
       if (groups != null && groups.contains(getValue())) {
         return true;
       }
diff --git a/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
index e57a8b3..6d1576f 100644
--- a/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.index.change.ChangeField;
 
 public class HasDraftByPredicate extends ChangeIndexPredicate {
diff --git a/java/com/google/gerrit/server/query/change/HasStarsPredicate.java b/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
index 0c99cdf..2fbd1e8 100644
--- a/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.index.change.ChangeField;
 
 public class HasStarsPredicate extends ChangeIndexPredicate {
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 74a0f71..6605c23 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -25,13 +25,13 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.query.InternalQuery;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.inject.Inject;
@@ -55,8 +55,13 @@
  * holding on to a single instance.
  */
 public class InternalChangeQuery extends InternalQuery<ChangeData, InternalChangeQuery> {
-  private static Predicate<ChangeData> ref(Branch.NameKey branch) {
-    return new RefPredicate(branch.get());
+  @FunctionalInterface
+  static interface ChangeIdPredicateFactory {
+    Predicate<ChangeData> create(Change.Id id);
+  }
+
+  private static Predicate<ChangeData> ref(BranchNameKey branch) {
+    return new RefPredicate(branch.branch());
   }
 
   private static Predicate<ChangeData> change(Change.Key key) {
@@ -78,6 +83,9 @@
   private final ChangeData.Factory changeDataFactory;
   private final ChangeNotes.Factory notesFactory;
 
+  // TODO(davido): Remove the below fields when support for legacy numeric fields is removed.
+  private final ChangeIdPredicateFactory predicateFactory;
+
   @Inject
   InternalChangeQuery(
       ChangeQueryProcessor queryProcessor,
@@ -88,6 +96,11 @@
     super(queryProcessor, indexes, indexConfig);
     this.changeDataFactory = changeDataFactory;
     this.notesFactory = notesFactory;
+    predicateFactory =
+        (id) ->
+            schema().useLegacyNumericFields()
+                ? new LegacyChangeIdPredicate(id)
+                : new LegacyChangeIdStrPredicate(id);
   }
 
   public List<ChangeData> byKey(Change.Key key) {
@@ -99,48 +112,48 @@
   }
 
   public List<ChangeData> byLegacyChangeId(Change.Id id) {
-    return query(new LegacyChangeIdPredicate(id));
+    return query(predicateFactory.create(id));
   }
 
   public List<ChangeData> byLegacyChangeIds(Collection<Change.Id> ids) {
     List<Predicate<ChangeData>> preds = new ArrayList<>(ids.size());
     for (Change.Id id : ids) {
-      preds.add(new LegacyChangeIdPredicate(id));
+      preds.add(predicateFactory.create(id));
     }
     return query(or(preds));
   }
 
-  public List<ChangeData> byBranchKey(Branch.NameKey branch, Change.Key key) {
+  public List<ChangeData> byBranchKey(BranchNameKey branch, Change.Key key) {
     return query(byBranchKeyPred(branch, key));
   }
 
   public List<ChangeData> byBranchKeyOpen(Project.NameKey project, String branch, Change.Key key) {
-    return query(and(byBranchKeyPred(new Branch.NameKey(project, branch), key), open()));
+    return query(and(byBranchKeyPred(BranchNameKey.create(project, branch), key), open()));
   }
 
   public static Predicate<ChangeData> byBranchKeyOpenPred(
       Project.NameKey project, String branch, Change.Key key) {
-    return and(byBranchKeyPred(new Branch.NameKey(project, branch), key), open());
+    return and(byBranchKeyPred(BranchNameKey.create(project, branch), key), open());
   }
 
-  private static Predicate<ChangeData> byBranchKeyPred(Branch.NameKey branch, Change.Key key) {
-    return and(ref(branch), project(branch.getParentKey()), change(key));
+  private static Predicate<ChangeData> byBranchKeyPred(BranchNameKey branch, Change.Key key) {
+    return and(ref(branch), project(branch.project()), change(key));
   }
 
   public List<ChangeData> byProject(Project.NameKey project) {
     return query(project(project));
   }
 
-  public List<ChangeData> byBranchOpen(Branch.NameKey branch) {
-    return query(and(ref(branch), project(branch.getParentKey()), open()));
+  public List<ChangeData> byBranchOpen(BranchNameKey branch) {
+    return query(and(ref(branch), project(branch.project()), open()));
   }
 
-  public List<ChangeData> byBranchNew(Branch.NameKey branch) {
-    return query(and(ref(branch), project(branch.getParentKey()), status(Change.Status.NEW)));
+  public List<ChangeData> byBranchNew(BranchNameKey branch) {
+    return query(and(ref(branch), project(branch.project()), status(Change.Status.NEW)));
   }
 
   public Iterable<ChangeData> byCommitsOnBranchNotMerged(
-      Repository repo, Branch.NameKey branch, Collection<String> hashes) throws IOException {
+      Repository repo, BranchNameKey branch, Collection<String> hashes) throws IOException {
     return byCommitsOnBranchNotMerged(
         repo,
         branch,
@@ -151,7 +164,7 @@
 
   @VisibleForTesting
   Iterable<ChangeData> byCommitsOnBranchNotMerged(
-      Repository repo, Branch.NameKey branch, Collection<String> hashes, int indexLimit)
+      Repository repo, BranchNameKey branch, Collection<String> hashes, int indexLimit)
       throws IOException {
     if (hashes.size() > indexLimit) {
       return byCommitsOnBranchNotMergedFromDatabase(repo, branch, hashes);
@@ -160,7 +173,7 @@
   }
 
   private Iterable<ChangeData> byCommitsOnBranchNotMergedFromDatabase(
-      Repository repo, Branch.NameKey branch, Collection<String> hashes) throws IOException {
+      Repository repo, BranchNameKey branch, Collection<String> hashes) throws IOException {
     Set<Change.Id> changeIds = Sets.newHashSetWithExpectedSize(hashes.size());
     String lastPrefix = null;
     for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
@@ -180,7 +193,7 @@
 
     List<ChangeNotes> notes =
         notesFactory.create(
-            branch.getParentKey(),
+            branch.project(),
             changeIds,
             cn -> {
               Change c = cn.getChange();
@@ -190,11 +203,11 @@
   }
 
   private Iterable<ChangeData> byCommitsOnBranchNotMergedFromIndex(
-      Branch.NameKey branch, Collection<String> hashes) {
+      BranchNameKey branch, Collection<String> hashes) {
     return query(
         and(
             ref(branch),
-            project(branch.getParentKey()),
+            project(branch.project()),
             not(status(Change.Status.MERGED)),
             or(commits(hashes))));
   }
@@ -241,8 +254,8 @@
     return query(byBranchCommitPred(project, branch, hash));
   }
 
-  public List<ChangeData> byBranchCommit(Branch.NameKey branch, String hash) {
-    return byBranchCommit(branch.getParentKey().get(), branch.get(), hash);
+  public List<ChangeData> byBranchCommit(BranchNameKey branch, String hash) {
+    return byBranchCommit(branch.project().get(), branch.branch(), hash);
   }
 
   public List<ChangeData> byBranchCommitOpen(String project, String branch, String hash) {
diff --git a/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
index ddb6f32..4b32b06 100644
--- a/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -16,8 +16,8 @@
 
 import static com.google.gerrit.server.index.change.ChangeField.REVIEWEDBY;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -25,7 +25,7 @@
 import java.util.Set;
 
 public class IsReviewedPredicate extends ChangeIndexPredicate {
-  protected static final Account.Id NOT_REVIEWED = new Account.Id(ChangeField.NOT_REVIEWED);
+  protected static final Account.Id NOT_REVIEWED = Account.id(ChangeField.NOT_REVIEWED);
 
   public static Predicate<ChangeData> create() {
     return Predicate.not(new IsReviewedPredicate(NOT_REVIEWED));
diff --git a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index c52d2a9..3a43fd3 100644
--- a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -93,7 +93,7 @@
       throws QueryParseException {
     CurrentUser user = args.getUser();
     if (user.isIdentifiedUser()) {
-      return user.asIdentifiedUser().state().getProjectWatches().keySet();
+      return user.asIdentifiedUser().state().projectWatches().keySet();
     }
     return Collections.emptySet();
   }
diff --git a/java/com/google/gerrit/server/query/change/LabelPredicate.java b/java/com/google/gerrit/server/query/change/LabelPredicate.java
index b5d375c..38d1dbe 100644
--- a/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.RangeUtil;
 import com.google.gerrit.index.query.RangeUtil.Range;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
diff --git a/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index fe4d4e1..d531236 100644
--- a/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 
 /** Predicate over change number (aka legacy ID or Change.Id). */
 public class LegacyChangeIdPredicate extends ChangeIndexPredicate {
diff --git a/java/com/google/gerrit/server/query/change/LegacyChangeIdStrPredicate.java b/java/com/google/gerrit/server/query/change/LegacyChangeIdStrPredicate.java
new file mode 100644
index 0000000..bae9c0d
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/LegacyChangeIdStrPredicate.java
@@ -0,0 +1,39 @@
+// 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.query.change;
+
+import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID_STR;
+
+import com.google.gerrit.entities.Change;
+
+/** Predicate over change number (aka legacy ID or Change.Id). */
+public class LegacyChangeIdStrPredicate extends ChangeIndexPredicate {
+  protected final Change.Id id;
+
+  public LegacyChangeIdStrPredicate(Change.Id id) {
+    super(LEGACY_ID_STR, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
+    this.id = id;
+  }
+
+  @Override
+  public boolean match(ChangeData object) {
+    return id.equals(object.getId());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/MessagePredicate.java b/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 0bd8c88..44cbd8e 100644
--- a/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -33,7 +33,12 @@
   @Override
   public boolean match(ChangeData object) {
     try {
-      Predicate<ChangeData> p = Predicate.and(new LegacyChangeIdPredicate(object.getId()), this);
+      Predicate<ChangeData> p =
+          Predicate.and(
+              index.getSchema().useLegacyNumericFields()
+                  ? new LegacyChangeIdPredicate(object.getId())
+                  : new LegacyChangeIdStrPredicate(object.getId()),
+              this);
       for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
         if (cData.getId().equals(object.getId())) {
           return true;
diff --git a/java/com/google/gerrit/server/query/change/OrSource.java b/java/com/google/gerrit/server/query/change/OrSource.java
index ba06b89..983d9b4 100644
--- a/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/java/com/google/gerrit/server/query/change/OrSource.java
@@ -14,17 +14,21 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.FieldBundle;
-import com.google.gerrit.index.query.ListResultSet;
+import com.google.gerrit.index.query.LazyResultSet;
 import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.ResultSet;
-import com.google.gerrit.reviewdb.client.Change;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 
 public class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
@@ -36,22 +40,29 @@
 
   @Override
   public ResultSet<ChangeData> read() {
-    // TODO(spearce) This probably should be more lazy.
-    //
-    List<ChangeData> r = new ArrayList<>();
-    Set<Change.Id> have = new HashSet<>();
-    for (Predicate<ChangeData> p : getChildren()) {
-      if (p instanceof ChangeDataSource) {
-        for (ChangeData cd : ((ChangeDataSource) p).read()) {
-          if (have.add(cd.getId())) {
-            r.add(cd);
-          }
-        }
-      } else {
-        throw new StorageException("No ChangeDataSource: " + p);
-      }
+    Optional<Predicate<ChangeData>> nonChangeDataSource =
+        getChildren().stream().filter(p -> !(p instanceof ChangeDataSource)).findAny();
+    if (nonChangeDataSource.isPresent()) {
+      throw new StorageException("No ChangeDataSource: " + nonChangeDataSource.get());
     }
-    return new ListResultSet<>(r);
+
+    // ResultSets are lazy. Calling #read here first and then dealing with ResultSets only when
+    // requested allows the index to run asynchronous queries.
+    List<ResultSet<ChangeData>> results =
+        getChildren().stream().map(p -> ((ChangeDataSource) p).read()).collect(toImmutableList());
+    return new LazyResultSet<>(
+        () -> {
+          List<ChangeData> r = new ArrayList<>();
+          Set<Change.Id> have = new HashSet<>();
+          for (ResultSet<ChangeData> resultSet : results) {
+            for (ChangeData result : resultSet) {
+              if (have.add(result.getId())) {
+                r.add(result);
+              }
+            }
+          }
+          return ImmutableList.copyOf(r);
+        });
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 08e6f33..e875499 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -19,11 +19,11 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryResult;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.data.ChangeAttribute;
diff --git a/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/java/com/google/gerrit/server/query/change/OwnerPredicate.java
index 100a66c..923a9ca 100644
--- a/java/com/google/gerrit/server/query/change/OwnerPredicate.java
+++ b/java/com/google/gerrit/server/query/change/OwnerPredicate.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.index.change.ChangeField;
 
 public class OwnerPredicate extends ChangeIndexPredicate {
diff --git a/java/com/google/gerrit/server/query/change/OwnerinPredicate.java b/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
index 41b3204..c2bc5d9 100644
--- a/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
+++ b/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.query.PostFilterPredicate;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.IdentifiedUser;
 
 public class OwnerinPredicate extends PostFilterPredicate<ChangeData> {
diff --git a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index 17d6448..5deb7f5 100644
--- a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
@@ -40,7 +40,7 @@
 
   protected static List<Predicate<ChangeData>> predicates(
       ProjectCache projectCache, ChildProjects childProjects, String value) {
-    ProjectState projectState = projectCache.get(new Project.NameKey(value));
+    ProjectState projectState = projectCache.get(Project.nameKey(value));
     if (projectState == null) {
       return Collections.emptyList();
     }
diff --git a/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/java/com/google/gerrit/server/query/change/ProjectPredicate.java
index febd6c6..db5a932 100644
--- a/java/com/google/gerrit/server/query/change/ProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ProjectPredicate.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.index.change.ChangeField;
 
 public class ProjectPredicate extends ChangeIndexPredicate {
@@ -24,7 +24,7 @@
   }
 
   protected Project.NameKey getValueKey() {
-    return new Project.NameKey(getValue());
+    return Project.nameKey(getValue());
   }
 
   @Override
@@ -34,7 +34,7 @@
       return false;
     }
 
-    Project.NameKey p = change.getDest().getParentKey();
+    Project.NameKey p = change.getDest().project();
     return p.equals(getValueKey());
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java b/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
index 744f4d2..c23a175 100644
--- a/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.index.change.ChangeField;
 
 public class ProjectPrefixPredicate extends ChangeIndexPredicate {
@@ -25,7 +25,7 @@
   @Override
   public boolean match(ChangeData object) {
     Change c = object.change();
-    return c != null && c.getDest().getParentKey().get().startsWith(getValue());
+    return c != null && c.getDest().project().get().startsWith(getValue());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/RefPredicate.java b/java/com/google/gerrit/server/query/change/RefPredicate.java
index 2ed4c99..1c70a62 100644
--- a/java/com/google/gerrit/server/query/change/RefPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.index.change.ChangeField;
 
 public class RefPredicate extends ChangeIndexPredicate {
@@ -28,7 +28,7 @@
     if (change == null) {
       return false;
     }
-    return getValue().equals(change.getDest().get());
+    return getValue().equals(change.getDest().branch());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
index 46a17f6..bbdfc66 100644
--- a/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.index.change.ChangeField;
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
@@ -44,7 +44,7 @@
       return false;
     }
 
-    Project.NameKey p = change.getDest().getParentKey();
+    Project.NameKey p = change.getDest().project();
     return pattern.run(p.get());
   }
 
diff --git a/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
index af211e6..6d404b4 100644
--- a/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.index.change.ChangeField;
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
@@ -42,7 +42,7 @@
     if (change == null) {
       return false;
     }
-    return pattern.run(change.getDest().get());
+    return pattern.run(change.getDest().branch());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index 0441afa..7713f24 100644
--- a/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.server.index.change.ChangeField.EXACT_TOPIC;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
diff --git a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index 19104d3..d783f76 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 
diff --git a/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
index 542a357..e5c6647 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.index.query.PostFilterPredicate;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 
diff --git a/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index a49e8c5..c451d46 100644
--- a/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
diff --git a/java/com/google/gerrit/server/query/change/StarPredicate.java b/java/com/google/gerrit/server/query/change/StarPredicate.java
index 6c5fd78..788f1a3 100644
--- a/java/com/google/gerrit/server/query/change/StarPredicate.java
+++ b/java/com/google/gerrit/server/query/change/StarPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.index.change.ChangeField;
 
diff --git a/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java b/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
index 0995a59..093447e 100644
--- a/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.index.change.ChangeField;
 
 public class SubmissionIdPredicate extends ChangeIndexPredicate {
diff --git a/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java b/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
index e59ae43..bc65422 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
@@ -17,8 +17,8 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
 import java.util.Set;
 
diff --git a/java/com/google/gerrit/server/query/group/GroupPredicates.java b/java/com/google/gerrit/server/query/group/GroupPredicates.java
index d02f6a4..17a7000 100644
--- a/java/com/google/gerrit/server/query/group/GroupPredicates.java
+++ b/java/com/google/gerrit/server/query/group/GroupPredicates.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.query.group;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupField;
 import java.util.Locale;
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 215e36f..4e60db5 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -20,13 +20,13 @@
 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.index.query.LimitPredicate;
 import com.google.gerrit.index.query.Predicate;
 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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.server.account.GroupBackend;
@@ -75,7 +75,7 @@
 
   @Operator
   public Predicate<InternalGroup> uuid(String uuid) {
-    return GroupPredicates.uuid(new AccountGroup.UUID(uuid));
+    return GroupPredicates.uuid(AccountGroup.uuid(uuid));
   }
 
   @Operator
@@ -169,7 +169,7 @@
   }
 
   private AccountGroup.UUID parseGroup(String groupNameOrUuid) throws QueryParseException {
-    Optional<InternalGroup> group = args.groupCache.get(new AccountGroup.UUID(groupNameOrUuid));
+    Optional<InternalGroup> group = args.groupCache.get(AccountGroup.uuid(groupNameOrUuid));
     if (group.isPresent()) {
       return group.get().getGroupUUID();
     }
diff --git a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
index 5749809..5732873 100644
--- a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
+++ b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
@@ -19,11 +19,11 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.query.InternalQuery;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/query/project/ProjectPredicates.java b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
index 5f13236..4e56fac 100644
--- a/java/com/google/gerrit/server/query/project/ProjectPredicates.java
+++ b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.project;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectField;
 import com.google.gerrit.index.project.ProjectPredicate;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.reviewdb.client.Project;
 import java.util.Locale;
 
 public class ProjectPredicates {
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index 8b4cb53..d234546 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -17,13 +17,13 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.query.LimitPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import java.util.List;
 
@@ -41,12 +41,12 @@
 
   @Operator
   public Predicate<ProjectData> name(String name) {
-    return ProjectPredicates.name(new Project.NameKey(name));
+    return ProjectPredicates.name(Project.nameKey(name));
   }
 
   @Operator
   public Predicate<ProjectData> parent(String parentName) {
-    return ProjectPredicates.parent(new Project.NameKey(parentName));
+    return ProjectPredicates.parent(Project.nameKey(parentName));
   }
 
   @Operator
diff --git a/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
index d39e55c..c659975 100644
--- a/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
+++ b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
@@ -18,9 +18,9 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.plugincontext.PluginSetEntryContext;
diff --git a/java/com/google/gerrit/server/quota/QuotaBackend.java b/java/com/google/gerrit/server/quota/QuotaBackend.java
index 11ce61f..694f4f1 100644
--- a/java/com/google/gerrit/server/quota/QuotaBackend.java
+++ b/java/com/google/gerrit/server/quota/QuotaBackend.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.quota;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.inject.ImplementedBy;
 
diff --git a/java/com/google/gerrit/server/quota/QuotaRequestContext.java b/java/com/google/gerrit/server/quota/QuotaRequestContext.java
index 90b501c..bfa7a35 100644
--- a/java/com/google/gerrit/server/quota/QuotaRequestContext.java
+++ b/java/com/google/gerrit/server/quota/QuotaRequestContext.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.quota;
 
 import com.google.auto.value.AutoValue;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import java.util.Optional;
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index dfb95940..fd341e9 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -10,8 +10,10 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
@@ -20,7 +22,6 @@
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/prettify:server",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/ioutil",
         "//java/com/google/gerrit/server/util/time",
@@ -29,6 +30,7 @@
         "//lib:blame-cache",
         "//lib:gson",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:servlet-api",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
@@ -38,6 +40,5 @@
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/server/restapi/access/ListAccess.java b/java/com/google/gerrit/server/restapi/access/ListAccess.java
index a3e9530..2520821 100644
--- a/java/com/google/gerrit/server/restapi/access/ListAccess.java
+++ b/java/com/google/gerrit/server/restapi/access/ListAccess.java
@@ -14,16 +14,13 @@
 
 package com.google.gerrit.server.restapi.access;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.project.GetAccess;
 import com.google.inject.Inject;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -47,13 +44,12 @@
   }
 
   @Override
-  public Map<String, ProjectAccessInfo> apply(TopLevelResource resource)
-      throws ResourceNotFoundException, ResourceConflictException, IOException,
-          PermissionBackendException {
+  public Response<Map<String, ProjectAccessInfo>> apply(TopLevelResource resource)
+      throws Exception {
     Map<String, ProjectAccessInfo> access = new TreeMap<>();
     for (String p : projects) {
-      access.put(p, getAccess.apply(new Project.NameKey(p)));
+      access.put(p, getAccess.apply(Project.nameKey(p)));
     }
-    return access;
+    return Response.ok(access);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/AddSshKey.java b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
index 7c05f4e..1fcf0bd 100644
--- a/java/com/google/gerrit/server/restapi/account/AddSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
@@ -104,7 +104,7 @@
         addKeyFactory.create(user, sshKey).send();
       } catch (EmailException e) {
         logger.atSevere().withCause(e).log(
-            "Cannot send SSH key added message to %s", user.getAccount().getPreferredEmail());
+            "Cannot send SSH key added message to %s", user.getAccount().preferredEmail());
       }
 
       user.getUserName().ifPresent(sshKeyCache::evict);
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index bf50e10..6ad6d97 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -22,6 +22,8 @@
 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.exceptions.InvalidSshKeyException;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -34,8 +36,6 @@
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountExternalIdCreator;
 import com.google.gerrit.server.account.AccountLoader;
@@ -44,6 +44,7 @@
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.group.GroupResolver;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
@@ -59,6 +60,7 @@
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -75,6 +77,7 @@
   private final PluginSetContext<AccountExternalIdCreator> externalIdCreators;
   private final Provider<GroupsUpdate> groupsUpdate;
   private final OutgoingEmailValidator validator;
+  private final AuthConfig authConfig;
 
   @Inject
   CreateAccount(
@@ -86,7 +89,8 @@
       AccountLoader.Factory infoLoader,
       PluginSetContext<AccountExternalIdCreator> externalIdCreators,
       @UserInitiated Provider<GroupsUpdate> groupsUpdate,
-      OutgoingEmailValidator validator) {
+      OutgoingEmailValidator validator,
+      AuthConfig authConfig) {
     this.seq = seq;
     this.groupResolver = groupResolver;
     this.authorizedKeys = authorizedKeys;
@@ -96,6 +100,7 @@
     this.externalIdCreators = externalIdCreators;
     this.groupsUpdate = groupsUpdate;
     this.validator = validator;
+    this.authConfig = authConfig;
   }
 
   @Override
@@ -109,17 +114,21 @@
   public Response<AccountInfo> apply(IdString id, AccountInput input)
       throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
           IOException, ConfigInvalidException, PermissionBackendException {
-    String username = id.get();
-    if (input.username != null && !username.equals(input.username)) {
+    String username = applyCaseOfUsername(id.get());
+    if (input.username != null && !username.equals(applyCaseOfUsername(input.username))) {
       throw new BadRequestException("username must match URL");
     }
     if (!ExternalId.isValidUsername(username)) {
       throw new BadRequestException("Invalid username '" + username + "'");
     }
 
+    if (input.name == null) {
+      input.name = input.username;
+    }
+
     Set<AccountGroup.UUID> groups = parseGroups(input.groups);
 
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     List<ExternalId> extIds = new ArrayList<>();
 
     if (input.email != null) {
@@ -175,6 +184,10 @@
     return Response.created(info);
   }
 
+  private String applyCaseOfUsername(String username) {
+    return authConfig.isUserNameToLowerCase() ? username.toLowerCase(Locale.US) : username;
+  }
+
   private Set<AccountGroup.UUID> parseGroups(List<String> groups)
       throws UnprocessableEntityException {
     Set<AccountGroup.UUID> groupUuids = new HashSet<>();
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index cc4cf21..ae45b68 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -97,11 +97,11 @@
       throw new MethodNotAllowedException("realm does not allow adding emails");
     }
 
-    return apply(rsrc.getUser(), id, input);
+    return Response.created(apply(rsrc.getUser(), id, input));
   }
 
   /** To be used from plugins that want to create emails without permission checks. */
-  public Response<EmailInfo> apply(IdentifiedUser user, IdString id, EmailInput input)
+  public EmailInfo apply(IdentifiedUser user, IdString id, EmailInput input)
       throws RestApiException, EmailException, MethodNotAllowedException, IOException,
           ConfigInvalidException, PermissionBackendException {
     String email = id.get().trim();
@@ -146,6 +146,6 @@
         throw e;
       }
     }
-    return Response.created(info);
+    return info;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index 4788301..a6fbb10 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -15,26 +15,27 @@
 package com.google.gerrit.server.restapi.account;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
 import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.CommentInfo;
 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.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -107,7 +108,7 @@
   }
 
   @Override
-  public ImmutableList<DeletedDraftCommentInfo> apply(
+  public Response<ImmutableList<DeletedDraftCommentInfo>> apply(
       AccountResource rsrc, DeleteDraftCommentsInput input)
       throws RestApiException, UpdateException {
     CurrentUser user = userProvider.get();
@@ -147,7 +148,8 @@
     // allowing partial failure would have little value.
     BatchUpdate.execute(updates.values(), BatchUpdateListener.NONE, false);
 
-    return ops.stream().map(Op::getResult).filter(Objects::nonNull).collect(toImmutableList());
+    return Response.ok(
+        ops.stream().map(Op::getResult).filter(Objects::nonNull).collect(toImmutableList()));
   }
 
   private Predicate<ChangeData> predicate(Account.Id accountId, DeleteDraftCommentsInput input)
@@ -180,8 +182,8 @@
       boolean dirty = false;
       for (Comment c : commentsUtil.draftByChangeAuthor(ctx.getNotes(), accountId)) {
         dirty = true;
-        PatchSet.Id psId = new PatchSet.Id(ctx.getChange().getId(), c.key.patchSetId);
-        setCommentRevId(c, patchListCache, ctx.getChange(), psUtil.get(ctx.getNotes(), psId));
+        PatchSet.Id psId = PatchSet.id(ctx.getChange().getId(), c.key.patchSetId);
+        setCommentCommitId(c, patchListCache, ctx.getChange(), psUtil.get(ctx.getNotes(), psId));
         commentsUtil.deleteComments(ctx.getUpdate(psId), Collections.singleton(c));
         comments.add(commentFormatter.format(c));
       }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
index 72a67db..5cdf4ae 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toSet;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.common.Input;
@@ -25,7 +26,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountException;
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
index 054f4bc..b470be8 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
@@ -74,7 +74,7 @@
       deleteKeySenderFactory.create(user, rsrc.getSshKey()).send();
     } catch (EmailException e) {
       logger.atSevere().withCause(e).log(
-          "Cannot send SSH key deletion message to %s", user.getAccount().getPreferredEmail());
+          "Cannot send SSH key deletion message to %s", user.getAccount().preferredEmail());
     }
     user.getUserName().ifPresent(sshKeyCache::evict);
 
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
index 798aad1..9f38b97 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
@@ -16,13 +16,13 @@
 
 import static java.util.stream.Collectors.toList;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountResource;
@@ -77,7 +77,7 @@
                 u.deleteProjectWatches(
                     input.stream()
                         .filter(Objects::nonNull)
-                        .map(w -> ProjectWatchKey.create(new Project.NameKey(w.project), w.filter))
+                        .map(w -> ProjectWatchKey.create(Project.nameKey(w.project), w.filter))
                         .collect(toList())));
     return Response.none();
   }
diff --git a/java/com/google/gerrit/server/restapi/account/EmailsCollection.java b/java/com/google/gerrit/server/restapi/account/EmailsCollection.java
index 434b9d6..7a498f4 100644
--- a/java/com/google/gerrit/server/restapi/account/EmailsCollection.java
+++ b/java/com/google/gerrit/server/restapi/account/EmailsCollection.java
@@ -63,7 +63,7 @@
     }
 
     if ("preferred".equals(id.get())) {
-      String email = rsrc.getUser().getAccount().getPreferredEmail();
+      String email = rsrc.getUser().getAccount().preferredEmail();
       if (Strings.isNullOrEmpty(email)) {
         throw new ResourceNotFoundException(id);
       }
diff --git a/java/com/google/gerrit/server/restapi/account/GetAccount.java b/java/com/google/gerrit/server/restapi/account/GetAccount.java
index 6544f8d..898b0bb 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAccount.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResource;
@@ -32,10 +33,10 @@
   }
 
   @Override
-  public AccountInfo apply(AccountResource rsrc) throws PermissionBackendException {
+  public Response<AccountInfo> apply(AccountResource rsrc) throws PermissionBackendException {
     AccountLoader loader = infoFactory.create(true);
     AccountInfo info = loader.get(rsrc.getUser().getAccountId());
     loader.fill();
-    return info;
+    return Response.ok(info);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetAgreements.java b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
index edcbc35..5feca66 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAgreements.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
@@ -18,12 +18,13 @@
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.common.AgreementInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
@@ -61,7 +62,7 @@
   }
 
   @Override
-  public List<AgreementInfo> apply(AccountResource resource)
+  public Response<List<AgreementInfo>> apply(AccountResource resource)
       throws RestApiException, PermissionBackendException {
     if (!agreementsEnabled) {
       throw new MethodNotAllowedException("contributor agreements disabled");
@@ -97,6 +98,6 @@
         results.add(agreementJson.format(ca));
       }
     }
-    return results;
+    return Response.ok(results);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetAvatarChangeUrl.java b/java/com/google/gerrit/server/restapi/account/GetAvatarChangeUrl.java
index 904b15f..e97e0a0 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAvatarChangeUrl.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAvatarChangeUrl.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.avatar.AvatarProvider;
@@ -33,7 +34,7 @@
   }
 
   @Override
-  public String apply(AccountResource rsrc) throws ResourceNotFoundException {
+  public Response<String> apply(AccountResource rsrc) throws ResourceNotFoundException {
     AvatarProvider impl = avatarProvider.get();
     if (impl == null) {
       throw new ResourceNotFoundException();
@@ -43,6 +44,6 @@
     if (Strings.isNullOrEmpty(url)) {
       throw new ResourceNotFoundException();
     }
-    return url;
+    return Response.ok(url);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index 77f1668..fa9ab18 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -28,9 +28,9 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OptionUtil;
 import com.google.gerrit.server.account.AccountLimits;
@@ -40,7 +40,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -79,7 +78,7 @@
   }
 
   @Override
-  public Object apply(AccountResource resource)
+  public Response<Map<String, Object>> apply(AccountResource resource)
       throws RestApiException, PermissionBackendException {
     permissionBackend.checkUsesDefaultCapabilities();
     PermissionBackend.WithUser perm = permissionBackend.currentUser();
@@ -97,9 +96,7 @@
     addRanges(have, limits);
     addPriority(have, limits);
 
-    return OutputFormat.JSON
-        .newGson()
-        .toJsonTree(have, new TypeToken<Map<String, Object>>() {}.getType());
+    return Response.ok(have);
   }
 
   private Set<GlobalOrPluginPermission> permissionsToTest() {
@@ -172,9 +169,9 @@
     }
 
     @Override
-    public BinaryResult apply(Capability resource) throws ResourceNotFoundException {
+    public Response<BinaryResult> apply(Capability resource) throws ResourceNotFoundException {
       permissionBackend.checkUsesDefaultCapabilities();
-      return BinaryResult.create("ok\n");
+      return Response.ok(BinaryResult.create("ok\n"));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetDetail.java b/java/com/google/gerrit/server/restapi/account/GetDetail.java
index 72044c4..b19559e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDetail.java
@@ -14,9 +14,10 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountDetailInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.InternalAccountDirectory;
@@ -36,12 +37,12 @@
   }
 
   @Override
-  public AccountDetailInfo apply(AccountResource rsrc) throws PermissionBackendException {
+  public Response<AccountDetailInfo> apply(AccountResource rsrc) throws PermissionBackendException {
     Account a = rsrc.getUser().getAccount();
-    AccountDetailInfo info = new AccountDetailInfo(a.getId().get());
-    info.registeredOn = a.getRegisteredOn();
+    AccountDetailInfo info = new AccountDetailInfo(a.id().get());
+    info.registeredOn = a.registeredOn();
     info.inactive = !a.isActive() ? true : null;
     directory.fillAccountInfo(Collections.singleton(info), EnumSet.allOf(FillOptions.class));
-    return info;
+    return Response.ok(info);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
index 40201a8..670ef3b 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
@@ -48,16 +49,17 @@
   }
 
   @Override
-  public DiffPreferencesInfo apply(AccountResource rsrc)
+  public Response<DiffPreferencesInfo> apply(AccountResource rsrc)
       throws RestApiException, ConfigInvalidException, IOException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
-    return accountCache
-        .get(id)
-        .map(AccountState::getDiffPreferences)
-        .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString())));
+    return Response.ok(
+        accountCache
+            .get(id)
+            .map(AccountState::diffPreferences)
+            .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString()))));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
index 0ecd6ea..409209e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
@@ -48,16 +49,17 @@
   }
 
   @Override
-  public EditPreferencesInfo apply(AccountResource rsrc)
+  public Response<EditPreferencesInfo> apply(AccountResource rsrc)
       throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
-    return accountCache
-        .get(id)
-        .map(AccountState::getEditPreferences)
-        .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString())));
+    return Response.ok(
+        accountCache
+            .get(id)
+            .map(AccountState::editPreferences)
+            .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString()))));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetEmail.java b/java/com/google/gerrit/server/restapi/account/GetEmail.java
index 3118380..afcdac2 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEmail.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.gerrit.extensions.common.EmailInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.inject.Inject;
@@ -26,10 +27,10 @@
   public GetEmail() {}
 
   @Override
-  public EmailInfo apply(AccountResource.Email rsrc) {
+  public Response<EmailInfo> apply(AccountResource.Email rsrc) {
     EmailInfo e = new EmailInfo();
     e.email = rsrc.getEmail();
-    e.preferred(rsrc.getUser().getAccount().getPreferredEmail());
-    return e;
+    e.preferred(rsrc.getUser().getAccount().preferredEmail());
+    return Response.ok(e);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetEmails.java b/java/com/google/gerrit/server/restapi/account/GetEmails.java
index 8c21536..9db9f05 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEmails.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEmails.java
@@ -19,6 +19,7 @@
 
 import com.google.gerrit.extensions.common.EmailInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResource;
@@ -43,22 +44,23 @@
   }
 
   @Override
-  public List<EmailInfo> apply(AccountResource rsrc)
+  public Response<List<EmailInfo>> apply(AccountResource rsrc)
       throws AuthException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
-    return rsrc.getUser().getEmailAddresses().stream()
-        .filter(Objects::nonNull)
-        .map(e -> toEmailInfo(rsrc, e))
-        .sorted(comparing((EmailInfo e) -> e.email))
-        .collect(toList());
+    return Response.ok(
+        rsrc.getUser().getEmailAddresses().stream()
+            .filter(Objects::nonNull)
+            .map(e -> toEmailInfo(rsrc, e))
+            .sorted(comparing((EmailInfo e) -> e.email))
+            .collect(toList()));
   }
 
   private static EmailInfo toEmailInfo(AccountResource rsrc, String email) {
     EmailInfo e = new EmailInfo();
     e.email = email;
-    e.preferred(rsrc.getUser().getAccount().getPreferredEmail());
+    e.preferred(rsrc.getUser().getAccount().preferredEmail());
     return e;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
index ef448dc..0e52af2 100644
--- a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.common.AccountExternalIdInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CurrentUser;
@@ -58,7 +59,7 @@
   }
 
   @Override
-  public List<AccountExternalIdInfo> apply(AccountResource resource)
+  public Response<List<AccountExternalIdInfo>> apply(AccountResource resource)
       throws RestApiException, IOException, PermissionBackendException {
     if (!self.get().hasSameAccountId(resource.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
@@ -66,7 +67,7 @@
 
     Collection<ExternalId> ids = externalIds.byAccount(resource.getUser().getAccountId());
     if (ids.isEmpty()) {
-      return ImmutableList.of();
+      return Response.ok(ImmutableList.of());
     }
     List<AccountExternalIdInfo> result = Lists.newArrayListWithCapacity(ids.size());
     for (ExternalId id : ids) {
@@ -83,7 +84,7 @@
       }
       result.add(info);
     }
-    return result;
+    return Response.ok(result);
   }
 
   private static Boolean toBoolean(boolean v) {
diff --git a/java/com/google/gerrit/server/restapi/account/GetGroups.java b/java/com/google/gerrit/server/restapi/account/GetGroups.java
index 569ff76..22492a7 100644
--- a/java/com/google/gerrit/server/restapi/account/GetGroups.java
+++ b/java/com/google/gerrit/server/restapi/account/GetGroups.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.GroupControl;
@@ -42,7 +43,8 @@
   }
 
   @Override
-  public List<GroupInfo> apply(AccountResource resource) throws PermissionBackendException {
+  public Response<List<GroupInfo>> apply(AccountResource resource)
+      throws PermissionBackendException {
     IdentifiedUser user = resource.getUser();
     Account.Id userId = user.getAccountId();
     Set<AccountGroup.UUID> knownGroups = user.getEffectiveGroups().getKnownGroups();
@@ -58,6 +60,6 @@
         visibleGroups.add(json.format(ctl.getGroup()));
       }
     }
-    return visibleGroups;
+    return Response.ok(visibleGroups);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetName.java b/java/com/google/gerrit/server/restapi/account/GetName.java
index bdf379e..ca33887 100644
--- a/java/com/google/gerrit/server/restapi/account/GetName.java
+++ b/java/com/google/gerrit/server/restapi/account/GetName.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.inject.Singleton;
@@ -22,7 +23,7 @@
 @Singleton
 public class GetName implements RestReadView<AccountResource> {
   @Override
-  public String apply(AccountResource rsrc) {
-    return Strings.nullToEmpty(rsrc.getUser().getAccount().getFullName());
+  public Response<String> apply(AccountResource rsrc) {
+    return Response.ok(Strings.nullToEmpty(rsrc.getUser().getAccount().fullName()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetOAuthToken.java b/java/com/google/gerrit/server/restapi/account/GetOAuthToken.java
index 395c159..24682c0 100644
--- a/java/com/google/gerrit/server/restapi/account/GetOAuthToken.java
+++ b/java/com/google/gerrit/server/restapi/account/GetOAuthToken.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResource;
@@ -50,7 +51,7 @@
   }
 
   @Override
-  public OAuthTokenInfo apply(AccountResource rsrc)
+  public Response<OAuthTokenInfo> apply(AccountResource rsrc)
       throws AuthException, ResourceNotFoundException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       throw new AuthException("not allowed to get access token");
@@ -66,7 +67,7 @@
     accessTokenInfo.providerId = accessToken.getProviderId();
     accessTokenInfo.expiresAt = Long.toString(accessToken.getExpiresAt());
     accessTokenInfo.type = BEARER_TYPE;
-    return accessTokenInfo;
+    return Response.ok(accessTokenInfo);
   }
 
   private static String getHostName(String canonicalWebUrl) {
diff --git a/java/com/google/gerrit/server/restapi/account/GetPreferences.java b/java/com/google/gerrit/server/restapi/account/GetPreferences.java
index 3d20642..d4d73c5 100644
--- a/java/com/google/gerrit/server/restapi/account/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetPreferences.java
@@ -14,15 +14,16 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.config.DownloadScheme;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
@@ -54,7 +55,7 @@
   }
 
   @Override
-  public GeneralPreferencesInfo apply(AccountResource rsrc)
+  public Response<GeneralPreferencesInfo> apply(AccountResource rsrc)
       throws RestApiException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
@@ -64,9 +65,9 @@
     GeneralPreferencesInfo preferencesInfo =
         accountCache
             .get(id)
-            .map(AccountState::getGeneralPreferences)
+            .map(AccountState::generalPreferences)
             .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString())));
-    return unsetDownloadSchemeIfUnsupported(preferencesInfo);
+    return Response.ok(unsetDownloadSchemeIfUnsupported(preferencesInfo));
   }
 
   private GeneralPreferencesInfo unsetDownloadSchemeIfUnsupported(
diff --git a/java/com/google/gerrit/server/restapi/account/GetSshKey.java b/java/com/google/gerrit/server/restapi/account/GetSshKey.java
index dc72663..58b5d12 100644
--- a/java/com/google/gerrit/server/restapi/account/GetSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/GetSshKey.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.gerrit.extensions.common.SshKeyInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountResource.SshKey;
@@ -24,7 +25,7 @@
 public class GetSshKey implements RestReadView<AccountResource.SshKey> {
 
   @Override
-  public SshKeyInfo apply(SshKey rsrc) {
-    return GetSshKeys.newSshKeyInfo(rsrc.getSshKey());
+  public Response<SshKeyInfo> apply(SshKey rsrc) {
+    return Response.ok(GetSshKeys.newSshKeyInfo(rsrc.getSshKey()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
index 408aa5f..0ca9b9e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -53,13 +54,13 @@
   }
 
   @Override
-  public List<SshKeyInfo> apply(AccountResource rsrc)
+  public Response<List<SshKeyInfo>> apply(AccountResource rsrc)
       throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
-    return apply(rsrc.getUser());
+    return Response.ok(apply(rsrc.getUser()));
   }
 
   public List<SshKeyInfo> apply(IdentifiedUser user)
diff --git a/java/com/google/gerrit/server/restapi/account/GetStatus.java b/java/com/google/gerrit/server/restapi/account/GetStatus.java
index bc7094f..447ad76 100644
--- a/java/com/google/gerrit/server/restapi/account/GetStatus.java
+++ b/java/com/google/gerrit/server/restapi/account/GetStatus.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.inject.Singleton;
@@ -22,7 +23,7 @@
 @Singleton
 public class GetStatus implements RestReadView<AccountResource> {
   @Override
-  public String apply(AccountResource rsrc) {
-    return Strings.nullToEmpty(rsrc.getUser().getAccount().getStatus());
+  public Response<String> apply(AccountResource rsrc) {
+    return Response.ok(Strings.nullToEmpty(rsrc.getUser().getAccount().status()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetUsername.java b/java/com/google/gerrit/server/restapi/account/GetUsername.java
index 01185c3..7e58f94 100644
--- a/java/com/google/gerrit/server/restapi/account/GetUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/GetUsername.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.inject.Inject;
@@ -27,7 +28,8 @@
   public GetUsername() {}
 
   @Override
-  public String apply(AccountResource rsrc) throws AuthException, ResourceNotFoundException {
-    return rsrc.getUser().getUserName().orElseThrow(ResourceNotFoundException::new);
+  public Response<String> apply(AccountResource rsrc)
+      throws AuthException, ResourceNotFoundException {
+    return Response.ok(rsrc.getUser().getUserName().orElseThrow(ResourceNotFoundException::new));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index fce324e..353e3f6 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -19,11 +19,12 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountState;
@@ -55,7 +56,7 @@
   }
 
   @Override
-  public List<ProjectWatchInfo> apply(AccountResource rsrc)
+  public Response<List<ProjectWatchInfo>> apply(AccountResource rsrc)
       throws AuthException, IOException, ConfigInvalidException, PermissionBackendException,
           ResourceNotFoundException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
@@ -64,12 +65,13 @@
 
     Account.Id accountId = rsrc.getUser().getAccountId();
     AccountState account = accounts.get(accountId).orElseThrow(ResourceNotFoundException::new);
-    return account.getProjectWatches().entrySet().stream()
-        .map(e -> toProjectWatchInfo(e.getKey(), e.getValue()))
-        .sorted(
-            comparing((ProjectWatchInfo pwi) -> pwi.project)
-                .thenComparing(pwi -> Strings.nullToEmpty(pwi.filter)))
-        .collect(toList());
+    return Response.ok(
+        account.projectWatches().entrySet().stream()
+            .map(e -> toProjectWatchInfo(e.getKey(), e.getValue()))
+            .sorted(
+                comparing((ProjectWatchInfo pwi) -> pwi.project)
+                    .thenComparing(pwi -> Strings.nullToEmpty(pwi.filter)))
+            .collect(toList()));
   }
 
   private static ProjectWatchInfo toProjectWatchInfo(
diff --git a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
index 14bd492..5236174 100644
--- a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 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.IdentifiedUser;
@@ -64,7 +65,7 @@
   }
 
   @Override
-  public List<ProjectWatchInfo> apply(AccountResource rsrc, List<ProjectWatchInfo> input)
+  public Response<List<ProjectWatchInfo>> apply(AccountResource rsrc, List<ProjectWatchInfo> input)
       throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
diff --git a/java/com/google/gerrit/server/restapi/account/PutAgreement.java b/java/com/google/gerrit/server/restapi/account/PutAgreement.java
index 147eec8..b4b3314 100644
--- a/java/com/google/gerrit/server/restapi/account/PutAgreement.java
+++ b/java/com/google/gerrit/server/restapi/account/PutAgreement.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.api.accounts.AgreementInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -27,7 +28,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountState;
@@ -93,7 +93,7 @@
 
     AccountState accountState = self.get().state();
     try {
-      addMembers.addMembers(uuid, ImmutableSet.of(accountState.getAccount().getId()));
+      addMembers.addMembers(uuid, ImmutableSet.of(accountState.account().id()));
     } catch (NoSuchGroupException e) {
       throw new ResourceConflictException("autoverify group not found", e);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index f029ef9..3e7753f 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -58,7 +58,7 @@
     try {
       rng = SecureRandom.getInstance("SHA1PRNG");
     } catch (NoSuchAlgorithmException e) {
-      throw new RuntimeException("Cannot create RNG for password generator", e);
+      throw new IllegalStateException("Cannot create RNG for password generator", e);
     }
   }
 
@@ -133,7 +133,7 @@
           .send();
     } catch (EmailException e) {
       logger.atSevere().withCause(e).log(
-          "Cannot send HttpPassword update message to %s", user.getAccount().getPreferredEmail());
+          "Cannot send HttpPassword update message to %s", user.getAccount().preferredEmail());
     }
 
     return Strings.isNullOrEmpty(newPassword) ? Response.none() : Response.ok(newPassword);
diff --git a/java/com/google/gerrit/server/restapi/account/PutName.java b/java/com/google/gerrit/server/restapi/account/PutName.java
index 9f9dd07..0389014 100644
--- a/java/com/google/gerrit/server/restapi/account/PutName.java
+++ b/java/com/google/gerrit/server/restapi/account/PutName.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.common.NameInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -22,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ServerInitiated;
@@ -91,8 +91,8 @@
             .get()
             .update("Set Full Name via API", accountId, u -> u.setFullName(newName))
             .orElseThrow(() -> new ResourceNotFoundException("account not found"));
-    return Strings.isNullOrEmpty(accountState.getAccount().getFullName())
+    return Strings.isNullOrEmpty(accountState.account().fullName())
         ? Response.none()
-        : Response.ok(accountState.getAccount().getFullName());
+        : Response.ok(accountState.account().fullName());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index b8edec3..2ddea2f 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -85,13 +85,13 @@
             "Set Preferred Email via API",
             user.getAccountId(),
             (a, u) -> {
-              if (preferredEmail.equals(a.getAccount().getPreferredEmail())) {
+              if (preferredEmail.equals(a.account().preferredEmail())) {
                 alreadyPreferred.set(true);
               } else {
                 // check if the user has a matching email
                 String matchingEmail = null;
                 for (String email :
-                    a.getExternalIds().stream()
+                    a.externalIds().stream()
                         .map(ExternalId::email)
                         .filter(Objects::nonNull)
                         .collect(toSet())) {
@@ -128,7 +128,7 @@
                     }
 
                     // claim the email now
-                    u.addExternalId(ExternalId.createEmail(a.getAccount().getId(), preferredEmail));
+                    u.addExternalId(ExternalId.createEmail(a.account().id(), preferredEmail));
                     matchingEmail = preferredEmail;
                   } else {
                     // Realm says that the email doesn't belong to the user. This can only happen as
diff --git a/java/com/google/gerrit/server/restapi/account/PutStatus.java b/java/com/google/gerrit/server/restapi/account/PutStatus.java
index 4f1128d..7e27489 100644
--- a/java/com/google/gerrit/server/restapi/account/PutStatus.java
+++ b/java/com/google/gerrit/server/restapi/account/PutStatus.java
@@ -73,8 +73,8 @@
             .get()
             .update("Set Status via API", user.getAccountId(), u -> u.setStatus(newStatus))
             .orElseThrow(() -> new ResourceNotFoundException("account not found"));
-    return Strings.isNullOrEmpty(accountState.getAccount().getStatus())
+    return Strings.isNullOrEmpty(accountState.account().status())
         ? Response.none()
-        : Response.ok(accountState.getAccount().getStatus());
+        : Response.ok(accountState.account().status());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index 8d949b8..d5897e5 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -17,15 +17,17 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.api.accounts.UsernameInput;
 import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountResource;
@@ -70,18 +72,12 @@
   }
 
   @Override
-  public String apply(AccountResource rsrc, UsernameInput input)
-      throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
-          ResourceConflictException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+  public Response<String> apply(AccountResource rsrc, UsernameInput input)
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
-    if (input == null) {
-      input = new UsernameInput();
-    }
-
     Account.Id accountId = rsrc.getUser().getAccountId();
     if (!externalIds.byAccount(accountId, SCHEME_USERNAME).isEmpty()) {
       throw new MethodNotAllowedException("Username cannot be changed.");
@@ -92,8 +88,8 @@
       throw new MethodNotAllowedException("realm does not allow editing username");
     }
 
-    if (Strings.isNullOrEmpty(input.username)) {
-      return input.username;
+    if (input == null || Strings.isNullOrEmpty(input.username)) {
+      throw new BadRequestException("input required");
     }
 
     if (!ExternalId.isValidUsername(input.username)) {
@@ -112,7 +108,7 @@
       // If we are using this identity, don't report the exception.
       Optional<ExternalId> other = externalIds.get(key);
       if (other.isPresent() && other.get().accountId().equals(accountId)) {
-        return input.username;
+        return Response.ok(input.username);
       }
 
       // Otherwise, someone else has this identity.
@@ -120,6 +116,6 @@
     }
 
     sshKeyCache.evict(input.username);
-    return input.username;
+    return Response.ok(input.username);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index 522301d..f9e753c 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -23,13 +24,13 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryResult;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountInfoComparator;
 import com.google.gerrit.server.account.AccountLoader;
@@ -149,14 +150,14 @@
   }
 
   @Override
-  public List<AccountInfo> apply(TopLevelResource rsrc)
+  public Response<List<AccountInfo>> apply(TopLevelResource rsrc)
       throws RestApiException, PermissionBackendException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
     }
 
     if (suggest && (!suggestConfig || query.length() < suggestFrom)) {
-      return Collections.emptyList();
+      return Response.ok(Collections.emptyList());
     }
 
     Set<FillOptions> fillOptions = EnumSet.of(FillOptions.ID);
@@ -216,7 +217,7 @@
       }
       QueryResult<AccountState> result = queryProcessor.query(queryPred);
       for (AccountState accountState : result.entities()) {
-        Account.Id id = accountState.getAccount().getId();
+        Account.Id id = accountState.account().id();
         matches.put(id, accountLoader.get(id));
       }
 
@@ -227,10 +228,10 @@
       if (!sorted.isEmpty() && result.more()) {
         sorted.get(sorted.size() - 1)._moreAccounts = true;
       }
-      return sorted;
+      return Response.ok(sorted);
     } catch (QueryParseException e) {
       if (suggest) {
-        return ImmutableList.of();
+        return Response.ok(ImmutableList.of());
       }
       throw new BadRequestException(e.getMessage());
     }
diff --git a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
index ee72ab7..cf56965 100644
--- a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+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.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountResource;
@@ -53,7 +54,7 @@
   }
 
   @Override
-  public DiffPreferencesInfo apply(AccountResource rsrc, DiffPreferencesInfo input)
+  public Response<DiffPreferencesInfo> apply(AccountResource rsrc, DiffPreferencesInfo input)
       throws RestApiException, ConfigInvalidException, RepositoryNotFoundException, IOException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
@@ -65,10 +66,11 @@
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
-    return accountsUpdateProvider
-        .get()
-        .update("Set Diff Preferences via API", id, u -> u.setDiffPreferences(input))
-        .map(AccountState::getDiffPreferences)
-        .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString())));
+    return Response.ok(
+        accountsUpdateProvider
+            .get()
+            .update("Set Diff Preferences via API", id, u -> u.setDiffPreferences(input))
+            .map(AccountState::diffPreferences)
+            .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString()))));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
index 27d32f2..085adaa 100644
--- a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+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.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountResource;
@@ -54,7 +55,7 @@
   }
 
   @Override
-  public EditPreferencesInfo apply(AccountResource rsrc, EditPreferencesInfo input)
+  public Response<EditPreferencesInfo> apply(AccountResource rsrc, EditPreferencesInfo input)
       throws RestApiException, RepositoryNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
@@ -66,10 +67,11 @@
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
-    return accountsUpdateProvider
-        .get()
-        .update("Set Edit Preferences via API", id, u -> u.setEditPreferences(input))
-        .map(AccountState::getEditPreferences)
-        .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString())));
+    return Response.ok(
+        accountsUpdateProvider
+            .get()
+            .update("Set Edit Preferences via API", id, u -> u.setEditPreferences(input))
+            .map(AccountState::editPreferences)
+            .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString()))));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/SetPreferences.java b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
index c6623db..3f2211e 100644
--- a/java/com/google/gerrit/server/restapi/account/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.config.DownloadScheme;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -22,15 +23,15 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+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.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AccountsUpdate;
-import com.google.gerrit.server.account.Preferences;
+import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -60,21 +61,22 @@
   }
 
   @Override
-  public GeneralPreferencesInfo apply(AccountResource rsrc, GeneralPreferencesInfo input)
+  public Response<GeneralPreferencesInfo> apply(AccountResource rsrc, GeneralPreferencesInfo input)
       throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     checkDownloadScheme(input.downloadScheme);
-    Preferences.validateMy(input.my);
+    StoredPreferences.validateMy(input.my);
     Account.Id id = rsrc.getUser().getAccountId();
 
-    return accountsUpdateProvider
-        .get()
-        .update("Set General Preferences via API", id, u -> u.setGeneralPreferences(input))
-        .map(AccountState::getGeneralPreferences)
-        .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString())));
+    return Response.ok(
+        accountsUpdateProvider
+            .get()
+            .update("Set General Preferences via API", id, u -> u.setGeneralPreferences(input))
+            .map(AccountState::generalPreferences)
+            .orElseThrow(() -> new ResourceNotFoundException(IdString.fromDecoded(id.toString()))));
   }
 
   private void checkDownloadScheme(String downloadScheme) throws BadRequestException {
diff --git a/java/com/google/gerrit/server/restapi/account/Stars.java b/java/com/google/gerrit/server/restapi/account/Stars.java
index c610adf..cdaa99d 100644
--- a/java/com/google/gerrit/server/restapi/account/Stars.java
+++ b/java/com/google/gerrit/server/restapi/account/Stars.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -97,14 +98,23 @@
 
     @Override
     @SuppressWarnings("unchecked")
-    public List<ChangeInfo> apply(AccountResource rsrc)
-        throws BadRequestException, AuthException, PermissionBackendException {
+    public Response<List<ChangeInfo>> apply(AccountResource rsrc) throws Exception {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to list stars of another account");
       }
+
+      // The type of the value in the response that is returned by QueryChanges depends on the
+      // number of queries that is provided as input. If a single query is provided as input the
+      // value type is {@code List<ChangeInfo>}, if multiple queries are provided as input the value
+      // type is {@code List<List<ChangeInfo>>) (one {@code List<ChangeInfo>} as result to each
+      // query). Since in this case we provide exactly one query ("has:stars") as input we know that
+      // the value always has the type {@code List<ChangeInfo>} and hence we can safely cast the
+      // value to this type.
       QueryChanges query = changes.list();
       query.addQuery("has:stars");
-      return (List<ChangeInfo>) query.apply(TopLevelResource.INSTANCE);
+      Response<?> response = query.apply(TopLevelResource.INSTANCE);
+      List<ChangeInfo> value = (List<ChangeInfo>) response.value();
+      return Response.ok(value);
     }
   }
 
@@ -120,11 +130,12 @@
     }
 
     @Override
-    public SortedSet<String> apply(AccountResource.Star rsrc) throws AuthException {
+    public Response<SortedSet<String>> apply(AccountResource.Star rsrc) throws AuthException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to get stars of another account");
       }
-      return starredChangesUtil.getLabels(self.get().getAccountId(), rsrc.getChange().getId());
+      return Response.ok(
+          starredChangesUtil.getLabels(self.get().getAccountId(), rsrc.getChange().getId()));
     }
   }
 
@@ -140,18 +151,19 @@
     }
 
     @Override
-    public Collection<String> apply(AccountResource.Star rsrc, StarsInput in)
+    public Response<Collection<String>> apply(AccountResource.Star rsrc, StarsInput in)
         throws AuthException, BadRequestException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to update stars of another account");
       }
       try {
-        return starredChangesUtil.star(
-            self.get().getAccountId(),
-            rsrc.getChange().getProject(),
-            rsrc.getChange().getId(),
-            in.add,
-            in.remove);
+        return Response.ok(
+            starredChangesUtil.star(
+                self.get().getAccountId(),
+                rsrc.getChange().getProject(),
+                rsrc.getChange().getId(),
+                in.add,
+                in.remove));
       } catch (IllegalLabelException e) {
         throw new BadRequestException(e.getMessage());
       }
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index c3327e4..df3b58e 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -15,13 +15,14 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountState;
@@ -67,7 +68,7 @@
   }
 
   @Override
-  protected ChangeInfo applyImpl(
+  protected Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AbandonInput input)
       throws RestApiException, UpdateException, PermissionBackendException, IOException,
           ConfigInvalidException {
@@ -84,7 +85,7 @@
             rsrc.getUser(),
             input.message,
             notifyResolver.resolve(notify, input.notifyDetails));
-    return json.noOptions().format(change);
+    return Response.ok(json.noOptions().format(change));
   }
 
   private NotifyHandling defaultNotify(Change change) {
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyFix.java b/java/com/google/gerrit/server/restapi/change/ApplyFix.java
index 085f9de..74c5bc2 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyFix.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyFix.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -21,8 +23,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.FixResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.edit.ChangeEdit;
@@ -39,7 +39,6 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.List;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 
 @Singleton
@@ -73,12 +72,11 @@
     Project.NameKey project = revisionResource.getProject();
     ProjectState projectState = projectCache.checkedGet(project);
     PatchSet patchSet = revisionResource.getPatchSet();
-    ObjectId patchSetCommitId = ObjectId.fromString(patchSet.getRevision().get());
 
     try (Repository repository = gitRepositoryManager.openRepository(project)) {
       List<TreeModification> treeModifications =
           fixReplacementInterpreter.toTreeModifications(
-              repository, projectState, patchSetCommitId, fixResource.getFixReplacements());
+              repository, projectState, patchSet.commitId(), fixResource.getFixReplacements());
       ChangeEdit changeEdit =
           changeEditModifier.combineWithModifiedPatchSetTree(
               repository, revisionResource.getNotes(), patchSet, treeModifications);
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index 53e89b3..8449f8a 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -17,7 +17,12 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.io.ByteStreams;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.DiffWebLinkInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.Input;
@@ -38,10 +43,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.change.ChangeEditResource;
 import com.google.gerrit.server.change.ChangeResource;
@@ -64,7 +65,6 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -379,16 +379,14 @@
     public Response<BinaryResult> apply(ChangeEditResource rsrc) throws AuthException, IOException {
       try {
         if (Patch.COMMIT_MSG.equals(rsrc.getPath())) {
-          return Response.ok(getMessage.apply(rsrc.getChangeResource()));
+          return getMessage.apply(rsrc.getChangeResource());
         }
 
         ChangeEdit edit = rsrc.getChangeEdit();
         return Response.ok(
             fileContentUtil.getContent(
                 projectCache.checkedGet(rsrc.getChangeResource().getProject()),
-                base
-                    ? ObjectId.fromString(edit.getBasePatchSet().getRevision().get())
-                    : edit.getEditCommit(),
+                base ? edit.getBasePatchSet().commitId() : edit.getEditCommit(),
                 rsrc.getPath(),
                 null));
       } catch (ResourceNotFoundException | BadRequestException e) {
@@ -407,22 +405,22 @@
     }
 
     @Override
-    public FileInfo apply(ChangeEditResource rsrc) {
+    public Response<FileInfo> apply(ChangeEditResource rsrc) {
       FileInfo r = new FileInfo();
       ChangeEdit edit = rsrc.getChangeEdit();
       Change change = edit.getChange();
-      List<DiffWebLinkInfo> links =
+      ImmutableList<DiffWebLinkInfo> links =
           webLinks.getDiffLinks(
               change.getProject().get(),
               change.getChangeId(),
-              edit.getBasePatchSet().getPatchSetId(),
-              edit.getBasePatchSet().getRefName(),
+              edit.getBasePatchSet().number(),
+              edit.getBasePatchSet().refName(),
               rsrc.getPath(),
               0,
               edit.getRefName(),
               rsrc.getPath());
       r.webLinks = links.isEmpty() ? null : links;
-      return r;
+      return Response.ok(r);
     }
 
     public static class FileInfo {
@@ -446,7 +444,7 @@
     }
 
     @Override
-    public Response<?> apply(ChangeResource rsrc, Input input)
+    public Response<Object> apply(ChangeResource rsrc, Input input)
         throws AuthException, IOException, BadRequestException, ResourceConflictException,
             PermissionBackendException {
       if (input == null || Strings.isNullOrEmpty(input.message)) {
@@ -481,7 +479,7 @@
     }
 
     @Override
-    public BinaryResult apply(ChangeResource rsrc)
+    public Response<BinaryResult> apply(ChangeResource rsrc)
         throws AuthException, IOException, ResourceNotFoundException {
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       String msg;
@@ -489,18 +487,17 @@
         if (base) {
           try (Repository repo = repoManager.openRepository(rsrc.getProject());
               RevWalk rw = new RevWalk(repo)) {
-            RevCommit commit =
-                rw.parseCommit(
-                    ObjectId.fromString(edit.get().getBasePatchSet().getRevision().get()));
+            RevCommit commit = rw.parseCommit(edit.get().getBasePatchSet().commitId());
             msg = commit.getFullMessage();
           }
         } else {
           msg = edit.get().getEditCommit().getFullMessage();
         }
 
-        return BinaryResult.create(msg)
-            .setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
-            .base64();
+        return Response.ok(
+            BinaryResult.create(msg)
+                .setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
+                .base64());
       }
       throw new ResourceNotFoundException();
     }
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java b/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
index 6ec4fdb..517fbdf 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
@@ -14,13 +14,15 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.IncludedInInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.IncludedIn;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -37,8 +39,9 @@
   }
 
   @Override
-  public IncludedInInfo apply(ChangeResource rsrc) throws RestApiException, IOException {
+  public Response<IncludedInInfo> apply(ChangeResource rsrc)
+      throws RestApiException, IOException, PermissionBackendException {
     PatchSet ps = psUtil.current(rsrc.getNotes());
-    return includedIn.apply(rsrc.getProject(), ps.getRevision().get());
+    return Response.ok(includedIn.apply(rsrc.getProject(), ps.commitId().name()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeMessages.java b/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
index 96c517f..fae9180 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.change.ChangeMessageResource;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.List;
@@ -50,11 +49,10 @@
   }
 
   @Override
-  public ChangeMessageResource parse(ChangeResource parent, IdString id)
-      throws ResourceNotFoundException, PermissionBackendException {
+  public ChangeMessageResource parse(ChangeResource parent, IdString id) throws Exception {
     String uuid = id.get();
 
-    List<ChangeMessageInfo> changeMessages = listChangeMessages.apply(parent);
+    List<ChangeMessageInfo> changeMessages = listChangeMessages.apply(parent).value();
     int index = -1;
     for (int i = 0; i < changeMessages.size(); ++i) {
       ChangeMessageInfo changeMessage = changeMessages.get(i);
diff --git a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
index 028eb52..3b0321d 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -23,8 +25,6 @@
 import com.google.gerrit.extensions.restapi.RestCollection;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeResource;
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java
index 72781c3..1a89935 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPick.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -17,14 +17,15 @@
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.common.CherryPickChangeInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -75,14 +76,12 @@
   }
 
   @Override
-  public CherryPickChangeInfo applyImpl(
+  public Response<CherryPickChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, CherryPickInput input)
       throws IOException, UpdateException, RestApiException, PermissionBackendException,
           ConfigInvalidException, NoSuchProjectException {
     input.parent = input.parent == null ? 1 : input.parent;
-    if (input.message == null || input.message.trim().isEmpty()) {
-      throw new BadRequestException("message must be non-empty");
-    } else if (input.destination == null || input.destination.trim().isEmpty()) {
+    if (input.destination == null || input.destination.trim().isEmpty()) {
       throw new BadRequestException("destination must be non-empty");
     }
 
@@ -103,13 +102,13 @@
               rsrc.getChange(),
               rsrc.getPatchSet(),
               input,
-              new Branch.NameKey(rsrc.getProject(), refName));
+              BranchNameKey.create(rsrc.getProject(), refName));
       CherryPickChangeInfo changeInfo =
           json.noOptions()
               .format(rsrc.getProject(), cherryPickResult.changeId(), CherryPickChangeInfo::new);
       changeInfo.containsGitConflicts =
           !cherryPickResult.filesWithGitConflicts().isEmpty() ? true : null;
-      return changeInfo;
+      return Response.ok(changeInfo);
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
     } catch (IntegrationException | NoSuchChangeException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 132310d..407c560 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -21,6 +21,11 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -28,11 +33,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -136,30 +136,116 @@
     this.notifyResolver = notifyResolver;
   }
 
+  /**
+   * This function is used for cherry picking a change.
+   *
+   * @param batchUpdateFactory Used for applying changes to the database.
+   * @param change Change to cherry pick.
+   * @param patch The patch of that change.
+   * @param input Input object for different configurations of cherry pick.
+   * @param dest Destination branch for the cherry pick.
+   * @return Result object that describes the cherry pick.
+   * @throws IOException Unable to open repository or read from the database.
+   * @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same
+   *     key exist in the branch.
+   * @throws IntegrationException Merge conflict or trees are identical after cherry pick.
+   * @throws UpdateException Problem updating the database using batchUpdateFactory.
+   * @throws RestApiException Error such as invalid SHA1
+   * @throws ConfigInvalidException Can't find account to notify.
+   * @throws NoSuchProjectException Can't find project state.
+   */
   public Result cherryPick(
       BatchUpdate.Factory batchUpdateFactory,
       Change change,
       PatchSet patch,
       CherryPickInput input,
-      Branch.NameKey dest)
+      BranchNameKey dest)
       throws IOException, InvalidChangeOperationException, IntegrationException, UpdateException,
           RestApiException, ConfigInvalidException, NoSuchProjectException {
     return cherryPick(
         batchUpdateFactory,
         change,
         change.getProject(),
-        ObjectId.fromString(patch.getRevision().get()),
+        patch.commitId(),
         input,
-        dest);
+        dest,
+        null,
+        null,
+        null);
   }
 
+  /**
+   * This function is called directly to cherry pick a commit. Also, it is used to cherry pick a
+   * change as well as long as sourceChange is not null.
+   *
+   * @param batchUpdateFactory Used for applying changes to the database.
+   * @param sourceChange Change to cherry pick. Can be null, and then the function will only cherry
+   *     pick a commit.
+   * @param project Project name
+   * @param sourceCommit Id of the commit to be cherry picked.
+   * @param input Input object for different configurations of cherry pick.
+   * @param dest Destination branch for the cherry pick.
+   * @return Result object that describes the cherry pick.
+   * @throws IOException Unable to open repository or read from the database.
+   * @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same
+   *     key exist in the branch.
+   * @throws IntegrationException Merge conflict or trees are identical after cherry pick.
+   * @throws UpdateException Problem updating the database using batchUpdateFactory.
+   * @throws RestApiException Error such as invalid SHA1
+   * @throws ConfigInvalidException Can't find account to notify.
+   * @throws NoSuchProjectException Can't find project state.
+   */
   public Result cherryPick(
       BatchUpdate.Factory batchUpdateFactory,
       @Nullable Change sourceChange,
       Project.NameKey project,
       ObjectId sourceCommit,
       CherryPickInput input,
-      Branch.NameKey dest)
+      BranchNameKey dest)
+      throws IOException, InvalidChangeOperationException, IntegrationException, UpdateException,
+          RestApiException, ConfigInvalidException, NoSuchProjectException {
+    return cherryPick(
+        batchUpdateFactory, sourceChange, project, sourceCommit, input, dest, null, null, null);
+  }
+
+  /**
+   * This function can be called directly to cherry-pick a change (or commit if sourceChange is
+   * null) with a few other parameters that are especially useful for cherry-picking a commit that
+   * is the revert-of another change.
+   *
+   * @param batchUpdateFactory Used for applying changes to the database.
+   * @param sourceChange Change to cherry pick. Can be null, and then the function will only cherry
+   *     pick a commit.
+   * @param project Project name
+   * @param sourceCommit Id of the commit to be cherry picked.
+   * @param input Input object for different configurations of cherry pick.
+   * @param dest Destination branch for the cherry pick.
+   * @param topic Topic name for the change created.
+   * @param revertedChange The id of the change that is reverted. This is used for the "revertOf"
+   *     field to mark the created cherry pick change as "revertOf" the original change that was
+   *     reverted.
+   * @param changeIdForNewChange The Change-Id that the new change that of the cherry pick will
+   *     have.
+   * @return Result object that describes the cherry pick.
+   * @throws IOException Unable to open repository or read from the database.
+   * @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same
+   *     key exist in the branch.
+   * @throws IntegrationException Merge conflict or trees are identical after cherry pick.
+   * @throws UpdateException Problem updating the database using batchUpdateFactory.
+   * @throws RestApiException Error such as invalid SHA1
+   * @throws ConfigInvalidException Can't find account to notify.
+   * @throws NoSuchProjectException Can't find project state.
+   */
+  public Result cherryPick(
+      BatchUpdate.Factory batchUpdateFactory,
+      @Nullable Change sourceChange,
+      Project.NameKey project,
+      ObjectId sourceCommit,
+      CherryPickInput input,
+      BranchNameKey dest,
+      @Nullable String topic,
+      @Nullable Change.Id revertedChange,
+      @Nullable ObjectId changeIdForNewChange)
       throws IOException, InvalidChangeOperationException, IntegrationException, UpdateException,
           RestApiException, ConfigInvalidException, NoSuchProjectException {
 
@@ -171,10 +257,10 @@
         ObjectInserter oi = git.newObjectInserter();
         ObjectReader reader = oi.newReader();
         CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) {
-      Ref destRef = git.getRefDatabase().exactRef(dest.get());
+      Ref destRef = git.getRefDatabase().exactRef(dest.branch());
       if (destRef == null) {
         throw new InvalidChangeOperationException(
-            String.format("Branch %s does not exist.", dest.get()));
+            String.format("Branch %s does not exist.", dest.branch()));
       }
 
       RevCommit baseCommit = getBaseCommit(destRef, project.get(), revWalk, input.base);
@@ -189,16 +275,22 @@
                 input.parent, commitToCherryPick.getParentCount()));
       }
 
+      String message = Strings.nullToEmpty(input.message).trim();
+      message = message.isEmpty() ? commitToCherryPick.getFullMessage() : message;
+
       Timestamp now = TimeUtil.nowTs();
       PersonIdent committerIdent = identifiedUser.newCommitterIdent(now, serverTimeZone);
 
-      final ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
-      String commitMessage = ChangeIdUtil.insertId(input.message, generatedChangeId).trim() + '\n';
+      final ObjectId generatedChangeId =
+          changeIdForNewChange != null
+              ? changeIdForNewChange
+              : CommitMessageUtil.generateChangeId();
+      String commitMessage = ChangeIdUtil.insertId(message, generatedChangeId).trim() + '\n';
 
       CodeReviewCommit cherryPickCommit;
-      ProjectState projectState = projectCache.checkedGet(dest.getParentKey());
+      ProjectState projectState = projectCache.checkedGet(dest.project());
       if (projectState == null) {
-        throw new NoSuchProjectException(dest.getParentKey());
+        throw new NoSuchProjectException(dest.project());
       }
       try {
         MergeUtil mergeUtil;
@@ -226,12 +318,12 @@
         final List<String> idList = cherryPickCommit.getFooterLines(FooterConstants.CHANGE_ID);
         if (!idList.isEmpty()) {
           final String idStr = idList.get(idList.size() - 1).trim();
-          changeKey = new Change.Key(idStr);
+          changeKey = Change.key(idStr);
         } else {
-          changeKey = new Change.Key("I" + generatedChangeId.name());
+          changeKey = Change.key("I" + generatedChangeId.name());
         }
 
-        Branch.NameKey newDest = new Branch.NameKey(project, destRef.getName());
+        BranchNameKey newDest = BranchNameKey.create(project, destRef.getName());
         List<ChangeData> destChanges =
             queryProvider.get().setLimit(2).byBranchKey(newDest, changeKey);
         if (destChanges.size() > 1) {
@@ -252,13 +344,22 @@
           } else {
             // Change key not found on destination branch. We can create a new
             // change.
-            String newTopic = null;
-            if (sourceChange != null && !Strings.isNullOrEmpty(sourceChange.getTopic())) {
-              newTopic = sourceChange.getTopic() + "-" + newDest.getShortName();
+            String newTopic = topic;
+            if (topic == null
+                && sourceChange != null
+                && !Strings.isNullOrEmpty(sourceChange.getTopic())) {
+              newTopic = sourceChange.getTopic() + "-" + newDest.shortName();
             }
             changeId =
                 createNewChange(
-                    bu, cherryPickCommit, dest.get(), newTopic, sourceChange, sourceCommit, input);
+                    bu,
+                    cherryPickCommit,
+                    dest.branch(),
+                    newTopic,
+                    sourceChange,
+                    sourceCommit,
+                    input,
+                    revertedChange);
           }
           bu.execute();
           return Result.create(changeId, cherryPickCommit.getFilesWithGitConflicts());
@@ -337,15 +438,20 @@
       String refName,
       String topic,
       @Nullable Change sourceChange,
-      ObjectId sourceCommit,
-      CherryPickInput input)
+      @Nullable ObjectId sourceCommit,
+      CherryPickInput input,
+      @Nullable Change.Id revertOf)
       throws IOException {
-    Change.Id changeId = new Change.Id(seq.nextChangeId());
+    Change.Id changeId = Change.id(seq.nextChangeId());
     ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
-    Branch.NameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
+    ins.setRevertOf(revertOf);
+    BranchNameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
     ins.setMessage(
-            messageForDestinationChange(
-                ins.getPatchSetId(), sourceBranch, sourceCommit, cherryPickCommit))
+            revertOf == null
+                ? messageForDestinationChange(
+                    ins.getPatchSetId(), sourceBranch, sourceCommit, cherryPickCommit)
+                : "Uploaded patch set 1.") // For revert commits, the message should not include
+        // cherry-pick information.
         .setTopic(topic)
         .setWorkInProgress(
             (sourceChange != null && sourceChange.isWorkInProgress())
@@ -373,12 +479,12 @@
 
   private String messageForDestinationChange(
       PatchSet.Id patchSetId,
-      Branch.NameKey sourceBranch,
+      BranchNameKey sourceBranch,
       ObjectId sourceCommit,
       CodeReviewCommit cherryPickCommit) {
     StringBuilder stringBuilder = new StringBuilder("Patch Set ").append(patchSetId.get());
     if (sourceBranch != null) {
-      stringBuilder.append(": Cherry Picked from branch ").append(sourceBranch.getShortName());
+      stringBuilder.append(": Cherry Picked from branch ").append(sourceBranch.shortName());
     } else {
       stringBuilder.append(": Cherry Picked from commit ").append(sourceCommit.getName());
     }
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
index f34f178..a3c8a97 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
@@ -15,14 +15,15 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.common.CherryPickChangeInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -42,7 +43,6 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.revwalk.RevCommit;
 
 @Singleton
 public class CherryPickCommit
@@ -70,13 +70,10 @@
   }
 
   @Override
-  public CherryPickChangeInfo applyImpl(
+  public Response<CherryPickChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input)
       throws IOException, UpdateException, RestApiException, PermissionBackendException,
           ConfigInvalidException, NoSuchProjectException {
-    RevCommit commit = rsrc.getCommit();
-    String message = Strings.nullToEmpty(input.message).trim();
-    input.message = message.isEmpty() ? commit.getFullMessage() : message;
     String destination = Strings.nullToEmpty(input.destination).trim();
     input.parent = input.parent == null ? 1 : input.parent;
     Project.NameKey projectName = rsrc.getProjectState().getNameKey();
@@ -100,15 +97,15 @@
               updateFactory,
               null,
               projectName,
-              commit,
+              rsrc.getCommit(),
               input,
-              new Branch.NameKey(rsrc.getProjectState().getNameKey(), refName));
+              BranchNameKey.create(rsrc.getProjectState().getNameKey(), refName));
       CherryPickChangeInfo changeInfo =
           json.noOptions()
               .format(projectName, cherryPickResult.changeId(), CherryPickChangeInfo::new);
       changeInfo.containsGitConflicts =
           !cherryPickResult.filesWithGitConflicts().isEmpty() ? true : null;
-      return changeInfo;
+      return Response.ok(changeInfo);
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
     } catch (IntegrationException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index 7112bbf..03898b1 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -22,6 +22,10 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.FixReplacement;
+import com.google.gerrit.entities.FixSuggestion;
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -29,10 +33,6 @@
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.FixReplacement;
-import com.google.gerrit.reviewdb.client.FixSuggestion;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/change/Comments.java b/java/com/google/gerrit/server/restapi/change/Comments.java
index d9a0d57..078c239 100644
--- a/java/com/google/gerrit/server/restapi/change/Comments.java
+++ b/java/com/google/gerrit/server/restapi/change/Comments.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Comment;
 import com.google.gerrit.extensions.registration.DynamicMap;
 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.reviewdb.client.Comment;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.CommentResource;
 import com.google.gerrit.server.change.RevisionResource;
@@ -58,7 +58,7 @@
     String uuid = id.get();
     ChangeNotes notes = rev.getNotes();
 
-    for (Comment c : commentsUtil.publishedByPatchSet(notes, rev.getPatchSet().getId())) {
+    for (Comment c : commentsUtil.publishedByPatchSet(notes, rev.getPatchSet().id())) {
       if (uuid.equals(c.key.uuid)) {
         return new CommentResource(rev, c);
       }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index f626db2..9d65940 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -21,6 +21,13 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.InvalidMergeStrategyException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -36,11 +43,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -64,6 +66,7 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.restapi.project.CommitsCollection;
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -79,6 +82,7 @@
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.TimeZone;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
@@ -87,6 +91,7 @@
 import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -101,7 +106,7 @@
 @Singleton
 public class CreateChange
     extends RetryingRestCollectionModifyView<
-        TopLevelResource, ChangeResource, ChangeInput, Response<ChangeInfo>> {
+        TopLevelResource, ChangeResource, ChangeInput, ChangeInfo> {
   private final String anonymousCowardName;
   private final GitRepositoryManager gitManager;
   private final Sequences seq;
@@ -113,6 +118,7 @@
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeJson.Factory jsonFactory;
   private final ChangeFinder changeFinder;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final PatchSetUtil psUtil;
   private final MergeUtil.Factory mergeUtilFactory;
   private final SubmitType submitType;
@@ -133,6 +139,7 @@
       ChangeInserter.Factory changeInserterFactory,
       ChangeJson.Factory json,
       ChangeFinder changeFinder,
+      Provider<InternalChangeQuery> queryProvider,
       RetryHelper retryHelper,
       PatchSetUtil psUtil,
       @GerritServerConfig Config config,
@@ -151,6 +158,7 @@
     this.changeInserterFactory = changeInserterFactory;
     this.jsonFactory = json;
     this.changeFinder = changeFinder;
+    this.queryProvider = queryProvider;
     this.psUtil = psUtil;
     this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
     this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
@@ -164,6 +172,9 @@
       BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
       throws IOException, InvalidChangeOperationException, RestApiException, UpdateException,
           PermissionBackendException, ConfigInvalidException {
+    if (!user.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
     IdentifiedUser me = user.get().asIdentifiedUser();
     checkAndSanitizeChangeInput(input, me);
 
@@ -209,6 +220,20 @@
     }
     input.subject = subject;
 
+    Optional<String> changeId = getChangeIdFromMessage(input.subject);
+    if (changeId.isPresent()) {
+      if (!queryProvider
+          .get()
+          .setLimit(1)
+          .byBranchKey(
+              BranchNameKey.create(input.project, input.branch), Change.key(changeId.get()))
+          .isEmpty()) {
+        throw new ResourceConflictException(
+            String.format(
+                "A change with Change-Id %s already exists for this branch.", changeId.get()));
+      }
+    }
+
     if (input.topic != null) {
       input.topic = Strings.emptyToNull(input.topic.trim());
     }
@@ -238,7 +263,7 @@
         input.workInProgress = true;
       } else {
         input.workInProgress =
-            firstNonNull(me.state().getGeneralPreferences().workInProgressByDefault, false);
+            firstNonNull(me.state().generalPreferences().workInProgressByDefault, false);
       }
     }
 
@@ -281,7 +306,7 @@
       if (input.baseChange != null) {
         ChangeNotes baseChange = getBaseChange(input.baseChange);
         basePatchSet = psUtil.current(baseChange);
-        groups = basePatchSet.getGroups();
+        groups = basePatchSet.groups();
       }
       ObjectId parentCommit =
           getParentCommit(
@@ -302,7 +327,7 @@
         c = newCommit(oi, rw, author, mergeTip, commitMessage);
       }
 
-      Change.Id changeId = new Change.Id(seq.nextChangeId());
+      Change.Id changeId = Change.id(seq.nextChangeId());
       ChangeInserter ins = changeInserterFactory.create(changeId, c, input.branch);
       ins.setMessage(String.format("Uploaded patch set %s.", ins.getPatchSetId().get()));
       ins.setTopic(input.topic);
@@ -318,7 +343,7 @@
         bu.execute();
       }
       return ins.getChange();
-    } catch (IllegalArgumentException e) {
+    } catch (InvalidMergeStrategyException e) {
       throw new BadRequestException(e.getMessage());
     }
   }
@@ -351,7 +376,7 @@
       throws BadRequestException, IOException, UnprocessableEntityException,
           ResourceConflictException {
     if (basePatchSet != null) {
-      return ObjectId.fromString(basePatchSet.getRevision().get());
+      return basePatchSet.commitId();
     }
 
     Ref destRef = repo.getRefDatabase().exactRef(inputBranch);
@@ -403,6 +428,17 @@
     return parentCommit;
   }
 
+  private Optional<String> getChangeIdFromMessage(String subject) {
+    int indexOfChangeId = ChangeIdUtil.indexOfChangeId(subject, "\n");
+    if (indexOfChangeId == -1) {
+      return Optional.empty();
+    }
+    return Optional.of(
+        subject.substring(
+            indexOfChangeId + 11 /* "Change-Id: "*/,
+            indexOfChangeId + 12 /* "Change-Id: I" */ + Constants.OBJECT_ID_STRING_LENGTH));
+  }
+
   private String getCommitMessage(String subject, IdentifiedUser me) {
     // Add a Change-Id line if there isn't already one
     String commitMessage = subject;
@@ -411,15 +447,14 @@
       commitMessage = ChangeIdUtil.insertId(commitMessage, id);
     }
 
-    if (Boolean.TRUE.equals(me.state().getGeneralPreferences().signedOffBy)) {
+    if (Boolean.TRUE.equals(me.state().generalPreferences().signedOffBy)) {
       commitMessage =
           Joiner.on("\n")
               .join(
                   commitMessage.trim(),
                   String.format(
                       "%s%s",
-                      SIGNED_OFF_BY_TAG,
-                      me.state().getAccount().getNameEmail(anonymousCowardName)));
+                      SIGNED_OFF_BY_TAG, me.state().account().getNameEmail(anonymousCowardName)));
     }
 
     return commitMessage;
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index b6e7628..f434e31 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -25,9 +27,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RevisionResource;
@@ -48,7 +47,7 @@
 
 @Singleton
 public class CreateDraftComment
-    extends RetryingRestModifyView<RevisionResource, DraftInput, Response<CommentInfo>> {
+    extends RetryingRestModifyView<RevisionResource, DraftInput, CommentInfo> {
   private final Provider<CommentJson> commentJson;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
@@ -84,7 +83,7 @@
 
     try (BatchUpdate bu =
         updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
-      Op op = new Op(rsrc.getPatchSet().getId(), in);
+      Op op = new Op(rsrc.getPatchSet().id(), in);
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
       return Response.created(
@@ -115,13 +114,14 @@
 
       comment =
           commentsUtil.newComment(
-              ctx, in.path, ps.getId(), in.side(), in.message.trim(), in.unresolved, parentUuid);
+              ctx, in.path, ps.id(), in.side(), in.message.trim(), in.unresolved, parentUuid);
       comment.setLineNbrAndRange(in.line, in.range);
       comment.tag = in.tag;
 
-      setCommentRevId(comment, patchListCache, ctx.getChange(), ps);
+      setCommentCommitId(comment, patchListCache, ctx.getChange(), ps);
 
-      commentsUtil.putComments(ctx.getUpdate(psId), Status.DRAFT, Collections.singleton(comment));
+      commentsUtil.putComments(
+          ctx.getUpdate(psId), Comment.Status.DRAFT, Collections.singleton(comment));
       return true;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index b7bcaf9..a159486 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -17,6 +17,11 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.InvalidMergeStrategyException;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.MergeInput;
@@ -28,10 +33,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -76,7 +77,7 @@
 
 @Singleton
 public class CreateMergePatchSet
-    extends RetryingRestModifyView<ChangeResource, MergePatchSetInput, Response<ChangeInfo>> {
+    extends RetryingRestModifyView<ChangeResource, MergePatchSetInput, ChangeInfo> {
   private final GitRepositoryManager gitManager;
   private final CommitsCollection commits;
   private final TimeZone serverTimeZone;
@@ -138,7 +139,7 @@
     PatchSet ps = psUtil.current(rsrc.getNotes());
     Change change = rsrc.getChange();
     Project.NameKey project = change.getProject();
-    Branch.NameKey dest = change.getDest();
+    BranchNameKey dest = change.getDest();
     try (Repository git = gitManager.openRepository(project);
         ObjectInserter oi = git.newObjectInserter();
         ObjectReader reader = oi.newReader();
@@ -154,10 +155,10 @@
       List<String> groups = null;
       if (!in.inheritParent && !in.baseChange.isEmpty()) {
         PatchSet basePS = findBasePatchSet(in.baseChange);
-        currentPsCommit = rw.parseCommit(ObjectId.fromString(basePS.getRevision().get()));
-        groups = basePS.getGroups();
+        currentPsCommit = rw.parseCommit(basePS.commitId());
+        groups = basePS.groups();
       } else {
-        currentPsCommit = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+        currentPsCommit = rw.parseCommit(ps.commitId());
       }
 
       Timestamp now = TimeUtil.nowTs();
@@ -176,7 +177,7 @@
               author,
               ObjectId.fromString(change.getKey().get().substring(1)));
 
-      PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.getId());
+      PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.id());
       PatchSetInserter psInserter =
           patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit);
       try (BatchUpdate bu = updateFactory.create(project, me, now)) {
@@ -194,6 +195,8 @@
 
       ChangeJson json = jsonFactory.create(ListChangesOption.CURRENT_REVISION);
       return Response.ok(json.format(psInserter.getChange()));
+    } catch (InvalidMergeStrategyException e) {
+      throw new BadRequestException(e.getMessage());
     }
   }
 
@@ -215,7 +218,7 @@
   private RevCommit createMergeCommit(
       MergePatchSetInput in,
       ProjectState projectState,
-      Branch.NameKey dest,
+      BranchNameKey dest,
       Repository git,
       ObjectInserter oi,
       RevWalk rw,
@@ -234,7 +237,7 @@
       parentCommit = currentPsCommit.getId();
     } else {
       // get the current branch tip of destination branch
-      Ref destRef = git.getRefDatabase().exactRef(dest.get());
+      Ref destRef = git.getRefDatabase().exactRef(dest.branch());
       if (destRef != null) {
         parentCommit = destRef.getObjectId();
       } else {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
index 02387be..834782f 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountLoader;
@@ -42,8 +42,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class DeleteAssignee
-    extends RetryingRestModifyView<ChangeResource, Input, Response<AccountInfo>> {
+public class DeleteAssignee extends RetryingRestModifyView<ChangeResource, Input, AccountInfo> {
 
   private final ChangeMessagesUtil cmUtil;
   private final AssigneeChanged assigneeChanged;
@@ -105,7 +104,7 @@
     }
 
     public Account.Id getDeletedAssignee() {
-      return deletedAssignee != null ? deletedAssignee.getAccount().getId() : null;
+      return deletedAssignee != null ? deletedAssignee.account().id() : null;
     }
 
     private void addMessage(
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChange.java b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
index 3021d81..aa4dcf0 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChange.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
@@ -16,12 +16,12 @@
 
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.DeleteChangeOp;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -36,7 +36,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class DeleteChange extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
+public class DeleteChange extends RetryingRestModifyView<ChangeResource, Input, Object>
     implements UiAction<ChangeResource> {
 
   private final DeleteChangeOp.Factory opFactory;
@@ -48,7 +48,7 @@
   }
 
   @Override
-  protected Response<?> applyImpl(
+  protected Response<Object> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
     if (!isChangeDeletable(rsrc)) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
index 0fb8e18..30cfad6 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
@@ -20,14 +20,14 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.DeleteChangeMessageInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountLoader;
@@ -53,7 +53,7 @@
 @Singleton
 public class DeleteChangeMessage
     extends RetryingRestModifyView<
-        ChangeMessageResource, DeleteChangeMessageInput, Response<ChangeMessageInfo>> {
+        ChangeMessageResource, DeleteChangeMessageInput, ChangeMessageInfo> {
 
   private final Provider<CurrentUser> userProvider;
   private final PermissionBackend permissionBackend;
@@ -146,7 +146,7 @@
 
   @Singleton
   public static class DefaultDeleteChangeMessage
-      extends RetryingRestModifyView<ChangeMessageResource, Input, Response<ChangeMessageInfo>> {
+      extends RetryingRestModifyView<ChangeMessageResource, Input, ChangeMessageInfo> {
     private final DeleteChangeMessage deleteChangeMessage;
 
     @Inject
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteComment.java b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
index 30a8efd7..95479a6 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
@@ -15,13 +15,14 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.CommentResource;
@@ -71,7 +72,7 @@
   }
 
   @Override
-  public CommentInfo applyImpl(
+  public Response<CommentInfo> applyImpl(
       BatchUpdate.Factory batchUpdateFactory, CommentResource rsrc, DeleteCommentInput input)
       throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException,
           UpdateException {
@@ -100,7 +101,7 @@
       throw new ResourceNotFoundException("comment not found: " + rsrc.getComment().key);
     }
 
-    return commentJson.get().newCommentFormatter().format(updatedComment.get());
+    return Response.ok(commentJson.get().newCommentFormatter().format(updatedComment.get()));
   }
 
   private static String getCommentNewMessage(String name, String reason) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
index de04d36..9296988 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -14,15 +14,15 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.DraftCommentResource;
@@ -42,7 +42,7 @@
 
 @Singleton
 public class DeleteDraftComment
-    extends RetryingRestModifyView<DraftCommentResource, Input, Response<CommentInfo>> {
+    extends RetryingRestModifyView<DraftCommentResource, Input, CommentInfo> {
 
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
@@ -88,13 +88,13 @@
       if (!maybeComment.isPresent()) {
         return false; // Nothing to do.
       }
-      PatchSet.Id psId = new PatchSet.Id(ctx.getChange().getId(), key.patchSetId);
+      PatchSet.Id psId = PatchSet.id(ctx.getChange().getId(), key.patchSetId);
       PatchSet ps = psUtil.get(ctx.getNotes(), psId);
       if (ps == null) {
         throw new ResourceNotFoundException("patch set not found: " + psId);
       }
       Comment c = maybeComment.get();
-      setCommentRevId(c, patchListCache, ctx.getChange(), ps);
+      setCommentCommitId(c, patchListCache, ctx.getChange(), ps);
       commentsUtil.deleteComments(ctx.getUpdate(psId), Collections.singleton(c));
       return true;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
index 8601e68..de7a683 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
@@ -36,7 +36,7 @@
 
 @Singleton
 public class DeletePrivate
-    extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>> {
+    extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, String> {
   private final PermissionBackend permissionBackend;
   private final SetPrivateOp.Factory setPrivateOpFactory;
 
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
index 12dbcdd..b98bb3b 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.DeleteReviewerByEmailOp;
 import com.google.gerrit.server.change.DeleteReviewerOp;
 import com.google.gerrit.server.change.NotifyResolver;
@@ -34,7 +34,7 @@
 
 @Singleton
 public class DeleteReviewer
-    extends RetryingRestModifyView<ReviewerResource, DeleteReviewerInput, Response<?>> {
+    extends RetryingRestModifyView<ReviewerResource, DeleteReviewerInput, Object> {
 
   private final DeleteReviewerOp.Factory deleteReviewerOpFactory;
   private final DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory;
@@ -50,7 +50,7 @@
   }
 
   @Override
-  protected Response<?> applyImpl(
+  protected Response<Object> applyImpl(
       BatchUpdate.Factory updateFactory, ReviewerResource rsrc, DeleteReviewerInput input)
       throws RestApiException, UpdateException {
     if (input == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 77894fb..1193ad6 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -19,6 +19,11 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -27,11 +32,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
@@ -64,7 +64,7 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
-public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Response<?>> {
+public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Object> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ApprovalsUtil approvalsUtil;
@@ -102,7 +102,7 @@
   }
 
   @Override
-  protected Response<?> applyImpl(
+  protected Response<Object> applyImpl(
       BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input)
       throws RestApiException, UpdateException, IOException, ConfigInvalidException {
     if (input == null) {
@@ -170,16 +170,16 @@
       boolean found = false;
       LabelTypes labelTypes = projectState.getLabelTypes(ctx.getNotes());
 
-      Account.Id accountId = accountState.getAccount().getId();
+      Account.Id accountId = accountState.account().id();
 
       for (PatchSetApproval a :
           approvalsUtil.byPatchSetUser(
               ctx.getNotes(), psId, accountId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
-        if (labelTypes.byLabel(a.getLabelId()) == null) {
+        if (labelTypes.byLabel(a.labelId()) == null) {
           continue; // Ignore undefined labels.
-        } else if (!a.getLabel().equals(label)) {
+        } else if (!a.label().equals(label)) {
           // Populate map for non-matching labels, needed by VoteDeleted.
-          newApprovals.put(a.getLabel(), a.getValue());
+          newApprovals.put(a.label(), a.value());
           continue;
         } else {
           try {
@@ -189,11 +189,11 @@
           }
         }
         // Set the approval to 0 if vote is being removed.
-        newApprovals.put(a.getLabel(), (short) 0);
+        newApprovals.put(a.label(), (short) 0);
         found = true;
 
         // Set old value, as required by VoteDeleted.
-        oldApprovals.put(a.getLabel(), a.getValue());
+        oldApprovals.put(a.label(), a.value());
         break;
       }
       if (!found) {
diff --git a/java/com/google/gerrit/server/restapi/change/DownloadContent.java b/java/com/google/gerrit/server/restapi/change/DownloadContent.java
index 1022cad..4a4a680 100644
--- a/java/com/google/gerrit/server/restapi/change/DownloadContent.java
+++ b/java/com/google/gerrit/server/restapi/change/DownloadContent.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.FileContentUtil;
 import com.google.gerrit.server.change.FileResource;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import java.io.IOException;
-import org.eclipse.jgit.lib.ObjectId;
 import org.kohsuke.args4j.Option;
 
 public class DownloadContent implements RestReadView<FileResource> {
@@ -41,12 +41,12 @@
   }
 
   @Override
-  public BinaryResult apply(FileResource rsrc)
+  public Response<BinaryResult> apply(FileResource rsrc)
       throws ResourceNotFoundException, IOException, NoSuchChangeException {
-    String path = rsrc.getPatchKey().get();
+    String path = rsrc.getPatchKey().fileName();
     RevisionResource rev = rsrc.getRevision();
-    ObjectId revstr = ObjectId.fromString(rev.getPatchSet().getRevision().get());
-    return fileContentUtil.downloadContent(
-        projectCache.checkedGet(rev.getProject()), revstr, path, parent);
+    return Response.ok(
+        fileContentUtil.downloadContent(
+            projectCache.checkedGet(rev.getProject()), rev.getPatchSet().commitId(), path, parent));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/DraftComments.java b/java/com/google/gerrit/server/restapi/change/DraftComments.java
index dd61ca0..bab1ac9 100644
--- a/java/com/google/gerrit/server/restapi/change/DraftComments.java
+++ b/java/com/google/gerrit/server/restapi/change/DraftComments.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Comment;
 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.reviewdb.client.Comment;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.DraftCommentResource;
@@ -66,7 +66,7 @@
     String uuid = id.get();
     for (Comment c :
         commentsUtil.draftByPatchSetAuthor(
-            rev.getPatchSet().getId(), rev.getAccountId(), rev.getNotes())) {
+            rev.getPatchSet().id(), rev.getAccountId(), rev.getNotes())) {
       if (uuid.equals(c.key.uuid)) {
         return new DraftCommentResource(rev, c);
       }
diff --git a/java/com/google/gerrit/server/restapi/change/Files.java b/java/com/google/gerrit/server/restapi/change/Files.java
index d892810..392aef7 100644
--- a/java/com/google/gerrit/server/restapi/change/Files.java
+++ b/java/com/google/gerrit/server/restapi/change/Files.java
@@ -19,6 +19,10 @@
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -31,10 +35,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
@@ -63,7 +63,6 @@
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -166,13 +165,13 @@
             Response.ok(
                 fileInfoJson.toFileInfoMap(
                     resource.getChange(),
-                    resource.getPatchSet().getRevision(),
+                    resource.getPatchSet().commitId(),
                     baseResource.getPatchSet()));
       } else if (parentNum != 0) {
         int parents =
             gApi.changes()
                 .id(resource.getChange().getChangeId())
-                .revision(resource.getPatchSet().getId().get())
+                .revision(resource.getPatchSet().id().get())
                 .commit(false)
                 .parents
                 .size();
@@ -182,7 +181,7 @@
         r =
             Response.ok(
                 fileInfoJson.toFileInfoMap(
-                    resource.getChange(), resource.getPatchSet().getRevision(), parentNum - 1));
+                    resource.getChange(), resource.getPatchSet().commitId(), parentNum - 1));
       } else {
         r = Response.ok(fileInfoJson.toFileInfoMap(resource.getChange(), resource.getPatchSet()));
       }
@@ -219,8 +218,7 @@
           ObjectReader or = git.newObjectReader();
           RevWalk rw = new RevWalk(or);
           TreeWalk tw = new TreeWalk(or)) {
-        RevCommit c =
-            rw.parseCommit(ObjectId.fromString(resource.getPatchSet().getRevision().get()));
+        RevCommit c = rw.parseCommit(resource.getPatchSet().commitId());
 
         tw.addTree(c.getTree());
         tw.setRecursive(true);
@@ -244,11 +242,11 @@
       Account.Id userId = user.getAccountId();
       PatchSet patchSetId = resource.getPatchSet();
       Optional<PatchSetWithReviewedFiles> o;
-      o = accountPatchReviewStore.call(s -> s.findReviewed(patchSetId.getId(), userId));
+      o = accountPatchReviewStore.call(s -> s.findReviewed(patchSetId.id(), userId));
 
       if (o.isPresent()) {
         PatchSetWithReviewedFiles res = o.get();
-        if (res.patchSetId().equals(patchSetId.getId())) {
+        if (res.patchSetId().equals(patchSetId.id())) {
           return res.files();
         }
 
@@ -327,7 +325,7 @@
         }
 
         accountPatchReviewStore.run(
-            s -> s.markReviewed(resource.getPatchSet().getId(), userId, pathList));
+            s -> s.markReviewed(resource.getPatchSet().id(), userId, pathList));
         return pathList;
       }
     }
diff --git a/java/com/google/gerrit/server/restapi/change/Fixes.java b/java/com/google/gerrit/server/restapi/change/Fixes.java
index 855d1f4..38240e3 100644
--- a/java/com/google/gerrit/server/restapi/change/Fixes.java
+++ b/java/com/google/gerrit/server/restapi/change/Fixes.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.FixSuggestion;
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.registration.DynamicMap;
 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.reviewdb.client.FixSuggestion;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.FixResource;
 import com.google.gerrit.server.change.RevisionResource;
@@ -54,7 +54,7 @@
     ChangeNotes changeNotes = revisionResource.getNotes();
 
     List<RobotComment> robotComments =
-        commentsUtil.robotCommentsByPatchSet(changeNotes, revisionResource.getPatchSet().getId());
+        commentsUtil.robotCommentsByPatchSet(changeNotes, revisionResource.getPatchSet().id());
     for (RobotComment robotComment : robotComments) {
       for (FixSuggestion fixSuggestion : robotComment.fixSuggestions) {
         if (Objects.equals(fixId, fixSuggestion.fixId)) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetArchive.java b/java/com/google/gerrit/server/restapi/change/GetArchive.java
index 1bd1bce..4ebcbdd 100644
--- a/java/com/google/gerrit/server/restapi/change/GetArchive.java
+++ b/java/com/google/gerrit/server/restapi/change/GetArchive.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 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.server.change.ArchiveFormat;
 import com.google.gerrit.server.change.RevisionResource;
@@ -27,7 +30,6 @@
 import java.io.OutputStream;
 import org.eclipse.jgit.api.ArchiveCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -47,7 +49,7 @@
   }
 
   @Override
-  public BinaryResult apply(RevisionResource rsrc)
+  public Response<BinaryResult> apply(RevisionResource rsrc)
       throws BadRequestException, IOException, MethodNotAllowedException {
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
@@ -65,7 +67,7 @@
       final RevCommit commit;
       String name;
       try (RevWalk rw = new RevWalk(repo)) {
-        commit = rw.parseCommit(ObjectId.fromString(rsrc.getPatchSet().getRevision().get()));
+        commit = rw.parseCommit(rsrc.getPatchSet().commitId());
         name = name(f, rw, commit);
       }
 
@@ -93,7 +95,7 @@
       bin.disableGzip().setContentType(f.getMimeType()).setAttachmentName(name);
 
       close = false;
-      return bin;
+      return Response.ok(bin);
     } finally {
       if (close) {
         repo.close();
@@ -104,6 +106,6 @@
   private static String name(ArchiveFormat format, RevWalk rw, RevCommit commit)
       throws IOException {
     return String.format(
-        "%s%s", rw.getObjectReader().abbreviate(commit, 7).name(), format.getDefaultSuffix());
+        "%s%s", abbreviateName(commit, rw.getObjectReader()), format.getDefaultSuffix());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetAssignee.java b/java/com/google/gerrit/server/restapi/change/GetAssignee.java
index f89fe1b..a5820bf 100644
--- a/java/com/google/gerrit/server/restapi/change/GetAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/GetAssignee.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/restapi/change/GetBlame.java b/java/com/google/gerrit/server/restapi/change/GetBlame.java
index 98a3a8a..12b4d44 100644
--- a/java/com/google/gerrit/server/restapi/change/GetBlame.java
+++ b/java/com/google/gerrit/server/restapi/change/GetBlame.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.BlameInfo;
 import com.google.gerrit.extensions.common.RangeInfo;
 import com.google.gerrit.extensions.restapi.CacheControl;
@@ -23,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -92,7 +92,7 @@
       String refName =
           resource.getRevision().getEdit().isPresent()
               ? resource.getRevision().getEdit().get().getRefName()
-              : resource.getRevision().getPatchSet().getRefName();
+              : resource.getRevision().getPatchSet().refName();
 
       Ref ref = repository.findRef(refName);
       if (ref == null) {
@@ -102,7 +102,7 @@
       RevCommit revCommit = revWalk.parseCommit(objectId);
       RevCommit[] parents = revCommit.getParents();
 
-      String path = resource.getPatchKey().getFileName();
+      String path = resource.getPatchKey().fileName();
 
       List<BlameInfo> result;
       if (!base) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetChangeMessage.java b/java/com/google/gerrit/server/restapi/change/GetChangeMessage.java
index f55785d..9e0e0e3 100644
--- a/java/com/google/gerrit/server/restapi/change/GetChangeMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/GetChangeMessage.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.ChangeMessageResource;
 import com.google.inject.Singleton;
@@ -23,7 +24,7 @@
 @Singleton
 public class GetChangeMessage implements RestReadView<ChangeMessageResource> {
   @Override
-  public ChangeMessageInfo apply(ChangeMessageResource resource) {
-    return resource.getChangeMessage();
+  public Response<ChangeMessageInfo> apply(ChangeMessageResource resource) {
+    return Response.ok(resource.getChangeMessage());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetComment.java b/java/com/google/gerrit/server/restapi/change/GetComment.java
index 0109c95..5103325 100644
--- a/java/com/google/gerrit/server/restapi/change/GetComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetComment.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.CommentResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -33,7 +34,7 @@
   }
 
   @Override
-  public CommentInfo apply(CommentResource rsrc) throws PermissionBackendException {
-    return commentJson.get().newCommentFormatter().format(rsrc.getComment());
+  public Response<CommentInfo> apply(CommentResource rsrc) throws PermissionBackendException {
+    return Response.ok(commentJson.get().newCommentFormatter().format(rsrc.getComment()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetCommit.java b/java/com/google/gerrit/server/restapi/change/GetCommit.java
index 29286cb..21a08dc 100644
--- a/java/com/google/gerrit/server/restapi/change/GetCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/GetCommit.java
@@ -15,18 +15,17 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.RevisionJson;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -55,8 +54,7 @@
     Project.NameKey p = rsrc.getChange().getProject();
     try (Repository repo = repoManager.openRepository(p);
         RevWalk rw = new RevWalk(repo)) {
-      String rev = rsrc.getPatchSet().getRevision().get();
-      RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
+      RevCommit commit = rw.parseCommit(rsrc.getPatchSet().commitId());
       rw.parseBody(commit);
       CommitInfo info =
           json.create(ImmutableSet.of())
diff --git a/java/com/google/gerrit/server/restapi/change/GetContent.java b/java/com/google/gerrit/server/restapi/change/GetContent.java
index 1d35ab5..bf7c51f 100644
--- a/java/com/google/gerrit/server/restapi/change/GetContent.java
+++ b/java/com/google/gerrit/server/restapi/change/GetContent.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.FileContentUtil;
 import com.google.gerrit.server.change.FileResource;
@@ -33,7 +34,6 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -61,25 +61,28 @@
   }
 
   @Override
-  public BinaryResult apply(FileResource rsrc)
+  public Response<BinaryResult> apply(FileResource rsrc)
       throws ResourceNotFoundException, IOException, BadRequestException {
-    String path = rsrc.getPatchKey().get();
+    String path = rsrc.getPatchKey().fileName();
     if (Patch.COMMIT_MSG.equals(path)) {
       String msg = getMessage(rsrc.getRevision().getChangeResource().getNotes());
-      return BinaryResult.create(msg)
-          .setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
-          .base64();
+      return Response.ok(
+          BinaryResult.create(msg)
+              .setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
+              .base64());
     } else if (Patch.MERGE_LIST.equals(path)) {
       byte[] mergeList = getMergeList(rsrc.getRevision().getChangeResource().getNotes());
-      return BinaryResult.create(mergeList)
-          .setContentType(FileContentUtil.TEXT_X_GERRIT_MERGE_LIST)
-          .base64();
+      return Response.ok(
+          BinaryResult.create(mergeList)
+              .setContentType(FileContentUtil.TEXT_X_GERRIT_MERGE_LIST)
+              .base64());
     }
-    return fileContentUtil.getContent(
-        projectCache.checkedGet(rsrc.getRevision().getProject()),
-        ObjectId.fromString(rsrc.getRevision().getPatchSet().getRevision().get()),
-        path,
-        parent);
+    return Response.ok(
+        fileContentUtil.getContent(
+            projectCache.checkedGet(rsrc.getRevision().getProject()),
+            rsrc.getRevision().getPatchSet().commitId(),
+            path,
+            parent));
   }
 
   private String getMessage(ChangeNotes notes) throws IOException {
@@ -91,7 +94,7 @@
 
     try (Repository git = gitManager.openRepository(notes.getProjectName());
         RevWalk revWalk = new RevWalk(git)) {
-      RevCommit commit = revWalk.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+      RevCommit commit = revWalk.parseCommit(ps.commitId());
       return commit.getFullMessage();
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(changeId, e);
@@ -108,9 +111,7 @@
     try (Repository git = gitManager.openRepository(notes.getProjectName());
         RevWalk revWalk = new RevWalk(git)) {
       return Text.forMergeList(
-              ComparisonType.againstAutoMerge(),
-              revWalk.getObjectReader(),
-              ObjectId.fromString(ps.getRevision().get()))
+              ComparisonType.againstAutoMerge(), revWalk.getObjectReader(), ps.commitId())
           .getContent();
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(changeId, e);
diff --git a/java/com/google/gerrit/server/restapi/change/GetDescription.java b/java/com/google/gerrit/server/restapi/change/GetDescription.java
index 1a7ec63..6794d81 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDescription.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDescription.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.inject.Singleton;
@@ -22,7 +22,7 @@
 @Singleton
 public class GetDescription implements RestReadView<RevisionResource> {
   @Override
-  public String apply(RevisionResource rsrc) {
-    return Strings.nullToEmpty(rsrc.getPatchSet().getDescription());
+  public Response<String> apply(RevisionResource rsrc) {
+    return Response.ok(rsrc.getPatchSet().description().orElse(""));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetDiff.java b/java/com/google/gerrit/server/restapi/change/GetDiff.java
index 6a7f1fa..39c5a3b 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDiff.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDiff.java
@@ -25,6 +25,9 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.common.ChangeType;
@@ -43,9 +46,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.jgit.diff.ReplaceEdit;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.change.FileContentUtil;
 import com.google.gerrit.server.change.FileResource;
@@ -140,14 +140,14 @@
 
     PatchScriptFactory psf;
     PatchSet basePatchSet = null;
-    PatchSet.Id pId = resource.getPatchKey().getParentKey();
-    String fileName = resource.getPatchKey().getFileName();
+    PatchSet.Id pId = resource.getPatchKey().patchSetId();
+    String fileName = resource.getPatchKey().fileName();
     ChangeNotes notes = resource.getRevision().getNotes();
     if (base != null) {
       RevisionResource baseResource =
           revisions.parse(resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
       basePatchSet = baseResource.getPatchSet();
-      psf = patchScriptFactoryFactory.create(notes, fileName, basePatchSet.getId(), pId, prefs);
+      psf = patchScriptFactoryFactory.create(notes, fileName, basePatchSet.id(), pId, prefs);
     } else if (parentNum > 0) {
       psf = patchScriptFactoryFactory.create(notes, fileName, parentNum - 1, pId, prefs);
     } else {
@@ -195,20 +195,20 @@
       ProjectState state = projectCache.get(resource.getRevision().getChange().getProject());
 
       DiffInfo result = new DiffInfo();
-      String revA = basePatchSet != null ? basePatchSet.getRefName() : content.commitIdA;
+      String revA = basePatchSet != null ? basePatchSet.refName() : content.commitIdA;
       String revB =
           resource.getRevision().getEdit().isPresent()
               ? resource.getRevision().getEdit().get().getRefName()
-              : resource.getRevision().getPatchSet().getRefName();
+              : resource.getRevision().getPatchSet().refName();
 
-      List<DiffWebLinkInfo> links =
+      ImmutableList<DiffWebLinkInfo> links =
           webLinks.getDiffLinks(
               state.getName(),
-              resource.getPatchKey().getParentKey().getParentKey().get(),
-              basePatchSet != null ? basePatchSet.getId().get() : null,
+              resource.getPatchKey().patchSetId().changeId().get(),
+              basePatchSet != null ? basePatchSet.id().get() : null,
               revA,
               MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName()),
-              resource.getPatchKey().getParentKey().get(),
+              resource.getPatchKey().patchSetId().get(),
               revB,
               ps.getNewName());
       result.webLinks = links.isEmpty() ? null : links;
@@ -271,7 +271,7 @@
   }
 
   private List<WebLinkInfo> getFileWebLinks(Project project, String rev, String file) {
-    List<WebLinkInfo> links = webLinks.getFileLinks(project.getName(), rev, file);
+    ImmutableList<WebLinkInfo> links = webLinks.getFileLinks(project.getName(), rev, file);
     return links.isEmpty() ? null : links;
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/GetDraftComment.java b/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
index ca5b56f..797dc9e 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.DraftCommentResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -33,7 +34,7 @@
   }
 
   @Override
-  public CommentInfo apply(DraftCommentResource rsrc) throws PermissionBackendException {
-    return commentJson.get().newCommentFormatter().format(rsrc.getComment());
+  public Response<CommentInfo> apply(DraftCommentResource rsrc) throws PermissionBackendException {
+    return Response.ok(commentJson.get().newCommentFormatter().format(rsrc.getComment()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetMergeList.java b/java/com/google/gerrit/server/restapi/change/GetMergeList.java
index 0c18a8f..0c67fd6 100644
--- a/java/com/google/gerrit/server/restapi/change/GetMergeList.java
+++ b/java/com/google/gerrit/server/restapi/change/GetMergeList.java
@@ -16,12 +16,12 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.RevisionJson;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -31,7 +31,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -67,8 +66,7 @@
     Project.NameKey p = rsrc.getChange().getProject();
     try (Repository repo = repoManager.openRepository(p);
         RevWalk rw = new RevWalk(repo)) {
-      String rev = rsrc.getPatchSet().getRevision().get();
-      RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
+      RevCommit commit = rw.parseCommit(rsrc.getPatchSet().commitId());
       rw.parseBody(commit);
 
       if (uninterestingParent < 1 || uninterestingParent > commit.getParentCount()) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java b/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
index 1d56669..c1c9a34 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
@@ -16,10 +16,10 @@
 
 import static java.util.stream.Collectors.toList;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/restapi/change/GetPatch.java b/java/com/google/gerrit/server/restapi/change/GetPatch.java
index b205ece..187ebce 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPatch.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -31,8 +33,6 @@
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 import org.eclipse.jgit.diff.DiffFormatter;
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -60,15 +60,15 @@
   }
 
   @Override
-  public BinaryResult apply(RevisionResource rsrc)
+  public Response<BinaryResult> apply(RevisionResource rsrc)
       throws ResourceConflictException, IOException, ResourceNotFoundException {
     final Repository repo = repoManager.openRepository(rsrc.getProject());
     boolean close = true;
     try {
       final RevWalk rw = new RevWalk(repo);
+      BinaryResult bin = null;
       try {
-        final RevCommit commit =
-            rw.parseCommit(ObjectId.fromString(rsrc.getPatchSet().getRevision().get()));
+        final RevCommit commit = rw.parseCommit(rsrc.getPatchSet().commitId());
         RevCommit[] parents = commit.getParents();
         if (parents.length > 1) {
           throw new ResourceConflictException("Revision has more than 1 parent.");
@@ -78,7 +78,7 @@
         final RevCommit base = parents[0];
         rw.parseBody(base);
 
-        BinaryResult bin =
+        bin =
             new BinaryResult() {
               @Override
               public void writeTo(OutputStream out) throws IOException {
@@ -132,10 +132,13 @@
         }
 
         close = false;
-        return bin;
+        return Response.ok(bin);
       } finally {
         if (close) {
           rw.close();
+          if (bin != null) {
+            bin.close();
+          }
         }
       }
     } finally {
@@ -189,7 +192,6 @@
   }
 
   private static String fileName(RevWalk rw, RevCommit commit) throws IOException {
-    AbbreviatedObjectId id = rw.getObjectReader().abbreviate(commit, 7);
-    return id.name() + ".diff";
+    return abbreviateName(commit, rw.getObjectReader()) + ".diff";
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetPureRevert.java b/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
index fa5cc36..765be5f 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.PureRevert;
@@ -46,9 +47,9 @@
   }
 
   @Override
-  public PureRevertInfo apply(ChangeResource rsrc)
+  public Response<PureRevertInfo> apply(ChangeResource rsrc)
       throws ResourceConflictException, IOException, BadRequestException, AuthException {
     boolean isPureRevert = pureRevert.get(rsrc.getNotes(), Optional.ofNullable(claimedOriginal));
-    return new PureRevertInfo(isPureRevert);
+    return Response.ok(new PureRevertInfo(isPureRevert));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetRelated.java b/java/com/google/gerrit/server/restapi/change/GetRelated.java
index 332cc4d..a846d50 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRelated.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRelated.java
@@ -19,14 +19,15 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
 import com.google.gerrit.extensions.api.changes.RelatedChangesInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CommonConverters;
 import com.google.gerrit.server.PatchSetUtil;
@@ -68,12 +69,12 @@
   }
 
   @Override
-  public RelatedChangesInfo apply(RevisionResource rsrc)
+  public Response<RelatedChangesInfo> apply(RevisionResource rsrc)
       throws RepositoryNotFoundException, IOException, NoSuchProjectException,
           PermissionBackendException {
     RelatedChangesInfo relatedChangesInfo = new RelatedChangesInfo();
     relatedChangesInfo.changes = getRelated(rsrc);
-    return relatedChangesInfo;
+    return Response.ok(relatedChangesInfo);
   }
 
   private List<RelatedChangeAndCommitInfo> getRelated(RevisionResource rsrc)
@@ -102,7 +103,7 @@
     for (RelatedChangesSorter.PatchSetData d : sorter.sort(cds, basePs)) {
       PatchSet ps = d.patchSet();
       RevCommit commit;
-      if (isEdit && ps.getId().equals(basePs.getId())) {
+      if (isEdit && ps.id().equals(basePs.id())) {
         // Replace base of an edit with the edit itself.
         ps = rsrc.getPatchSet();
         commit = rsrc.getEdit().get().getEditCommit();
@@ -114,7 +115,7 @@
 
     if (result.size() == 1) {
       RelatedChangeAndCommitInfo r = result.get(0);
-      if (r.commit != null && r.commit.commit.equals(rsrc.getPatchSet().getRevision().get())) {
+      if (r.commit != null && r.commit.commit.equals(rsrc.getPatchSet().commitId().name())) {
         return Collections.emptyList();
       }
     }
@@ -123,13 +124,13 @@
 
   @VisibleForTesting
   public static Set<String> getAllGroups(ChangeNotes notes, PatchSetUtil psUtil) {
-    return psUtil.byChange(notes).stream().flatMap(ps -> ps.getGroups().stream()).collect(toSet());
+    return psUtil.byChange(notes).stream().flatMap(ps -> ps.groups().stream()).collect(toSet());
   }
 
   private void reloadChangeIfStale(List<ChangeData> cds, PatchSet wantedPs) {
     for (ChangeData cd : cds) {
-      if (cd.getId().equals(wantedPs.getId().getParentKey())) {
-        if (cd.patchSet(wantedPs.getId()) == null) {
+      if (cd.getId().equals(wantedPs.id().changeId())) {
+        if (cd.patchSet(wantedPs.id()) == null) {
           cd.reloadChange();
         }
       }
@@ -144,7 +145,7 @@
     if (change != null) {
       info.changeId = change.getKey().get();
       info._changeNumber = change.getChangeId();
-      info._revisionNumber = ps != null ? ps.getPatchSetId() : null;
+      info._revisionNumber = ps != null ? ps.number() : null;
       PatchSet.Id curr = change.currentPatchSetId();
       info._currentRevisionNumber = curr != null ? curr.get() : null;
       info.status = ChangeUtil.status(change).toUpperCase(Locale.US);
diff --git a/java/com/google/gerrit/server/restapi/change/GetReviewer.java b/java/com/google/gerrit/server/restapi/change/GetReviewer.java
index 73760da..a672b176 100644
--- a/java/com/google/gerrit/server/restapi/change/GetReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/GetReviewer.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 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.server.change.ReviewerJson;
 import com.google.gerrit.server.change.ReviewerResource;
@@ -33,7 +34,8 @@
   }
 
   @Override
-  public List<ReviewerInfo> apply(ReviewerResource rsrc) throws PermissionBackendException {
-    return json.format(rsrc);
+  public Response<List<ReviewerInfo>> apply(ReviewerResource rsrc)
+      throws PermissionBackendException {
+    return Response.ok(json.format(rsrc));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetRobotComment.java b/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
index 75d994d..4ff9942 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.RobotCommentResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -33,7 +34,8 @@
   }
 
   @Override
-  public RobotCommentInfo apply(RobotCommentResource rsrc) throws PermissionBackendException {
-    return commentJson.get().newRobotCommentFormatter().format(rsrc.getComment());
+  public Response<RobotCommentInfo> apply(RobotCommentResource rsrc)
+      throws PermissionBackendException {
+    return Response.ok(commentJson.get().newRobotCommentFormatter().format(rsrc.getComment()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetTopic.java b/java/com/google/gerrit/server/restapi/change/GetTopic.java
index 7ab1cb1..6951fa5 100644
--- a/java/com/google/gerrit/server/restapi/change/GetTopic.java
+++ b/java/com/google/gerrit/server/restapi/change/GetTopic.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.inject.Singleton;
@@ -22,7 +23,7 @@
 @Singleton
 public class GetTopic implements RestReadView<ChangeResource> {
   @Override
-  public String apply(ChangeResource rsrc) {
-    return Strings.nullToEmpty(rsrc.getChange().getTopic());
+  public Response<String> apply(ChangeResource rsrc) {
+    return Response.ok(Strings.nullToEmpty(rsrc.getChange().getTopic()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Index.java b/java/com/google/gerrit/server/restapi/change/Index.java
index 90dad98..5a17c07 100644
--- a/java/com/google/gerrit/server/restapi/change/Index.java
+++ b/java/com/google/gerrit/server/restapi/change/Index.java
@@ -30,7 +30,7 @@
 import java.io.IOException;
 
 @Singleton
-public class Index extends RetryingRestModifyView<ChangeResource, Input, Response<?>> {
+public class Index extends RetryingRestModifyView<ChangeResource, Input, Object> {
   private final PermissionBackend permissionBackend;
   private final ChangeIndexer indexer;
 
@@ -42,7 +42,7 @@
   }
 
   @Override
-  protected Response<?> applyImpl(
+  protected Response<Object> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws IOException, AuthException, PermissionBackendException {
     permissionBackend.currentUser().check(GlobalPermission.MAINTAIN_SERVER);
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
index 403922d..edd6201 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.entities.Comment;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.query.change.ChangeData;
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
index f8fda73..cc35a5e 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Comment;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -59,12 +60,12 @@
   }
 
   @Override
-  public Map<String, List<CommentInfo>> apply(ChangeResource rsrc)
+  public Response<Map<String, List<CommentInfo>>> apply(ChangeResource rsrc)
       throws AuthException, PermissionBackendException {
     if (requireAuthentication() && !rsrc.getUser().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    return getCommentFormatter().format(listComments(rsrc));
+    return Response.ok(getCommentFormatter().format(listComments(rsrc)));
   }
 
   public List<CommentInfo> getComments(ChangeResource rsrc)
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
index a2e3d4b..bfc9f12 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
@@ -16,9 +16,10 @@
 
 import static com.google.gerrit.server.ChangeMessagesUtil.createChangeMessageInfo;
 
+import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
@@ -41,13 +42,14 @@
   }
 
   @Override
-  public List<ChangeMessageInfo> apply(ChangeResource resource) throws PermissionBackendException {
+  public Response<List<ChangeMessageInfo>> apply(ChangeResource resource)
+      throws PermissionBackendException {
     List<ChangeMessage> messages = changeMessagesUtil.byChange(resource.getNotes());
     List<ChangeMessageInfo> messageInfos =
         messages.stream()
             .map(m -> createChangeMessageInfo(m, accountLoader))
             .collect(Collectors.toList());
     accountLoader.fill();
-    return messageInfos;
+    return Response.ok(messageInfos);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
index e5840fd..719a477 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
@@ -42,14 +43,15 @@
   }
 
   @Override
-  public Map<String, List<RobotCommentInfo>> apply(ChangeResource rsrc)
+  public Response<Map<String, List<RobotCommentInfo>>> apply(ChangeResource rsrc)
       throws AuthException, PermissionBackendException {
     ChangeData cd = changeDataFactory.create(rsrc.getNotes());
-    return commentJson
-        .get()
-        .setFillAccounts(true)
-        .setFillPatchSet(true)
-        .newRobotCommentFormatter()
-        .format(commentsUtil.robotCommentsByChange(cd.notes()));
+    return Response.ok(
+        commentJson
+            .get()
+            .setFillAccounts(true)
+            .setFillPatchSet(true)
+            .newRobotCommentFormatter()
+            .format(commentsUtil.robotCommentsByChange(cd.notes())));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListReviewers.java b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
index 12732ff..25ef480 100644
--- a/java/com/google/gerrit/server/restapi/change/ListReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Account;
 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.reviewdb.client.Account;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ReviewerJson;
@@ -44,7 +45,7 @@
   }
 
   @Override
-  public List<ReviewerInfo> apply(ChangeResource rsrc) throws PermissionBackendException {
+  public Response<List<ReviewerInfo>> apply(ChangeResource rsrc) throws PermissionBackendException {
     Map<String, ReviewerResource> reviewers = new LinkedHashMap<>();
     for (Account.Id accountId : approvalsUtil.getReviewers(rsrc.getNotes()).all()) {
       if (!reviewers.containsKey(accountId.toString())) {
@@ -56,6 +57,6 @@
         reviewers.put(adr.toString(), new ReviewerResource(rsrc, adr));
       }
     }
-    return json.format(reviewers.values());
+    return Response.ok(json.format(reviewers.values()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java b/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
index b39ba63..de05d2a 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.entities.Comment;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -37,6 +37,6 @@
   @Override
   protected Iterable<Comment> listComments(RevisionResource rsrc) {
     ChangeNotes notes = rsrc.getNotes();
-    return commentsUtil.publishedByPatchSet(notes, rsrc.getPatchSet().getId());
+    return commentsUtil.publishedByPatchSet(notes, rsrc.getPatchSet().id());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
index a46bd6c..199a752 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Comment;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -40,7 +41,7 @@
 
   protected Iterable<Comment> listComments(RevisionResource rsrc) {
     return commentsUtil.draftByPatchSetAuthor(
-        rsrc.getPatchSet().getId(), rsrc.getAccountId(), rsrc.getNotes());
+        rsrc.getPatchSet().id(), rsrc.getAccountId(), rsrc.getNotes());
   }
 
   protected boolean includeAuthorInfo() {
@@ -48,13 +49,14 @@
   }
 
   @Override
-  public Map<String, List<CommentInfo>> apply(RevisionResource rsrc)
+  public Response<Map<String, List<CommentInfo>>> apply(RevisionResource rsrc)
       throws PermissionBackendException {
-    return commentJson
-        .get()
-        .setFillAccounts(includeAuthorInfo())
-        .newCommentFormatter()
-        .format(listComments(rsrc));
+    return Response.ok(
+        commentJson
+            .get()
+            .setFillAccounts(includeAuthorInfo())
+            .newCommentFormatter()
+            .format(listComments(rsrc)));
   }
 
   public ImmutableList<CommentInfo> getComments(RevisionResource rsrc)
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
index 920cde9..73b1f59 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Account;
 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.reviewdb.client.Account;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ReviewerJson;
 import com.google.gerrit.server.change.ReviewerResource;
@@ -45,7 +46,7 @@
   }
 
   @Override
-  public List<ReviewerInfo> apply(RevisionResource rsrc)
+  public Response<List<ReviewerInfo>> apply(RevisionResource rsrc)
       throws MethodNotAllowedException, PermissionBackendException {
     if (!rsrc.isCurrent()) {
       throw new MethodNotAllowedException("Cannot list reviewers on non-current patch set");
@@ -62,6 +63,6 @@
         reviewers.put(address.toString(), new ReviewerResource(rsrc, address));
       }
     }
-    return json.format(reviewers.values());
+    return Response.ok(json.format(reviewers.values()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
index bbf46a3..bbbe12d 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -39,13 +40,14 @@
   }
 
   @Override
-  public Map<String, List<RobotCommentInfo>> apply(RevisionResource rsrc)
+  public Response<Map<String, List<RobotCommentInfo>>> apply(RevisionResource rsrc)
       throws PermissionBackendException {
-    return commentJson
-        .get()
-        .setFillAccounts(true)
-        .newRobotCommentFormatter()
-        .format(listComments(rsrc));
+    return Response.ok(
+        commentJson
+            .get()
+            .setFillAccounts(true)
+            .newRobotCommentFormatter()
+            .format(listComments(rsrc)));
   }
 
   public ImmutableList<RobotCommentInfo> getComments(RevisionResource rsrc)
@@ -58,6 +60,6 @@
   }
 
   private Iterable<RobotComment> listComments(RevisionResource rsrc) {
-    return commentsUtil.robotCommentsByPatchSet(rsrc.getNotes(), rsrc.getPatchSet().getId());
+    return commentsUtil.robotCommentsByPatchSet(rsrc.getNotes(), rsrc.getPatchSet().id());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Mergeable.java b/java/com/google/gerrit/server/restapi/change/Mergeable.java
index ece8938..9b17ed6 100644
--- a/java/com/google/gerrit/server/restapi/change/Mergeable.java
+++ b/java/com/google/gerrit/server/restapi/change/Mergeable.java
@@ -14,17 +14,17 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.change.RevisionResource;
@@ -50,8 +50,6 @@
 import org.kohsuke.args4j.Option;
 
 public class Mergeable implements RestReadView<RevisionResource> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   @Option(
       name = "--other-branches",
       aliases = {"-o"},
@@ -89,7 +87,7 @@
   }
 
   @Override
-  public MergeableInfo apply(RevisionResource resource)
+  public Response<MergeableInfo> apply(RevisionResource resource)
       throws AuthException, ResourceConflictException, BadRequestException, IOException {
     Change change = resource.getChange();
     PatchSet ps = resource.getPatchSet();
@@ -97,17 +95,17 @@
 
     if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
-    } else if (!ps.getId().equals(change.currentPatchSetId())) {
+    } else if (!ps.id().equals(change.currentPatchSetId())) {
       // Only the current revision is mergeable. Others always fail.
-      return result;
+      return Response.ok(result);
     }
 
     ChangeData cd = changeDataFactory.create(resource.getNotes());
     result.submitType = getSubmitType(cd);
 
     try (Repository git = gitManager.openRepository(change.getProject())) {
-      ObjectId commit = toId(ps);
-      Ref ref = git.getRefDatabase().exactRef(change.getDest().get());
+      ObjectId commit = ps.commitId();
+      Ref ref = git.getRefDatabase().exactRef(change.getDest().branch());
       ProjectState projectState = projectCache.get(change.getProject());
       String strategy = mergeUtilFactory.create(projectState).mergeStrategyName();
       result.strategy = strategy;
@@ -132,7 +130,7 @@
         }
       }
     }
-    return result;
+    return Response.ok(result);
   }
 
   private SubmitType getSubmitType(ChangeData cd) {
@@ -161,15 +159,6 @@
     return refresh(change, commit, ref, submitType, strategy, git, old);
   }
 
-  private static ObjectId toId(PatchSet ps) {
-    try {
-      return ObjectId.fromString(ps.getRevision().get());
-    } catch (IllegalArgumentException e) {
-      logger.atSevere().log("Invalid revision on patch set %s", ps);
-      return null;
-    }
-  }
-
   private boolean refresh(
       final Change change,
       ObjectId commit,
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index f2335b1..7d4c4d1 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -23,6 +23,14 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.LabelId;
+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.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.MoveInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -30,16 +38,9 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -114,7 +115,7 @@
   }
 
   @Override
-  protected ChangeInfo applyImpl(
+  protected Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, MoveInput input)
       throws RestApiException, UpdateException, PermissionBackendException, IOException {
     if (!moveEnabled) {
@@ -135,7 +136,7 @@
       throw new ResourceConflictException("Change is " + ChangeUtil.status(change));
     }
 
-    Branch.NameKey newDest = new Branch.NameKey(project, input.destinationBranch);
+    BranchNameKey newDest = BranchNameKey.create(project, input.destinationBranch);
     if (change.getDest().equals(newDest)) {
       throw new ResourceConflictException("Change is already destined for the specified branch");
     }
@@ -157,14 +158,14 @@
       u.addOp(change.getId(), op);
       u.execute();
     }
-    return json.noOptions().format(op.getChange());
+    return Response.ok(json.noOptions().format(op.getChange()));
   }
 
   private class Op implements BatchUpdateOp {
     private final MoveInput input;
 
     private Change change;
-    private Branch.NameKey newDestKey;
+    private BranchNameKey newDestKey;
 
     Op(MoveInput input) {
       this.input = input;
@@ -183,8 +184,8 @@
       }
 
       Project.NameKey projectKey = change.getProject();
-      newDestKey = new Branch.NameKey(projectKey, input.destinationBranch);
-      Branch.NameKey changePrevDest = change.getDest();
+      newDestKey = BranchNameKey.create(projectKey, input.destinationBranch);
+      BranchNameKey changePrevDest = change.getDest();
       if (changePrevDest.equals(newDestKey)) {
         throw new ResourceConflictException("Change is already destined for the specified branch");
       }
@@ -193,8 +194,7 @@
       try (Repository repo = repoManager.openRepository(projectKey);
           RevWalk revWalk = new RevWalk(repo)) {
         RevCommit currPatchsetRevCommit =
-            revWalk.parseCommit(
-                ObjectId.fromString(psUtil.current(ctx.getNotes()).getRevision().get()));
+            revWalk.parseCommit(psUtil.current(ctx.getNotes()).commitId());
         if (currPatchsetRevCommit.getParentCount() > 1) {
           throw new ResourceConflictException("Merge commit cannot be moved");
         }
@@ -216,7 +216,7 @@
       if (!asChanges(queryProvider.get().byBranchKey(newDestKey, changeKey)).isEmpty()) {
         throw new ResourceConflictException(
             "Destination "
-                + newDestKey.getShortName()
+                + newDestKey.shortName()
                 + " has a different change with same change key "
                 + changeKey);
       }
@@ -227,16 +227,16 @@
 
       PatchSet.Id psId = change.currentPatchSetId();
       ChangeUpdate update = ctx.getUpdate(psId);
-      update.setBranch(newDestKey.get());
+      update.setBranch(newDestKey.branch());
       change.setDest(newDestKey);
 
       updateApprovals(ctx, update, psId, projectKey);
 
       StringBuilder msgBuf = new StringBuilder();
       msgBuf.append("Change destination moved from ");
-      msgBuf.append(changePrevDest.getShortName());
+      msgBuf.append(changePrevDest.shortName());
       msgBuf.append(" to ");
-      msgBuf.append(newDestKey.getShortName());
+      msgBuf.append(newDestKey.shortName());
       if (!Strings.isNullOrEmpty(input.message)) {
         msgBuf.append("\n\n");
         msgBuf.append(input.message);
@@ -262,7 +262,7 @@
           approvalsUtil.byPatchSet(
               ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
         ProjectState projectState = projectCache.checkedGet(project);
-        LabelType type = projectState.getLabelTypes(ctx.getNotes()).byLabel(psa.getLabelId());
+        LabelType type = projectState.getLabelTypes(ctx.getNotes()).byLabel(psa.labelId());
         // Only keep veto votes, defined as votes where:
         // 1- the label function allows minimum values to block submission.
         // 2- the vote holds the minimum value.
@@ -271,12 +271,13 @@
         }
 
         // Remove votes from NoteDb.
-        update.removeApprovalFor(psa.getAccountId(), psa.getLabel());
+        update.removeApprovalFor(psa.accountId(), psa.label());
         approvals.add(
-            new PatchSetApproval(
-                new PatchSetApproval.Key(psId, psa.getAccountId(), new LabelId(psa.getLabel())),
-                (short) 0,
-                ctx.getWhen()));
+            PatchSetApproval.builder()
+                .key(PatchSetApproval.key(psId, psa.accountId(), LabelId.create(psa.label())))
+                .value(0)
+                .granted(ctx.getWhen())
+                .build());
       }
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/PostHashtags.java b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
index da499a9..516dead 100644
--- a/java/com/google/gerrit/server/restapi/change/PostHashtags.java
+++ b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
@@ -33,8 +33,7 @@
 
 @Singleton
 public class PostHashtags
-    extends RetryingRestModifyView<
-        ChangeResource, HashtagsInput, Response<ImmutableSortedSet<String>>>
+    extends RetryingRestModifyView<ChangeResource, HashtagsInput, ImmutableSortedSet<String>>
     implements UiAction<ChangeResource> {
   private final SetHashtagsOp.Factory hashtagsFactory;
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostPrivate.java b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
index 5aa2ecc..f008df3 100644
--- a/java/com/google/gerrit/server/restapi/change/PostPrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
@@ -17,13 +17,13 @@
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -39,8 +39,7 @@
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
-public class PostPrivate
-    extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>>
+public class PostPrivate extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, String>
     implements UiAction<ChangeResource> {
   private final PermissionBackend permissionBackend;
   private final SetPrivateOp.Factory setPrivateOpFactory;
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 8c94ba3..e0d0a04 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -16,7 +16,8 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -29,15 +30,28 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
+import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.HashCode;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 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.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.FixReplacement;
+import com.google.gerrit.entities.FixSuggestion;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -61,20 +75,11 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentValidationFailure;
+import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.FixReplacement;
-import com.google.gerrit.reviewdb.client.FixSuggestion;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -107,12 +112,14 @@
 import com.google.gerrit.server.permissions.LabelPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.CommentsRejectedException;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -134,18 +141,21 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.OptionalInt;
 import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 
 @Singleton
 public class PostReview
-    extends RetryingRestModifyView<RevisionResource, ReviewInput, Response<ReviewResult>> {
+    extends RetryingRestModifyView<RevisionResource, ReviewInput, ReviewResult> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  public static final String ERROR_ADDING_REVIEWER = "error adding reviewer";
+  private static final String ERROR_ADDING_REVIEWER = "error adding reviewer";
   public static final String ERROR_WIP_READY_MUTUALLY_EXCLUSIVE =
       "work_in_progress and ready are mutually exclusive";
 
@@ -172,6 +182,7 @@
   private final WorkInProgressOp.Factory workInProgressOpFactory;
   private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
+  private final PluginSetContext<CommentValidator> commentValidators;
   private final boolean strictLabels;
 
   @Inject
@@ -194,7 +205,8 @@
       @GerritServerConfig Config gerritConfig,
       WorkInProgressOp.Factory workInProgressOpFactory,
       ProjectCache projectCache,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      PluginSetContext<CommentValidator> commentValidators) {
     super(retryHelper);
     this.changeResourceFactory = changeResourceFactory;
     this.changeDataFactory = changeDataFactory;
@@ -214,6 +226,7 @@
     this.workInProgressOpFactory = workInProgressOpFactory;
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
+    this.commentValidators = commentValidators;
     this.strictLabels = gerritConfig.getBoolean("change", "strictLabels", false);
   }
 
@@ -236,7 +249,10 @@
     }
     ProjectState projectState = projectCache.checkedGet(revision.getProject());
     LabelTypes labelTypes = projectState.getLabelTypes(revision.getNotes());
+
     input.drafts = firstNonNull(input.drafts, DraftHandling.KEEP);
+    logger.atFine().log("draft handling = %s", input.drafts);
+
     if (input.onBehalfOf != null) {
       revision = onBehalfOf(revision, labelTypes, input);
     }
@@ -244,10 +260,11 @@
       checkLabels(revision, labelTypes, input.labels);
     }
     if (input.comments != null) {
-      cleanUpComments(input.comments);
+      input.comments = cleanUpComments(input.comments);
       checkComments(revision, input.comments);
     }
     if (input.robotComments != null) {
+      input.robotComments = cleanUpComments(input.robotComments);
       checkRobotComments(revision, input.robotComments);
     }
 
@@ -357,8 +374,7 @@
 
       // Add the review op.
       bu.addOp(
-          revision.getChange().getId(),
-          new Op(projectState, revision.getPatchSet().getId(), input));
+          revision.getChange().getId(), new Op(projectState, revision.getPatchSet().id(), input));
 
       // Notify based on ReviewInput, ignoring the notify settings from any AddReviewerInputs.
       NotifyResolver.Result notify =
@@ -538,37 +554,30 @@
     }
   }
 
-  private static <T extends CommentInput> void cleanUpComments(
+  private static <T extends CommentInput> Map<String, List<T>> cleanUpComments(
       Map<String, List<T>> commentsPerPath) {
-    Iterator<List<T>> mapValueIterator = commentsPerPath.values().iterator();
-    while (mapValueIterator.hasNext()) {
-      List<T> comments = mapValueIterator.next();
+    Map<String, List<T>> cleanedUpCommentMap = new HashMap<>();
+    for (Map.Entry<String, List<T>> e : commentsPerPath.entrySet()) {
+      String path = e.getKey();
+      List<T> comments = e.getValue();
+
       if (comments == null) {
-        mapValueIterator.remove();
         continue;
       }
 
-      cleanUpComments(comments);
-      if (comments.isEmpty()) {
-        mapValueIterator.remove();
+      List<T> cleanedUpComments = cleanUpComments(comments);
+      if (!cleanedUpComments.isEmpty()) {
+        cleanedUpCommentMap.put(path, cleanedUpComments);
       }
     }
+    return cleanedUpCommentMap;
   }
 
-  private static <T extends CommentInput> void cleanUpComments(List<T> comments) {
-    Iterator<T> commentsIterator = comments.iterator();
-    while (commentsIterator.hasNext()) {
-      T comment = commentsIterator.next();
-      if (comment == null) {
-        commentsIterator.remove();
-        continue;
-      }
-
-      comment.message = Strings.nullToEmpty(comment.message).trim();
-      if (comment.message.isEmpty()) {
-        commentsIterator.remove();
-      }
-    }
+  private static <T extends CommentInput> List<T> cleanUpComments(List<T> comments) {
+    return comments.stream()
+        .filter(Objects::nonNull)
+        .filter(comment -> !Strings.nullToEmpty(comment.message).trim().isEmpty())
+        .collect(toList());
   }
 
   private <T extends CommentInput> void checkComments(
@@ -577,7 +586,7 @@
     Set<String> revisionFilePaths = getAffectedFilePaths(revision);
     for (Map.Entry<String, List<T>> entry : commentsPerPath.entrySet()) {
       String path = entry.getKey();
-      PatchSet.Id patchSetId = revision.getPatchSet().getId();
+      PatchSet.Id patchSetId = revision.getPatchSet().id();
       ensurePathRefersToAvailableOrMagicFile(path, revisionFilePaths, patchSetId);
 
       List<T> comments = entry.getValue();
@@ -591,7 +600,7 @@
 
   private Set<String> getAffectedFilePaths(RevisionResource revision)
       throws PatchListNotAvailableException {
-    ObjectId newId = ObjectId.fromString(revision.getPatchSet().getRevision().get());
+    ObjectId newId = revision.getPatchSet().commitId();
     DiffSummaryKey key =
         DiffSummaryKey.fromPatchListKey(
             PatchListKey.againstDefaultBase(newId, Whitespace.IGNORE_NONE));
@@ -626,7 +635,6 @@
   private void checkRobotComments(
       RevisionResource revision, Map<String, List<RobotCommentInput>> in)
       throws BadRequestException, PatchListNotAvailableException {
-    cleanUpComments(in);
     for (Map.Entry<String, List<RobotCommentInput>> e : in.entrySet()) {
       String commentPath = e.getKey();
       for (RobotCommentInput c : e.getValue()) {
@@ -799,7 +807,10 @@
     }
   }
 
-  /** Used to compare Comments with CommentInput comments. */
+  /**
+   * Used to compare existing {@link Comment}-s with {@link CommentInput} comments by copying only
+   * the fields to compare.
+   */
   @AutoValue
   abstract static class CommentSetEntry {
     private static CommentSetEntry create(
@@ -861,12 +872,11 @@
     @Override
     public boolean updateChange(ChangeContext ctx)
         throws ResourceConflictException, UnprocessableEntityException, IOException,
-            PatchListNotAvailableException {
+            PatchListNotAvailableException, CommentsRejectedException {
       user = ctx.getIdentifiedUser();
       notes = ctx.getNotes();
       ps = psUtil.get(ctx.getNotes(), psId);
-      boolean dirty = false;
-      dirty |= insertComments(ctx);
+      boolean dirty = insertComments(ctx);
       dirty |= insertRobotComments(ctx);
       dirty |= updateLabels(projectState, ctx);
       dirty |= insertMessage(ctx);
@@ -895,14 +905,19 @@
     }
 
     private boolean insertComments(ChangeContext ctx)
-        throws UnprocessableEntityException, PatchListNotAvailableException {
-      Map<String, List<CommentInput>> map = in.comments;
-      if (map == null) {
-        map = Collections.emptyMap();
+        throws UnprocessableEntityException, PatchListNotAvailableException,
+            CommentsRejectedException {
+      Map<String, List<CommentInput>> inputComments = in.comments;
+      if (inputComments == null) {
+        inputComments = Collections.emptyMap();
       }
 
-      Map<String, Comment> drafts = Collections.emptyMap();
-      if (!map.isEmpty() || in.drafts != DraftHandling.KEEP) {
+      // HashMap instead of Collections.emptyMap() avoids warning about remove() on immutable
+      // object.
+      Map<String, Comment> drafts = new HashMap<>();
+      // If there are inputComments we need the deduplication loop below, so we have to read (and
+      // publish) drafts here.
+      if (!inputComments.isEmpty() || in.drafts != DraftHandling.KEEP) {
         if (in.drafts == DraftHandling.PUBLISH_ALL_REVISIONS) {
           drafts = changeDrafts(ctx);
         } else {
@@ -910,51 +925,84 @@
         }
       }
 
+      // This will be populated with Comment-s created from inputComments.
       List<Comment> toPublish = new ArrayList<>();
 
-      Set<CommentSetEntry> existingIds =
+      Set<CommentSetEntry> existingComments =
           in.omitDuplicateComments ? readExistingComments(ctx) : Collections.emptySet();
 
-      for (Map.Entry<String, List<CommentInput>> ent : map.entrySet()) {
-        String path = ent.getKey();
-        for (CommentInput c : ent.getValue()) {
-          String parent = Url.decode(c.inReplyTo);
-          Comment e = drafts.remove(Url.decode(c.id));
-          if (e == null) {
-            e = commentsUtil.newComment(ctx, path, psId, c.side(), c.message, c.unresolved, parent);
+      // Deduplication:
+      // - Ignore drafts with the same ID as an inputComment here. These are deleted later.
+      // - Swallow comments that already exist.
+      for (Map.Entry<String, List<CommentInput>> entry : inputComments.entrySet()) {
+        String path = entry.getKey();
+        for (CommentInput inputComment : entry.getValue()) {
+          Comment comment = drafts.remove(Url.decode(inputComment.id));
+          if (comment == null) {
+            String parent = Url.decode(inputComment.inReplyTo);
+            comment =
+                commentsUtil.newComment(
+                    ctx,
+                    path,
+                    psId,
+                    inputComment.side(),
+                    inputComment.message,
+                    inputComment.unresolved,
+                    parent);
           } else {
-            e.writtenOn = ctx.getWhen();
-            e.side = c.side();
-            e.message = c.message;
+            // In ChangeUpdate#putComment() the draft with the same ID will be deleted.
+            comment.writtenOn = ctx.getWhen();
+            comment.side = inputComment.side();
+            comment.message = inputComment.message;
           }
 
-          setCommentRevId(e, patchListCache, ctx.getChange(), ps);
-          e.setLineNbrAndRange(c.line, c.range);
-          e.tag = in.tag;
+          setCommentCommitId(comment, patchListCache, ctx.getChange(), ps);
+          comment.setLineNbrAndRange(inputComment.line, inputComment.range);
+          comment.tag = in.tag;
 
-          if (existingIds.contains(CommentSetEntry.create(e))) {
+          if (existingComments.contains(CommentSetEntry.create(comment))) {
             continue;
           }
-          toPublish.add(e);
+          toPublish.add(comment);
         }
       }
 
       switch (in.drafts) {
         case PUBLISH:
         case PUBLISH_ALL_REVISIONS:
+          validateComments(Streams.concat(drafts.values().stream(), toPublish.stream()));
           publishCommentUtil.publish(ctx, psId, drafts.values(), in.tag);
           comments.addAll(drafts.values());
           break;
         case KEEP:
         default:
+          validateComments(toPublish.stream());
           break;
       }
-      ChangeUpdate u = ctx.getUpdate(psId);
-      commentsUtil.putComments(u, Status.PUBLISHED, toPublish);
+      ChangeUpdate changeUpdate = ctx.getUpdate(psId);
+      commentsUtil.putComments(changeUpdate, Comment.Status.PUBLISHED, toPublish);
       comments.addAll(toPublish);
       return !toPublish.isEmpty();
     }
 
+    private void validateComments(Stream<Comment> comments) throws CommentsRejectedException {
+      ImmutableList<CommentForValidation> draftsForValidation =
+          comments
+              .map(
+                  comment ->
+                      CommentForValidation.create(
+                          comment.lineNbr > 0
+                              ? CommentForValidation.CommentType.INLINE_COMMENT
+                              : CommentForValidation.CommentType.FILE_COMMENT,
+                          comment.message))
+              .collect(toImmutableList());
+      ImmutableList<CommentValidationFailure> draftValidationFailures =
+          PublishCommentUtil.findInvalidComments(commentValidators, draftsForValidation);
+      if (!draftValidationFailures.isEmpty()) {
+        throw new CommentsRejectedException(draftValidationFailures);
+      }
+    }
+
     private boolean insertRobotComments(ChangeContext ctx) throws PatchListNotAvailableException {
       if (in.robotComments == null) {
         return false;
@@ -1003,7 +1051,7 @@
       robotComment.properties = robotCommentInput.properties;
       robotComment.setLineNbrAndRange(robotCommentInput.line, robotCommentInput.range);
       robotComment.tag = in.tag;
-      setCommentRevId(robotComment, patchListCache, ctx.getChange(), ps);
+      setCommentCommitId(robotComment, patchListCache, ctx.getChange(), ps);
       robotComment.fixSuggestions = createFixSuggestionsFromInput(robotCommentInput.fixSuggestions);
       return robotComment;
     }
@@ -1049,27 +1097,25 @@
     }
 
     private Map<String, Comment> changeDrafts(ChangeContext ctx) {
-      Map<String, Comment> drafts = new HashMap<>();
-      for (Comment c : commentsUtil.draftByChangeAuthor(ctx.getNotes(), user.getAccountId())) {
-        c.tag = in.tag;
-        drafts.put(c.key.uuid, c);
-      }
-      return drafts;
+      return commentsUtil.draftByChangeAuthor(ctx.getNotes(), user.getAccountId()).stream()
+          .collect(
+              Collectors.toMap(
+                  c -> c.key.uuid,
+                  c -> {
+                    c.tag = in.tag;
+                    return c;
+                  }));
     }
 
     private Map<String, Comment> patchSetDrafts(ChangeContext ctx) {
-      Map<String, Comment> drafts = new HashMap<>();
-      for (Comment c :
-          commentsUtil.draftByPatchSetAuthor(psId, user.getAccountId(), ctx.getNotes())) {
-        drafts.put(c.key.uuid, c);
-      }
-      return drafts;
+      return commentsUtil.draftByPatchSetAuthor(psId, user.getAccountId(), ctx.getNotes()).stream()
+          .collect(Collectors.toMap(c -> c.key.uuid, c -> c));
     }
 
     private Map<String, Short> approvalsByKey(Collection<PatchSetApproval> patchsetApprovals) {
       Map<String, Short> labels = new HashMap<>();
       for (PatchSetApproval psa : patchsetApprovals) {
-        labels.put(psa.getLabel(), psa.getValue());
+        labels.put(psa.label(), psa.value());
       }
       return labels;
     }
@@ -1106,15 +1152,9 @@
     }
 
     private boolean isReviewer(ChangeContext ctx) {
-      if (ctx.getAccountId().equals(ctx.getChange().getOwner())) {
-        return true;
-      }
       ChangeData cd = changeDataFactory.create(ctx.getNotes());
       ReviewerSet reviewers = cd.reviewers();
-      if (reviewers.byState(REVIEWER).contains(ctx.getAccountId())) {
-        return true;
-      }
-      return false;
+      return reviewers.byState(REVIEWER).contains(ctx.getAccountId());
     }
 
     private boolean updateLabels(ProjectState projectState, ChangeContext ctx)
@@ -1149,35 +1189,40 @@
           // User requested delete of this label.
           oldApprovals.put(normName, null);
           if (c != null) {
-            if (c.getValue() != 0) {
+            if (c.value() != 0) {
               addLabelDelta(normName, (short) 0);
               oldApprovals.put(normName, previous.get(normName));
             }
             del.add(c);
             update.putApproval(normName, (short) 0);
           }
-        } else if (c != null && c.getValue() != ent.getValue()) {
-          c.setValue(ent.getValue());
-          c.setGranted(ctx.getWhen());
-          c.setTag(in.tag);
-          ctx.getUser().updateRealAccountId(c::setRealAccountId);
+        } else if (c != null && c.value() != ent.getValue()) {
+          PatchSetApproval.Builder b =
+              c.toBuilder()
+                  .value(ent.getValue())
+                  .granted(ctx.getWhen())
+                  .tag(Optional.ofNullable(in.tag));
+          ctx.getUser().updateRealAccountId(b::realAccountId);
+          c = b.build();
           ups.add(c);
-          addLabelDelta(normName, c.getValue());
+          addLabelDelta(normName, c.value());
           oldApprovals.put(normName, previous.get(normName));
-          approvals.put(normName, c.getValue());
+          approvals.put(normName, c.value());
           update.putApproval(normName, ent.getValue());
-        } else if (c != null && c.getValue() == ent.getValue()) {
+        } else if (c != null && c.value() == ent.getValue()) {
           current.put(normName, c);
           oldApprovals.put(normName, null);
-          approvals.put(normName, c.getValue());
+          approvals.put(normName, c.value());
         } else if (c == null) {
-          c = ApprovalsUtil.newApproval(psId, user, lt.getLabelId(), ent.getValue(), ctx.getWhen());
-          c.setTag(in.tag);
-          c.setGranted(ctx.getWhen());
+          c =
+              ApprovalsUtil.newApproval(psId, user, lt.getLabelId(), ent.getValue(), ctx.getWhen())
+                  .tag(Optional.ofNullable(in.tag))
+                  .granted(ctx.getWhen())
+                  .build();
           ups.add(c);
-          addLabelDelta(normName, c.getValue());
+          addLabelDelta(normName, c.value());
           oldApprovals.put(normName, previous.get(normName));
-          approvals.put(normName, c.getValue());
+          approvals.put(normName, c.value());
           update.putReviewer(user.getAccountId(), REVIEWER);
           update.putApproval(normName, ent.getValue());
         }
@@ -1219,7 +1264,7 @@
       List<String> disallowed = new ArrayList<>(labelTypes.getLabelTypes().size());
 
       for (PatchSetApproval psa : del) {
-        LabelType lt = requireNonNull(labelTypes.byLabel(psa.getLabel()));
+        LabelType lt = requireNonNull(labelTypes.byLabel(psa.label()));
         String normName = lt.getName();
         if (!lt.allowPostSubmit()) {
           disallowed.add(normName);
@@ -1231,7 +1276,7 @@
       }
 
       for (PatchSetApproval psa : ups) {
-        LabelType lt = requireNonNull(labelTypes.byLabel(psa.getLabel()));
+        LabelType lt = requireNonNull(labelTypes.byLabel(psa.label()));
         String normName = lt.getName();
         if (!lt.allowPostSubmit()) {
           disallowed.add(normName);
@@ -1240,8 +1285,8 @@
         if (prev == null) {
           continue;
         }
-        checkState(prev != psa.getValue()); // Should be filtered out above.
-        if (prev > psa.getValue()) {
+        checkState(prev != psa.value()); // Should be filtered out above.
+        if (prev > psa.value()) {
           reduced.add(psa);
         }
         // No need to set postSubmit bit, which is set automatically when parsing from NoteDb.
@@ -1256,7 +1301,7 @@
         throw new ResourceConflictException(
             "Cannot reduce vote on labels for closed change: "
                 + reduced.stream()
-                    .map(PatchSetApproval::getLabel)
+                    .map(PatchSetApproval::label)
                     .distinct()
                     .sorted()
                     .collect(joining(", ")));
@@ -1283,18 +1328,16 @@
           }
 
           LabelId labelId = labelTypes.get(0).getLabelId();
-          PatchSetApproval c = ApprovalsUtil.newApproval(psId, user, labelId, 0, ctx.getWhen());
-          c.setTag(in.tag);
-          c.setGranted(ctx.getWhen());
-          ups.add(c);
+          ups.add(
+              ApprovalsUtil.newApproval(psId, user, labelId, 0, ctx.getWhen())
+                  .tag(Optional.ofNullable(in.tag))
+                  .granted(ctx.getWhen())
+                  .build());
         } else {
           // Pick a random label that is about to be deleted and keep it.
           Iterator<PatchSetApproval> i = del.iterator();
-          PatchSetApproval c = i.next();
-          c.setValue((short) 0);
-          c.setGranted(ctx.getWhen());
+          ups.add(i.next().toBuilder().value(0).granted(ctx.getWhen()).build());
           i.remove();
-          ups.add(c);
         }
       }
       ctx.getUpdate(ctx.getChange().currentPatchSetId()).putReviewer(user.getAccountId(), REVIEWER);
@@ -1317,7 +1360,7 @@
           continue;
         }
 
-        LabelType lt = labelTypes.byLabel(a.getLabelId());
+        LabelType lt = labelTypes.byLabel(a.labelId());
         if (lt != null) {
           current.put(lt.getName(), a);
         } else {
@@ -1327,7 +1370,7 @@
       return current;
     }
 
-    private boolean insertMessage(ChangeContext ctx) {
+    private boolean insertMessage(ChangeContext ctx) throws CommentsRejectedException {
       String msg = Strings.nullToEmpty(in.message).trim();
 
       StringBuilder buf = new StringBuilder();
@@ -1340,6 +1383,15 @@
         buf.append(String.format("\n\n(%d comments)", comments.size()));
       }
       if (!msg.isEmpty()) {
+        ImmutableList<CommentValidationFailure> messageValidationFailure =
+            PublishCommentUtil.findInvalidComments(
+                commentValidators,
+                ImmutableList.of(
+                    CommentForValidation.create(
+                        CommentForValidation.CommentType.CHANGE_MESSAGE, msg)));
+        if (!messageValidationFailure.isEmpty()) {
+          throw new CommentsRejectedException(messageValidationFailure);
+        }
         buf.append("\n\n").append(msg);
       } else if (in.ready) {
         buf.append("\n\n" + START_REVIEW_MESSAGE);
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index 8abd964..f74643c 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 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.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.ReviewerAdder;
@@ -59,7 +60,7 @@
   }
 
   @Override
-  protected AddReviewerResult applyImpl(
+  protected Response<AddReviewerResult> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AddReviewerInput input)
       throws IOException, RestApiException, UpdateException, PermissionBackendException,
           ConfigInvalidException {
@@ -69,7 +70,7 @@
 
     ReviewerAddition addition = reviewerAdder.prepare(rsrc.getNotes(), rsrc.getUser(), input, true);
     if (addition.op == null) {
-      return addition.result;
+      return Response.ok(addition.result);
     }
     try (BatchUpdate bu =
         updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
@@ -81,7 +82,7 @@
 
     // Re-read change to take into account results of the update.
     addition.gatherResults(changeDataFactory.create(rsrc.getProject(), rsrc.getId()));
-    return addition.result;
+    return Response.ok(addition.result);
   }
 
   private NotifyResolver.Result resolveNotify(ChangeResource rsrc, AddReviewerInput input)
diff --git a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
index c3cee0e..e6a60d5 100644
--- a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
+++ b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
@@ -15,17 +15,18 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.PreconditionFailedException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.ArchiveFormat;
@@ -80,7 +81,7 @@
   }
 
   @Override
-  public BinaryResult apply(RevisionResource rsrc)
+  public Response<BinaryResult> apply(RevisionResource rsrc)
       throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (Strings.isNullOrEmpty(format)) {
@@ -105,7 +106,7 @@
       throw new MethodNotAllowedException("Anonymous users cannot submit");
     }
 
-    return getBundles(rsrc, f);
+    return Response.ok(getBundles(rsrc, f));
   }
 
   private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
diff --git a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
index a47037c..44f35a0 100644
--- a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
@@ -39,7 +39,7 @@
 
 @Singleton
 public class PublishChangeEdit
-    extends RetryingRestModifyView<ChangeResource, PublishChangeEditInput, Response<?>> {
+    extends RetryingRestModifyView<ChangeResource, PublishChangeEditInput, Object> {
   private final ChangeEditUtil editUtil;
   private final NotifyResolver notifyResolver;
   private final ContributorAgreementsChecker contributorAgreementsChecker;
@@ -57,7 +57,7 @@
   }
 
   @Override
-  protected Response<?> applyImpl(
+  protected Response<Object> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in)
       throws IOException, RestApiException, UpdateException, ConfigInvalidException,
           NoSuchProjectException {
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
index a9a6f12..d4a14d4 100644
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -22,9 +22,12 @@
 import com.google.gerrit.extensions.common.AccountInfo;
 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.webui.UiAction;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeResource;
@@ -53,6 +56,7 @@
   private final ReviewerAdder reviewerAdder;
   private final AccountLoader.Factory accountLoaderFactory;
   private final PermissionBackend permissionBackend;
+  private final ApprovalsUtil approvalsUtil;
 
   @Inject
   PutAssignee(
@@ -61,17 +65,19 @@
       RetryHelper retryHelper,
       ReviewerAdder reviewerAdder,
       AccountLoader.Factory accountLoaderFactory,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ApprovalsUtil approvalsUtil) {
     super(retryHelper);
     this.accountResolver = accountResolver;
     this.assigneeFactory = assigneeFactory;
     this.reviewerAdder = reviewerAdder;
     this.accountLoaderFactory = accountLoaderFactory;
     this.permissionBackend = permissionBackend;
+    this.approvalsUtil = approvalsUtil;
   }
 
   @Override
-  protected AccountInfo applyImpl(
+  protected Response<AccountInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AssigneeInput input)
       throws RestApiException, UpdateException, IOException, PermissionBackendException,
           ConfigInvalidException {
@@ -97,12 +103,15 @@
       SetAssigneeOp op = assigneeFactory.create(assignee);
       bu.addOp(rsrc.getId(), op);
 
-      ReviewerAddition reviewersAddition = addAssigneeAsCC(rsrc, input.assignee);
-      reviewersAddition.op.suppressEmail();
-      bu.addOp(rsrc.getId(), reviewersAddition.op);
+      ReviewerSet currentReviewers = approvalsUtil.getReviewers(rsrc.getNotes());
+      if (!currentReviewers.all().contains(assignee.getAccountId())) {
+        ReviewerAddition reviewersAddition = addAssigneeAsCC(rsrc, input.assignee);
+        reviewersAddition.op.suppressEmail();
+        bu.addOp(rsrc.getId(), reviewersAddition.op);
+      }
 
       bu.execute();
-      return accountLoaderFactory.create(true).fillOne(assignee.getAccountId());
+      return Response.ok(accountLoaderFactory.create(true).fillOne(assignee.getAccountId()));
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/PutDescription.java b/java/com/google/gerrit/server/restapi/change/PutDescription.java
index 0ec38e1..451d010 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDescription.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.common.DescriptionInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RevisionResource;
@@ -39,7 +39,7 @@
 
 @Singleton
 public class PutDescription
-    extends RetryingRestModifyView<RevisionResource, DescriptionInput, Response<String>>
+    extends RetryingRestModifyView<RevisionResource, DescriptionInput, String>
     implements UiAction<RevisionResource> {
   private final ChangeMessagesUtil cmUtil;
   private final PatchSetUtil psUtil;
@@ -57,7 +57,7 @@
       throws UpdateException, RestApiException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.EDIT_DESCRIPTION);
 
-    Op op = new Op(input != null ? input : new DescriptionInput(), rsrc.getPatchSet().getId());
+    Op op = new Op(input != null ? input : new DescriptionInput(), rsrc.getPatchSet().id());
     try (BatchUpdate u =
         updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       u.addOp(rsrc.getChange().getId(), op);
@@ -84,7 +84,7 @@
     public boolean updateChange(ChangeContext ctx) {
       ChangeUpdate update = ctx.getUpdate(psId);
       newDescription = Strings.nullToEmpty(input.description);
-      oldDescription = Strings.nullToEmpty(psUtil.get(ctx.getNotes(), psId).getDescription());
+      oldDescription = psUtil.get(ctx.getNotes(), psId).description().orElse("");
       if (oldDescription.equals(newDescription)) {
         return false;
       }
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index f6c9abe..5696fcb 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -23,9 +25,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.DraftCommentResource;
@@ -49,7 +48,7 @@
 
 @Singleton
 public class PutDraftComment
-    extends RetryingRestModifyView<DraftCommentResource, DraftInput, Response<CommentInfo>> {
+    extends RetryingRestModifyView<DraftCommentResource, DraftInput, CommentInfo> {
 
   private final DeleteDraftComment delete;
   private final CommentsUtil commentsUtil;
@@ -124,7 +123,7 @@
       // user.
       ctx.getUser().updateRealAccountId(comment::setRealAuthor);
 
-      PatchSet.Id psId = new PatchSet.Id(ctx.getChange().getId(), origComment.key.patchSetId);
+      PatchSet.Id psId = PatchSet.id(ctx.getChange().getId(), origComment.key.patchSetId);
       ChangeUpdate update = ctx.getUpdate(psId);
 
       PatchSet ps = psUtil.get(ctx.getNotes(), psId);
@@ -138,9 +137,9 @@
         commentsUtil.deleteComments(update, Collections.singleton(origComment));
         comment.key.filename = in.path;
       }
-      setCommentRevId(comment, patchListCache, ctx.getChange(), ps);
+      setCommentCommitId(comment, patchListCache, ctx.getChange(), ps);
       commentsUtil.putComments(
-          update, Status.DRAFT, Collections.singleton(update(comment, in, ctx.getWhen())));
+          update, Comment.Status.DRAFT, Collections.singleton(update(comment, in, ctx.getWhen())));
       return true;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index 3c7259d..cb73747 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.CommitMessageInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -21,8 +23,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -58,8 +58,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
-public class PutMessage
-    extends RetryingRestModifyView<ChangeResource, CommitMessageInput, Response<?>> {
+public class PutMessage extends RetryingRestModifyView<ChangeResource, CommitMessageInput, String> {
 
   private final GitRepositoryManager repositoryManager;
   private final Provider<CurrentUser> userProvider;
@@ -116,7 +115,7 @@
     try (Repository repository = repositoryManager.openRepository(resource.getProject());
         RevWalk revWalk = new RevWalk(repository);
         ObjectInserter objectInserter = repository.newObjectInserter()) {
-      RevCommit patchSetCommit = revWalk.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+      RevCommit patchSetCommit = revWalk.parseCommit(ps.commitId());
 
       String currentCommitMessage = patchSetCommit.getFullMessage();
       if (input.message.equals(currentCommitMessage)) {
@@ -129,7 +128,7 @@
         // Ensure that BatchUpdate will update the same repo
         bu.setRepository(repository, new RevWalk(objectInserter.newReader()), objectInserter);
 
-        PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.getId());
+        PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.id());
         ObjectId newCommit =
             createCommit(objectInserter, patchSetCommit, sanitizedCommitMessage, ts);
         PatchSetInserter inserter = psInserterFactory.create(resource.getNotes(), psId, newCommit);
diff --git a/java/com/google/gerrit/server/restapi/change/PutTopic.java b/java/com/google/gerrit/server/restapi/change/PutTopic.java
index abfa49b..cfeb884 100644
--- a/java/com/google/gerrit/server/restapi/change/PutTopic.java
+++ b/java/com/google/gerrit/server/restapi/change/PutTopic.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.extensions.api.changes.TopicInput;
 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.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.ChangeResource;
@@ -41,7 +41,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class PutTopic extends RetryingRestModifyView<ChangeResource, TopicInput, Response<String>>
+public class PutTopic extends RetryingRestModifyView<ChangeResource, TopicInput, String>
     implements UiAction<ChangeResource> {
   private final ChangeMessagesUtil cmUtil;
   private final TopicEdited topicEdited;
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
index e1ed8d6..bf4d197 100644
--- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java
+++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -21,18 +21,23 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 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.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryRequiresAuthException;
 import com.google.gerrit.index.query.QueryResult;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -45,6 +50,8 @@
   private final ChangeJson.Factory json;
   private final ChangeQueryBuilder qb;
   private final ChangeQueryProcessor imp;
+  private final Provider<CurrentUser> userProvider;
+  private final PermissionBackend permissionBackend;
   private EnumSet<ListChangesOption> options;
 
   @Option(
@@ -87,16 +94,32 @@
     imp.setNoLimit(on);
   }
 
+  @Option(name = "--skip-visibility", usage = "Skip visibility check, only for administrators")
+  public void skipVisibility(boolean on) throws AuthException, PermissionBackendException {
+    if (on) {
+      CurrentUser user = userProvider.get();
+      permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+      imp.enforceVisibility(false);
+    }
+  }
+
   @Override
   public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
     imp.setDynamicBean(plugin, dynamicBean);
   }
 
   @Inject
-  QueryChanges(ChangeJson.Factory json, ChangeQueryBuilder qb, ChangeQueryProcessor qp) {
+  QueryChanges(
+      ChangeJson.Factory json,
+      ChangeQueryBuilder qb,
+      ChangeQueryProcessor qp,
+      Provider<CurrentUser> userProvider,
+      PermissionBackend permissionBackend) {
     this.json = json;
     this.qb = qb;
     this.imp = qp;
+    this.userProvider = userProvider;
+    this.permissionBackend = permissionBackend;
 
     options = EnumSet.noneOf(ListChangesOption.class);
   }
@@ -113,7 +136,7 @@
   }
 
   @Override
-  public List<?> apply(TopLevelResource rsrc)
+  public Response<List<?>> apply(TopLevelResource rsrc)
       throws BadRequestException, AuthException, PermissionBackendException {
     List<List<ChangeInfo>> out;
     try {
@@ -124,7 +147,7 @@
       logger.atFine().withCause(e).log("Reject change query with 400 Bad Request: %s", queries);
       throw new BadRequestException(e.getMessage(), e);
     }
-    return out.size() == 1 ? out.get(0) : out;
+    return Response.ok(out.size() == 1 ? out.get(0) : out);
   }
 
   private List<List<ChangeInfo>> query() throws QueryParseException, PermissionBackendException {
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 782b91aa..af8f971 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -17,19 +17,20 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeJson;
@@ -98,7 +99,7 @@
   }
 
   @Override
-  protected ChangeInfo applyImpl(
+  protected Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, RebaseInput input)
       throws UpdateException, RestApiException, IOException, PermissionBackendException {
     // Not allowed to rebase if the current patch set is locked.
@@ -131,14 +132,14 @@
               .setFireRevisionCreated(true));
       bu.execute();
     }
-    return json.create(OPTIONS).format(change.getProject(), change.getId());
+    return Response.ok(json.create(OPTIONS).format(change.getProject(), change.getId()));
   }
 
   private ObjectId findBaseRev(
       Repository repo, RevWalk rw, RevisionResource rsrc, RebaseInput input)
       throws RestApiException, IOException, NoSuchChangeException, AuthException,
           PermissionBackendException {
-    Branch.NameKey destRefKey = rsrc.getChange().getDest();
+    BranchNameKey destRefKey = rsrc.getChange().getDest();
     if (input == null || input.base == null) {
       return rebaseUtil.findBaseRevision(rsrc.getPatchSet(), destRefKey, repo, rw);
     }
@@ -147,10 +148,10 @@
     String str = input.base.trim();
     if (str.equals("")) {
       // Remove existing dependency to other patch set.
-      Ref destRef = repo.exactRef(destRefKey.get());
+      Ref destRef = repo.exactRef(destRefKey.branch());
       if (destRef == null) {
         throw new ResourceConflictException(
-            "can't rebase onto tip of branch " + destRefKey.get() + "; branch doesn't exist");
+            "can't rebase onto tip of branch " + destRefKey.branch() + "; branch doesn't exist");
       }
       return destRef.getObjectId();
     }
@@ -167,8 +168,8 @@
           String.format("Base change not found: %s", input.base), e);
     }
 
-    PatchSet.Id baseId = base.patchSet().getId();
-    if (change.getId().equals(baseId.getParentKey())) {
+    PatchSet.Id baseId = base.patchSet().id();
+    if (change.getId().equals(baseId.changeId())) {
       throw new ResourceConflictException("cannot rebase change onto itself");
     }
 
@@ -189,18 +190,18 @@
               + baseChange.getKey()
               + " is a descendant of the current change - recursion not allowed");
     }
-    return ObjectId.fromString(base.patchSet().getRevision().get());
+    return base.patchSet().commitId();
   }
 
   private boolean isMergedInto(RevWalk rw, PatchSet base, PatchSet tip) throws IOException {
-    ObjectId baseId = ObjectId.fromString(base.getRevision().get());
-    ObjectId tipId = ObjectId.fromString(tip.getRevision().get());
+    ObjectId baseId = base.commitId();
+    ObjectId tipId = tip.commitId();
     return rw.isMergedInto(rw.parseCommit(baseId), rw.parseCommit(tipId));
   }
 
   private boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
     // Prevent rebase of exotic changes (merge commit, no ancestor).
-    RevCommit c = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+    RevCommit c = rw.parseCommit(ps.commitId());
     return c.getParentCount() == 1;
   }
 
@@ -238,7 +239,7 @@
     }
 
     boolean enabled = false;
-    try (Repository repo = repoManager.openRepository(change.getDest().getParentKey());
+    try (Repository repo = repoManager.openRepository(change.getDest().project());
         RevWalk rw = new RevWalk(repo)) {
       if (hasOneParent(rw, rsrc.getPatchSet())) {
         enabled = rebaseUtil.canRebase(rsrc.getPatchSet(), change.getDest(), repo, rw);
@@ -272,14 +273,15 @@
     }
 
     @Override
-    protected ChangeInfo applyImpl(
+    protected Response<ChangeInfo> applyImpl(
         BatchUpdate.Factory updateFactory, ChangeResource rsrc, RebaseInput input)
-        throws UpdateException, RestApiException, IOException, PermissionBackendException {
+        throws Exception {
       PatchSet ps = psUtil.current(rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
       }
-      return rebase.applyImpl(updateFactory, new RevisionResource(rsrc, ps), input);
+      return Response.ok(
+          rebase.applyImpl(updateFactory, new RevisionResource(rsrc, ps), input).value());
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
index 81294ed..7be8765 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -33,7 +33,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 @Singleton
-public class RebaseChangeEdit extends RetryingRestModifyView<ChangeResource, Input, Response<?>> {
+public class RebaseChangeEdit extends RetryingRestModifyView<ChangeResource, Input, Object> {
   private final GitRepositoryManager repositoryManager;
   private final ChangeEditModifier editModifier;
 
@@ -48,7 +48,8 @@
   }
 
   @Override
-  protected Response<?> applyImpl(BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input in)
+  protected Response<Object> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input in)
       throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     Project.NameKey project = rsrc.getProject();
     try (Repository repository = repositoryManager.openRepository(project)) {
diff --git a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
index 1421ab6..8040847 100644
--- a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
@@ -24,10 +24,10 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -74,8 +74,8 @@
       throws IOException, PermissionBackendException {
     checkArgument(!in.isEmpty(), "Input may not be empty");
     // Map of all patch sets, keyed by commit SHA-1.
-    Map<String, PatchSetData> byId = collectById(in);
-    PatchSetData start = byId.get(startPs.getRevision().get());
+    Map<ObjectId, PatchSetData> byId = collectById(in);
+    PatchSetData start = byId.get(startPs.commitId());
     checkArgument(start != null, "%s not found in %s", startPs, in);
 
     // Map of patch set -> immediate parent.
@@ -89,12 +89,12 @@
 
     for (ChangeData cd : in) {
       for (PatchSet ps : cd.patchSets()) {
-        PatchSetData thisPsd = requireNonNull(byId.get(ps.getRevision().get()));
-        if (cd.getId().equals(start.id()) && !ps.getId().equals(start.psId())) {
+        PatchSetData thisPsd = requireNonNull(byId.get(ps.commitId()));
+        if (cd.getId().equals(start.id()) && !ps.id().equals(start.psId())) {
           otherPatchSetsOfStart.add(thisPsd);
         }
         for (RevCommit p : thisPsd.commit().getParents()) {
-          PatchSetData parentPsd = byId.get(p.name());
+          PatchSetData parentPsd = byId.get(p);
           if (parentPsd != null) {
             parents.put(thisPsd, parentPsd);
             children.put(parentPsd, thisPsd);
@@ -112,9 +112,9 @@
     return result;
   }
 
-  private Map<String, PatchSetData> collectById(List<ChangeData> in) throws IOException {
+  private Map<ObjectId, PatchSetData> collectById(List<ChangeData> in) throws IOException {
     Project.NameKey project = in.get(0).change().getProject();
-    Map<String, PatchSetData> result = Maps.newHashMapWithExpectedSize(in.size() * 3);
+    Map<ObjectId, PatchSetData> result = Maps.newHashMapWithExpectedSize(in.size() * 3);
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       rw.setRetainBody(true);
@@ -126,10 +126,9 @@
             project,
             cd.change().getProject());
         for (PatchSet ps : cd.patchSets()) {
-          String id = ps.getRevision().get();
-          RevCommit c = rw.parseCommit(ObjectId.fromString(id));
+          RevCommit c = rw.parseCommit(ps.commitId());
           PatchSetData psd = PatchSetData.create(cd, ps, c);
-          result.put(id, psd);
+          result.put(ps.commitId(), psd);
         }
       }
     }
@@ -252,16 +251,16 @@
     abstract RevCommit commit();
 
     PatchSet.Id psId() {
-      return patchSet().getId();
+      return patchSet().id();
     }
 
     Change.Id id() {
-      return psId().getParentKey();
+      return psId().changeId();
     }
 
     @Override
     public final int hashCode() {
-      return Objects.hash(patchSet().getId(), commit());
+      return Objects.hash(patchSet().id(), commit());
     }
 
     @Override
@@ -270,7 +269,7 @@
         return false;
       }
       PatchSetData o = (PatchSetData) obj;
-      return Objects.equals(patchSet().getId(), o.patchSet().getId())
+      return Objects.equals(patchSet().id(), o.patchSet().id())
           && Objects.equals(commit(), o.commit());
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index 5f56cdb..679d4f8 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -16,16 +16,17 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Change.Status;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchSetUtil;
@@ -81,7 +82,7 @@
   }
 
   @Override
-  protected ChangeInfo applyImpl(
+  protected Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, RestoreInput input)
       throws RestApiException, UpdateException, PermissionBackendException, IOException {
     // Not allowed to restore if the current patch set is locked.
@@ -95,7 +96,7 @@
         updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       u.addOp(rsrc.getId(), op).execute();
     }
-    return json.noOptions().format(op.change);
+    return Response.ok(json.noOptions().format(op.change));
   }
 
   private class Op implements BatchUpdateOp {
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index dd51e7f..e196abc 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -18,33 +18,32 @@
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
 
-import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeJson;
-import com.google.gerrit.server.change.ChangeMessages;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.extensions.events.ChangeReverted;
+import com.google.gerrit.server.git.CommitUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mail.send.RevertedSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -66,24 +65,19 @@
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
-import java.text.MessageFormat;
 import java.util.HashSet;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.ChangeIdUtil;
 
 @Singleton
 public class Revert extends RetryingRestModifyView<ChangeResource, RevertInput, ChangeInfo>
@@ -98,12 +92,12 @@
   private final PatchSetUtil psUtil;
   private final RevertedSender.Factory revertedSenderFactory;
   private final ChangeJson.Factory json;
-  private final Provider<PersonIdent> serverIdent;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeReverted changeReverted;
   private final ContributorAgreementsChecker contributorAgreements;
   private final ProjectCache projectCache;
   private final NotifyResolver notifyResolver;
+  private final CommitUtil commitUtil;
 
   @Inject
   Revert(
@@ -116,12 +110,12 @@
       PatchSetUtil psUtil,
       RevertedSender.Factory revertedSenderFactory,
       ChangeJson.Factory json,
-      @GerritPersonIdent Provider<PersonIdent> serverIdent,
       ApprovalsUtil approvalsUtil,
       ChangeReverted changeReverted,
       ContributorAgreementsChecker contributorAgreements,
       ProjectCache projectCache,
-      NotifyResolver notifyResolver) {
+      NotifyResolver notifyResolver,
+      CommitUtil commitUtil) {
     super(retryHelper);
     this.permissionBackend = permissionBackend;
     this.repoManager = repoManager;
@@ -131,16 +125,16 @@
     this.psUtil = psUtil;
     this.revertedSenderFactory = revertedSenderFactory;
     this.json = json;
-    this.serverIdent = serverIdent;
     this.approvalsUtil = approvalsUtil;
     this.changeReverted = changeReverted;
     this.contributorAgreements = contributorAgreements;
     this.projectCache = projectCache;
     this.notifyResolver = notifyResolver;
+    this.commitUtil = commitUtil;
   }
 
   @Override
-  public ChangeInfo applyImpl(
+  public Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, RevertInput input)
       throws IOException, RestApiException, UpdateException, NoSuchChangeException,
           PermissionBackendException, NoSuchProjectException, ConfigInvalidException {
@@ -154,13 +148,12 @@
     projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
     Change.Id revertId = revert(updateFactory, rsrc.getNotes(), rsrc.getUser(), input);
-    return json.noOptions().format(rsrc.getProject(), revertId);
+    return Response.ok(json.noOptions().format(rsrc.getProject(), revertId));
   }
 
   private Change.Id revert(
       BatchUpdate.Factory updateFactory, ChangeNotes notes, CurrentUser user, RevertInput input)
       throws IOException, RestApiException, UpdateException, ConfigInvalidException {
-    String message = Strings.emptyToNull(input.message);
     Change.Id changeIdToRevert = notes.getChangeId();
     PatchSet.Id patchSetId = notes.getChange().currentPatchSetId();
     PatchSet patch = psUtil.get(notes, patchSetId);
@@ -173,50 +166,25 @@
         ObjectInserter oi = git.newObjectInserter();
         ObjectReader reader = oi.newReader();
         RevWalk revWalk = new RevWalk(reader)) {
-      RevCommit commitToRevert =
-          revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
-      if (commitToRevert.getParentCount() == 0) {
-        throw new ResourceConflictException("Cannot revert initial commit");
-      }
 
       Timestamp now = TimeUtil.nowTs();
-      PersonIdent committerIdent = serverIdent.get();
-      PersonIdent authorIdent =
-          user.asIdentifiedUser().newCommitterIdent(now, committerIdent.getTimeZone());
-
-      RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
-      revWalk.parseHeaders(parentToCommitToRevert);
-
-      CommitBuilder revertCommitBuilder = new CommitBuilder();
-      revertCommitBuilder.addParentId(commitToRevert);
-      revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree());
-      revertCommitBuilder.setAuthor(authorIdent);
-      revertCommitBuilder.setCommitter(authorIdent);
-
-      Change changeToRevert = notes.getChange();
-      if (message == null) {
-        message =
-            MessageFormat.format(
-                ChangeMessages.get().revertChangeDefaultMessage,
-                changeToRevert.getSubject(),
-                patch.getRevision().get());
-      }
-
       ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
-      revertCommitBuilder.setMessage(ChangeIdUtil.insertId(message, generatedChangeId, true));
+      Change changeToRevert = notes.getChange();
+      ObjectId revertCommitId =
+          commitUtil.createRevertCommit(
+              input.message, notes, user, generatedChangeId, now, oi, revWalk);
 
-      Change.Id changeId = new Change.Id(seq.nextChangeId());
-      ObjectId id = oi.insert(revertCommitBuilder);
-      RevCommit revertCommit = revWalk.parseCommit(id);
+      RevCommit revertCommit = revWalk.parseCommit(revertCommitId);
 
+      Change.Id changeId = Change.id(seq.nextChangeId());
       NotifyResolver.Result notify =
           notifyResolver.resolve(
               firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
 
       ChangeInserter ins =
           changeInserterFactory
-              .create(changeId, revertCommit, notes.getChange().getDest().get())
-              .setTopic(changeToRevert.getTopic());
+              .create(changeId, revertCommit, notes.getChange().getDest().branch())
+              .setTopic(input.topic == null ? changeToRevert.getTopic() : input.topic.trim());
       ins.setMessage("Uploaded patch set 1.");
 
       ReviewerSet reviewerSet = approvalsUtil.getReviewers(notes);
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewed.java b/java/com/google/gerrit/server/restapi/change/Reviewed.java
index 4594503..7152799 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewed.java
@@ -40,9 +40,9 @@
           accountPatchReviewStore.call(
               s ->
                   s.markReviewed(
-                      resource.getPatchKey().getParentKey(),
+                      resource.getPatchKey().patchSetId(),
                       resource.getAccountId(),
-                      resource.getPatchKey().getFileName()));
+                      resource.getPatchKey().fileName()));
       return reviewFlagUpdated ? Response.created("") : Response.ok("");
     }
   }
@@ -61,9 +61,9 @@
       accountPatchReviewStore.run(
           s ->
               s.clearReviewed(
-                  resource.getPatchKey().getParentKey(),
+                  resource.getPatchKey().patchSetId(),
                   resource.getAccountId(),
-                  resource.getPatchKey().getFileName()));
+                  resource.getPatchKey().fileName()));
       return Response.none();
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index 92f7185..f07d815 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
@@ -22,18 +21,20 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.FanOutExecutor;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.ReviewerSuggestion;
 import com.google.gerrit.server.change.SuggestedReviewer;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.plugincontext.PluginMapContext;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -44,11 +45,11 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -63,13 +64,6 @@
 public class ReviewerRecommender {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private static final double BASE_REVIEWER_WEIGHT = 10;
-  private static final double BASE_OWNER_WEIGHT = 1;
-  private static final double BASE_COMMENT_WEIGHT = 0.5;
-  private static final double[] WEIGHTS =
-      new double[] {
-        BASE_REVIEWER_WEIGHT, BASE_OWNER_WEIGHT, BASE_COMMENT_WEIGHT,
-      };
   private static final long PLUGIN_QUERY_TIMEOUT = 500; // ms
 
   private final ChangeQueryBuilder changeQueryBuilder;
@@ -78,6 +72,7 @@
   private final Provider<InternalChangeQuery> queryProvider;
   private final ExecutorService executor;
   private final ApprovalsUtil approvalsUtil;
+  private final AccountCache accountCache;
 
   @Inject
   ReviewerRecommender(
@@ -86,16 +81,19 @@
       Provider<InternalChangeQuery> queryProvider,
       @FanOutExecutor ExecutorService executor,
       ApprovalsUtil approvalsUtil,
-      @GerritServerConfig Config config) {
+      @GerritServerConfig Config config,
+      AccountCache accountCache) {
     this.changeQueryBuilder = changeQueryBuilder;
     this.config = config;
     this.queryProvider = queryProvider;
     this.reviewerSuggestionPluginMap = reviewerSuggestionPluginMap;
     this.executor = executor;
     this.approvalsUtil = approvalsUtil;
+    this.accountCache = accountCache;
   }
 
   public List<Account.Id> suggestReviewers(
+      ReviewerState reviewerState,
       @Nullable ChangeNotes changeNotes,
       SuggestReviewers suggestReviewers,
       ProjectState projectState,
@@ -109,12 +107,7 @@
     double baseWeight = config.getInt("addReviewer", "baseWeight", 1);
     logger.atFine().log("base weight: %s", baseWeight);
 
-    Map<Account.Id, MutableDouble> reviewerScores;
-    if (Strings.isNullOrEmpty(query)) {
-      reviewerScores = baseRankingForEmptyQuery(baseWeight);
-    } else {
-      reviewerScores = baseRankingForCandidateList(candidateList, projectState, baseWeight);
-    }
+    Map<Account.Id, MutableDouble> reviewerScores = baseRanking(baseWeight, query, candidateList);
     logger.atFine().log("Base reviewer scores: %s", reviewerScores);
 
     // Send the query along with a candidate list to all plugins and merge the
@@ -178,7 +171,7 @@
       // Remove existing reviewers
       approvalsUtil
           .getReviewers(changeNotes)
-          .byState(REVIEWER)
+          .byState(ReviewerStateInternal.fromReviewerState(reviewerState))
           .forEach(
               r -> {
                 if (reviewerScores.remove(r) != null) {
@@ -196,7 +189,18 @@
     return sortedSuggestions;
   }
 
-  private Map<Account.Id, MutableDouble> baseRankingForEmptyQuery(double baseWeight)
+  /**
+   * @param baseWeight The weight applied to the ordering of the reviewers.
+   * @param query Query to match. For example, it can try to match all users that start with "Ab".
+   * @param candidateList The list of candidates based on the query. If query is empty, this list is
+   *     also empty.
+   * @return Map of account ids that match the query and their appropriate ranking (the better the
+   *     ranking, the better it is to suggest them as reviewers).
+   * @throws IOException Can't find owner="self" account.
+   * @throws ConfigInvalidException Can't find owner="self" account.
+   */
+  private Map<Account.Id, MutableDouble> baseRanking(
+      double baseWeight, String query, List<Account.Id> candidateList)
       throws IOException, ConfigInvalidException {
     // Get the user's last 25 changes, check approvals
     try {
@@ -206,14 +210,15 @@
               .setLimit(25)
               .setRequestedFields(ChangeField.APPROVAL)
               .query(changeQueryBuilder.owner("self"));
-      Map<Account.Id, MutableDouble> suggestions = new HashMap<>();
+      Map<Account.Id, MutableDouble> suggestions = new LinkedHashMap<>();
+      // Put those candidates at the bottom of the list
+      candidateList.stream().forEach(id -> suggestions.put(id, new MutableDouble(0)));
+
       for (ChangeData cd : result) {
         for (PatchSetApproval approval : cd.currentApprovals()) {
-          Account.Id id = approval.getAccountId();
-          if (suggestions.containsKey(id)) {
-            suggestions.get(id).add(baseWeight);
-          } else {
-            suggestions.put(id, new MutableDouble(baseWeight));
+          Account.Id id = approval.accountId();
+          if (Strings.isNullOrEmpty(query) || accountMatchesQuery(id, query)) {
+            suggestions.computeIfAbsent(id, (ignored) -> new MutableDouble(0)).add(baseWeight);
           }
         }
       }
@@ -225,63 +230,15 @@
     }
   }
 
-  private Map<Account.Id, MutableDouble> baseRankingForCandidateList(
-      List<Account.Id> candidates, ProjectState projectState, double baseWeight)
-      throws IOException, ConfigInvalidException {
-    // Get each reviewer's activity based on number of applied labels
-    // (weighted 10d), number of comments (weighted 0.5d) and number of owned
-    // changes (weighted 1d).
-    Map<Account.Id, MutableDouble> reviewers = new LinkedHashMap<>();
-    if (candidates.isEmpty()) {
-      return reviewers;
-    }
-    List<Predicate<ChangeData>> predicates = new ArrayList<>();
-    for (Account.Id id : candidates) {
-      try {
-        Predicate<ChangeData> projectQuery = changeQueryBuilder.project(projectState.getName());
-
-        // Get all labels for this project and create a compound OR query to
-        // fetch all changes where users have applied one of these labels
-        List<LabelType> labelTypes = projectState.getLabelTypes().getLabelTypes();
-        List<Predicate<ChangeData>> labelPredicates = new ArrayList<>(labelTypes.size());
-        for (LabelType type : labelTypes) {
-          labelPredicates.add(changeQueryBuilder.label(type.getName() + ",user=" + id));
-        }
-        Predicate<ChangeData> reviewerQuery =
-            Predicate.and(projectQuery, Predicate.or(labelPredicates));
-
-        Predicate<ChangeData> ownerQuery =
-            Predicate.and(projectQuery, changeQueryBuilder.owner(id.toString()));
-        Predicate<ChangeData> commentedByQuery =
-            Predicate.and(projectQuery, changeQueryBuilder.commentby(id.toString()));
-
-        predicates.add(reviewerQuery);
-        predicates.add(ownerQuery);
-        predicates.add(commentedByQuery);
-        reviewers.put(id, new MutableDouble());
-      } catch (QueryParseException e) {
-        // Unhandled: If an exception is thrown, we won't increase the
-        // candidates's score
-        logger.atSevere().withCause(e).log("Exception while suggesting reviewers");
+  private boolean accountMatchesQuery(Account.Id id, String query) {
+    Optional<Account> account = accountCache.get(id).map(AccountState::account);
+    if (account.isPresent() && account.get().isActive()) {
+      if ((account.get().fullName() != null && account.get().fullName().startsWith(query))
+          || (account.get().preferredEmail() != null
+              && account.get().preferredEmail().startsWith(query))) {
+        return true;
       }
     }
-
-    List<List<ChangeData>> result = queryProvider.get().setLimit(25).noFields().query(predicates);
-
-    Iterator<List<ChangeData>> queryResultIterator = result.iterator();
-    Iterator<Account.Id> reviewersIterator = reviewers.keySet().iterator();
-
-    int i = 0;
-    Account.Id currentId = null;
-    while (queryResultIterator.hasNext()) {
-      List<ChangeData> currentResult = queryResultIterator.next();
-      if (i % WEIGHTS.length == 0) {
-        currentId = reviewersIterator.next();
-      }
-
-      reviewers.get(currentId).add(WEIGHTS[i % WEIGHTS.length] * baseWeight * currentResult.size());
-      i++;
-    }
-    return reviewers;
+    return false;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewers.java b/java/com/google/gerrit/server/restapi/change/Reviewers.java
index 546ca01..b2714da 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewers.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
@@ -22,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ReviewerResource;
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index f5907e7..676cc07 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -24,21 +24,26 @@
 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.Project;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.GroupBaseInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.ResultSet;
+import com.google.gerrit.index.query.TooManyTermsInQueryException;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
@@ -49,6 +54,7 @@
 import com.google.gerrit.server.change.ReviewerAdder;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.index.account.AccountIndexRewriter;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -113,12 +119,9 @@
     }
   }
 
-  // Generate a candidate list at 3x the size of what the user wants to see to
-  // give the ranking algorithm a good set of candidates it can work with
-  private static final int CANDIDATE_LIST_MULTIPLIER = 3;
-
   private final AccountLoader.Factory accountLoaderFactory;
   private final AccountQueryBuilder accountQueryBuilder;
+  private final AccountIndexRewriter accountIndexRewriter;
   private final GroupBackend groupBackend;
   private final GroupMembers groupMembers;
   private final ReviewerRecommender reviewerRecommender;
@@ -132,6 +135,7 @@
   ReviewersUtil(
       AccountLoader.Factory accountLoaderFactory,
       AccountQueryBuilder accountQueryBuilder,
+      AccountIndexRewriter accountIndexRewriter,
       GroupBackend groupBackend,
       GroupMembers groupMembers,
       ReviewerRecommender reviewerRecommender,
@@ -142,6 +146,7 @@
       Provider<CurrentUser> self) {
     this.accountLoaderFactory = accountLoaderFactory;
     this.accountQueryBuilder = accountQueryBuilder;
+    this.accountIndexRewriter = accountIndexRewriter;
     this.groupBackend = groupBackend;
     this.groupMembers = groupMembers;
     this.reviewerRecommender = reviewerRecommender;
@@ -157,12 +162,13 @@
   }
 
   public List<SuggestedReviewerInfo> suggestReviewers(
+      ReviewerState reviewerState,
       @Nullable ChangeNotes changeNotes,
       SuggestReviewers suggestReviewers,
       ProjectState projectState,
       VisibilityControl visibilityControl,
       boolean excludeGroups)
-      throws IOException, ConfigInvalidException, PermissionBackendException {
+      throws IOException, ConfigInvalidException, PermissionBackendException, BadRequestException {
     CurrentUser currentUser = self.get();
     if (changeNotes != null) {
       logger.atFine().log(
@@ -190,7 +196,8 @@
     }
 
     List<Account.Id> sortedRecommendations =
-        recommendAccounts(changeNotes, suggestReviewers, projectState, candidateList);
+        recommendAccounts(
+            reviewerState, changeNotes, suggestReviewers, projectState, candidateList);
     logger.atFine().log("Sorted recommendations: %s", sortedRecommendations);
 
     // Filter accounts by visibility and enforce limit
@@ -221,37 +228,59 @@
     return suggestedReviewers;
   }
 
-  private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers) {
+  private static Account.Id fromIdField(FieldBundle f, boolean useLegacyNumericFields) {
+    if (useLegacyNumericFields) {
+      return Account.id(f.getValue(AccountField.ID).intValue());
+    }
+    return Account.id(Integer.valueOf(f.getValue(AccountField.ID_STR)));
+  }
+
+  private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers)
+      throws BadRequestException {
     try (Timer0.Context ctx = metrics.queryAccountsLatency.start()) {
-      try {
-        // For performance reasons we don't use AccountQueryProvider as it would always load the
-        // complete account from the cache (or worse, from NoteDb) even though we only need the ID
-        // which we can directly get from the returned results.
-        Predicate<AccountState> pred =
-            Predicate.and(
-                AccountPredicates.isActive(),
-                accountQueryBuilder.defaultQuery(suggestReviewers.getQuery()));
-        logger.atFine().log("accounts index query: %s", pred);
-        ResultSet<FieldBundle> result =
-            accountIndexes
-                .getSearchIndex()
-                .getSource(
-                    pred,
-                    QueryOptions.create(
-                        indexConfig,
-                        0,
-                        suggestReviewers.getLimit() * CANDIDATE_LIST_MULTIPLIER,
-                        ImmutableSet.of(AccountField.ID.getName())))
-                .readRaw();
-        List<Account.Id> matches =
-            result.toList().stream()
-                .map(f -> new Account.Id(f.getValue(AccountField.ID).intValue()))
-                .collect(toList());
-        logger.atFine().log("Matches: %s", matches);
-        return matches;
-      } catch (QueryParseException e) {
+      // For performance reasons we don't use AccountQueryProvider as it would always load the
+      // complete account from the cache (or worse, from NoteDb) even though we only need the ID
+      // which we can directly get from the returned results.
+      Predicate<AccountState> pred =
+          Predicate.and(
+              AccountPredicates.isActive(),
+              accountQueryBuilder.defaultQuery(suggestReviewers.getQuery()));
+      logger.atFine().log("accounts index query: %s", pred);
+      accountIndexRewriter.validateMaxTermsInQuery(pred);
+      boolean useLegacyNumericFields =
+          accountIndexes.getSearchIndex().getSchema().useLegacyNumericFields();
+      FieldDef<AccountState, ?> idField =
+          useLegacyNumericFields ? AccountField.ID : AccountField.ID_STR;
+      ResultSet<FieldBundle> result =
+          accountIndexes
+              .getSearchIndex()
+              .getSource(
+                  pred,
+                  QueryOptions.create(
+                      indexConfig,
+                      0,
+                      suggestReviewers.getLimit(),
+                      ImmutableSet.of(idField.getName())))
+              .readRaw();
+      List<Account.Id> matches =
+          result.toList().stream()
+              .map(f -> fromIdField(f, useLegacyNumericFields))
+              .collect(toList());
+      logger.atFine().log("Matches: %s", matches);
+      return matches;
+    } catch (TooManyTermsInQueryException e) {
+      throw new BadRequestException(e.getMessage());
+    } catch (QueryParseException e) {
+      logger.atWarning().withCause(e).log("Suggesting accounts failed, return empty result.");
+      return ImmutableList.of();
+    } catch (StorageException e) {
+      if (e.getCause() instanceof TooManyTermsInQueryException) {
+        throw new BadRequestException(e.getMessage());
+      }
+      if (e.getCause() instanceof QueryParseException) {
         return ImmutableList.of();
       }
+      throw e;
     }
   }
 
@@ -286,6 +315,7 @@
   }
 
   private List<Account.Id> recommendAccounts(
+      ReviewerState reviewerState,
       @Nullable ChangeNotes changeNotes,
       SuggestReviewers suggestReviewers,
       ProjectState projectState,
@@ -293,7 +323,7 @@
       throws IOException, ConfigInvalidException {
     try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) {
       return reviewerRecommender.suggestReviewers(
-          changeNotes, suggestReviewers, projectState, candidateList);
+          reviewerState, changeNotes, suggestReviewers, projectState, candidateList);
     }
   }
 
@@ -405,7 +435,7 @@
 
       // require that at least one member in the group can see the change
       for (Account account : members) {
-        if (visibilityControl.isVisibleTo(account.getId())) {
+        if (visibilityControl.isVisibleTo(account.id())) {
           if (needsConfirmation) {
             result.allowedWithConfirmation = true;
           } else {
diff --git a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
index a41143c..ac0945d 100644
--- a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.Account;
 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.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
 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/change/Revisions.java b/java/com/google/gerrit/server/restapi/change/Revisions.java
index c5cce4f..7ca989c 100644
--- a/java/com/google/gerrit/server/restapi/change/Revisions.java
+++ b/java/com/google/gerrit/server/restapi/change/Revisions.java
@@ -16,14 +16,15 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.PatchSet;
 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.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.RevisionResource;
@@ -36,11 +37,13 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
 
 @Singleton
 public class Revisions implements ChildCollection<ChangeResource, RevisionResource> {
@@ -121,42 +124,46 @@
     } else if (id.length() < 6 && id.matches("^[1-9][0-9]{0,4}$")) {
       // Legacy patch set number syntax.
       return byLegacyPatchSetId(change, id);
-    } else if (id.length() < 4 || id.length() > RevId.LEN) {
+    } else if (id.length() < 4 || id.length() > ObjectIds.STR_LEN) {
       // Require a minimum of 4 digits.
       // Impossibly long identifier will never match.
       return Collections.emptyList();
     } else {
       List<RevisionResource> out = new ArrayList<>();
       for (PatchSet ps : psUtil.byChange(change.getNotes())) {
-        if (ps.getRevision() != null && ps.getRevision().get().startsWith(id)) {
+        if (ObjectIds.matchesAbbreviation(ps.commitId(), id)) {
           out.add(new RevisionResource(change, ps));
         }
       }
       // Not an existing patch set on a change, but might be an edit.
-      if (out.isEmpty() && id.length() == RevId.LEN) {
-        return loadEdit(change, new RevId(id));
+      if (out.isEmpty() && ObjectId.isId(id)) {
+        return loadEdit(change, ObjectId.fromString(id));
       }
       return out;
     }
   }
 
   private List<RevisionResource> byLegacyPatchSetId(ChangeResource change, String id) {
-    PatchSet ps =
-        psUtil.get(change.getNotes(), new PatchSet.Id(change.getId(), Integer.parseInt(id)));
+    PatchSet ps = psUtil.get(change.getNotes(), PatchSet.id(change.getId(), Integer.parseInt(id)));
     if (ps != null) {
       return Collections.singletonList(new RevisionResource(change, ps));
     }
     return Collections.emptyList();
   }
 
-  private List<RevisionResource> loadEdit(ChangeResource change, RevId revid)
+  private List<RevisionResource> loadEdit(ChangeResource change, @Nullable ObjectId commitId)
       throws AuthException, IOException {
     Optional<ChangeEdit> edit = editUtil.byChange(change.getNotes(), change.getUser());
     if (edit.isPresent()) {
-      PatchSet ps = new PatchSet(new PatchSet.Id(change.getId(), 0));
-      RevId editRevId = new RevId(ObjectId.toString(edit.get().getEditCommit()));
-      ps.setRevision(editRevId);
-      if (revid == null || editRevId.equals(revid)) {
+      RevCommit editCommit = edit.get().getEditCommit();
+      PatchSet ps =
+          PatchSet.builder()
+              .id(PatchSet.id(change.getId(), 0))
+              .commitId(editCommit)
+              .uploader(change.getUser().getAccountId())
+              .createdOn(new Timestamp(editCommit.getCommitterIdent().getWhen().getTime()))
+              .build();
+      if (commitId == null || editCommit.equals(commitId)) {
         return Collections.singletonList(new RevisionResource(change, ps, edit));
       }
     }
diff --git a/java/com/google/gerrit/server/restapi/change/RobotComments.java b/java/com/google/gerrit/server/restapi/change/RobotComments.java
index 1aa8c2a..9f81d0a 100644
--- a/java/com/google/gerrit/server/restapi/change/RobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/RobotComments.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.registration.DynamicMap;
 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.reviewdb.client.RobotComment;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.RobotCommentResource;
@@ -59,7 +59,7 @@
     String uuid = id.get();
     ChangeNotes notes = rev.getNotes();
 
-    for (RobotComment c : commentsUtil.robotCommentsByPatchSet(notes, rev.getPatchSet().getId())) {
+    for (RobotComment c : commentsUtil.robotCommentsByPatchSet(notes, rev.getPatchSet().id())) {
       if (uuid.equals(c.key.uuid)) {
         return new RobotCommentResource(rev, c);
       }
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index aacf58b..288806c 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -17,12 +17,12 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.NotifyResolver;
@@ -39,7 +39,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class SetReadyForReview extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
+public class SetReadyForReview extends RetryingRestModifyView<ChangeResource, Input, String>
     implements UiAction<ChangeResource> {
   private final WorkInProgressOp.Factory opFactory;
 
@@ -50,7 +50,7 @@
   }
 
   @Override
-  protected Response<?> applyImpl(
+  protected Response<String> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
diff --git a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
index 852813e..3fb0295 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -17,12 +17,12 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.NotifyResolver;
@@ -39,7 +39,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class SetWorkInProgress extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
+public class SetWorkInProgress extends RetryingRestModifyView<ChangeResource, Input, String>
     implements UiAction<ChangeResource> {
   private final WorkInProgressOp.Factory opFactory;
 
@@ -50,7 +50,7 @@
   }
 
   @Override
-  protected Response<?> applyImpl(
+  protected Response<String> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 596769b..137eb32 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -14,29 +14,32 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -50,11 +53,9 @@
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -109,7 +110,6 @@
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
   private final ChangeData.Factory changeDataFactory;
-  private final ChangeNotes.Factory changeNotesFactory;
   private final Provider<MergeOp> mergeOpProvider;
   private final Provider<MergeSuperSet> mergeSuperSet;
   private final AccountResolver accountResolver;
@@ -129,7 +129,6 @@
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
       ChangeData.Factory changeDataFactory,
-      ChangeNotes.Factory changeNotesFactory,
       Provider<MergeOp> mergeOpProvider,
       Provider<MergeSuperSet> mergeSuperSet,
       AccountResolver accountResolver,
@@ -140,7 +139,6 @@
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
     this.changeDataFactory = changeDataFactory;
-    this.changeNotesFactory = changeNotesFactory;
     this.mergeOpProvider = mergeOpProvider;
     this.mergeSuperSet = mergeSuperSet;
     this.accountResolver = accountResolver;
@@ -175,7 +173,7 @@
   }
 
   @Override
-  public Output apply(RevisionResource rsrc, SubmitInput input)
+  public Response<Output> apply(RevisionResource rsrc, SubmitInput input)
       throws RestApiException, RepositoryNotFoundException, IOException, PermissionBackendException,
           UpdateException, ConfigInvalidException {
     input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf);
@@ -188,44 +186,48 @@
     }
     projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
-    return new Output(mergeChange(rsrc, submitter, input));
+    return mergeChange(rsrc, submitter, input);
   }
 
-  public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
-      throws RestApiException, IOException, UpdateException, ConfigInvalidException,
-          PermissionBackendException {
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public Response<Output> mergeChange(
+      RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
+      throws RestApiException, IOException {
     Change change = rsrc.getChange();
     if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) {
       throw new ResourceConflictException(
-          String.format("destination branch \"%s\" not found.", change.getDest().get()));
-    } else if (!rsrc.getPatchSet().getId().equals(change.currentPatchSetId())) {
+          String.format("destination branch \"%s\" not found.", change.getDest().branch()));
+    } else if (!rsrc.getPatchSet().id().equals(change.currentPatchSetId())) {
       // TODO Allow submitting non-current revision by changing the current.
       throw new ResourceConflictException(
           String.format(
-              "revision %s is not current revision", rsrc.getPatchSet().getRevision().get()));
+              "revision %s is not current revision", rsrc.getPatchSet().commitId().name()));
     }
 
     try (MergeOp op = mergeOpProvider.get()) {
-      op.merge(change, submitter, true, input, false);
-    }
+      Change updatedChange;
 
-    // Read the ChangeNotes only after MergeOp is fully done (including MergeOp#close) to be sure
-    // to have the correct state of the repo.
-    try (ReadonlyRequestWindow readonlyRequestWindow = PerThreadCache.openReadonlyRequestWindow()) {
-      change = changeNotesFactory.createChecked(change.getProject(), change.getId()).getChange();
-    } catch (NoSuchChangeException e) {
-      throw new ResourceConflictException("change is deleted", e);
-    }
+      try (ReadonlyRequestWindow readonlyRequestWindow =
+          PerThreadCache.openReadonlyRequestWindow()) {
+        updatedChange = op.merge(change, submitter, true, input, false);
+      } catch (Exception e) {
+        Throwables.throwIfInstanceOf(e, RestApiException.class);
+        return Response.<Output>internalServerError(e).traceId(op.getTraceId().orElse(null));
+      }
 
-    if (change.isMerged()) {
-      return change;
+      if (updatedChange.isMerged()) {
+        return Response.ok(new Output(updatedChange));
+      }
+
+      String msg =
+          String.format(
+              "change %s of project %s unexpectedly had status %s after submit attempt",
+              updatedChange.getId(), updatedChange.getProject(), updatedChange.getStatus());
+      logger.atWarning().log(msg);
+      throw new RestApiException(msg);
     }
-    if (change.isNew()) {
-      throw new RestApiException("change unexpectedly had status NEW after submit attempt");
-    }
-    throw new ResourceConflictException("change is " + ChangeUtil.status(change));
   }
 
   /**
@@ -358,12 +360,11 @@
           .setVisible(true)
           .setEnabled(Boolean.TRUE.equals(enabled));
     }
-    RevId revId = resource.getPatchSet().getRevision();
     Map<String, String> params =
         ImmutableMap.of(
-            "patchSet", String.valueOf(resource.getPatchSet().getPatchSetId()),
-            "branch", change.getDest().getShortName(),
-            "commit", ObjectId.fromString(revId.get()).abbreviate(7).name(),
+            "patchSet", String.valueOf(resource.getPatchSet().number()),
+            "branch", change.getDest().shortName(),
+            "commit", abbreviateName(resource.getPatchSet().commitId()),
             "submitSize", String.valueOf(cs.size()));
     ParameterizedString tp = cs.size() > 1 ? titlePatternWithAncestors : titlePattern;
     return new UiAction.Description()
@@ -379,10 +380,10 @@
       mergeabilityMap.add(change);
     }
 
-    ListMultimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch();
-    for (Branch.NameKey branch : cbb.keySet()) {
+    ListMultimap<BranchNameKey, ChangeData> cbb = cs.changesByBranch();
+    for (BranchNameKey branch : cbb.keySet()) {
       Collection<ChangeData> targetBranch = cbb.get(branch);
-      HashMap<Change.Id, RevCommit> commits = findCommits(targetBranch, branch.getParentKey());
+      HashMap<Change.Id, RevCommit> commits = findCommits(targetBranch, branch.project());
 
       Set<ObjectId> allParents = Sets.newHashSetWithExpectedSize(cs.size());
       for (RevCommit commit : commits.values()) {
@@ -427,9 +428,7 @@
     try (Repository repo = repoManager.openRepository(project);
         RevWalk walk = new RevWalk(repo)) {
       for (ChangeData change : changes) {
-        RevCommit commit =
-            walk.parseCommit(
-                ObjectId.fromString(psUtil.current(change.notes()).getRevision().get()));
+        RevCommit commit = walk.parseCommit(psUtil.current(change.notes()).commitId());
         commits.put(change.getId(), commit);
       }
     }
@@ -468,16 +467,20 @@
     }
 
     @Override
-    public ChangeInfo apply(ChangeResource rsrc, SubmitInput input)
-        throws RestApiException, RepositoryNotFoundException, IOException,
-            PermissionBackendException, UpdateException, ConfigInvalidException {
+    public Response<ChangeInfo> apply(ChangeResource rsrc, SubmitInput input) throws Exception {
       PatchSet ps = psUtil.current(rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
       }
 
-      Output out = submit.apply(new RevisionResource(rsrc, ps), input);
-      return json.noOptions().format(out.change);
+      Response<Output> response = submit.apply(new RevisionResource(rsrc, ps), input);
+      if (response instanceof Response.InternalServerError) {
+        Response.InternalServerError<?> ise = (Response.InternalServerError<?>) response;
+        return Response.<ChangeInfo>internalServerError(ise.cause())
+            .traceId(ise.traceId().orElse(null));
+      }
+
+      return Response.ok(json.noOptions().format(response.value().change));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
index cefbfdb..214a001 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -19,6 +19,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
@@ -26,8 +27,8 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.WalkSorter;
@@ -58,7 +59,8 @@
       EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.SUBMITTABLE);
 
   private static final Comparator<ChangeData> COMPARATOR =
-      Comparator.comparing(ChangeData::project).thenComparing(cd -> cd.getId().id, reverseOrder());
+      Comparator.comparing(ChangeData::project)
+          .thenComparing(cd -> cd.getId().get(), reverseOrder());
 
   private final ChangeJson.Factory json;
   private final Provider<InternalChangeQuery> queryProvider;
@@ -107,14 +109,14 @@
   }
 
   @Override
-  public Object apply(ChangeResource resource)
+  public Response<Object> apply(ChangeResource resource)
       throws AuthException, BadRequestException, ResourceConflictException, IOException,
           PermissionBackendException {
     SubmittedTogetherInfo info = applyInfo(resource);
     if (options.isEmpty()) {
-      return info.changes;
+      return Response.ok(info.changes);
     }
-    return info;
+    return Response.ok(info);
   }
 
   public SubmittedTogetherInfo applyInfo(ChangeResource resource)
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
index f5a2751..d247b5b 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 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.RestReadView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
@@ -38,15 +40,31 @@
 public class SuggestChangeReviewers extends SuggestReviewers
     implements RestReadView<ChangeResource> {
 
+  private final PermissionBackend permissionBackend;
+  private final Provider<CurrentUser> self;
+  private final ProjectCache projectCache;
+
+  private boolean excludeGroups;
+  private ReviewerState reviewerState = ReviewerState.REVIEWER;
+
   @Option(
       name = "--exclude-groups",
       aliases = {"-e"},
       usage = "exclude groups from query")
-  boolean excludeGroups;
+  public SuggestChangeReviewers setExcludeGroups(boolean excludeGroups) {
+    this.excludeGroups = excludeGroups;
+    return this;
+  }
 
-  private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> self;
-  private final ProjectCache projectCache;
+  @Option(
+      name = "--reviewer-state",
+      usage =
+          "The type of reviewers that should be suggested"
+              + " (can be 'REVIEWER' or 'CC', default is 'REVIEWER')")
+  public SuggestChangeReviewers setReviewerState(ReviewerState reviewerState) {
+    this.reviewerState = reviewerState;
+    return this;
+  }
 
   @Inject
   SuggestChangeReviewers(
@@ -63,18 +81,24 @@
   }
 
   @Override
-  public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
+  public Response<List<SuggestedReviewerInfo>> apply(ChangeResource rsrc)
       throws AuthException, BadRequestException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    return reviewersUtil.suggestReviewers(
-        rsrc.getNotes(),
-        this,
-        projectCache.checkedGet(rsrc.getProject()),
-        getVisibility(rsrc),
-        excludeGroups);
+    if (reviewerState.equals(ReviewerState.REMOVED)) {
+      throw new BadRequestException(
+          String.format("Unsupported reviewer state: %s", ReviewerState.REMOVED));
+    }
+    return Response.ok(
+        reviewersUtil.suggestReviewers(
+            reviewerState,
+            rsrc.getNotes(),
+            this,
+            projectCache.checkedGet(rsrc.getProject()),
+            getVisibility(rsrc),
+            excludeGroups));
   }
 
   private VisibilityControl getVisibility(ChangeResource rsrc) {
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
index 370bdb2..bae2e52 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
@@ -15,9 +15,6 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.MoreObjects;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.TestSubmitRuleInfo;
@@ -25,30 +22,26 @@
 import com.google.gerrit.extensions.common.TestSubmitRuleInput.Filters;
 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.RestModifyView;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.rules.DefaultSubmitRule;
+import com.google.gerrit.server.rules.PrologOptions;
 import com.google.gerrit.server.rules.PrologRule;
 import com.google.gerrit.server.rules.RulesCache;
 import com.google.inject.Inject;
 import java.util.LinkedHashMap;
-import java.util.List;
 import org.kohsuke.args4j.Option;
 
 public class TestSubmitRule implements RestModifyView<RevisionResource, TestSubmitRuleInput> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private final ChangeData.Factory changeDataFactory;
   private final RulesCache rules;
   private final AccountLoader.Factory accountInfoFactory;
   private final ProjectCache projectCache;
-  private final DefaultSubmitRule defaultSubmitRule;
   private final PrologRule prologRule;
 
   @Option(name = "--filters", usage = "impact of filters in parent projects")
@@ -60,56 +53,41 @@
       RulesCache rules,
       AccountLoader.Factory infoFactory,
       ProjectCache projectCache,
-      DefaultSubmitRule defaultSubmitRule,
       PrologRule prologRule) {
     this.changeDataFactory = changeDataFactory;
     this.rules = rules;
     this.accountInfoFactory = infoFactory;
     this.projectCache = projectCache;
-    this.defaultSubmitRule = defaultSubmitRule;
     this.prologRule = prologRule;
   }
 
   @Override
-  public List<TestSubmitRuleInfo> apply(RevisionResource rsrc, TestSubmitRuleInput input)
+  public Response<TestSubmitRuleInfo> apply(RevisionResource rsrc, TestSubmitRuleInput input)
       throws AuthException, PermissionBackendException, BadRequestException {
     if (input == null) {
       input = new TestSubmitRuleInput();
     }
-    if (input.rule != null && !rules.isProjectRulesEnabled()) {
+    if (input.rule == null) {
+      throw new BadRequestException("rule is required");
+    }
+    if (!rules.isProjectRulesEnabled()) {
       throw new AuthException("project rules are disabled");
     }
     input.filters = MoreObjects.firstNonNull(input.filters, filters);
 
-    SubmitRuleOptions opts =
-        SubmitRuleOptions.builder()
-            .skipFilters(input.filters == Filters.SKIP)
-            .rule(input.rule)
-            .logErrors(logger.atFine().isEnabled())
-            .build();
-
     ProjectState projectState = projectCache.get(rsrc.getProject());
     if (projectState == null) {
       throw new BadRequestException("project not found");
     }
     ChangeData cd = changeDataFactory.create(rsrc.getNotes());
-    List<SubmitRecord> records;
-    if (projectState.hasPrologRules() || input.rule != null) {
-      records = ImmutableList.copyOf(prologRule.evaluate(cd, opts));
-    } else {
-      // No rules were provided as input and we have no rules.pl. This means we are supposed to run
-      // the default rules. Nowadays, the default rules are implemented in Java, not Prolog.
-      // Therefore, we call the DefaultRuleEvaluator instead.
-      records = ImmutableList.copyOf(defaultSubmitRule.evaluate(cd, opts));
-    }
+    SubmitRecord record =
+        prologRule.evaluate(
+            cd, PrologOptions.dryRunOptions(input.rule, input.filters == Filters.SKIP));
 
-    List<TestSubmitRuleInfo> out = Lists.newArrayListWithCapacity(records.size());
     AccountLoader accounts = accountInfoFactory.create(true);
-    for (SubmitRecord r : records) {
-      out.add(newSubmitRuleInfo(r, accounts));
-    }
+    TestSubmitRuleInfo out = newSubmitRuleInfo(record, accounts);
     accounts.fill();
-    return out;
+    return Response.ok(out);
   }
 
   private static TestSubmitRuleInfo newSubmitRuleInfo(SubmitRecord r, AccountLoader accounts) {
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
index 4c7fad2..cb52fcb 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
@@ -15,83 +15,92 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.MoreObjects;
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.TestSubmitRuleInput;
 import com.google.gerrit.extensions.common.TestSubmitRuleInput.Filters;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.PrologOptions;
+import com.google.gerrit.server.rules.PrologRule;
 import com.google.gerrit.server.rules.RulesCache;
 import com.google.inject.Inject;
 import org.kohsuke.args4j.Option;
 
 public class TestSubmitType implements RestModifyView<RevisionResource, TestSubmitRuleInput> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private final ChangeData.Factory changeDataFactory;
   private final RulesCache rules;
-  private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
+  private final PrologRule prologRule;
 
   @Option(name = "--filters", usage = "impact of filters in parent projects")
   private Filters filters = Filters.RUN;
 
   @Inject
-  TestSubmitType(
-      ChangeData.Factory changeDataFactory,
-      RulesCache rules,
-      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
+  TestSubmitType(ChangeData.Factory changeDataFactory, RulesCache rules, PrologRule prologRule) {
     this.changeDataFactory = changeDataFactory;
     this.rules = rules;
-    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
+    this.prologRule = prologRule;
   }
 
   @Override
-  public SubmitType apply(RevisionResource rsrc, TestSubmitRuleInput input)
+  public Response<SubmitType> apply(RevisionResource rsrc, TestSubmitRuleInput input)
       throws AuthException, BadRequestException {
     if (input == null) {
       input = new TestSubmitRuleInput();
     }
-    if (input.rule != null && !rules.isProjectRulesEnabled()) {
+    if (input.rule == null) {
+      throw new BadRequestException("rule is required");
+    }
+    if (!rules.isProjectRulesEnabled()) {
       throw new AuthException("project rules are disabled");
     }
     input.filters = MoreObjects.firstNonNull(input.filters, filters);
 
-    SubmitRuleOptions opts =
-        SubmitRuleOptions.builder()
-            .logErrors(logger.atFine().isEnabled())
-            .skipFilters(input.filters == Filters.SKIP)
-            .rule(input.rule)
-            .build();
-
-    SubmitRuleEvaluator evaluator = submitRuleEvaluatorFactory.create(opts);
     ChangeData cd = changeDataFactory.create(rsrc.getNotes());
-    SubmitTypeRecord rec = evaluator.getSubmitType(cd);
+    SubmitTypeRecord rec =
+        prologRule.getSubmitType(
+            cd, PrologOptions.dryRunOptions(input.rule, input.filters == Filters.SKIP));
 
     if (rec.status != SubmitTypeRecord.Status.OK) {
       throw new BadRequestException(String.format("rule produced invalid result: %s", rec));
     }
 
-    return rec.type;
+    return Response.ok(rec.type);
   }
 
   public static class Get implements RestReadView<RevisionResource> {
-    private final TestSubmitType test;
+    private final ChangeData.Factory changeDataFactory;
+    private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
 
     @Inject
-    Get(TestSubmitType test) {
-      this.test = test;
+    Get(
+        ChangeData.Factory changeDataFactory,
+        SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
+      this.changeDataFactory = changeDataFactory;
+      this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
     }
 
     @Override
-    public SubmitType apply(RevisionResource resource) throws AuthException, BadRequestException {
-      return test.apply(resource, null);
+    public Response<SubmitType> apply(RevisionResource resource)
+        throws AuthException, ResourceConflictException {
+      SubmitRuleEvaluator evaluator =
+          submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults());
+      ChangeData cd = changeDataFactory.create(resource.getNotes());
+      SubmitTypeRecord rec = evaluator.getSubmitType(cd);
+
+      if (rec.status != SubmitTypeRecord.Status.OK) {
+        throw new ResourceConflictException(String.format("rule produced invalid result: %s", rec));
+      }
+
+      return Response.ok(rec.type);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Votes.java b/java/com/google/gerrit/server/restapi/change/Votes.java
index 31efe54..d899002 100644
--- a/java/com/google/gerrit/server/restapi/change/Votes.java
+++ b/java/com/google/gerrit/server/restapi/change/Votes.java
@@ -14,15 +14,16 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.entities.PatchSetApproval;
 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.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.VoteResource;
@@ -71,7 +72,8 @@
     }
 
     @Override
-    public Map<String, Short> apply(ReviewerResource rsrc) throws MethodNotAllowedException {
+    public Response<Map<String, Short>> apply(ReviewerResource rsrc)
+        throws MethodNotAllowedException {
       if (rsrc.getRevisionResource() != null && !rsrc.getRevisionResource().isCurrent()) {
         throw new MethodNotAllowedException("Cannot list votes on non-current patch set");
       }
@@ -85,9 +87,9 @@
               null,
               null);
       for (PatchSetApproval psa : byPatchSetUser) {
-        votes.put(psa.getLabel(), psa.getValue());
+        votes.put(psa.label(), psa.value());
       }
-      return votes;
+      return Response.ok(votes);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/CheckConsistency.java b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
index 61d5c79..50e774a 100644
--- a/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
+++ b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.CheckGroupsResultInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
 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.account.AccountsConsistencyChecker;
@@ -54,7 +55,7 @@
   }
 
   @Override
-  public ConsistencyCheckInfo apply(ConfigResource resource, ConsistencyCheckInput input)
+  public Response<ConsistencyCheckInfo> apply(ConfigResource resource, ConsistencyCheckInput input)
       throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
 
@@ -80,6 +81,6 @@
           new CheckGroupsResultInfo(groupsConsistencyChecker.check());
     }
 
-    return consistencyCheckInfo;
+    return Response.ok(consistencyCheckInfo);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java b/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
index b6dcd35..b56f1b8 100644
--- a/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
diff --git a/java/com/google/gerrit/server/restapi/config/GetCache.java b/java/com/google/gerrit/server/restapi/config/GetCache.java
index 5abaf1e..93600ea 100644
--- a/java/com/google/gerrit/server/restapi/config/GetCache.java
+++ b/java/com/google/gerrit/server/restapi/config/GetCache.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.CacheResource;
 import com.google.inject.Singleton;
@@ -22,7 +23,7 @@
 public class GetCache implements RestReadView<CacheResource> {
 
   @Override
-  public ListCaches.CacheInfo apply(CacheResource rsrc) {
-    return new ListCaches.CacheInfo(rsrc.getName(), rsrc.getCache());
+  public Response<ListCaches.CacheInfo> apply(CacheResource rsrc) {
+    return Response.ok(new ListCaches.CacheInfo(rsrc.getName(), rsrc.getCache()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java b/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
index 13c2818..44c71b3 100644
--- a/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
@@ -17,8 +17,9 @@
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.Preferences;
+import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -41,10 +42,10 @@
   }
 
   @Override
-  public DiffPreferencesInfo apply(ConfigResource configResource)
+  public Response<DiffPreferencesInfo> apply(ConfigResource configResource)
       throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
     try (Repository git = gitManager.openRepository(allUsersName)) {
-      return Preferences.readDefaultDiffPreferences(allUsersName, git);
+      return Response.ok(StoredPreferences.readDefaultDiffPreferences(allUsersName, git));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java b/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
index 2ec547b..a5ab967 100644
--- a/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
@@ -17,8 +17,9 @@
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.Preferences;
+import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -40,10 +41,10 @@
   }
 
   @Override
-  public EditPreferencesInfo apply(ConfigResource configResource)
+  public Response<EditPreferencesInfo> apply(ConfigResource configResource)
       throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
     try (Repository git = gitManager.openRepository(allUsersName)) {
-      return Preferences.readDefaultEditPreferences(allUsersName, git);
+      return Response.ok(StoredPreferences.readDefaultEditPreferences(allUsersName, git));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetPreferences.java b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
index 4dbbc8c..8da9134 100644
--- a/java/com/google/gerrit/server/restapi/config/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
@@ -15,8 +15,9 @@
 package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.Preferences;
+import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -38,10 +39,10 @@
   }
 
   @Override
-  public GeneralPreferencesInfo apply(ConfigResource rsrc)
+  public Response<GeneralPreferencesInfo> apply(ConfigResource rsrc)
       throws IOException, ConfigInvalidException {
     try (Repository git = gitMgr.openRepository(allUsersName)) {
-      return Preferences.readDefaultGeneralPreferences(allUsersName, git);
+      return Response.ok(StoredPreferences.readDefaultGeneralPreferences(allUsersName, git));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index aa0e350..2d504c7 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.extensions.config.CloneCommand;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.server.EnableSignedPush;
@@ -133,7 +134,7 @@
   }
 
   @Override
-  public ServerInfo apply(ConfigResource rsrc) throws PermissionBackendException {
+  public Response<ServerInfo> apply(ConfigResource rsrc) throws PermissionBackendException {
     ServerInfo info = new ServerInfo();
     info.accounts = getAccountsInfo();
     info.auth = getAuthInfo();
@@ -148,7 +149,7 @@
 
     info.user = getUserInfo();
     info.receive = getReceiveInfo();
-    return info;
+    return Response.ok(info);
   }
 
   private AccountsInfo getAccountsInfo() {
@@ -225,7 +226,9 @@
             + "\u2026";
     info.updateDelay =
         (int) ConfigUtil.getTimeUnit(config, "change", null, "updateDelay", 300, TimeUnit.SECONDS);
-    info.submitWholeTopic = MergeSuperSet.wholeTopicEnabled(config);
+    info.submitWholeTopic = toBoolean(MergeSuperSet.wholeTopicEnabled(config));
+    info.excludeMergeableInChangeInfo =
+        toBoolean(this.config.getBoolean("change", "api", "excludeMergeableInChangeInfo", false));
     info.disablePrivateChanges =
         toBoolean(this.config.getBoolean("change", null, "disablePrivateChanges", false));
     return info;
diff --git a/java/com/google/gerrit/server/restapi/config/GetSummary.java b/java/com/google/gerrit/server/restapi/config/GetSummary.java
index 1d8da63..d0a1498 100644
--- a/java/com/google/gerrit/server/restapi/config/GetSummary.java
+++ b/java/com/google/gerrit/server/restapi/config/GetSummary.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.SitePath;
@@ -69,7 +70,7 @@
   }
 
   @Override
-  public SummaryInfo apply(ConfigResource rsrc) {
+  public Response<SummaryInfo> apply(ConfigResource rsrc) {
     if (gc) {
       System.gc();
       System.runFinalization();
@@ -83,7 +84,7 @@
     if (jvm) {
       summary.jvmSummary = getJvmSummary();
     }
-    return summary;
+    return Response.ok(summary);
   }
 
   private TaskSummaryInfo getTaskSummary() {
diff --git a/java/com/google/gerrit/server/restapi/config/GetTask.java b/java/com/google/gerrit/server/restapi/config/GetTask.java
index a32f3ba..513c99a 100644
--- a/java/com/google/gerrit/server/restapi/config/GetTask.java
+++ b/java/com/google/gerrit/server/restapi/config/GetTask.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.TaskResource;
 import com.google.inject.Singleton;
@@ -22,7 +23,7 @@
 public class GetTask implements RestReadView<TaskResource> {
 
   @Override
-  public ListTasks.TaskInfo apply(TaskResource rsrc) {
-    return new ListTasks.TaskInfo(rsrc.getTask());
+  public Response<ListTasks.TaskInfo> apply(TaskResource rsrc) {
+    return Response.ok(new ListTasks.TaskInfo(rsrc.getTask()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/IndexChanges.java b/java/com/google/gerrit/server/restapi/config/IndexChanges.java
index 6ba93e8..904c44f 100644
--- a/java/com/google/gerrit/server/restapi/config/IndexChanges.java
+++ b/java/com/google/gerrit/server/restapi/config/IndexChanges.java
@@ -51,7 +51,7 @@
   }
 
   @Override
-  public Object apply(ConfigResource resource, Input input) {
+  public Response<String> apply(ConfigResource resource, Input input) {
     if (input == null || input.changes == null) {
       return Response.ok("Nothing to index");
     }
diff --git a/java/com/google/gerrit/server/restapi/config/ListCaches.java b/java/com/google/gerrit/server/restapi/config/ListCaches.java
index f310ed7..ccafbe8 100644
--- a/java/com/google/gerrit/server/restapi/config/ListCaches.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCaches.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.cache.PersistentCache;
 import com.google.gerrit.server.config.ConfigResource;
@@ -69,21 +70,22 @@
   }
 
   @Override
-  public Object apply(ConfigResource rsrc) {
+  public Response<Object> apply(ConfigResource rsrc) {
     if (format == null) {
-      return getCacheInfos();
+      return Response.ok(getCacheInfos());
     }
     Stream<String> cacheNames =
         Streams.stream(cacheMap)
             .map(e -> cacheNameOf(e.getPluginName(), e.getExportName()))
             .sorted();
     if (OutputFormat.TEXT_LIST.equals(format)) {
-      return BinaryResult.create(cacheNames.collect(joining("\n")))
-          .base64()
-          .setContentType("text/plain")
-          .setCharacterEncoding(UTF_8);
+      return Response.ok(
+          BinaryResult.create(cacheNames.collect(joining("\n")))
+              .base64()
+              .setContentType("text/plain")
+              .setCharacterEncoding(UTF_8));
     }
-    return cacheNames.collect(toImmutableList());
+    return Response.ok(cacheNames.collect(toImmutableList()));
   }
 
   public enum CacheType {
diff --git a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
index cacbbf5..6c8bf74 100644
--- a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.CapabilityConstants;
 import com.google.gerrit.server.config.ConfigResource;
@@ -43,13 +44,14 @@
   }
 
   @Override
-  public Map<String, CapabilityInfo> apply(ConfigResource resource)
+  public Response<Map<String, CapabilityInfo>> apply(ConfigResource resource)
       throws ResourceNotFoundException, IllegalAccessException, NoSuchFieldException {
     permissionBackend.checkUsesDefaultCapabilities();
-    return ImmutableMap.<String, CapabilityInfo>builder()
-        .putAll(collectCoreCapabilities())
-        .putAll(collectPluginCapabilities())
-        .build();
+    return Response.ok(
+        ImmutableMap.<String, CapabilityInfo>builder()
+            .putAll(collectCoreCapabilities())
+            .putAll(collectPluginCapabilities())
+            .build());
   }
 
   public Map<String, CapabilityInfo> collectPluginCapabilities() {
diff --git a/java/com/google/gerrit/server/restapi/config/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
index 7402c15..6a3ca42 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -17,9 +17,10 @@
 import static java.util.Comparator.comparing;
 import static java.util.stream.Collectors.toList;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.WorkQueue;
@@ -62,7 +63,7 @@
   }
 
   @Override
-  public List<TaskInfo> apply(ConfigResource resource)
+  public Response<List<TaskInfo>> apply(ConfigResource resource)
       throws AuthException, PermissionBackendException {
     CurrentUser user = self.get();
     if (!user.isIdentifiedUser()) {
@@ -72,7 +73,7 @@
     List<TaskInfo> allTasks = getTasks();
     try {
       permissionBackend.user(user).check(GlobalPermission.VIEW_QUEUE);
-      return allTasks;
+      return Response.ok(allTasks);
     } catch (AuthException e) {
       // Fall through to filter tasks.
     }
@@ -83,7 +84,7 @@
       if (task.projectName != null) {
         Boolean visible = visibilityCache.get(task.projectName);
         if (visible == null) {
-          Project.NameKey nameKey = new Project.NameKey(task.projectName);
+          Project.NameKey nameKey = Project.nameKey(task.projectName);
           ProjectState state = projectCache.get(nameKey);
           if (state == null || !state.statePermitsRead()) {
             visible = false;
@@ -102,7 +103,7 @@
         }
       }
     }
-    return visibleTasks;
+    return Response.ok(visibleTasks);
   }
 
   private List<TaskInfo> getTasks() {
diff --git a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
index 0685a58..9ce7ffd 100644
--- a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
+++ b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.api.config.ConfigUpdateEntryInfo;
 import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.config.ConfigResource;
@@ -47,17 +48,18 @@
   }
 
   @Override
-  public Map<String, List<ConfigUpdateEntryInfo>> apply(ConfigResource resource, Input input)
-      throws RestApiException, PermissionBackendException {
+  public Response<Map<String, List<ConfigUpdateEntryInfo>>> apply(
+      ConfigResource resource, Input input) throws RestApiException, PermissionBackendException {
     permissions.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     Multimap<UpdateResult, ConfigUpdateEntry> updates = config.reloadConfig();
     if (updates.isEmpty()) {
-      return Collections.emptyMap();
+      return Response.ok(Collections.emptyMap());
     }
-    return updates.asMap().entrySet().stream()
-        .collect(
-            Collectors.toMap(
-                e -> e.getKey().name().toLowerCase(), e -> toEntryInfos(e.getValue())));
+    return Response.ok(
+        updates.asMap().entrySet().stream()
+            .collect(
+                Collectors.toMap(
+                    e -> e.getKey().name().toLowerCase(), e -> toEntryInfos(e.getValue()))));
   }
 
   private static List<ConfigUpdateEntryInfo> toEntryInfos(
diff --git a/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
index 068f332..96654a9 100644
--- a/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
@@ -21,9 +21,10 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.Preferences;
+import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -54,7 +55,8 @@
   }
 
   @Override
-  public DiffPreferencesInfo apply(ConfigResource configResource, DiffPreferencesInfo input)
+  public Response<DiffPreferencesInfo> apply(
+      ConfigResource configResource, DiffPreferencesInfo input)
       throws BadRequestException, IOException, ConfigInvalidException {
     if (input == null) {
       throw new BadRequestException("input must be provided");
@@ -64,9 +66,9 @@
     }
 
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
-      DiffPreferencesInfo updatedPrefs = Preferences.updateDefaultDiffPreferences(md, input);
+      DiffPreferencesInfo updatedPrefs = StoredPreferences.updateDefaultDiffPreferences(md, input);
       accountCache.evictAll();
-      return updatedPrefs;
+      return Response.ok(updatedPrefs);
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
index daca734..4bb420b 100644
--- a/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
@@ -21,9 +21,10 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.Preferences;
+import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -54,7 +55,8 @@
   }
 
   @Override
-  public EditPreferencesInfo apply(ConfigResource configResource, EditPreferencesInfo input)
+  public Response<EditPreferencesInfo> apply(
+      ConfigResource configResource, EditPreferencesInfo input)
       throws BadRequestException, IOException, ConfigInvalidException {
     if (input == null) {
       throw new BadRequestException("input must be provided");
@@ -64,9 +66,9 @@
     }
 
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
-      EditPreferencesInfo updatedPrefs = Preferences.updateDefaultEditPreferences(md, input);
+      EditPreferencesInfo updatedPrefs = StoredPreferences.updateDefaultEditPreferences(md, input);
       accountCache.evictAll();
-      return updatedPrefs;
+      return Response.ok(updatedPrefs);
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/config/SetPreferences.java b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
index 6a0c22b..c88c1119 100644
--- a/java/com/google/gerrit/server/restapi/config/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
@@ -21,9 +21,10 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.Preferences;
+import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -54,16 +55,17 @@
   }
 
   @Override
-  public GeneralPreferencesInfo apply(ConfigResource rsrc, GeneralPreferencesInfo input)
+  public Response<GeneralPreferencesInfo> apply(ConfigResource rsrc, GeneralPreferencesInfo input)
       throws BadRequestException, IOException, ConfigInvalidException {
     if (!hasSetFields(input)) {
       throw new BadRequestException("unsupported option");
     }
-    Preferences.validateMy(input.my);
+    StoredPreferences.validateMy(input.my);
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
-      GeneralPreferencesInfo updatedPrefs = Preferences.updateDefaultGeneralPreferences(md, input);
+      GeneralPreferencesInfo updatedPrefs =
+          StoredPreferences.updateDefaultGeneralPreferences(md, input);
       accountCache.evictAll();
-      return updatedPrefs;
+      return Response.ok(updatedPrefs);
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/config/TasksCollection.java b/java/com/google/gerrit/server/restapi/config/TasksCollection.java
index 9a7aa3a..837d071 100644
--- a/java/com/google/gerrit/server/restapi/config/TasksCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/TasksCollection.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
@@ -21,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.TaskResource;
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index b2b14a1..caff206 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -18,19 +18,19 @@
 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.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
@@ -111,7 +111,7 @@
   }
 
   @Override
-  public List<AccountInfo> apply(GroupResource resource, Input input)
+  public Response<List<AccountInfo>> apply(GroupResource resource, Input input)
       throws AuthException, NotInternalGroupException, UnprocessableEntityException, IOException,
           ConfigInvalidException, ResourceNotFoundException, PermissionBackendException {
     GroupDescription.Internal internalGroup =
@@ -130,7 +130,7 @@
         throw new UnprocessableEntityException(
             String.format("Account Inactive: %s", nameOrEmailOrId));
       }
-      newMemberIds.add(a.getId());
+      newMemberIds.add(a.id());
     }
 
     AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
@@ -139,14 +139,14 @@
     } catch (NoSuchGroupException e) {
       throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid), e);
     }
-    return toAccountInfoList(newMemberIds);
+    return Response.ok(toAccountInfoList(newMemberIds));
   }
 
   Account findAccount(String nameOrEmailOrId)
       throws UnprocessableEntityException, IOException, ConfigInvalidException {
     AccountResolver.Result result = accountResolver.resolve(nameOrEmailOrId);
     try {
-      return result.asUnique().getAccount();
+      return result.asUnique().account();
     } catch (UnresolvableAccountException e) {
       switch (authType) {
         case HTTP_LDAP:
@@ -193,7 +193,7 @@
       req.setSkipAuthentication(true);
       return accountCache
           .get(accountManager.authenticate(req).getAccountId())
-          .map(AccountState::getAccount);
+          .map(AccountState::account);
     } catch (AccountException e) {
       return Optional.empty();
     }
@@ -221,15 +221,14 @@
     }
 
     @Override
-    public AccountInfo apply(GroupResource resource, IdString id, Input input)
-        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, IOException,
-            ConfigInvalidException, PermissionBackendException {
+    public Response<AccountInfo> apply(GroupResource resource, IdString id, Input input)
+        throws Exception {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = id.get();
       try {
-        List<AccountInfo> list = put.apply(resource, in);
+        List<AccountInfo> list = put.apply(resource, in).value();
         if (list.size() == 1) {
-          return list.get(0);
+          return Response.created(list.get(0));
         }
         throw new IllegalStateException();
       } catch (UnprocessableEntityException e) {
@@ -248,7 +247,7 @@
     }
 
     @Override
-    public AccountInfo apply(MemberResource resource, Input input)
+    public Response<AccountInfo> apply(MemberResource resource, Input input)
         throws PermissionBackendException {
       // Do nothing, the user is already a member.
       return get.apply(resource);
diff --git a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
index 5c879e7..3fd3f29 100644
--- a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
@@ -19,17 +19,17 @@
 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.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResolver;
@@ -91,7 +91,7 @@
   }
 
   @Override
-  public List<GroupInfo> apply(GroupResource resource, Input input)
+  public Response<List<GroupInfo>> apply(GroupResource resource, Input input)
       throws NotInternalGroupException, AuthException, UnprocessableEntityException,
           ResourceNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
@@ -118,7 +118,7 @@
     } catch (NoSuchGroupException e) {
       throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid), e);
     }
-    return result;
+    return Response.ok(result);
   }
 
   private void addSubgroups(
@@ -142,15 +142,14 @@
     }
 
     @Override
-    public GroupInfo apply(GroupResource resource, IdString id, Input input)
-        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, IOException,
-            ConfigInvalidException, PermissionBackendException {
+    public Response<GroupInfo> apply(GroupResource resource, IdString id, Input input)
+        throws Exception {
       AddSubgroups.Input in = new AddSubgroups.Input();
       in.groups = ImmutableList.of(id.get());
       try {
-        List<GroupInfo> list = addSubgroups.apply(resource, in);
+        List<GroupInfo> list = addSubgroups.apply(resource, in).value();
         if (list.size() == 1) {
-          return list.get(0);
+          return Response.created(list.get(0));
         }
         throw new IllegalStateException();
       } catch (UnprocessableEntityException e) {
@@ -169,7 +168,7 @@
     }
 
     @Override
-    public GroupInfo apply(SubgroupResource resource, Input input)
+    public Response<GroupInfo> apply(SubgroupResource resource, Input input)
         throws PermissionBackendException {
       // Do nothing, the group is already included.
       return get.get().apply(resource);
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index eaedcd7..7c5e7ed 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -19,6 +19,8 @@
 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.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -29,12 +31,11 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.UserInitiated;
@@ -122,7 +123,7 @@
   }
 
   @Override
-  public GroupInfo apply(TopLevelResource resource, IdString id, GroupInput input)
+  public Response<GroupInfo> apply(TopLevelResource resource, IdString id, GroupInput input)
       throws AuthException, BadRequestException, UnprocessableEntityException,
           ResourceConflictException, IOException, ConfigInvalidException, ResourceNotFoundException,
           PermissionBackendException {
@@ -137,6 +138,16 @@
     AccountGroup.UUID ownerUuid = owner(input);
     CreateGroupArgs args = new CreateGroupArgs();
     args.setGroupName(name);
+    args.uuid = Strings.isNullOrEmpty(input.uuid) ? null : AccountGroup.UUID.parse(input.uuid);
+    if (args.uuid != null) {
+      if (!AccountGroup.isInternalGroup(args.uuid)) {
+        throw new BadRequestException(String.format("invalid group UUID '%s'", args.uuid.get()));
+      }
+      if (groupCache.get(args.uuid).isPresent()) {
+        throw new ResourceConflictException(
+            String.format("group with UUID '%s' already exists", args.uuid.get()));
+      }
+    }
     args.groupDescription = Strings.emptyToNull(input.description);
     args.visibleToAll = MoreObjects.firstNonNull(input.visibleToAll, defaultVisibleToAll);
     args.ownerGroupUuid = ownerUuid;
@@ -148,7 +159,7 @@
           throw new UnprocessableEntityException(
               String.format("Account Inactive: %s", nameOrEmailOrId));
         }
-        members.add(a.getId());
+        members.add(a.id());
       }
       args.initialMembers = members;
     } else {
@@ -165,7 +176,7 @@
       throw new ResourceConflictException(e.getMessage(), e);
     }
 
-    return json.format(new InternalGroupDescription(createGroup(args)));
+    return Response.created(json.format(new InternalGroupDescription(createGroup(args))));
   }
 
   private AccountGroup.UUID owner(GroupInput input) throws UnprocessableEntityException {
@@ -193,11 +204,13 @@
       }
     }
 
-    AccountGroup.Id groupId = new AccountGroup.Id(sequences.nextGroupId());
+    AccountGroup.Id groupId = AccountGroup.id(sequences.nextGroupId());
     AccountGroup.UUID uuid =
-        GroupUUID.make(
-            createGroupArgs.getGroupName(),
-            self.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone()));
+        MoreObjects.firstNonNull(
+            createGroupArgs.uuid,
+            GroupUUID.make(
+                createGroupArgs.getGroupName(),
+                self.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone())));
     InternalGroupCreation groupCreation =
         InternalGroupCreation.builder()
             .setGroupUUID(uuid)
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
index b5771a3..b9fd000 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
@@ -16,6 +16,8 @@
 
 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.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -23,8 +25,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.GroupControl;
@@ -68,7 +68,7 @@
 
     Set<Account.Id> membersToRemove = new HashSet<>();
     for (String nameOrEmail : input.members) {
-      membersToRemove.add(accountResolver.resolve(nameOrEmail).asUnique().getAccount().getId());
+      membersToRemove.add(accountResolver.resolve(nameOrEmail).asUnique().account().id());
     }
     AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
     try {
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
index 9ecfa1f..b9d6ca8 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
@@ -17,6 +17,7 @@
 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.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -24,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResolver;
diff --git a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
index 1a781d9..508547d 100644
--- a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
@@ -17,15 +17,16 @@
 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.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.GroupAuditEventInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
@@ -74,7 +75,7 @@
   }
 
   @Override
-  public List<? extends GroupAuditEventInfo> apply(GroupResource rsrc)
+  public Response<List<? extends GroupAuditEventInfo>> apply(GroupResource rsrc)
       throws AuthException, NotInternalGroupException, IOException, ConfigInvalidException,
           PermissionBackendException {
     GroupDescription.Internal group =
@@ -90,22 +91,24 @@
     try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
       for (AccountGroupMemberAudit auditEvent :
           groups.getMembersAudit(allUsersRepo, group.getGroupUUID())) {
-        AccountInfo member = accountLoader.get(auditEvent.getMemberId());
+        AccountInfo member = accountLoader.get(auditEvent.memberId());
 
         auditEvents.add(
             GroupAuditEventInfo.createAddUserEvent(
-                accountLoader.get(auditEvent.getAddedBy()), auditEvent.getAddedOn(), member));
+                accountLoader.get(auditEvent.addedBy()), auditEvent.addedOn(), member));
 
         if (!auditEvent.isActive()) {
           auditEvents.add(
               GroupAuditEventInfo.createRemoveUserEvent(
-                  accountLoader.get(auditEvent.getRemovedBy()), auditEvent.getRemovedOn(), member));
+                  accountLoader.get(auditEvent.removedBy().orElse(null)),
+                  auditEvent.removedOn(),
+                  member));
         }
       }
 
-      for (AccountGroupByIdAud auditEvent :
+      for (AccountGroupByIdAudit auditEvent :
           groups.getSubgroupsAudit(allUsersRepo, group.getGroupUUID())) {
-        AccountGroup.UUID includedGroupUUID = auditEvent.getIncludeUUID();
+        AccountGroup.UUID includedGroupUUID = auditEvent.includeUuid();
         Optional<InternalGroup> includedGroup = groupCache.get(includedGroupUUID);
         GroupInfo member;
         if (includedGroup.isPresent()) {
@@ -121,14 +124,14 @@
 
         auditEvents.add(
             GroupAuditEventInfo.createAddGroupEvent(
-                accountLoader.get(auditEvent.getAddedBy()),
-                auditEvent.getKey().getAddedOn(),
-                member));
+                accountLoader.get(auditEvent.addedBy()), auditEvent.addedOn(), member));
 
         if (!auditEvent.isActive()) {
           auditEvents.add(
               GroupAuditEventInfo.createRemoveGroupEvent(
-                  accountLoader.get(auditEvent.getRemovedBy()), auditEvent.getRemovedOn(), member));
+                  accountLoader.get(auditEvent.removedBy().orElse(null)),
+                  auditEvent.removedOn(),
+                  member));
         }
       }
     }
@@ -137,6 +140,6 @@
 
     // sort by date and then reverse so that the newest audit event comes first
     auditEvents.sort(comparing((GroupAuditEventInfo a) -> a.date).reversed());
-    return auditEvents;
+    return Response.ok(auditEvents);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetDescription.java b/java/com/google/gerrit/server/restapi/group/GetDescription.java
index c34fda7..b770281 100644
--- a/java/com/google/gerrit/server/restapi/group/GetDescription.java
+++ b/java/com/google/gerrit/server/restapi/group/GetDescription.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.inject.Singleton;
@@ -23,9 +24,9 @@
 @Singleton
 public class GetDescription implements RestReadView<GroupResource> {
   @Override
-  public String apply(GroupResource resource) throws NotInternalGroupException {
+  public Response<String> apply(GroupResource resource) throws NotInternalGroupException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
-    return Strings.nullToEmpty(group.getDescription());
+    return Response.ok(Strings.nullToEmpty(group.getDescription()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetDetail.java b/java/com/google/gerrit/server/restapi/group/GetDetail.java
index c757383..f6b8930 100644
--- a/java/com/google/gerrit/server/restapi/group/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/group/GetDetail.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -32,7 +33,7 @@
   }
 
   @Override
-  public GroupInfo apply(GroupResource rsrc) throws PermissionBackendException {
-    return json.format(rsrc);
+  public Response<GroupInfo> apply(GroupResource rsrc) throws PermissionBackendException {
+    return Response.ok(json.format(rsrc));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetGroup.java b/java/com/google/gerrit/server/restapi/group/GetGroup.java
index 3ae447b..4785d25 100644
--- a/java/com/google/gerrit/server/restapi/group/GetGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/GetGroup.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -31,7 +32,7 @@
   }
 
   @Override
-  public GroupInfo apply(GroupResource resource) throws PermissionBackendException {
-    return json.format(resource.getGroup());
+  public Response<GroupInfo> apply(GroupResource resource) throws PermissionBackendException {
+    return Response.ok(json.format(resource.getGroup()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetMember.java b/java/com/google/gerrit/server/restapi/group/GetMember.java
index 63a8a1b..8dbcd27 100644
--- a/java/com/google/gerrit/server/restapi/group/GetMember.java
+++ b/java/com/google/gerrit/server/restapi/group/GetMember.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.group.MemberResource;
@@ -32,10 +33,10 @@
   }
 
   @Override
-  public AccountInfo apply(MemberResource rsrc) throws PermissionBackendException {
+  public Response<AccountInfo> apply(MemberResource rsrc) throws PermissionBackendException {
     AccountLoader loader = infoFactory.create(true);
     AccountInfo info = loader.get(rsrc.getMember().getAccountId());
     loader.fill();
-    return info;
+    return Response.ok(info);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetName.java b/java/com/google/gerrit/server/restapi/group/GetName.java
index 8cc1fe0..131dbe4 100644
--- a/java/com/google/gerrit/server/restapi/group/GetName.java
+++ b/java/com/google/gerrit/server/restapi/group/GetName.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.group;
 
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.inject.Singleton;
@@ -22,7 +23,7 @@
 public class GetName implements RestReadView<GroupResource> {
 
   @Override
-  public String apply(GroupResource resource) {
-    return resource.getName();
+  public Response<String> apply(GroupResource resource) {
+    return Response.ok(resource.getName());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetOptions.java b/java/com/google/gerrit/server/restapi/group/GetOptions.java
index e5bfe30..5d8ba02 100644
--- a/java/com/google/gerrit/server/restapi/group/GetOptions.java
+++ b/java/com/google/gerrit/server/restapi/group/GetOptions.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.inject.Singleton;
@@ -23,7 +24,7 @@
 public class GetOptions implements RestReadView<GroupResource> {
 
   @Override
-  public GroupOptionsInfo apply(GroupResource resource) {
-    return GroupJson.createOptions(resource.getGroup());
+  public Response<GroupOptionsInfo> apply(GroupResource resource) {
+    return Response.ok(GroupJson.createOptions(resource.getGroup()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetOwner.java b/java/com/google/gerrit/server/restapi/group/GetOwner.java
index 10e1b23..e8bdfaa 100644
--- a/java/com/google/gerrit/server/restapi/group/GetOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/GetOwner.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
@@ -38,13 +39,13 @@
   }
 
   @Override
-  public GroupInfo apply(GroupResource resource)
+  public Response<GroupInfo> apply(GroupResource resource)
       throws NotInternalGroupException, ResourceNotFoundException, PermissionBackendException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     try {
       GroupControl c = controlFactory.validateFor(group.getOwnerGroupUUID());
-      return json.format(c.getGroup());
+      return Response.ok(json.format(c.getGroup()));
     } catch (NoSuchGroupException e) {
       throw new ResourceNotFoundException(group.getOwnerGroupUUID().get(), e);
     }
diff --git a/java/com/google/gerrit/server/restapi/group/GetSubgroup.java b/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
index 4466180..c209511 100644
--- a/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
+++ b/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.SubgroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -31,7 +32,7 @@
   }
 
   @Override
-  public GroupInfo apply(SubgroupResource rsrc) throws PermissionBackendException {
-    return json.format(rsrc.getMemberDescription());
+  public Response<GroupInfo> apply(SubgroupResource rsrc) throws PermissionBackendException {
+    return Response.ok(json.format(rsrc.getMemberDescription()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GroupJson.java b/java/com/google/gerrit/server/restapi/group/GroupJson.java
index 12b9d61..99c9df7 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupJson.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupJson.java
@@ -20,11 +20,11 @@
 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.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
diff --git a/java/com/google/gerrit/server/restapi/group/Index.java b/java/com/google/gerrit/server/restapi/group/Index.java
index 1267f7a..d64669f 100644
--- a/java/com/google/gerrit/server/restapi/group/Index.java
+++ b/java/com/google/gerrit/server/restapi/group/Index.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.restapi.group;
 
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index 7c52300..1802ea6 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -23,17 +23,18 @@
 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.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.GroupInfo;
 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.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
@@ -250,18 +251,16 @@
   }
 
   @Override
-  public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
-      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
+  public Response<SortedMap<String, GroupInfo>> apply(TopLevelResource resource) throws Exception {
     SortedMap<String, GroupInfo> output = new TreeMap<>();
     for (GroupInfo info : get()) {
       output.put(MoreObjects.firstNonNull(info.name, "Group " + Url.decode(info.id)), info);
       info.name = null;
     }
-    return output;
+    return Response.ok(output);
   }
 
-  public List<GroupInfo> get()
-      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
+  public List<GroupInfo> get() throws Exception {
     if (!Strings.isNullOrEmpty(suggest)) {
       return suggestGroups();
     }
@@ -279,7 +278,7 @@
     }
 
     if (user != null) {
-      return accountGetGroups.apply(new AccountResource(userFactory.create(user)));
+      return accountGetGroups.apply(new AccountResource(userFactory.create(user))).value();
     }
 
     return getAllGroups();
diff --git a/java/com/google/gerrit/server/restapi/group/ListMembers.java b/java/com/google/gerrit/server/restapi/group/ListMembers.java
index 75be44c..23f0aa7 100644
--- a/java/com/google/gerrit/server/restapi/group/ListMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/ListMembers.java
@@ -21,10 +21,11 @@
 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.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountInfoComparator;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.GroupCache;
@@ -65,14 +66,14 @@
   }
 
   @Override
-  public List<AccountInfo> apply(GroupResource resource)
+  public Response<List<AccountInfo>> apply(GroupResource resource)
       throws NotInternalGroupException, PermissionBackendException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (recursive) {
-      return getTransitiveMembers(group, resource.getControl());
+      return Response.ok(getTransitiveMembers(group, resource.getControl()));
     }
-    return getDirectMembers(group, resource.getControl());
+    return Response.ok(getDirectMembers(group, resource.getControl()));
   }
 
   public List<AccountInfo> getTransitiveMembers(AccountGroup.UUID groupUuid)
diff --git a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
index bb72a10..540718f 100644
--- a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
@@ -19,10 +19,11 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -45,12 +46,12 @@
   }
 
   @Override
-  public List<GroupInfo> apply(GroupResource rsrc)
+  public Response<List<GroupInfo>> apply(GroupResource rsrc)
       throws NotInternalGroupException, PermissionBackendException {
     GroupDescription.Internal group =
         rsrc.asInternalGroup().orElseThrow(NotInternalGroupException::new);
 
-    return getDirectSubgroups(group, rsrc.getControl());
+    return Response.ok(getDirectSubgroups(group, rsrc.getControl()));
   }
 
   public List<GroupInfo> getDirectSubgroups(
diff --git a/java/com/google/gerrit/server/restapi/group/PutDescription.java b/java/com/google/gerrit/server/restapi/group/PutDescription.java
index c4e6f09..8fe4b20 100644
--- a/java/com/google/gerrit/server/restapi/group/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/group/PutDescription.java
@@ -16,13 +16,13 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.DescriptionInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
diff --git a/java/com/google/gerrit/server/restapi/group/PutName.java b/java/com/google/gerrit/server/restapi/group/PutName.java
index adc20d5..9a3c87d 100644
--- a/java/com/google/gerrit/server/restapi/group/PutName.java
+++ b/java/com/google/gerrit/server/restapi/group/PutName.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.NameInput;
@@ -23,8 +24,8 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
@@ -45,7 +46,7 @@
   }
 
   @Override
-  public String apply(GroupResource rsrc, NameInput input)
+  public Response<String> apply(GroupResource rsrc, NameInput input)
       throws NotInternalGroupException, AuthException, BadRequestException,
           ResourceConflictException, ResourceNotFoundException, IOException,
           ConfigInvalidException {
@@ -62,11 +63,11 @@
     }
 
     if (internalGroup.getName().equals(newName)) {
-      return newName;
+      return Response.ok(newName);
     }
 
     renameGroup(internalGroup, newName);
-    return newName;
+    return Response.ok(newName);
   }
 
   private void renameGroup(GroupDescription.Internal group, String newName)
@@ -74,7 +75,7 @@
           ConfigInvalidException {
     AccountGroup.UUID groupUuid = group.getGroupUUID();
     InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey(newName)).build();
+        InternalGroupUpdate.builder().setName(AccountGroup.nameKey(newName)).build();
     try {
       groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
     } catch (NoSuchGroupException e) {
diff --git a/java/com/google/gerrit/server/restapi/group/PutOptions.java b/java/com/google/gerrit/server/restapi/group/PutOptions.java
index 747d899..53bf571 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOptions.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOptions.java
@@ -15,13 +15,14 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
@@ -42,7 +43,7 @@
   }
 
   @Override
-  public GroupOptionsInfo apply(GroupResource resource, GroupOptionsInfo input)
+  public Response<GroupOptionsInfo> apply(GroupResource resource, GroupOptionsInfo input)
       throws NotInternalGroupException, AuthException, BadRequestException,
           ResourceNotFoundException, IOException, ConfigInvalidException {
     GroupDescription.Internal internalGroup =
@@ -73,6 +74,6 @@
     if (input.visibleToAll) {
       options.visibleToAll = true;
     }
-    return options;
+    return Response.ok(options);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/PutOwner.java b/java/com/google/gerrit/server/restapi/group/PutOwner.java
index f766a84..04129af 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOwner.java
@@ -16,15 +16,16 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.api.groups.OwnerInput;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.group.GroupResolver;
 import com.google.gerrit.server.group.GroupResource;
@@ -54,7 +55,7 @@
   }
 
   @Override
-  public GroupInfo apply(GroupResource resource, OwnerInput input)
+  public Response<GroupInfo> apply(GroupResource resource, OwnerInput input)
       throws ResourceNotFoundException, NotInternalGroupException, AuthException,
           BadRequestException, UnprocessableEntityException, IOException, ConfigInvalidException,
           PermissionBackendException {
@@ -79,6 +80,6 @@
         throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid), e);
       }
     }
-    return json.format(owner);
+    return Response.ok(json.format(owner));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/QueryGroups.java b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
index b8dd28d..a233111 100644
--- a/java/com/google/gerrit/server/restapi/group/QueryGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 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.extensions.restapi.TopLevelResource;
 import com.google.gerrit.index.query.QueryParseException;
@@ -97,7 +98,7 @@
   }
 
   @Override
-  public List<GroupInfo> apply(TopLevelResource resource)
+  public Response<List<GroupInfo>> apply(TopLevelResource resource)
       throws BadRequestException, MethodNotAllowedException, PermissionBackendException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
@@ -129,7 +130,7 @@
       if (!groupInfos.isEmpty() && result.more()) {
         groupInfos.get(groupInfos.size() - 1)._moreGroups = true;
       }
-      return groupInfos;
+      return Response.ok(groupInfos);
     } catch (QueryParseException e) {
       throw new BadRequestException(e.getMessage());
     }
diff --git a/java/com/google/gerrit/server/restapi/project/BanCommit.java b/java/com/google/gerrit/server/restapi/project/BanCommit.java
index 3d101b2..64e38b0 100644
--- a/java/com/google/gerrit/server/restapi/project/BanCommit.java
+++ b/java/com/google/gerrit/server/restapi/project/BanCommit.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.api.projects.BanCommitInput;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.git.BanCommitResult;
@@ -45,7 +46,7 @@
   }
 
   @Override
-  protected BanResultInfo applyImpl(
+  protected Response<BanResultInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ProjectResource rsrc, BanCommitInput input)
       throws RestApiException, UpdateException, IOException, PermissionBackendException {
     BanResultInfo r = new BanResultInfo();
@@ -65,7 +66,7 @@
       r.alreadyBanned = transformCommits(result.getAlreadyBannedCommits());
       r.ignored = transformCommits(result.getIgnoredObjectIds());
     }
-    return r;
+    return Response.ok(r);
   }
 
   private static List<String> transformCommits(List<ObjectId> commits) {
diff --git a/java/com/google/gerrit/server/restapi/project/BranchesCollection.java b/java/com/google/gerrit/server/restapi/project/BranchesCollection.java
index 5fbb4f8..2d78bb0 100644
--- a/java/com/google/gerrit/server/restapi/project/BranchesCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/BranchesCollection.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
@@ -21,8 +23,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/restapi/project/Check.java b/java/com/google/gerrit/server/restapi/project/Check.java
index a6fd764..66a2df4 100644
--- a/java/com/google/gerrit/server/restapi/project/Check.java
+++ b/java/com/google/gerrit/server/restapi/project/Check.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -40,9 +41,9 @@
   }
 
   @Override
-  public CheckProjectResultInfo apply(ProjectResource rsrc, CheckProjectInput input)
+  public Response<CheckProjectResultInfo> apply(ProjectResource rsrc, CheckProjectInput input)
       throws AuthException, BadRequestException, ResourceConflictException, Exception {
     permissionBackend.user(rsrc.getUser()).check(GlobalPermission.ADMINISTRATE_SERVER);
-    return projectsConsistencyChecker.check(rsrc.getNameKey(), input);
+    return Response.ok(projectsConsistencyChecker.check(rsrc.getNameKey(), input));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccess.java b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
index 67b68b5..037a953 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
@@ -14,17 +14,18 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_HEADS;
+import static com.google.gerrit.entities.RefNames.REFS_HEADS;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
 import com.google.gerrit.extensions.api.config.AccessCheckInput;
 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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.DefaultPermissionMappings;
@@ -59,7 +60,7 @@
   }
 
   @Override
-  public AccessCheckInfo apply(ProjectResource rsrc, AccessCheckInput input)
+  public Response<AccessCheckInfo> apply(ProjectResource rsrc, AccessCheckInput input)
       throws PermissionBackendException, RestApiException, IOException, ConfigInvalidException {
     permissionBackend.user(rsrc.getUser()).check(GlobalPermission.VIEW_ACCESS);
 
@@ -72,7 +73,7 @@
       throw new BadRequestException("input requires 'account'");
     }
 
-    Account.Id match = accountResolver.resolve(input.account).asUnique().getAccount().getId();
+    Account.Id match = accountResolver.resolve(input.account).asUnique().account().id();
 
     AccessCheckInfo info = new AccessCheckInfo();
     try {
@@ -83,7 +84,7 @@
     } catch (AuthException e) {
       info.message = String.format("user %s cannot see project %s", match, rsrc.getName());
       info.status = HttpServletResponse.SC_FORBIDDEN;
-      return info;
+      return Response.ok(info);
     }
 
     RefPermission refPerm;
@@ -106,7 +107,7 @@
       try {
         permissionBackend
             .absentUser(match)
-            .ref(new Branch.NameKey(rsrc.getNameKey(), input.ref))
+            .ref(BranchNameKey.create(rsrc.getNameKey(), input.ref))
             .check(refPerm);
       } catch (AuthException e) {
         info.status = HttpServletResponse.SC_FORBIDDEN;
@@ -114,7 +115,7 @@
             String.format(
                 "user %s lacks permission %s for %s in project %s",
                 match, input.permission, input.ref, rsrc.getName());
-        return info;
+        return Response.ok(info);
       }
     } else {
       // We say access is okay if there are no refs, but this warrants a warning,
@@ -126,6 +127,6 @@
       }
     }
     info.status = HttpServletResponse.SC_OK;
-    return info;
+    return Response.ok(info);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java b/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
index 770e8c3..6aaa678 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
 import com.google.gerrit.extensions.api.config.AccessCheckInput;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -48,7 +49,7 @@
   }
 
   @Override
-  public AccessCheckInfo apply(ProjectResource rsrc)
+  public Response<AccessCheckInfo> apply(ProjectResource rsrc)
       throws PermissionBackendException, RestApiException, IOException, ConfigInvalidException {
 
     AccessCheckInput input = new AccessCheckInput();
diff --git a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
index de2ac64..4864fde 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.gerrit.exceptions.InvalidMergeStrategyException;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -75,7 +77,7 @@
   }
 
   @Override
-  public MergeableInfo apply(BranchResource resource)
+  public Response<MergeableInfo> apply(BranchResource resource)
       throws IOException, BadRequestException, ResourceNotFoundException {
     if (!(submitType.equals(SubmitType.MERGE_ALWAYS)
         || submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
@@ -106,7 +108,7 @@
         result.mergeable = true;
         result.commitMerged = true;
         result.contentMerged = true;
-        return result;
+        return Response.ok(result);
       }
 
       if (m.merge(false, targetCommit, sourceCommit)) {
@@ -119,9 +121,9 @@
           result.conflicts = ((ResolveMerger) m).getUnmergedPaths();
         }
       }
-    } catch (IllegalArgumentException e) {
+    } catch (InvalidMergeStrategyException e) {
       throw new BadRequestException(e.getMessage());
     }
-    return result;
+    return Response.ok(result);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java b/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
index 8cc8298..e566858 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.IncludedInInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.IncludedIn;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.CommitResource;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -35,9 +37,10 @@
   }
 
   @Override
-  public IncludedInInfo apply(CommitResource rsrc) throws RestApiException, IOException {
+  public Response<IncludedInInfo> apply(CommitResource rsrc)
+      throws RestApiException, IOException, PermissionBackendException {
     RevCommit commit = rsrc.getCommit();
     Project.NameKey project = rsrc.getProjectState().getNameKey();
-    return includedIn.apply(project, commit.getId().getName());
+    return Response.ok(includedIn.apply(project, commit.getId().getName()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
index d6c4469..d5380c6 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -16,14 +16,16 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
+import com.google.common.base.Throwables;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicMap;
 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.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.project.CommitResource;
@@ -32,6 +34,9 @@
 import com.google.gerrit.server.project.Reachable;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryHelper.Action;
+import com.google.gerrit.server.update.RetryHelper.ActionType;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -49,6 +54,7 @@
 public class CommitsCollection implements ChildCollection<ProjectResource, CommitResource> {
   private final DynamicMap<RestView<CommitResource>> views;
   private final GitRepositoryManager repoManager;
+  private final RetryHelper retryHelper;
   private final ChangeIndexCollection indexes;
   private final Provider<InternalChangeQuery> queryProvider;
   private final Reachable reachable;
@@ -57,11 +63,13 @@
   public CommitsCollection(
       DynamicMap<RestView<CommitResource>> views,
       GitRepositoryManager repoManager,
+      RetryHelper retryHelper,
       ChangeIndexCollection indexes,
       Provider<InternalChangeQuery> queryProvider,
       Reachable reachable) {
     this.views = views;
     this.repoManager = repoManager;
+    this.retryHelper = retryHelper;
     this.indexes = indexes;
     this.queryProvider = queryProvider;
     this.reachable = reachable;
@@ -115,7 +123,13 @@
     // Check first if any change references the commit in question. This is much cheaper than ref
     // visibility filtering and reachability computation.
     List<ChangeData> changes =
-        queryProvider.get().enforceVisibility(true).setLimit(1).byProjectCommit(project, commit);
+        executeIndexQuery(
+            () ->
+                queryProvider
+                    .get()
+                    .enforceVisibility(true)
+                    .setLimit(1)
+                    .byProjectCommit(project, commit));
     if (!changes.isEmpty()) {
       return true;
     }
@@ -128,4 +142,14 @@
             .collect(toImmutableList());
     return reachable.fromRefs(project, repo, commit, refs);
   }
+
+  private <T> T executeIndexQuery(Action<T> action) {
+    try {
+      return retryHelper.execute(
+          ActionType.INDEX_QUERY, action, StorageException.class::isInstance);
+    } catch (Exception e) {
+      Throwables.throwIfUnchecked(e);
+      throw new StorageException(e);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
index 37bc265..5deace9 100644
--- a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
+++ b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
@@ -17,6 +17,8 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
@@ -25,8 +27,6 @@
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.PluginConfig;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 0741377..fe48301 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -16,6 +16,10 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -24,10 +28,6 @@
 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.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeJson;
@@ -112,7 +112,7 @@
     List<AccessSection> additions = setAccess.getAccessSections(input.add);
 
     Project.NameKey newParentProjectName =
-        input.parent == null ? null : new Project.NameKey(input.parent);
+        input.parent == null ? null : Project.nameKey(input.parent);
 
     try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
       ProjectConfig config = projectConfigFactory.read(md);
@@ -134,11 +134,10 @@
 
       md.setMessage("Review access change");
       md.setInsertChangeId(true);
-      Change.Id changeId = new Change.Id(seq.nextChangeId());
+      Change.Id changeId = Change.id(seq.nextChangeId());
 
       RevCommit commit =
-          config.commitToNewRef(
-              md, new PatchSet.Id(changeId, Change.INITIAL_PATCH_SET_ID).toRefName());
+          config.commitToNewRef(md, PatchSet.id(changeId, Change.INITIAL_PATCH_SET_ID).toRefName());
 
       if (commit.name().equals(oldCommitSha1)) {
         throw new BadRequestException("no change");
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index 01e0cdf..fd6e024 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -14,19 +14,20 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+import static com.google.gerrit.entities.RefNames.isConfigRef;
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -82,7 +83,7 @@
   }
 
   @Override
-  public BranchInfo apply(ProjectResource rsrc, IdString id, BranchInput input)
+  public Response<BranchInfo> apply(ProjectResource rsrc, IdString id, BranchInput input)
       throws BadRequestException, AuthException, ResourceConflictException, IOException,
           PermissionBackendException, NoSuchProjectException {
     String ref = id.get();
@@ -112,7 +113,7 @@
               + "\"");
     }
 
-    final Branch.NameKey name = new Branch.NameKey(rsrc.getNameKey(), ref);
+    final BranchNameKey name = BranchNameKey.create(rsrc.getNameKey(), ref);
     try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       ObjectId revid = RefUtil.parseBaseRevision(repo, rsrc.getNameKey(), input.revision);
       RevWalk rw = RefUtil.verifyConnected(repo, revid);
@@ -144,7 +145,7 @@
           case NEW:
           case NO_CHANGE:
             referenceUpdated.fire(
-                name.getParentKey(), u, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
+                name.project(), u, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
             break;
           case LOCK_FAILURE:
             if (repo.getRefDatabase().exactRef(ref) != null) {
@@ -182,7 +183,7 @@
         info.ref = ref;
         info.revision = revid.getName();
 
-        if (isConfigRef(name.get())) {
+        if (isConfigRef(name.branch())) {
           // Never allow to delete the meta config branch.
           info.canDelete = null;
         } else {
@@ -192,7 +193,7 @@
                   ? true
                   : null;
         }
-        return info;
+        return Response.created(info);
       } catch (IOException err) {
         logger.atSevere().withCause(err).log("Cannot create branch \"%s\"", name);
         throw err;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateDashboard.java b/java/com/google/gerrit/server/restapi/project/CreateDashboard.java
index e8b6236..314df73 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateDashboard.java
@@ -19,15 +19,12 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.DashboardResource;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 import org.kohsuke.args4j.Option;
 
 @Singleton
@@ -45,14 +42,16 @@
 
   @Override
   public Response<DashboardInfo> apply(ProjectResource parent, IdString id, SetDashboardInput input)
-      throws RestApiException, IOException, PermissionBackendException {
+      throws Exception {
     parent.getProjectState().checkStatePermitsWrite();
     if (!DashboardsCollection.isDefaultDashboard(id)) {
       throw new ResourceNotFoundException(id);
     }
     SetDefaultDashboard set = setDefault.get();
     set.inherited = inherited;
-    return set.apply(
-        DashboardResource.projectDefault(parent.getProjectState(), parent.getUser()), input);
+    return Response.created(
+        set.apply(
+                DashboardResource.projectDefault(parent.getProjectState(), parent.getUser()), input)
+            .value());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index 8b81f10..a5a0034 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -33,7 +34,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index 6a04e0d..5cfb118 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
 import com.google.gerrit.server.WebLinks;
@@ -78,7 +79,7 @@
   }
 
   @Override
-  public TagInfo apply(ProjectResource resource, IdString id, TagInput input)
+  public Response<TagInfo> apply(ProjectResource resource, IdString id, TagInput input)
       throws RestApiException, IOException, PermissionBackendException, NoSuchProjectException {
     String ref = id.get();
     if (input == null) {
@@ -146,7 +147,8 @@
             result.getObjectId(),
             resource.getUser().asIdentifiedUser().state());
         try (RevWalk w = new RevWalk(repo)) {
-          return ListTags.createTagInfo(perm, result, w, resource.getProjectState(), links);
+          return Response.created(
+              ListTags.createTagInfo(perm, result, w, resource.getProjectState(), links));
         }
       }
     } catch (InvalidRevisionException e) {
diff --git a/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java b/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
index 4dc83e8..ca48109 100644
--- a/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
+import static com.google.gerrit.entities.RefNames.REFS_DASHBOARDS;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
@@ -22,6 +22,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
 import com.google.gerrit.extensions.api.projects.DashboardSectionInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -33,7 +34,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.UrlEncoded;
 import com.google.gerrit.server.git.GitRepositoryManager;
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
index b94fb51..6248a61 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+import static com.google.gerrit.entities.RefNames.isConfigRef;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 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.reviewdb.client.RefNames;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.BranchResource;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -47,12 +47,12 @@
   @Override
   public Response<?> apply(BranchResource rsrc, Input input)
       throws RestApiException, IOException, PermissionBackendException {
-    if (RefNames.HEAD.equals(rsrc.getBranchKey().get())) {
+    if (RefNames.HEAD.equals(rsrc.getBranchKey().branch())) {
       throw new MethodNotAllowedException("not allowed to delete HEAD");
-    } else if (isConfigRef(rsrc.getBranchKey().get())) {
+    } else if (isConfigRef(rsrc.getBranchKey().branch())) {
       // Never allow to delete the meta config branch.
       throw new MethodNotAllowedException(
-          "not allowed to delete branch " + rsrc.getBranchKey().get());
+          "not allowed to delete branch " + rsrc.getBranchKey().branch());
     }
 
     if (!queryProvider.get().setLimit(1).byBranchOpen(rsrc.getBranchKey()).isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranches.java b/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
index ba25e33..ca5962e 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
@@ -17,13 +17,13 @@
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 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.reviewdb.client.RefNames;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteDashboard.java b/java/com/google/gerrit/server/restapi/project/DeleteDashboard.java
index 2702d58..9d9e5f5 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteDashboard.java
@@ -18,14 +18,11 @@
 import com.google.gerrit.extensions.api.projects.SetDashboardInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 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.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.DashboardResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 
 @Singleton
 public class DeleteDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
@@ -38,7 +35,7 @@
 
   @Override
   public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
-      throws RestApiException, IOException, PermissionBackendException {
+      throws Exception {
     if (resource.isProjectDefault()) {
       SetDashboardInput in = new SetDashboardInput();
       in.commitMessage = input != null ? input.commitMessage : null;
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index dae759a..1979d61 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+import static com.google.gerrit.entities.RefNames.isConfigRef;
 import static java.lang.String.format;
 import static org.eclipse.jgit.lib.Constants.R_REFS;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
@@ -25,11 +25,11 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -260,7 +260,7 @@
     }
 
     if (!refName.startsWith(R_TAGS)) {
-      Branch.NameKey branchKey = new Branch.NameKey(projectState.getNameKey(), ref.getName());
+      BranchNameKey branchKey = BranchNameKey.create(projectState.getNameKey(), ref.getName());
       if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) {
         command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes");
       }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTag.java b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
index 33955ee..545b752 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTag.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+import static com.google.gerrit.entities.RefNames.isConfigRef;
 
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
diff --git a/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java b/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
index 09f973b..0ee8279 100644
--- a/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
@@ -14,14 +14,16 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
 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.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.server.change.FileInfoJson;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListKey;
@@ -32,6 +34,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Map;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.kohsuke.args4j.Option;
 
@@ -82,7 +85,8 @@
     }
 
     @Override
-    public Object apply(CommitResource resource) throws PatchListNotAvailableException {
+    public Response<Map<String, FileInfo>> apply(CommitResource resource)
+        throws PatchListNotAvailableException {
       RevCommit commit = resource.getCommit();
       PatchListKey key;
 
@@ -94,7 +98,7 @@
         key = PatchListKey.againstCommit(null, commit, DiffPreferencesInfo.Whitespace.IGNORE_NONE);
       }
 
-      return fileInfoJson.toFileInfoMap(resource.getProjectState().getNameKey(), key);
+      return Response.ok(fileInfoJson.toFileInfoMap(resource.getProjectState().getNameKey(), key));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
index 800e75e..537d45a 100644
--- a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
+++ b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
@@ -19,13 +19,13 @@
 
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.UrlFormatter;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -70,12 +70,12 @@
   }
 
   @Override
-  public Object apply(ProjectResource rsrc, Input input) {
+  public Response<?> apply(ProjectResource rsrc, Input input) {
     Project.NameKey project = rsrc.getNameKey();
     if (input.async) {
       return applyAsync(project, input);
     }
-    return applySync(project, input);
+    return Response.ok(applySync(project, input));
   }
 
   private Response.Accepted applyAsync(Project.NameKey project, Input input) {
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 171c2e5..079df502 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -29,6 +29,9 @@
 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.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
 import com.google.gerrit.extensions.api.access.PermissionInfo;
 import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
@@ -37,10 +40,8 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.GroupBackend;
@@ -116,18 +117,16 @@
     this.projectConfigFactory = projectConfigFactory;
   }
 
-  public ProjectAccessInfo apply(Project.NameKey nameKey)
-      throws ResourceNotFoundException, ResourceConflictException, IOException,
-          PermissionBackendException {
+  public ProjectAccessInfo apply(Project.NameKey nameKey) throws Exception {
     ProjectState state = projectCache.checkedGet(nameKey);
     if (state == null) {
       throw new ResourceNotFoundException(nameKey.get());
     }
-    return apply(new ProjectResource(state, user.get()));
+    return apply(new ProjectResource(state, user.get())).value();
   }
 
   @Override
-  public ProjectAccessInfo apply(ProjectResource rsrc)
+  public Response<ProjectAccessInfo> apply(ProjectResource rsrc)
       throws ResourceNotFoundException, ResourceConflictException, IOException,
           PermissionBackendException {
     // Load the current configuration from the repository, ensuring it's the most
@@ -275,7 +274,7 @@
             .filter(e -> e.getValue() != null)
             .collect(toMap(e -> e.getKey().get(), Map.Entry::getValue));
 
-    return info;
+    return Response.ok(info);
   }
 
   private void loadGroup(Map<AccountGroup.UUID, GroupInfo> groups, AccountGroup.UUID id) {
diff --git a/java/com/google/gerrit/server/restapi/project/GetBranch.java b/java/com/google/gerrit/server/restapi/project/GetBranch.java
index 7d32f3d..52a47a4 100644
--- a/java/com/google/gerrit/server/restapi/project/GetBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/GetBranch.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.BranchResource;
@@ -34,8 +35,8 @@
   }
 
   @Override
-  public BranchInfo apply(BranchResource rsrc)
+  public Response<BranchInfo> apply(BranchResource rsrc)
       throws ResourceNotFoundException, IOException, PermissionBackendException {
-    return list.get().toBranchInfo(rsrc);
+    return Response.ok(list.get().toBranchInfo(rsrc));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/GetChildProject.java b/java/com/google/gerrit/server/restapi/project/GetChildProject.java
index e69907e..b90f6ee 100644
--- a/java/com/google/gerrit/server/restapi/project/GetChildProject.java
+++ b/java/com/google/gerrit/server/restapi/project/GetChildProject.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.project.ChildProjectResource;
 import com.google.gerrit.server.project.ProjectJson;
@@ -37,9 +38,9 @@
   }
 
   @Override
-  public ProjectInfo apply(ChildProjectResource rsrc) throws ResourceNotFoundException {
+  public Response<ProjectInfo> apply(ChildProjectResource rsrc) throws ResourceNotFoundException {
     if (recursive || rsrc.isDirectChild()) {
-      return json.format(rsrc.getChild().getProject());
+      return Response.ok(json.format(rsrc.getChild().getProject()));
     }
     throw new ResourceNotFoundException(rsrc.getChild().getName());
   }
diff --git a/java/com/google/gerrit/server/restapi/project/GetCommit.java b/java/com/google/gerrit/server/restapi/project/GetCommit.java
index 1c1ae90..cca6a1a 100644
--- a/java/com/google/gerrit/server/restapi/project/GetCommit.java
+++ b/java/com/google/gerrit/server/restapi/project/GetCommit.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.git.CommitUtil;
 import com.google.gerrit.server.project.CommitResource;
@@ -25,7 +26,7 @@
 public class GetCommit implements RestReadView<CommitResource> {
 
   @Override
-  public CommitInfo apply(CommitResource rsrc) throws IOException {
-    return CommitUtil.toCommitInfo(rsrc.getCommit());
+  public Response<CommitInfo> apply(CommitResource rsrc) throws IOException {
+    return Response.ok(CommitUtil.toCommitInfo(rsrc.getCommit()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/GetConfig.java b/java/com/google/gerrit/server/restapi/project/GetConfig.java
index b3ad962..ce45e7d 100644
--- a/java/com/google/gerrit/server/restapi/project/GetConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/GetConfig.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.EnableSignedPush;
@@ -53,15 +54,16 @@
   }
 
   @Override
-  public ConfigInfo apply(ProjectResource resource) {
-    return new ConfigInfoImpl(
-        serverEnableSignedPush,
-        resource.getProjectState(),
-        resource.getUser(),
-        pluginConfigEntries,
-        cfgFactory,
-        allProjects,
-        uiActions,
-        views);
+  public Response<ConfigInfo> apply(ProjectResource resource) {
+    return Response.ok(
+        new ConfigInfoImpl(
+            serverEnableSignedPush,
+            resource.getProjectState(),
+            resource.getUser(),
+            pluginConfigEntries,
+            cfgFactory,
+            allProjects,
+            uiActions,
+            views));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/GetContent.java b/java/com/google/gerrit/server/restapi/project/GetContent.java
index 132b644..4e3fc8e 100644
--- a/java/com/google/gerrit/server/restapi/project/GetContent.java
+++ b/java/com/google/gerrit/server/restapi/project/GetContent.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.FileContentUtil;
 import com.google.gerrit.server.project.FileResource;
@@ -34,8 +35,9 @@
   }
 
   @Override
-  public BinaryResult apply(FileResource rsrc)
+  public Response<BinaryResult> apply(FileResource rsrc)
       throws ResourceNotFoundException, BadRequestException, IOException {
-    return fileContentUtil.getContent(rsrc.getProjectState(), rsrc.getRev(), rsrc.getPath(), null);
+    return Response.ok(
+        fileContentUtil.getContent(rsrc.getProjectState(), rsrc.getRev(), rsrc.getPath(), null));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/GetDashboard.java b/java/com/google/gerrit/server/restapi/project/GetDashboard.java
index 2ec67e7..928a36f 100644
--- a/java/com/google/gerrit/server/restapi/project/GetDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/GetDashboard.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
+import static com.google.gerrit.entities.RefNames.REFS_DASHBOARDS;
 import static com.google.gerrit.server.restapi.project.DashboardsCollection.isDefaultDashboard;
 
 import com.google.common.base.Splitter;
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.Url;
@@ -56,7 +57,7 @@
   }
 
   @Override
-  public DashboardInfo apply(DashboardResource rsrc)
+  public Response<DashboardInfo> apply(DashboardResource rsrc)
       throws RestApiException, IOException, PermissionBackendException {
     if (inherited && !rsrc.isProjectDefault()) {
       throw new BadRequestException("inherited flag can only be used with default");
@@ -71,13 +72,14 @@
       }
     }
 
-    return DashboardsCollection.parse(
-        rsrc.getProjectState().getProject(),
-        rsrc.getRefName().substring(REFS_DASHBOARDS.length()),
-        rsrc.getPathName(),
-        rsrc.getConfig(),
-        rsrc.getProjectState().getName(),
-        true);
+    return Response.ok(
+        DashboardsCollection.parse(
+            rsrc.getProjectState().getProject(),
+            rsrc.getRefName().substring(REFS_DASHBOARDS.length()),
+            rsrc.getPathName(),
+            rsrc.getConfig(),
+            rsrc.getProjectState().getName(),
+            true));
   }
 
   private DashboardResource defaultOf(ProjectState projectState, CurrentUser user)
diff --git a/java/com/google/gerrit/server/restapi/project/GetDescription.java b/java/com/google/gerrit/server/restapi/project/GetDescription.java
index d387ff1..2561b91 100644
--- a/java/com/google/gerrit/server/restapi/project/GetDescription.java
+++ b/java/com/google/gerrit/server/restapi/project/GetDescription.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Singleton;
@@ -22,7 +23,7 @@
 @Singleton
 public class GetDescription implements RestReadView<ProjectResource> {
   @Override
-  public String apply(ProjectResource rsrc) {
-    return Strings.nullToEmpty(rsrc.getProjectState().getProject().getDescription());
+  public Response<String> apply(ProjectResource rsrc) {
+    return Response.ok(Strings.nullToEmpty(rsrc.getProjectState().getProject().getDescription()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/GetHead.java b/java/com/google/gerrit/server/restapi/project/GetHead.java
index 043991f..4e0a144 100644
--- a/java/com/google/gerrit/server/restapi/project/GetHead.java
+++ b/java/com/google/gerrit/server/restapi/project/GetHead.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -52,7 +53,7 @@
   }
 
   @Override
-  public String apply(ProjectResource rsrc)
+  public Response<String> apply(ProjectResource rsrc)
       throws AuthException, ResourceNotFoundException, IOException, PermissionBackendException {
     rsrc.getProjectState().statePermitsRead();
     try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
@@ -66,12 +67,12 @@
             .project(rsrc.getNameKey())
             .ref(n)
             .check(RefPermission.READ);
-        return n;
+        return Response.ok(n);
       } else if (head.getObjectId() != null) {
         try (RevWalk rw = new RevWalk(repo)) {
           RevCommit commit = rw.parseCommit(head.getObjectId());
           if (commits.canRead(rsrc.getProjectState(), repo, commit)) {
-            return head.getObjectId().name();
+            return Response.ok(head.getObjectId().name());
           }
           throw new AuthException("not allowed to see HEAD");
         } catch (MissingObjectException | IncorrectObjectTypeException e) {
diff --git a/java/com/google/gerrit/server/restapi/project/GetParent.java b/java/com/google/gerrit/server/restapi/project/GetParent.java
index a4942e3..9b93d5b 100644
--- a/java/com/google/gerrit/server/restapi/project/GetParent.java
+++ b/java/com/google/gerrit/server/restapi/project/GetParent.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
@@ -31,9 +32,9 @@
   }
 
   @Override
-  public String apply(ProjectResource resource) {
+  public Response<String> apply(ProjectResource resource) {
     Project project = resource.getProjectState().getProject();
     Project.NameKey parentName = project.getParent(allProjectsName);
-    return parentName != null ? parentName.get() : "";
+    return Response.ok(parentName != null ? parentName.get() : "");
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/GetProject.java b/java/com/google/gerrit/server/restapi/project/GetProject.java
index 26159e4..2f7d370 100644
--- a/java/com/google/gerrit/server/restapi/project/GetProject.java
+++ b/java/com/google/gerrit/server/restapi/project/GetProject.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.project.ProjectResource;
@@ -32,7 +33,7 @@
   }
 
   @Override
-  public ProjectInfo apply(ProjectResource rsrc) {
-    return json.format(rsrc.getProjectState());
+  public Response<ProjectInfo> apply(ProjectResource rsrc) {
+    return Response.ok(json.format(rsrc.getProjectState()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/GetReflog.java b/java/com/google/gerrit/server/restapi/project/GetReflog.java
index a690042..f9c6fd9 100644
--- a/java/com/google/gerrit/server/restapi/project/GetReflog.java
+++ b/java/com/google/gerrit/server/restapi/project/GetReflog.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.api.projects.ReflogEntryInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CommonConverters;
@@ -89,7 +90,7 @@
   }
 
   @Override
-  public List<ReflogEntryInfo> apply(BranchResource rsrc)
+  public Response<List<ReflogEntryInfo>> apply(BranchResource rsrc)
       throws RestApiException, IOException, PermissionBackendException {
     permissionBackend
         .user(rsrc.getUser())
@@ -123,7 +124,7 @@
           }
         }
       }
-      return Lists.transform(entries, this::newReflogEntryInfo);
+      return Response.ok(Lists.transform(entries, this::newReflogEntryInfo));
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/GetStatistics.java b/java/com/google/gerrit/server/restapi/project/GetStatistics.java
index a408062..db97855 100644
--- a/java/com/google/gerrit/server/restapi/project/GetStatistics.java
+++ b/java/com/google/gerrit/server/restapi/project/GetStatistics.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ProjectResource;
@@ -42,11 +43,11 @@
   }
 
   @Override
-  public RepositoryStatistics apply(ProjectResource rsrc)
+  public Response<RepositoryStatistics> apply(ProjectResource rsrc)
       throws ResourceNotFoundException, ResourceConflictException {
     try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       GarbageCollectCommand gc = Git.wrap(repo).gc();
-      return new RepositoryStatistics(gc.getStatistics());
+      return Response.ok(new RepositoryStatistics(gc.getStatistics()));
     } catch (GitAPIException | JGitInternalException e) {
       throw new ResourceConflictException(e.getMessage());
     } catch (IOException e) {
diff --git a/java/com/google/gerrit/server/restapi/project/GetTag.java b/java/com/google/gerrit/server/restapi/project/GetTag.java
index 6d5a510..6ab2f8b 100644
--- a/java/com/google/gerrit/server/restapi/project/GetTag.java
+++ b/java/com/google/gerrit/server/restapi/project/GetTag.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.project.TagResource;
 import com.google.inject.Singleton;
@@ -23,7 +24,7 @@
 public class GetTag implements RestReadView<TagResource> {
 
   @Override
-  public TagInfo apply(TagResource resource) {
-    return resource.getTagInfo();
+  public Response<TagInfo> apply(TagResource resource) {
+    return Response.ok(resource.getTagInfo());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/Index.java b/java/com/google/gerrit/server/restapi/project/Index.java
index bc58b23..b14380a 100644
--- a/java/com/google/gerrit/server/restapi/project/Index.java
+++ b/java/com/google/gerrit/server/restapi/project/Index.java
@@ -18,21 +18,18 @@
 
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.projects.IndexProjectInput;
 import com.google.gerrit.extensions.common.ProjectInfo;
 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.index.project.ProjectIndexer;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.index.IndexExecutor;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 import java.util.concurrent.Future;
 
 @RequiresCapability(GlobalCapability.MAINTAIN_SERVER)
@@ -53,14 +50,14 @@
   }
 
   @Override
-  public Response.Accepted apply(ProjectResource rsrc, IndexProjectInput input)
-      throws IOException, PermissionBackendException, RestApiException {
+  public Response.Accepted apply(ProjectResource rsrc, IndexProjectInput input) throws Exception {
     String response = "Project " + rsrc.getName() + " submitted for reindexing";
 
     reindex(rsrc.getNameKey(), input.async);
     if (Boolean.TRUE.equals(input.indexChildren)) {
-      for (ProjectInfo child : listChildProjectsProvider.get().withRecursive(true).apply(rsrc)) {
-        reindex(new Project.NameKey(child.name), input.async);
+      for (ProjectInfo child :
+          listChildProjectsProvider.get().withRecursive(true).apply(rsrc).value()) {
+        reindex(Project.nameKey(child.name), input.async);
       }
 
       response += " (indexing children recursively)";
diff --git a/java/com/google/gerrit/server/restapi/project/IndexChanges.java b/java/com/google/gerrit/server/restapi/project/IndexChanges.java
index b6b3d6b..45a6616 100644
--- a/java/com/google/gerrit/server/restapi/project/IndexChanges.java
+++ b/java/com/google/gerrit/server/restapi/project/IndexChanges.java
@@ -19,11 +19,11 @@
 import com.google.common.io.ByteStreams;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.index.IndexExecutor;
diff --git a/java/com/google/gerrit/server/restapi/project/ListBranches.java b/java/com/google/gerrit/server/restapi/project/ListBranches.java
index ae9ef28..fecdc8e 100644
--- a/java/com/google/gerrit/server/restapi/project/ListBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/ListBranches.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+import static com.google.gerrit.entities.RefNames.isConfigRef;
 
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.common.ActionInfo;
@@ -26,11 +27,11 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.extensions.webui.UiActions;
@@ -127,15 +128,16 @@
   }
 
   @Override
-  public List<BranchInfo> apply(ProjectResource rsrc)
+  public Response<ImmutableList<BranchInfo>> apply(ProjectResource rsrc)
       throws RestApiException, IOException, PermissionBackendException {
     rsrc.getProjectState().checkStatePermitsRead();
-    return new RefFilter<BranchInfo>(Constants.R_HEADS)
-        .subString(matchSubstring)
-        .regex(matchRegex)
-        .start(start)
-        .limit(limit)
-        .filter(allBranches(rsrc));
+    return Response.ok(
+        new RefFilter<BranchInfo>(Constants.R_HEADS)
+            .subString(matchSubstring)
+            .regex(matchRegex)
+            .start(start)
+            .limit(limit)
+            .filter(allBranches(rsrc)));
   }
 
   BranchInfo toBranchInfo(BranchResource rsrc)
@@ -278,7 +280,8 @@
       info.actions.put(d.getId(), new ActionInfo(d));
     }
 
-    List<WebLinkInfo> links = webLinks.getBranchLinks(projectState.getName(), ref.getName());
+    ImmutableList<WebLinkInfo> links =
+        webLinks.getBranchLinks(projectState.getName(), ref.getName());
     info.webLinks = links.isEmpty() ? null : links;
     return info;
   }
diff --git a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
index 3879720..0bd053e 100644
--- a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
@@ -16,12 +16,13 @@
 
 import static java.util.stream.Collectors.toList;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -65,7 +66,7 @@
   }
 
   @Override
-  public List<ProjectInfo> apply(ProjectResource rsrc)
+  public Response<List<ProjectInfo>> apply(ProjectResource rsrc)
       throws PermissionBackendException, RestApiException {
     if (limit < 0) {
       throw new BadRequestException("limit must be a positive number");
@@ -75,20 +76,17 @@
     }
     rsrc.getProjectState().checkStatePermitsRead();
     if (recursive) {
-      return childProjects.list(rsrc.getNameKey());
+      return Response.ok(childProjects.list(rsrc.getNameKey()));
     }
 
-    return directChildProjects(rsrc.getNameKey());
+    return Response.ok(directChildProjects(rsrc.getNameKey()));
   }
 
   private List<ProjectInfo> directChildProjects(Project.NameKey parent) throws RestApiException {
     PermissionBackend.WithUser currentUser = permissionBackend.currentUser();
     return queryProvider.get().withQuery("parent:" + parent.get()).withLimit(limit).apply().stream()
         .filter(
-            p ->
-                currentUser
-                    .project(new Project.NameKey(p.name))
-                    .testOrFalse(ProjectPermission.ACCESS))
+            p -> currentUser.project(Project.nameKey(p.name)).testOrFalse(ProjectPermission.ACCESS))
         .collect(toList());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index fd8668a..4406719 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -14,15 +14,16 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
+import static com.google.gerrit.entities.RefNames.REFS_DASHBOARDS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -63,11 +64,11 @@
   }
 
   @Override
-  public List<?> apply(ProjectResource rsrc)
+  public Response<List<?>> apply(ProjectResource rsrc)
       throws ResourceNotFoundException, IOException, PermissionBackendException {
     String project = rsrc.getName();
     if (!inherited) {
-      return scan(rsrc.getProjectState(), project, true);
+      return Response.ok(scan(rsrc.getProjectState(), project, true));
     }
 
     List<List<DashboardInfo>> all = new ArrayList<>();
@@ -83,7 +84,7 @@
         all.add(list);
       }
     }
-    return all;
+    return Response.ok(all);
   }
 
   private Collection<ProjectState> tree(ProjectResource rsrc) throws PermissionBackendException {
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index c583923..6384282 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -27,6 +27,9 @@
 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.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.ProjectInfo;
@@ -35,13 +38,11 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 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.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.GroupControl;
@@ -303,16 +304,17 @@
   }
 
   @Override
-  public Object apply(TopLevelResource resource)
+  public Response<Object> apply(TopLevelResource resource)
       throws BadRequestException, PermissionBackendException {
     if (format == OutputFormat.TEXT) {
       ByteArrayOutputStream buf = new ByteArrayOutputStream();
       displayToStream(buf);
-      return BinaryResult.create(buf.toByteArray())
-          .setContentType("text/plain")
-          .setCharacterEncoding(UTF_8);
+      return Response.ok(
+          BinaryResult.create(buf.toByteArray())
+              .setContentType("text/plain")
+              .setCharacterEncoding(UTF_8));
     }
-    return apply();
+    return Response.ok(apply());
   }
 
   public SortedMap<String, ProjectInfo> apply()
@@ -505,7 +507,7 @@
           continue;
         }
 
-        List<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get());
+        ImmutableList<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get());
         info.webLinks = links.isEmpty() ? null : links;
 
         if (stdout == null || format.isJson()) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 08b9b84..ff6d30e5 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -14,18 +14,19 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+import static com.google.gerrit.entities.RefNames.isConfigRef;
 import static java.util.Comparator.comparing;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CommonConverters;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -116,7 +117,7 @@
   }
 
   @Override
-  public List<TagInfo> apply(ProjectResource resource)
+  public Response<ImmutableList<TagInfo>> apply(ProjectResource resource)
       throws IOException, ResourceNotFoundException, RestApiException, PermissionBackendException {
     resource.getProjectState().checkStatePermitsRead();
 
@@ -137,12 +138,13 @@
 
     tags.sort(comparing(t -> t.ref));
 
-    return new RefFilter<TagInfo>(Constants.R_TAGS)
-        .start(start)
-        .limit(limit)
-        .subString(matchSubstring)
-        .regex(matchRegex)
-        .filter(tags);
+    return Response.ok(
+        new RefFilter<TagInfo>(Constants.R_TAGS)
+            .start(start)
+            .limit(limit)
+            .subString(matchSubstring)
+            .regex(matchRegex)
+            .filter(tags));
   }
 
   public TagInfo get(ProjectResource resource, IdString id)
@@ -182,7 +184,7 @@
           perm.testOrFalse(RefPermission.DELETE) && projectState.statePermitsWrite() ? true : null;
     }
 
-    List<WebLinkInfo> webLinks = links.getTagLinks(projectState.getName(), ref.getName());
+    ImmutableList<WebLinkInfo> webLinks = links.getTagLinks(projectState.getName(), ref.getName());
     if (object instanceof RevTag) {
       // Annotated or signed tag
       RevTag tag = (RevTag) object;
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectNode.java b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
index c83e473..1e6200c 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectNode.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.util.TreeFormatter.TreeNode;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index 31c90e5..7d6f7ef 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -30,7 +31,6 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -138,7 +138,7 @@
       throws IOException, PermissionBackendException, ResourceConflictException {
     id = ProjectUtil.sanitizeProjectName(id);
 
-    Project.NameKey nameKey = new Project.NameKey(id);
+    Project.NameKey nameKey = Project.nameKey(id);
     ProjectState state = projectCache.checkedGet(nameKey);
     if (state == null) {
       return null;
diff --git a/java/com/google/gerrit/server/restapi/project/PutBranch.java b/java/com/google/gerrit/server/restapi/project/PutBranch.java
index fec8abf..02fc6689 100644
--- a/java/com/google/gerrit/server/restapi/project/PutBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/PutBranch.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.project.BranchResource;
 import com.google.inject.Singleton;
@@ -25,7 +26,8 @@
 public class PutBranch implements RestModifyView<BranchResource, BranchInput> {
 
   @Override
-  public BranchInfo apply(BranchResource rsrc, BranchInput input) throws ResourceConflictException {
+  public Response<BranchInfo> apply(BranchResource rsrc, BranchInput input)
+      throws ResourceConflictException {
     throw new ResourceConflictException("Branch \"" + rsrc.getRef() + "\" already exists");
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index 56ddbb4..4aecb4a 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -17,6 +17,8 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ConfigValue;
@@ -26,11 +28,10 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.EnableSignedPush;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -108,13 +109,13 @@
   }
 
   @Override
-  public ConfigInfo apply(ProjectResource rsrc, ConfigInput input)
+  public Response<ConfigInfo> apply(ProjectResource rsrc, ConfigInput input)
       throws RestApiException, PermissionBackendException {
     permissionBackend
         .currentUser()
         .project(rsrc.getNameKey())
         .check(ProjectPermission.WRITE_CONFIG);
-    return apply(rsrc.getProjectState(), input);
+    return Response.ok(apply(rsrc.getProjectState(), input));
   }
 
   public ConfigInfo apply(ProjectState projectState, ConfigInput input)
diff --git a/java/com/google/gerrit/server/restapi/project/PutDescription.java b/java/com/google/gerrit/server/restapi/project/PutDescription.java
index c3a063d..a0b9feb 100644
--- a/java/com/google/gerrit/server/restapi/project/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/project/PutDescription.java
@@ -16,13 +16,13 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.DescriptionInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.permissions.PermissionBackend;
diff --git a/java/com/google/gerrit/server/restapi/project/PutTag.java b/java/com/google/gerrit/server/restapi/project/PutTag.java
index 06c5157..b6f3f24 100644
--- a/java/com/google/gerrit/server/restapi/project/PutTag.java
+++ b/java/com/google/gerrit/server/restapi/project/PutTag.java
@@ -17,13 +17,15 @@
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.project.TagResource;
 
 public class PutTag implements RestModifyView<TagResource, TagInput> {
 
   @Override
-  public TagInfo apply(TagResource resource, TagInput input) throws ResourceConflictException {
+  public Response<TagInfo> apply(TagResource resource, TagInput input)
+      throws ResourceConflictException {
     throw new ResourceConflictException("Tag \"" + resource.getTagInfo().ref + "\" already exists");
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/QueryProjects.java b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
index 8727df3..7066d9a 100644
--- a/java/com/google/gerrit/server/restapi/project/QueryProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 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.extensions.restapi.TopLevelResource;
 import com.google.gerrit.index.project.ProjectData;
@@ -86,9 +87,9 @@
   }
 
   @Override
-  public List<ProjectInfo> apply(TopLevelResource resource)
+  public Response<List<ProjectInfo>> apply(TopLevelResource resource)
       throws BadRequestException, MethodNotAllowedException {
-    return apply();
+    return Response.ok(apply());
   }
 
   public List<ProjectInfo> apply() throws BadRequestException, MethodNotAllowedException {
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccess.java b/java/com/google/gerrit/server/restapi/project/SetAccess.java
index 95bc75f7..02c1b54 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccess.java
@@ -17,23 +17,20 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
@@ -41,7 +38,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -80,9 +76,8 @@
   }
 
   @Override
-  public ProjectAccessInfo apply(ProjectResource rsrc, ProjectAccessInput input)
-      throws ResourceNotFoundException, ResourceConflictException, IOException, AuthException,
-          BadRequestException, UnprocessableEntityException, PermissionBackendException {
+  public Response<ProjectAccessInfo> apply(ProjectResource rsrc, ProjectAccessInput input)
+      throws Exception {
     MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
 
     ProjectConfig config;
@@ -117,7 +112,7 @@
           identifiedUser.get(),
           config,
           rsrc.getNameKey(),
-          input.parent == null ? null : new Project.NameKey(input.parent),
+          input.parent == null ? null : Project.nameKey(input.parent),
           !checkedAdmin);
 
       if (!Strings.isNullOrEmpty(input.message)) {
@@ -138,6 +133,6 @@
       throw new ResourceConflictException(rsrc.getName(), e);
     }
 
-    return getAccess.apply(rsrc.getNameKey());
+    return Response.ok(getAccess.apply(rsrc.getNameKey()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index e206319..ea29fb3 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -21,6 +21,7 @@
 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.Project;
 import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
 import com.google.gerrit.extensions.api.access.PermissionInfo;
@@ -29,7 +30,6 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.group.GroupResolver;
diff --git a/java/com/google/gerrit/server/restapi/project/SetDashboard.java b/java/com/google/gerrit/server/restapi/project/SetDashboard.java
index 2804b7c..e8e0c0d 100644
--- a/java/com/google/gerrit/server/restapi/project/SetDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/SetDashboard.java
@@ -18,14 +18,11 @@
 import com.google.gerrit.extensions.api.projects.SetDashboardInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 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.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.DashboardResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 
 @Singleton
 public class SetDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
@@ -38,7 +35,7 @@
 
   @Override
   public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
-      throws RestApiException, IOException, PermissionBackendException {
+      throws Exception {
     if (resource.isProjectDefault()) {
       return defaultSetter.get().apply(resource, input);
     }
diff --git a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
index 3917bee..49b5cab 100644
--- a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
 import com.google.gerrit.extensions.api.projects.SetDashboardInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -23,12 +24,9 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 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.reviewdb.client.Project;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.DashboardResource;
 import com.google.gerrit.server.project.ProjectCache;
@@ -36,7 +34,6 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Option;
@@ -70,7 +67,7 @@
 
   @Override
   public Response<DashboardInfo> apply(DashboardResource rsrc, SetDashboardInput input)
-      throws RestApiException, IOException, PermissionBackendException {
+      throws Exception {
     if (input == null) {
       input = new SetDashboardInput(); // Delete would set input to null.
     }
@@ -119,9 +116,9 @@
       cache.evict(rsrc.getProjectState().getProject());
 
       if (target != null) {
-        DashboardInfo info = get.get().apply(target);
-        info.isDefault = true;
-        return Response.ok(info);
+        Response<DashboardInfo> response = get.get().apply(target);
+        response.value().isDefault = true;
+        return response;
       }
       return Response.none();
     } catch (RepositoryNotFoundException notFound) {
diff --git a/java/com/google/gerrit/server/restapi/project/SetHead.java b/java/com/google/gerrit/server/restapi/project/SetHead.java
index 430d709..0afea5c 100644
--- a/java/com/google/gerrit/server/restapi/project/SetHead.java
+++ b/java/com/google/gerrit/server/restapi/project/SetHead.java
@@ -15,15 +15,16 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.HeadInput;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -63,7 +64,7 @@
   }
 
   @Override
-  public String apply(ProjectResource rsrc, HeadInput input)
+  public Response<String> apply(ProjectResource rsrc, HeadInput input)
       throws AuthException, ResourceNotFoundException, BadRequestException,
           UnprocessableEntityException, IOException, PermissionBackendException {
     if (input == null || Strings.isNullOrEmpty(input.ref)) {
@@ -109,7 +110,7 @@
 
         fire(rsrc.getNameKey(), oldHead, newHead);
       }
-      return ref;
+      return Response.ok(ref);
     } catch (RepositoryNotFoundException e) {
       throw new ResourceNotFoundException(rsrc.getName(), e);
     }
diff --git a/java/com/google/gerrit/server/restapi/project/SetParent.java b/java/com/google/gerrit/server/restapi/project/SetParent.java
index 8e0363f..a610dd4 100644
--- a/java/com/google/gerrit/server/restapi/project/SetParent.java
+++ b/java/com/google/gerrit/server/restapi/project/SetParent.java
@@ -20,14 +20,15 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimap;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.ParentInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
@@ -85,11 +86,11 @@
   }
 
   @Override
-  public String apply(ProjectResource rsrc, ParentInput input)
+  public Response<String> apply(ProjectResource rsrc, ParentInput input)
       throws AuthException, ResourceConflictException, ResourceNotFoundException,
           UnprocessableEntityException, IOException, PermissionBackendException,
           BadRequestException {
-    return apply(rsrc, input, true);
+    return Response.ok(apply(rsrc, input, true));
   }
 
   public String apply(ProjectResource rsrc, ParentInput input, boolean checkIfAdmin)
@@ -155,7 +156,7 @@
 
     newParent = Strings.emptyToNull(newParent);
     if (newParent != null) {
-      ProjectState parent = cache.get(new Project.NameKey(newParent));
+      ProjectState parent = cache.get(Project.nameKey(newParent));
       if (parent == null) {
         throw new UnprocessableEntityException("parent project " + newParent + " not found");
       }
diff --git a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
index 2be6c19..32aec59 100644
--- a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
+++ b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
@@ -20,20 +20,19 @@
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 /**
  * Java implementation of Gerrit's default pre-submit rules behavior: check if the labels have the
@@ -63,13 +62,13 @@
   }
 
   @Override
-  public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions options) {
+  public Optional<SubmitRecord> evaluate(ChangeData cd) {
     ProjectState projectState = projectCache.get(cd.project());
 
     // In case at least one project has a rules.pl file, we let Prolog handle it.
     // The Prolog rules engine will also handle the labels for us.
     if (projectState == null || projectState.hasPrologRules()) {
-      return Collections.emptyList();
+      return Optional.empty();
     }
 
     SubmitRecord submitRecord = new SubmitRecord();
@@ -86,7 +85,7 @@
 
       submitRecord.errorMessage = "Unable to fetch labels and approvals for the change";
       submitRecord.status = SubmitRecord.Status.RULE_ERROR;
-      return Collections.singletonList(submitRecord);
+      return Optional.of(submitRecord);
     }
 
     submitRecord.labels = new ArrayList<>(labelTypes.size());
@@ -99,7 +98,7 @@
 
         submitRecord.errorMessage = "Unable to find the LabelFunction for label " + t.getName();
         submitRecord.status = SubmitRecord.Status.RULE_ERROR;
-        return Collections.singletonList(submitRecord);
+        return Optional.of(submitRecord);
       }
 
       Collection<PatchSetApproval> approvalsForLabel = getApprovalsForLabel(approvals, t);
@@ -119,13 +118,13 @@
       }
     }
 
-    return Collections.singletonList(submitRecord);
+    return Optional.of(submitRecord);
   }
 
   private static List<PatchSetApproval> getApprovalsForLabel(
       List<PatchSetApproval> approvals, LabelType t) {
     return approvals.stream()
-        .filter(input -> input.getLabel().equals(t.getLabelId().get()))
+        .filter(input -> input.label().equals(t.getLabelId().get()))
         .collect(toImmutableList());
   }
 }
diff --git a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
index 759be39..54915fb 100644
--- a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
+++ b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
@@ -17,23 +17,22 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 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.PatchSetApproval;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.Exports;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.AbstractModule;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 
 /**
  * Rule to require an approval from a user that did not upload the current patch set or block
@@ -56,7 +55,7 @@
   }
 
   @Override
-  public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions options) {
+  public Optional<SubmitRecord> evaluate(ChangeData cd) {
     List<LabelType> labelTypes;
     List<PatchSetApproval> approvals;
     try {
@@ -64,21 +63,21 @@
       approvals = cd.currentApprovals();
     } catch (StorageException e) {
       logger.atWarning().withCause(e).log(E_UNABLE_TO_FETCH_LABELS);
-      return singletonRuleError(E_UNABLE_TO_FETCH_LABELS);
+      return ruleError(E_UNABLE_TO_FETCH_LABELS);
     }
 
     boolean shouldIgnoreSelfApproval = labelTypes.stream().anyMatch(LabelType::ignoreSelfApproval);
     if (!shouldIgnoreSelfApproval) {
       // Shortcut to avoid further processing if no label should ignore uploader approvals
-      return ImmutableList.of();
+      return Optional.empty();
     }
 
     Account.Id uploader;
     try {
-      uploader = cd.currentPatchSet().getUploader();
+      uploader = cd.currentPatchSet().uploader();
     } catch (StorageException e) {
       logger.atWarning().withCause(e).log(E_UNABLE_TO_FETCH_UPLOADER);
-      return singletonRuleError(E_UNABLE_TO_FETCH_UPLOADER);
+      return ruleError(E_UNABLE_TO_FETCH_UPLOADER);
     }
 
     SubmitRecord submitRecord = new SubmitRecord();
@@ -120,10 +119,10 @@
     }
 
     if (submitRecord.labels.isEmpty()) {
-      return ImmutableList.of();
+      return Optional.empty();
     }
 
-    return ImmutableList.of(submitRecord);
+    return Optional.of(submitRecord);
   }
 
   private static boolean labelCheckPassed(SubmitRecord.Label label) {
@@ -140,18 +139,18 @@
     return false;
   }
 
-  private static Collection<SubmitRecord> singletonRuleError(String reason) {
+  private static Optional<SubmitRecord> ruleError(String reason) {
     SubmitRecord submitRecord = new SubmitRecord();
     submitRecord.errorMessage = reason;
     submitRecord.status = SubmitRecord.Status.RULE_ERROR;
-    return ImmutableList.of(submitRecord);
+    return Optional.of(submitRecord);
   }
 
   @VisibleForTesting
   static Collection<PatchSetApproval> filterOutPositiveApprovalsOfUser(
       Collection<PatchSetApproval> approvals, Account.Id user) {
     return approvals.stream()
-        .filter(input -> input.getValue() < 0 || !input.getAccountId().equals(user))
+        .filter(input -> input.value() < 0 || !input.accountId().equals(user))
         .collect(toImmutableList());
   }
 
@@ -159,7 +158,7 @@
   static Collection<PatchSetApproval> filterApprovalsByLabel(
       Collection<PatchSetApproval> approvals, LabelType t) {
     return approvals.stream()
-        .filter(input -> input.getLabelId().get().equals(t.getLabelId().get()))
+        .filter(input -> input.labelId().get().equals(t.getLabelId().get()))
         .collect(toImmutableList());
   }
 }
diff --git a/java/com/google/gerrit/server/rules/PrologOptions.java b/java/com/google/gerrit/server/rules/PrologOptions.java
new file mode 100644
index 0000000..a176f04
--- /dev/null
+++ b/java/com/google/gerrit/server/rules/PrologOptions.java
@@ -0,0 +1,60 @@
+// 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.rules;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
+
+@AutoValue
+public abstract class PrologOptions {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public static PrologOptions defaultOptions() {
+    return new AutoValue_PrologOptions.Builder().logErrors(true).skipFilters(false).build();
+  }
+
+  public static PrologOptions dryRunOptions(String ruleToTest, boolean skipFilters) {
+    return new AutoValue_PrologOptions.Builder()
+        .logErrors(logger.atFine().isEnabled())
+        .skipFilters(skipFilters)
+        .rule(ruleToTest)
+        .build();
+  }
+
+  /** Whether errors should be logged. */
+  abstract boolean logErrors();
+
+  /** Whether Prolog filters from parent projects should be skipped. */
+  abstract boolean skipFilters();
+
+  /**
+   * Prolog rule that should be run. If not given, the Prolog rule that is configured for the
+   * project is used (the rule from rules.pl in refs/meta/config).
+   */
+  abstract Optional<String> rule();
+
+  @AutoValue.Builder
+  abstract static class Builder {
+    abstract PrologOptions.Builder logErrors(boolean logErrors);
+
+    abstract PrologOptions.Builder skipFilters(boolean skipFilters);
+
+    abstract PrologOptions.Builder rule(@Nullable String rule);
+
+    abstract PrologOptions build();
+  }
+}
diff --git a/java/com/google/gerrit/server/rules/PrologRule.java b/java/com/google/gerrit/server/rules/PrologRule.java
index 0c54f40..bf1d545 100644
--- a/java/com/google/gerrit/server/rules/PrologRule.java
+++ b/java/com/google/gerrit/server/rules/PrologRule.java
@@ -18,12 +18,10 @@
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.util.Collection;
-import java.util.Collections;
+import java.util.Optional;
 
 @Singleton
 public class PrologRule implements SubmitRule {
@@ -37,21 +35,29 @@
   }
 
   @Override
-  public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions opts) {
+  public Optional<SubmitRecord> evaluate(ChangeData cd) {
     ProjectState projectState = projectCache.get(cd.project());
     // We only want to run the Prolog engine if we have at least one rules.pl file to use.
-    if ((projectState == null || !projectState.hasPrologRules()) && opts.rule() == null) {
-      return Collections.emptyList();
+    if ((projectState == null || !projectState.hasPrologRules())) {
+      return Optional.empty();
     }
 
+    return Optional.of(evaluate(cd, PrologOptions.defaultOptions()));
+  }
+
+  public SubmitRecord evaluate(ChangeData cd, PrologOptions opts) {
     return getEvaluator(cd, opts).evaluate();
   }
 
-  private PrologRuleEvaluator getEvaluator(ChangeData cd, SubmitRuleOptions opts) {
-    return factory.create(cd, opts);
+  public SubmitTypeRecord getSubmitType(ChangeData cd) {
+    return getSubmitType(cd, PrologOptions.defaultOptions());
   }
 
-  public SubmitTypeRecord getSubmitType(ChangeData cd, SubmitRuleOptions opts) {
+  public SubmitTypeRecord getSubmitType(ChangeData cd, PrologOptions opts) {
     return getEvaluator(cd, opts).getSubmitType();
   }
+
+  private PrologRuleEvaluator getEvaluator(ChangeData cd, PrologOptions opts) {
+    return factory.create(cd, opts);
+  }
 }
diff --git a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
index 9cde54c..72dc46a 100644
--- a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
+++ b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.rules;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.project.SubmitRuleEvaluator.createRuleError;
 import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultRuleError;
 import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultTypeError;
@@ -24,10 +25,10 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.Emails;
@@ -35,7 +36,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RuleEvalException;
-import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -51,7 +51,6 @@
 import com.googlecode.prolog_cafe.lang.VariableTerm;
 import java.io.StringReader;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
@@ -73,7 +72,7 @@
 
   public interface Factory {
     /** Returns a new {@link PrologRuleEvaluator} with the specified options */
-    PrologRuleEvaluator create(ChangeData cd, SubmitRuleOptions options);
+    PrologRuleEvaluator create(ChangeData cd, PrologOptions options);
   }
 
   /**
@@ -95,7 +94,7 @@
   private final PrologEnvironment.Factory envFactory;
   private final ChangeData cd;
   private final ProjectState projectState;
-  private final SubmitRuleOptions opts;
+  private final PrologOptions opts;
   private Term submitRule;
 
   @AssistedInject
@@ -107,7 +106,7 @@
       PrologEnvironment.Factory envFactory,
       ProjectCache projectCache,
       @Assisted ChangeData cd,
-      @Assisted SubmitRuleOptions options) {
+      @Assisted PrologOptions options) {
     this.accountCache = accountCache;
     this.accounts = accounts;
     this.emails = emails;
@@ -141,10 +140,9 @@
   /**
    * Evaluate the submit rules.
    *
-   * @return List of {@link SubmitRecord} objects returned from the evaluated rules, including any
-   *     errors.
+   * @return {@link SubmitRecord} returned from the evaluated rules. Can include errors.
    */
-  public Collection<SubmitRecord> evaluate() {
+  public SubmitRecord evaluate() {
     Change change;
     try {
       change = cd.change();
@@ -159,12 +157,6 @@
       return ruleError("Error looking up change " + cd.getId(), e);
     }
 
-    if (!opts.allowClosed() && change.isClosed()) {
-      SubmitRecord rec = new SubmitRecord();
-      rec.status = SubmitRecord.Status.CLOSED;
-      return Collections.singletonList(rec);
-    }
-
     List<Term> results;
     try {
       results =
@@ -200,28 +192,32 @@
    * output. Later after the loop the out collection is reversed to restore it to the original
    * ordering.
    */
-  public List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
-    boolean foundOk = false;
-    List<SubmitRecord> out = new ArrayList<>(results.size());
+  public SubmitRecord resultsToSubmitRecord(Term submitRule, List<Term> results) {
+    checkState(!results.isEmpty(), "the list of Prolog terms must not be empty");
+
+    SubmitRecord resultSubmitRecord = new SubmitRecord();
+    resultSubmitRecord.labels = new ArrayList<>();
     for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
       Term submitRecord = results.get(resultIdx);
-      SubmitRecord rec = new SubmitRecord();
-      out.add(rec);
 
       if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
         return invalidResult(submitRule, submitRecord);
       }
 
-      if ("ok".equals(submitRecord.name())) {
-        rec.status = SubmitRecord.Status.OK;
-
-      } else if ("not_ready".equals(submitRecord.name())) {
-        rec.status = SubmitRecord.Status.NOT_READY;
-
-      } else {
+      if (!"ok".equals(submitRecord.name()) && !"not_ready".equals(submitRecord.name())) {
         return invalidResult(submitRule, submitRecord);
       }
 
+      // This transformation is required to adapt Prolog's behavior to the way Gerrit handles
+      // SubmitRecords, as defined in the SubmitRecord#allRecordsOK method.
+      // When several rules are defined in Prolog, they are all matched to a SubmitRecord. We want
+      // the change to be submittable when at least one result is OK.
+      if ("ok".equals(submitRecord.name())) {
+        resultSubmitRecord.status = SubmitRecord.Status.OK;
+      } else if ("not_ready".equals(submitRecord.name()) && resultSubmitRecord.status == null) {
+        resultSubmitRecord.status = SubmitRecord.Status.NOT_READY;
+      }
+
       // Unpack the one argument. This should also be a structure with one
       // argument per label that needs to be reported on to the caller.
       //
@@ -231,8 +227,6 @@
         return invalidResult(submitRule, submitRecord);
       }
 
-      rec.labels = new ArrayList<>(submitRecord.arity());
-
       for (Term state : ((StructureTerm) submitRecord).args()) {
         if (!(state instanceof StructureTerm)
             || 2 != state.arity()
@@ -241,7 +235,7 @@
         }
 
         SubmitRecord.Label lbl = new SubmitRecord.Label();
-        rec.labels.add(lbl);
+        resultSubmitRecord.labels.add(lbl);
 
         lbl.label = checkLabelName(state.arg(0).name());
         Term status = state.arg(1);
@@ -272,24 +266,12 @@
         }
       }
 
-      if (rec.status == SubmitRecord.Status.OK) {
-        foundOk = true;
+      if (resultSubmitRecord.status == SubmitRecord.Status.OK) {
         break;
       }
     }
-    Collections.reverse(out);
-
-    // This transformation is required to adapt Prolog's behavior to the way Gerrit handles
-    // SubmitRecords, as defined in the SubmitRecord#allRecordsOK method.
-    // When several rules are defined in Prolog, they are all matched to a SubmitRecord. We want
-    // the change to be submittable when at least one result is OK.
-    if (foundOk) {
-      for (SubmitRecord record : out) {
-        record.status = SubmitRecord.Status.OK;
-      }
-    }
-
-    return out;
+    Collections.reverse(resultSubmitRecord.labels);
+    return resultSubmitRecord;
   }
 
   @VisibleForTesting
@@ -306,7 +288,7 @@
     return VALID_LABEL_MATCHER.retainFrom(name);
   }
 
-  private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) {
+  private SubmitRecord invalidResult(Term rule, Term record, String reason) {
     return ruleError(
         String.format(
             "Submit rule %s for change %s of %s output invalid result: %s%s",
@@ -317,15 +299,15 @@
             (reason == null ? "" : ". Reason: " + reason)));
   }
 
-  private List<SubmitRecord> invalidResult(Term rule, Term record) {
+  private SubmitRecord invalidResult(Term rule, Term record) {
     return invalidResult(rule, record, null);
   }
 
-  private List<SubmitRecord> ruleError(String err) {
+  private SubmitRecord ruleError(String err) {
     return ruleError(err, null);
   }
 
-  private List<SubmitRecord> ruleError(String err, Exception e) {
+  private SubmitRecord ruleError(String err, Exception e) {
     if (opts.logErrors()) {
       logger.atSevere().withCause(e).log(err);
       return defaultRuleError();
@@ -465,22 +447,22 @@
     PrologEnvironment env;
     try {
       PrologMachineCopy pmc;
-      if (opts.rule() == null) {
+      if (opts.rule().isPresent()) {
+        pmc = rulesCache.loadMachine("stdin", new StringReader(opts.rule().get()));
+      } else {
         pmc =
             rulesCache.loadMachine(
                 projectState.getNameKey(), projectState.getConfig().getRulesId());
-      } else {
-        pmc = rulesCache.loadMachine("stdin", new StringReader(opts.rule()));
       }
       env = envFactory.create(pmc);
     } catch (CompileException err) {
       String msg;
-      if (opts.rule() == null) {
+      if (opts.rule().isPresent()) {
+        msg = err.getMessage();
+      } else {
         msg =
             String.format(
                 "Cannot load rules.pl for %s: %s", projectState.getName(), err.getMessage());
-      } else {
-        msg = err.getMessage();
       }
       throw new RuleEvalException(msg, err);
     }
@@ -540,7 +522,7 @@
     if (status instanceof StructureTerm && status.arity() == 1) {
       Term who = status.arg(0);
       if (isUser(who)) {
-        label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
+        label.appliedBy = Account.id(((IntegerTerm) who.arg(0)).intValue());
       } else {
         throw new UserTermExpected(label);
       }
diff --git a/java/com/google/gerrit/server/rules/RulesCache.java b/java/com/google/gerrit/server/rules/RulesCache.java
index d4e90f9..0cd21a4 100644
--- a/java/com/google/gerrit/server/rules/RulesCache.java
+++ b/java/com/google/gerrit/server/rules/RulesCache.java
@@ -19,8 +19,8 @@
 import com.google.common.base.Joiner;
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
diff --git a/java/com/google/gerrit/server/rules/StoredValues.java b/java/com/google/gerrit/server/rules/StoredValues.java
index 30d329d..1e08a24 100644
--- a/java/com/google/gerrit/server/rules/StoredValues.java
+++ b/java/com/google/gerrit/server/rules/StoredValues.java
@@ -16,12 +16,12 @@
 
 import static com.google.gerrit.server.rules.StoredValue.create;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -42,7 +42,6 @@
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 
@@ -97,9 +96,8 @@
           PatchListCache plCache = env.getArgs().getPatchListCache();
           Change change = getChange(engine);
           Project.NameKey project = change.getProject();
-          ObjectId b = ObjectId.fromString(ps.getRevision().get());
           Whitespace ws = Whitespace.IGNORE_NONE;
-          PatchListKey plKey = PatchListKey.againstDefaultBase(b, ws);
+          PatchListKey plKey = PatchListKey.againstDefaultBase(ps.commitId(), ws);
           PatchList patchList;
           try {
             patchList = plCache.get(plKey, project);
diff --git a/java/com/google/gerrit/server/rules/SubmitRule.java b/java/com/google/gerrit/server/rules/SubmitRule.java
index 2a68683..b221117 100644
--- a/java/com/google/gerrit/server/rules/SubmitRule.java
+++ b/java/com/google/gerrit/server/rules/SubmitRule.java
@@ -15,15 +15,14 @@
 
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
-import java.util.Collection;
+import java.util.Optional;
 
 /**
  * Allows plugins to decide whether a change is ready to be submitted or not.
  *
- * <p>For a given {@link ChangeData}, each plugin is called and returns a {@link Collection} of
- * {@link SubmitRecord}. This collection can be empty, or contain one or several values.
+ * <p>For a given {@link ChangeData}, each plugin is called and returns a {@link Optional} of {@link
+ * SubmitRecord}.
  *
  * <p>A Change can only be submitted if all the plugins give their consent.
  *
@@ -39,6 +38,9 @@
  */
 @ExtensionPoint
 public interface SubmitRule {
-  /** Returns a {@link Collection} of {@link SubmitRecord} status for the change. */
-  Collection<SubmitRecord> evaluate(ChangeData changeData, SubmitRuleOptions options);
+  /**
+   * Returns a {@link Optional} of {@link SubmitRecord} status for the change. {@code
+   * Optional#empty()} if the SubmitRule was a no-op.
+   */
+  Optional<SubmitRecord> evaluate(ChangeData changeData);
 }
diff --git a/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 9446b7c..c15efba 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.schema;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_SEQUENCES;
+import static com.google.gerrit.entities.RefNames.REFS_SEQUENCES;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -30,8 +30,8 @@
 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.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
diff --git a/java/com/google/gerrit/server/schema/AllProjectsInput.java b/java/com/google/gerrit/server/schema/AllProjectsInput.java
index 7231b18..6e11a5d 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsInput.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsInput.java
@@ -21,8 +21,8 @@
 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.extensions.client.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 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 d2f5ef1..4904028 100644
--- a/java/com/google/gerrit/server/schema/AllUsersCreator.java
+++ b/java/com/google/gerrit/server/schema/AllUsersCreator.java
@@ -26,8 +26,8 @@
 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.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
index ee99c67..cee0862 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -9,23 +9,23 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/gpg",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
-        "//java/com/google/gerrit/server/util/time",
+        "//java/com/google/gerrit/server/logging",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib:jgit-archive",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/commons:dbcp",
         "//lib/flogger:api",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/log:jsonevent-layout",
         "//lib/log:log4j",
     ],
diff --git a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index 0612bb9..49dcc46 100644
--- a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -21,19 +21,22 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import java.nio.file.Path;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
@@ -212,14 +215,22 @@
 
   @Override
   public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Mark file as reviewed",
+                Metadata.builder()
+                    .patchSetId(psId.get())
+                    .accountId(accountId.get())
+                    .filePath(path)
+                    .build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "INSERT INTO account_patch_reviews "
                     + "(account_id, change_id, patch_set_id, file_name) VALUES "
                     + "(?, ?, ?, ?)")) {
       stmt.setInt(1, accountId.get());
-      stmt.setInt(2, psId.getParentKey().get());
+      stmt.setInt(2, psId.changeId().get());
       stmt.setInt(3, psId.get());
       stmt.setString(4, path);
       stmt.executeUpdate();
@@ -239,7 +250,15 @@
       return;
     }
 
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Mark files as reviewed",
+                Metadata.builder()
+                    .patchSetId(psId.get())
+                    .accountId(accountId.get())
+                    .resourceCount(paths.size())
+                    .build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "INSERT INTO account_patch_reviews "
@@ -247,7 +266,7 @@
                     + "(?, ?, ?, ?)")) {
       for (String path : paths) {
         stmt.setInt(1, accountId.get());
-        stmt.setInt(2, psId.getParentKey().get());
+        stmt.setInt(2, psId.changeId().get());
         stmt.setInt(3, psId.get());
         stmt.setString(4, path);
         stmt.addBatch();
@@ -264,14 +283,22 @@
 
   @Override
   public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Clear reviewed flag",
+                Metadata.builder()
+                    .patchSetId(psId.get())
+                    .accountId(accountId.get())
+                    .filePath(path)
+                    .build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "DELETE FROM account_patch_reviews "
                     + "WHERE account_id = ? AND change_id = ? AND "
                     + "patch_set_id = ? AND file_name = ?")) {
       stmt.setInt(1, accountId.get());
-      stmt.setInt(2, psId.getParentKey().get());
+      stmt.setInt(2, psId.changeId().get());
       stmt.setInt(3, psId.get());
       stmt.setString(4, path);
       stmt.executeUpdate();
@@ -282,12 +309,16 @@
 
   @Override
   public void clearReviewed(PatchSet.Id psId) {
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Clear all reviewed flags of patch set",
+                Metadata.builder().patchSetId(psId.get()).build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "DELETE FROM account_patch_reviews "
                     + "WHERE change_id = ? AND patch_set_id = ?")) {
-      stmt.setInt(1, psId.getParentKey().get());
+      stmt.setInt(1, psId.changeId().get());
       stmt.setInt(2, psId.get());
       stmt.executeUpdate();
     } catch (SQLException e) {
@@ -297,7 +328,11 @@
 
   @Override
   public void clearReviewed(Change.Id changeId) {
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Clear all reviewed flags of change",
+                Metadata.builder().changeId(changeId.get()).build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement("DELETE FROM account_patch_reviews WHERE change_id = ?")) {
       stmt.setInt(1, changeId.get());
@@ -309,7 +344,11 @@
 
   @Override
   public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId) {
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Find reviewed flags",
+                Metadata.builder().patchSetId(psId.get()).accountId(accountId.get()).build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "SELECT patch_set_id, file_name FROM account_patch_reviews APR1 "
@@ -319,11 +358,11 @@
                     + "AND APR1.change_id = APR2.change_id "
                     + "AND patch_set_id <= ?)")) {
       stmt.setInt(1, accountId.get());
-      stmt.setInt(2, psId.getParentKey().get());
+      stmt.setInt(2, psId.changeId().get());
       stmt.setInt(3, psId.get());
       try (ResultSet rs = stmt.executeQuery()) {
         if (rs.next()) {
-          PatchSet.Id id = new PatchSet.Id(psId.getParentKey(), rs.getInt("patch_set_id"));
+          PatchSet.Id id = PatchSet.id(psId.changeId(), rs.getInt("patch_set_id"));
           ImmutableSet.Builder<String> builder = ImmutableSet.builder();
           do {
             builder.add(rs.getString("file_name"));
diff --git a/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
index b0a3370..dd82be2 100644
--- a/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
@@ -22,6 +22,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.SQLException;
+import java.sql.Statement;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
@@ -50,4 +51,17 @@
         return new StorageException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
     }
   }
+
+  @Override
+  protected void doCreateTable(Statement stmt) throws SQLException {
+    stmt.executeUpdate(
+        "CREATE TABLE IF NOT EXISTS account_patch_reviews ("
+            + "account_id INTEGER DEFAULT 0 NOT NULL, "
+            + "change_id INTEGER DEFAULT 0 NOT NULL, "
+            + "patch_set_id INTEGER DEFAULT 0 NOT NULL, "
+            + "file_name VARCHAR(255) DEFAULT '' NOT NULL, "
+            + "CONSTRAINT primary_key_account_patch_reviews "
+            + "PRIMARY KEY (change_id, patch_set_id, account_id, file_name)"
+            + ")");
+  }
 }
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java b/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
index df95ff7..0e22af9 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
@@ -20,8 +20,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
index 33534fc..0360ec0 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
@@ -14,15 +14,20 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.ProvisionException;
+import org.eclipse.jgit.lib.Config;
 
 public class NoteDbSchemaVersionCheck implements LifecycleListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public static Module module() {
     return new LifecycleModule() {
       @Override
@@ -34,11 +39,16 @@
 
   private final NoteDbSchemaVersionManager versionManager;
   private final SitePaths sitePaths;
+  private Config gerritConfig;
 
   @Inject
-  NoteDbSchemaVersionCheck(NoteDbSchemaVersionManager versionManager, SitePaths sitePaths) {
+  NoteDbSchemaVersionCheck(
+      NoteDbSchemaVersionManager versionManager,
+      SitePaths sitePaths,
+      @GerritServerConfig Config gerritConfig) {
     this.versionManager = versionManager;
     this.sitePaths = sitePaths;
+    this.gerritConfig = gerritConfig;
   }
 
   @Override
@@ -53,7 +63,18 @@
                 sitePaths.site_path.toAbsolutePath()));
       }
       int expected = NoteDbSchemaVersions.LATEST;
-      if (current != expected) {
+
+      if (current > expected
+          && gerritConfig.getBoolean("gerrit", "experimentalRollingUpgrade", false)) {
+        logger.atWarning().log(
+            "Gerrit has detected refs/meta/version %d different than the expected %d."
+                + "Bear in mind that this is supported ONLY for rolling upgrades to immediate next "
+                + "Gerrit version (e.g. v3.1 to v3.2). If this is not expected, remove gerrit.experimentalRollingUpgrade "
+                + "from $GERRIT_SITE/etc/gerrit.config and restart Gerrit."
+                + "Please note that gerrit.experimentalRollingUpgrade is intended to be used "
+                + "for the rolling upgrade phase only and should be disabled afterwards.",
+            current, expected);
+      } else if (current != expected) {
         String advice =
             current > expected
                 ? "Downgrade is not supported"
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.java
index 7ff0ea6..803872c 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.schema;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_VERSION;
+import static com.google.gerrit.entities.RefNames.REFS_VERSION;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.exceptions.StorageException;
diff --git a/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
index 9e12807..5e7dbf0 100644
--- a/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
+++ b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
@@ -20,8 +20,8 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
diff --git a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
index 0a5823a..26f1990 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
@@ -17,10 +17,10 @@
 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.exceptions.DuplicateKeyException;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.GroupUUID;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -218,8 +218,8 @@
   private InternalGroupCreation getGroupCreation(Sequences seqs, GroupReference groupReference) {
     int next = seqs.nextGroupId();
     return InternalGroupCreation.builder()
-        .setNameKey(new AccountGroup.NameKey(groupReference.getName()))
-        .setId(new AccountGroup.Id(next))
+        .setNameKey(AccountGroup.nameKey(groupReference.getName()))
+        .setId(AccountGroup.id(next))
         .setGroupUUID(groupReference.getUUID())
         .build();
   }
diff --git a/java/com/google/gerrit/server/schema/VersionedAccountPreferences.java b/java/com/google/gerrit/server/schema/VersionedAccountPreferences.java
index 297fdcd..468c26b 100644
--- a/java/com/google/gerrit/server/schema/VersionedAccountPreferences.java
+++ b/java/com/google/gerrit/server/schema/VersionedAccountPreferences.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.schema;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
diff --git a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
index 63837b2..a6424b9 100644
--- a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
+++ b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.server.schema.testing;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import java.io.IOException;
@@ -151,8 +152,8 @@
 
     Set<String> subsections1 = config1.getSubsections(section);
     Set<String> subsections2 = config2.getSubsections(section);
-    assertThat(subsections1)
-        .named("section \"%s\"", section)
+    assertWithMessage("section \"%s\"", section)
+        .that(subsections1)
         .containsExactlyElementsIn(subsections2);
 
     subsections1.forEach(s -> assertSubsectionEquivalent(config1, config2, section, s));
@@ -163,12 +164,12 @@
     Set<String> subsectionNames1 = config1.getNames(section, subsection);
     Set<String> subsectionNames2 = config2.getNames(section, subsection);
     String name = String.format("subsection \"%s\" of section \"%s\"", subsection, section);
-    assertThat(subsectionNames1).named(name).containsExactlyElementsIn(subsectionNames2);
+    assertWithMessage(name).that(subsectionNames1).containsExactlyElementsIn(subsectionNames2);
 
     subsectionNames1.forEach(
         n ->
-            assertThat(config1.getStringList(section, subsection, n))
-                .named(name)
+            assertWithMessage(name)
+                .that(config1.getStringList(section, subsection, n))
                 .asList()
                 .containsExactlyElementsIn(config2.getStringList(section, subsection, n)));
   }
diff --git a/java/com/google/gerrit/server/schema/testing/BUILD b/java/com/google/gerrit/server/schema/testing/BUILD
index d641c47..77bb777 100644
--- a/java/com/google/gerrit/server/schema/testing/BUILD
+++ b/java/com/google/gerrit/server/schema/testing/BUILD
@@ -7,10 +7,10 @@
     testonly = True,
     srcs = glob(["*.java"]),
     deps = [
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
         "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/server/securestore/testing/BUILD b/java/com/google/gerrit/server/securestore/testing/BUILD
index c2582b9..9afc44a 100644
--- a/java/com/google/gerrit/server/securestore/testing/BUILD
+++ b/java/com/google/gerrit/server/securestore/testing/BUILD
@@ -8,6 +8,6 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/server",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/server/ssh/NoSshKeyCache.java b/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
index 74bb50c..3ba446c 100644
--- a/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
+++ b/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.ssh;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
diff --git a/java/com/google/gerrit/server/ssh/SshKeyCreator.java b/java/com/google/gerrit/server/ssh/SshKeyCreator.java
index beaf1ba..2241d86 100644
--- a/java/com/google/gerrit/server/ssh/SshKeyCreator.java
+++ b/java/com/google/gerrit/server/ssh/SshKeyCreator.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.ssh;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountSshKey;
 
 public interface SshKeyCreator {
diff --git a/java/com/google/gerrit/server/submit/ChangeSet.java b/java/com/google/gerrit/server/submit/ChangeSet.java
index e721be41ab..65e0b48 100644
--- a/java/com/google/gerrit/server/submit/ChangeSet.java
+++ b/java/com/google/gerrit/server/submit/ChangeSet.java
@@ -20,9 +20,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.query.change.ChangeData;
 import java.util.Collection;
 import java.util.LinkedHashMap;
@@ -76,8 +76,8 @@
     return changeData;
   }
 
-  public ListMultimap<Branch.NameKey, ChangeData> changesByBranch() {
-    ListMultimap<Branch.NameKey, ChangeData> ret =
+  public ListMultimap<BranchNameKey, ChangeData> changesByBranch() {
+    ListMultimap<BranchNameKey, ChangeData> ret =
         MultimapBuilder.hashKeys().arrayListValues().build();
     for (ChangeData cd : changeData.values()) {
       ret.put(cd.change().getDest(), cd);
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index 8ff3cc5..8b7b2cd 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -19,11 +19,11 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetInfo;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.MergeTip;
@@ -162,7 +162,7 @@
               ctx.getUpdate(psId),
               psId,
               newCommit,
-              prevPs != null ? prevPs.getGroups() : ImmutableList.of(),
+              prevPs != null ? prevPs.groups() : ImmutableList.of(),
               null,
               null);
       ctx.getChange().setCurrentPatchSet(patchSetInfo);
diff --git a/java/com/google/gerrit/server/submit/CommitMergeStatus.java b/java/com/google/gerrit/server/submit/CommitMergeStatus.java
index 12172dd..bf8b840 100644
--- a/java/com/google/gerrit/server/submit/CommitMergeStatus.java
+++ b/java/com/google/gerrit/server/submit/CommitMergeStatus.java
@@ -17,7 +17,7 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -49,7 +49,8 @@
           + "Please rebase the change locally and upload the rebased commit for review."),
 
   SKIPPED_IDENTICAL_TREE(
-      "Marking change merged without cherry-picking to branch, as the resulting commit would be empty."),
+      "Marking change merged without cherry-picking to branch, as the resulting commit would be"
+          + " empty."),
 
   MISSING_DEPENDENCY("Depends on change that was not submitted."),
 
@@ -102,24 +103,22 @@
           commit, otherCommit, caller != null ? caller.getLoggableName() : "<user-not-available>");
     } else if (changes.size() == 1) {
       ChangeData cd = changes.get(0);
-      if (cd.currentPatchSet().getRevision().get().equals(otherCommit)) {
+      if (cd.currentPatchSet().commitId().name().equals(otherCommit)) {
         return String.format(
             "Commit %s depends on commit %s of change %d which cannot be merged.",
             commit, otherCommit, cd.getId().get());
       }
       Optional<PatchSet> patchSet =
-          cd.patchSets().stream()
-              .filter(ps -> ps.getRevision().get().equals(otherCommit))
-              .findAny();
+          cd.patchSets().stream().filter(ps -> ps.commitId().name().equals(otherCommit)).findAny();
       if (patchSet.isPresent()) {
         return String.format(
             "Commit %s depends on commit %s, which is outdated patch set %d of change %d."
                 + " The latest patch set is %d.",
             commit,
             otherCommit,
-            patchSet.get().getId().get(),
+            patchSet.get().id().get(),
             cd.getId().get(),
-            cd.currentPatchSet().getId().get());
+            cd.currentPatchSet().id().get());
       }
       // should not happen, fall-back to default message
       return String.format(
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index a1f56eb..c94d49e 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -16,9 +16,9 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.NotifyResolver;
diff --git a/java/com/google/gerrit/server/submit/FastForwardOp.java b/java/com/google/gerrit/server/submit/FastForwardOp.java
index 08f5abb..c83e113 100644
--- a/java/com/google/gerrit/server/submit/FastForwardOp.java
+++ b/java/com/google/gerrit/server/submit/FastForwardOp.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.server.submit.CommitMergeStatus.EMPTY_COMMIT;
 
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
+import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.update.RepoContext;
 
diff --git a/java/com/google/gerrit/server/submit/GitModules.java b/java/com/google/gerrit/server/submit/GitModules.java
index d49f53f..f8f6bc4 100644
--- a/java/com/google/gerrit/server/submit/GitModules.java
+++ b/java/com/google/gerrit/server/submit/GitModules.java
@@ -16,9 +16,9 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.SubmoduleSubscription;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
@@ -45,7 +45,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
-    GitModules create(Branch.NameKey project, MergeOpRepoManager m);
+    GitModules create(BranchNameKey project, MergeOpRepoManager m);
   }
 
   private static final String GIT_MODULES = ".gitmodules";
@@ -55,16 +55,16 @@
   @Inject
   GitModules(
       @CanonicalWebUrl @Nullable String canonicalWebUrl,
-      @Assisted Branch.NameKey branch,
+      @Assisted BranchNameKey branch,
       @Assisted MergeOpRepoManager orm)
       throws IOException {
-    Project.NameKey project = branch.getParentKey();
+    Project.NameKey project = branch.project();
     logger.atFine().log("Loading .gitmodules of %s for project %s", branch, project);
     try {
       OpenRepo or = orm.getRepo(project);
-      ObjectId id = or.repo.resolve(branch.get());
+      ObjectId id = or.repo.resolve(branch.branch());
       if (id == null) {
-        throw new IOException("Cannot open branch " + branch.get());
+        throw new IOException("Cannot open branch " + branch.branch());
       }
       RevCommit commit = or.rw.parseCommit(id);
 
@@ -80,7 +80,7 @@
         config = new BlobBasedConfig(null, or.repo, commit, GIT_MODULES);
       } catch (ConfigInvalidException e) {
         throw new IOException(
-            "Could not read .gitmodules of super project: " + branch.getParentKey(), e);
+            "Could not read .gitmodules of super project: " + branch.project(), e);
       }
       subscriptions =
           new SubmoduleSectionParser(config, canonicalWebUrl, branch).parseAllSections();
@@ -89,7 +89,7 @@
     }
   }
 
-  Collection<SubmoduleSubscription> subscribedTo(Branch.NameKey src) {
+  Collection<SubmoduleSubscription> subscribedTo(BranchNameKey src) {
     Collection<SubmoduleSubscription> ret = new ArrayList<>();
     for (SubmoduleSubscription s : subscriptions) {
       if (s.getSubmodule().equals(src)) {
diff --git a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
index 1219124..ec6b35a 100644
--- a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
@@ -23,12 +23,12 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -53,7 +53,6 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
@@ -75,12 +74,12 @@
 
   @AutoValue
   abstract static class QueryKey {
-    private static QueryKey create(Branch.NameKey branch, Iterable<String> hashes) {
+    private static QueryKey create(BranchNameKey branch, Iterable<String> hashes) {
       return new AutoValue_LocalMergeSuperSetComputation_QueryKey(
           branch, ImmutableSet.copyOf(hashes));
     }
 
-    abstract Branch.NameKey branch();
+    abstract BranchNameKey branch();
 
     abstract ImmutableSet<String> hashes();
   }
@@ -88,7 +87,7 @@
   private final PermissionBackend permissionBackend;
   private final Provider<InternalChangeQuery> queryProvider;
   private final Map<QueryKey, ImmutableList<ChangeData>> queryCache;
-  private final Map<Branch.NameKey, Optional<RevCommit>> heads;
+  private final Map<BranchNameKey, Optional<RevCommit>> heads;
   private final ProjectCache projectCache;
   private final ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory;
 
@@ -115,10 +114,10 @@
 
     // For each target branch we run a separate rev walk to find open changes
     // reachable from changes already in the merge super set.
-    ImmutableListMultimap<Branch.NameKey, ChangeData> bc =
+    ImmutableListMultimap<BranchNameKey, ChangeData> bc =
         byBranch(Iterables.concat(changeSet.changes(), changeSet.nonVisibleChanges()));
-    for (Branch.NameKey b : bc.keySet()) {
-      OpenRepo or = getRepo(orm, b.getParentKey());
+    for (BranchNameKey b : bc.keySet()) {
+      OpenRepo or = getRepo(orm, b.project());
       List<RevCommit> visibleCommits = new ArrayList<>();
       List<RevCommit> nonVisibleCommits = new ArrayList<>();
       for (ChangeData cd : bc.get(b)) {
@@ -135,8 +134,7 @@
         }
 
         // Get the underlying git commit object
-        String objIdStr = cd.currentPatchSet().getRevision().get();
-        RevCommit commit = or.rw.parseCommit(ObjectId.fromString(objIdStr));
+        RevCommit commit = or.rw.parseCommit(cd.currentPatchSet().commitId());
 
         // Always include the input, even if merged. This allows
         // SubmitStrategyOp to correct the situation later, assuming it gets
@@ -161,9 +159,9 @@
     return new ChangeSet(visibleChanges, nonVisibleChanges);
   }
 
-  private static ImmutableListMultimap<Branch.NameKey, ChangeData> byBranch(
+  private static ImmutableListMultimap<BranchNameKey, ChangeData> byBranch(
       Iterable<ChangeData> changes) {
-    ImmutableListMultimap.Builder<Branch.NameKey, ChangeData> builder =
+    ImmutableListMultimap.Builder<BranchNameKey, ChangeData> builder =
         ImmutableListMultimap.builder();
     for (ChangeData cd : changes) {
       builder.put(cd.change().getDest(), cd);
@@ -213,7 +211,7 @@
 
   private ChangeSet byCommitsOnBranchNotMerged(
       OpenRepo or,
-      Branch.NameKey branch,
+      BranchNameKey branch,
       Set<String> visibleHashes,
       Set<String> nonVisibleHashes,
       CurrentUser user)
@@ -236,7 +234,7 @@
   }
 
   private ImmutableList<ChangeData> byCommitsOnBranchNotMerged(
-      OpenRepo or, Branch.NameKey branch, Set<String> hashes) throws IOException {
+      OpenRepo or, BranchNameKey branch, Set<String> hashes) throws IOException {
     if (hashes.isEmpty()) {
       return ImmutableList.of();
     }
@@ -252,7 +250,7 @@
   }
 
   private Set<String> walkChangesByHashes(
-      Collection<RevCommit> sourceCommits, Set<String> ignoreHashes, OpenRepo or, Branch.NameKey b)
+      Collection<RevCommit> sourceCommits, Set<String> ignoreHashes, OpenRepo or, BranchNameKey b)
       throws IOException {
     Set<String> destHashes = new HashSet<>();
     or.rw.reset();
@@ -276,10 +274,10 @@
     return destHashes;
   }
 
-  private void markHeadUninteresting(OpenRepo or, Branch.NameKey b) throws IOException {
+  private void markHeadUninteresting(OpenRepo or, BranchNameKey b) throws IOException {
     Optional<RevCommit> head = heads.get(b);
     if (head == null) {
-      Ref ref = or.repo.getRefDatabase().exactRef(b.get());
+      Ref ref = or.repo.getRefDatabase().exactRef(b.branch());
       head = ref != null ? Optional.of(or.rw.parseCommit(ref.getObjectId())) : Optional.empty();
       heads.put(b, head);
     }
diff --git a/java/com/google/gerrit/server/submit/MergeOneOp.java b/java/com/google/gerrit/server/submit/MergeOneOp.java
index 9806bdf..4555a32 100644
--- a/java/com/google/gerrit/server/submit/MergeOneOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOneOp.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.server.submit.CommitMergeStatus.EMPTY_COMMIT;
 
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
+import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.update.RepoContext;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 63d60a6..adb75a4 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -36,6 +36,11 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -48,11 +53,6 @@
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
@@ -91,6 +91,7 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -119,7 +120,7 @@
 
   public static class CommitStatus {
     private final ImmutableMap<Change.Id, ChangeData> changes;
-    private final ImmutableSetMultimap<Branch.NameKey, Change.Id> byBranch;
+    private final ImmutableSetMultimap<BranchNameKey, Change.Id> byBranch;
     private final Map<Change.Id, CodeReviewCommit> commits;
     private final ListMultimap<Change.Id, String> problems;
     private final boolean allowClosed;
@@ -128,7 +129,7 @@
       checkArgument(
           !cs.furtherHiddenChanges(), "CommitStatus must not be called with hidden changes");
       changes = cs.changesById();
-      ImmutableSetMultimap.Builder<Branch.NameKey, Change.Id> bb = ImmutableSetMultimap.builder();
+      ImmutableSetMultimap.Builder<BranchNameKey, Change.Id> bb = ImmutableSetMultimap.builder();
       for (ChangeData cd : cs.changes()) {
         bb.put(cd.change().getDest(), cd.getId());
       }
@@ -142,7 +143,7 @@
       return changes.keySet();
     }
 
-    public ImmutableSet<Change.Id> getChangeIds(Branch.NameKey branch) {
+    public ImmutableSet<Change.Id> getChangeIds(BranchNameKey branch) {
       return byBranch.get(branch);
     }
 
@@ -233,6 +234,9 @@
   private final RetryHelper retryHelper;
   private final ChangeData.Factory changeDataFactory;
 
+  // Changes that were updated by this MergeOp.
+  private final Map<Change.Id, Change> updatedChanges;
+
   private Timestamp ts;
   private RequestId submissionId;
   private IdentifiedUser caller;
@@ -244,6 +248,7 @@
   private Set<Project.NameKey> allProjects;
   private boolean dryrun;
   private TopicMetrics topicMetrics;
+  private String traceId;
 
   @Inject
   MergeOp(
@@ -273,6 +278,7 @@
     this.retryHelper = retryHelper;
     this.topicMetrics = topicMetrics;
     this.changeDataFactory = changeDataFactory;
+    this.updatedChanges = new HashMap<>();
   }
 
   @Override
@@ -296,7 +302,7 @@
       throw new IllegalStateException(
           String.format(
               "SubmitRuleEvaluator.evaluate for change %s returned empty list for %s in %s",
-              cd.getId(), patchSet.getId(), cd.change().getProject().get()));
+              cd.getId(), patchSet.id(), cd.change().getProject().get()));
     }
 
     for (SubmitRecord record : results) {
@@ -318,7 +324,7 @@
           throw new IllegalStateException(
               String.format(
                   "Unexpected SubmitRecord status %s for %s in %s",
-                  record.status, patchSet.getId().getId(), cd.change().getProject().get()));
+                  record.status, patchSet.id().getId(), cd.change().getProject().get()));
       }
     }
     throw new IllegalStateException();
@@ -428,8 +434,9 @@
    * @throws RestApiException if an error occurred.
    * @throws PermissionBackendException if permissions can't be checked
    * @throws IOException an error occurred reading from NoteDb.
+   * @return the merged change
    */
-  public void merge(
+  public Change merge(
       Change change,
       IdentifiedUser caller,
       boolean checkSubmitRules,
@@ -511,11 +518,22 @@
                     retryHelper
                         .getDefaultTimeout(ActionType.CHANGE_UPDATE)
                         .multipliedBy(cs.projects().size()))
+                .caller(getClass())
+                .retryWithTrace(t -> !(t instanceof RestApiException))
+                .onAutoTrace(traceId -> this.traceId = traceId)
                 .build());
 
         if (projects > 1) {
           topicMetrics.topicSubmissionsCompleted.increment();
         }
+
+        // It's expected that callers invoke this method only for open changes and that the provided
+        // change either gets updated to merged or that this method fails with an exception. For
+        // safety, fall-back to return the provided change if there was no update for this change
+        // (e.g. caller provided a change that was already merged).
+        return updatedChanges.containsKey(change.getId())
+            ? updatedChanges.get(change.getId())
+            : change;
       } catch (IOException e) {
         // Anything before the merge attempt is an error
         throw new StorageException(e);
@@ -523,6 +541,10 @@
     }
   }
 
+  public Optional<String> getTraceId() {
+    return Optional.ofNullable(traceId);
+  }
+
   private void openRepoManager() {
     if (orm != null) {
       orm.close();
@@ -573,18 +595,18 @@
       throws IntegrationException, RestApiException, UpdateException {
     checkArgument(!cs.furtherHiddenChanges(), "cannot integrate hidden changes into history");
     logger.atFine().log("Beginning merge attempt on %s", cs);
-    Map<Branch.NameKey, BranchBatch> toSubmit = new HashMap<>();
+    Map<BranchNameKey, BranchBatch> toSubmit = new HashMap<>();
 
-    ListMultimap<Branch.NameKey, ChangeData> cbb;
+    ListMultimap<BranchNameKey, ChangeData> cbb;
     try {
       cbb = cs.changesByBranch();
     } catch (StorageException e) {
       throw new IntegrationException("Error reading changes to submit", e);
     }
-    Set<Branch.NameKey> branches = cbb.keySet();
+    Set<BranchNameKey> branches = cbb.keySet();
 
-    for (Branch.NameKey branch : branches) {
-      OpenRepo or = openRepo(branch.getParentKey());
+    for (BranchNameKey branch : branches) {
+      OpenRepo or = openRepo(branch.project());
       if (or != null) {
         toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
       }
@@ -597,10 +619,17 @@
       SubmoduleOp submoduleOp = subOpFactory.create(branches, orm);
       List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit, submoduleOp, dryrun);
       this.allProjects = submoduleOp.getProjectsInOrder();
-      BatchUpdate.execute(
-          orm.batchUpdates(allProjects),
-          new SubmitStrategyListener(submitInput, strategies, commitStatus),
-          dryrun);
+      try {
+        BatchUpdate.execute(
+            orm.batchUpdates(allProjects),
+            new SubmitStrategyListener(submitInput, strategies, commitStatus),
+            dryrun);
+      } finally {
+        // If the BatchUpdate fails it can be that merging some of the changes was actually
+        // successful. This is why we must to collect the updated changes also when an exception was
+        // thrown.
+        strategies.forEach(s -> updatedChanges.putAll(s.getUpdatedChanges()));
+      }
     } catch (NoSuchProjectException e) {
       throw new ResourceNotFoundException(e.getMessage());
     } catch (IOException | SubmoduleException e) {
@@ -641,14 +670,14 @@
   }
 
   private List<SubmitStrategy> getSubmitStrategies(
-      Map<Branch.NameKey, BranchBatch> toSubmit, SubmoduleOp submoduleOp, boolean dryrun)
+      Map<BranchNameKey, BranchBatch> toSubmit, SubmoduleOp submoduleOp, boolean dryrun)
       throws IntegrationException, NoSuchProjectException, IOException {
     List<SubmitStrategy> strategies = new ArrayList<>();
-    Set<Branch.NameKey> allBranches = submoduleOp.getBranchesInOrder();
+    Set<BranchNameKey> allBranches = submoduleOp.getBranchesInOrder();
     Set<CodeReviewCommit> allCommits =
         toSubmit.values().stream().map(BranchBatch::commits).flatMap(Set::stream).collect(toSet());
-    for (Branch.NameKey branch : allBranches) {
-      OpenRepo or = orm.getRepo(branch.getParentKey());
+    for (BranchNameKey branch : allBranches) {
+      OpenRepo or = orm.getRepo(branch.project());
       if (toSubmit.containsKey(branch)) {
         BranchBatch submitting = toSubmit.get(branch);
         logger.atFine().log("adding ops for branch batch %s", submitting);
@@ -769,55 +798,47 @@
       }
 
       PatchSet ps;
-      Branch.NameKey destBranch = chg.getDest();
+      BranchNameKey destBranch = chg.getDest();
       try {
         ps = cd.currentPatchSet();
       } catch (StorageException e) {
         commitStatus.logProblem(changeId, e);
         continue;
       }
-      if (ps == null || ps.getRevision() == null || ps.getRevision().get() == null) {
-        commitStatus.logProblem(changeId, "Missing patch set or revision on change");
+      if (ps == null) {
+        commitStatus.logProblem(changeId, "Missing patch set on change");
         continue;
       }
 
-      String idstr = ps.getRevision().get();
-      ObjectId id;
-      try {
-        id = ObjectId.fromString(idstr);
-      } catch (IllegalArgumentException e) {
-        commitStatus.logProblem(changeId, e);
-        continue;
-      }
-
-      if (!revisions.containsEntry(id, ps.getId())) {
-        if (revisions.containsValue(ps.getId())) {
+      ObjectId id = ps.commitId();
+      if (!revisions.containsEntry(id, ps.id())) {
+        if (revisions.containsValue(ps.id())) {
           // TODO This is actually an error, the patch set ref exists but points to a revision that
           // is different from the revision that we have stored for the patch set in the change
           // meta data.
           commitStatus.logProblem(
               changeId,
               "Revision "
-                  + idstr
+                  + id.name()
                   + " of patch set "
-                  + ps.getPatchSetId()
+                  + ps.number()
                   + " does not match the revision of the patch set ref "
-                  + ps.getId().toRefName());
+                  + ps.id().toRefName());
           continue;
         }
 
         // The patch set ref is not found but we want to merge the change. We can't safely do that
-        // if the patch set ref is missing. In a multi-master setup this can indicate a replication
-        // lag (e.g. the change meta data was already replicated, but the replication of the patch
-        // set ref is still pending).
+        // if the patch set ref is missing. In a cluster setups with multiple primary nodes this can
+        // indicate a replication lag (e.g. the change meta data was already replicated, but the
+        // replication of the patch set ref is still pending).
         commitStatus.logProblem(
             changeId,
             "Patch set ref "
-                + ps.getId().toRefName()
+                + ps.id().toRefName()
                 + " not found. Expected patch set ref of "
-                + ps.getPatchSetId()
+                + ps.number()
                 + " to point to revision "
-                + idstr);
+                + id.name());
         continue;
       }
 
@@ -830,13 +851,12 @@
       }
 
       commit.setNotes(notes);
-      commit.setPatchsetId(ps.getId());
+      commit.setPatchsetId(ps.id());
       commitStatus.put(commit);
 
       MergeValidators mergeValidators = mergeValidatorsFactory.create();
       try {
-        mergeValidators.validatePreMerge(
-            or.repo, commit, or.project, destBranch, ps.getId(), caller);
+        mergeValidators.validatePreMerge(or.repo, commit, or.project, destBranch, ps.id(), caller);
       } catch (MergeValidationException mve) {
         commitStatus.problem(changeId, mve.getMessage());
         continue;
diff --git a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
index 764aca8..c2577e7 100644
--- a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
+++ b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
@@ -18,9 +18,9 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.Maps;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -67,7 +67,7 @@
     BatchUpdate update;
 
     private final ObjectReader reader;
-    private final Map<Branch.NameKey, OpenBranch> branches;
+    private final Map<BranchNameKey, OpenBranch> branches;
 
     private OpenRepo(Repository repo, ProjectState project) {
       this.repo = repo;
@@ -84,7 +84,7 @@
       branches = Maps.newHashMapWithExpectedSize(1);
     }
 
-    OpenBranch getBranch(Branch.NameKey branch) throws IntegrationException {
+    OpenBranch getBranch(BranchNameKey branch) throws IntegrationException {
       OpenBranch ob = branches.get(branch);
       if (ob == null) {
         ob = new OpenBranch(this, branch);
@@ -134,13 +134,13 @@
     final CodeReviewCommit oldTip;
     MergeTip mergeTip;
 
-    OpenBranch(OpenRepo or, Branch.NameKey name) throws IntegrationException {
+    OpenBranch(OpenRepo or, BranchNameKey name) throws IntegrationException {
       try {
-        update = or.repo.updateRef(name.get());
+        update = or.repo.updateRef(name.branch());
         if (update.getOldObjectId() != null) {
           oldTip = or.rw.parseCommit(update.getOldObjectId());
-        } else if (Objects.equals(or.repo.getFullBranch(), name.get())
-            || Objects.equals(RefNames.REFS_CONFIG, name.get())) {
+        } else if (Objects.equals(or.repo.getFullBranch(), name.branch())
+            || Objects.equals(RefNames.REFS_CONFIG, name.branch())) {
           oldTip = null;
           update.setExpectedOldObjectId(ObjectId.zeroId());
         } else {
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSet.java b/java/com/google/gerrit/server/submit/MergeSuperSet.java
index d729833..bcebc7f 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSet.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSet.java
@@ -19,9 +19,9 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.logging.TraceContext;
diff --git a/java/com/google/gerrit/server/submit/RebaseSorter.java b/java/com/google/gerrit/server/submit/RebaseSorter.java
index 829ee9c..4775768 100644
--- a/java/com/google/gerrit/server/submit/RebaseSorter.java
+++ b/java/com/google/gerrit/server/submit/RebaseSorter.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.submit;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -108,7 +108,7 @@
     return sorted;
   }
 
-  private boolean isAlreadyMerged(CodeReviewCommit commit, Branch.NameKey dest) throws IOException {
+  private boolean isAlreadyMerged(CodeReviewCommit commit, BranchNameKey dest) throws IOException {
     try (CodeReviewRevWalk mirw = CodeReviewCommit.newRevWalk(rw.getObjectReader())) {
       mirw.reset();
       mirw.markStart(commit);
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index b2ad4e2..65e18ad 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -19,12 +19,12 @@
 import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.RebaseChangeOp;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -235,7 +235,7 @@
                 ctx.getUpdate(newPatchSetId),
                 newPatchSetId,
                 newCommit,
-                prevPs != null ? prevPs.getGroups() : ImmutableList.of(),
+                prevPs != null ? prevPs.groups() : ImmutableList.of(),
                 null,
                 null);
       }
@@ -310,7 +310,6 @@
       throws IntegrationException {
     // Test for merge instead of cherry pick to avoid false negatives
     // on commit chains.
-    return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)
-        && args.mergeUtil.canMerge(args.mergeSorter, repo, mergeTip, toMerge);
+    return args.mergeUtil.canMerge(args.mergeSorter, repo, mergeTip, toMerge);
   }
 }
diff --git a/java/com/google/gerrit/server/submit/SubmitDryRun.java b/java/com/google/gerrit/server/submit/SubmitDryRun.java
index 391d956..ff1a1f0 100644
--- a/java/com/google/gerrit/server/submit/SubmitDryRun.java
+++ b/java/com/google/gerrit/server/submit/SubmitDryRun.java
@@ -20,8 +20,8 @@
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -112,7 +112,7 @@
       SubmitType submitType,
       Repository repo,
       CodeReviewRevWalk rw,
-      Branch.NameKey destBranch,
+      BranchNameKey destBranch,
       ObjectId tip,
       ObjectId toMerge,
       Set<RevCommit> alreadyAccepted)
@@ -155,10 +155,10 @@
     }
   }
 
-  private ProjectState getProject(Branch.NameKey branch) throws NoSuchProjectException {
-    ProjectState p = projectCache.get(branch.getParentKey());
+  private ProjectState getProject(BranchNameKey branch) throws NoSuchProjectException {
+    ProjectState p = projectCache.get(branch.project());
     if (p == null) {
-      throw new NoSuchProjectException(branch.getParentKey());
+      throw new NoSuchProjectException(branch.project());
     }
     return p;
   }
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index dc221f8..4c68e1b 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -14,14 +14,16 @@
 
 package com.google.gerrit.server.submit;
 
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -55,7 +57,9 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -82,7 +86,7 @@
     interface Factory {
       Arguments create(
           SubmitType submitType,
-          Branch.NameKey destBranch,
+          BranchNameKey destBranch,
           CommitStatus commitStatus,
           CodeReviewRevWalk rw,
           IdentifiedUser caller,
@@ -114,7 +118,7 @@
     final ProjectConfig.Factory projectConfigFactory;
     final SetPrivateOp.Factory setPrivateOpFactory;
 
-    final Branch.NameKey destBranch;
+    final BranchNameKey destBranch;
     final CodeReviewRevWalk rw;
     final CommitStatus commitStatus;
     final IdentifiedUser caller;
@@ -152,7 +156,7 @@
         Provider<InternalChangeQuery> queryProvider,
         ProjectConfig.Factory projectConfigFactory,
         SetPrivateOp.Factory setPrivateOpFactory,
-        @Assisted Branch.NameKey destBranch,
+        @Assisted BranchNameKey destBranch,
         @Assisted CommitStatus commitStatus,
         @Assisted CodeReviewRevWalk rw,
         @Assisted IdentifiedUser caller,
@@ -197,8 +201,8 @@
 
       this.project =
           requireNonNull(
-              projectCache.get(destBranch.getParentKey()),
-              () -> String.format("project not found: %s", destBranch.getParentKey()));
+              projectCache.get(destBranch.project()),
+              () -> String.format("project not found: %s", destBranch.project()));
       this.mergeSorter =
           new MergeSorter(caller, rw, alreadyAccepted, canMergeFlag, queryProvider, incoming);
       this.rebaseSorter =
@@ -217,8 +221,24 @@
 
   final Arguments args;
 
+  private final Set<SubmitStrategyOp> submitStrategyOps;
+
   SubmitStrategy(Arguments args) {
     this.args = requireNonNull(args);
+    this.submitStrategyOps = new HashSet<>();
+  }
+
+  /**
+   * Returns the updated changed after this submit strategy has been executed.
+   *
+   * @return the updated changes after this submit strategy has been executed
+   */
+  public ImmutableMap<Change.Id, Change> getUpdatedChanges() {
+    return submitStrategyOps.stream()
+        .map(SubmitStrategyOp::getUpdatedChange)
+        .filter(Optional::isPresent)
+        .map(Optional::get)
+        .collect(toImmutableMap(c -> c.getId(), c -> c));
   }
 
   /**
@@ -249,8 +269,10 @@
     for (CodeReviewCommit c : difference) {
       Change.Id id = c.change().getId();
       bu.addOp(id, args.setPrivateOpFactory.create(false, null));
-      bu.addOp(id, new ImplicitIntegrateOp(args, c));
+      ImplicitIntegrateOp implicitIntegrateOp = new ImplicitIntegrateOp(args, c);
+      bu.addOp(id, implicitIntegrateOp);
       maybeAddTestHelperOp(bu, id);
+      this.submitStrategyOps.add(implicitIntegrateOp);
     }
 
     // Then ops for explicitly merged changes
@@ -258,6 +280,7 @@
       bu.addOp(op.getId(), args.setPrivateOpFactory.create(false, null));
       bu.addOp(op.getId(), op);
       maybeAddTestHelperOp(bu, op.getId());
+      this.submitStrategyOps.add(op);
     }
   }
 
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
index 30326f7..cba572bc 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.submit;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -48,7 +48,7 @@
       RevFlag canMergeFlag,
       Set<RevCommit> alreadyAccepted,
       Set<CodeReviewCommit> incoming,
-      Branch.NameKey destBranch,
+      BranchNameKey destBranch,
       IdentifiedUser caller,
       MergeTip mergeTip,
       CommitStatus commitStatus,
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyListener.java b/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
index 782cd7b..f8bcfc1 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
@@ -17,9 +17,9 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.submit.MergeOp.CommitStatus;
@@ -129,7 +129,7 @@
 
         case ALREADY_MERGED:
           // Already an ancestor of tip.
-          alreadyMerged.add(commit.getPatchsetId().getParentKey());
+          alreadyMerged.add(commit.getPatchsetId().changeId());
           break;
 
         case PATH_CONFLICT:
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 14522a2..79f062d 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -20,19 +20,18 @@
 import static java.util.Comparator.comparing;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.base.Function;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.LabelId;
+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.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
@@ -89,12 +88,12 @@
     return toMerge;
   }
 
-  protected final Branch.NameKey getDest() {
+  protected final BranchNameKey getDest() {
     return toMerge.change().getDest();
   }
 
   protected final Project.NameKey getProject() {
-    return getDest().getParentKey();
+    return getDest().project();
   }
 
   @Override
@@ -132,14 +131,15 @@
     // Needed by postUpdate, at which point mergeTip will have advanced further,
     // so it's easier to just snapshot the command.
     command =
-        new ReceiveCommand(firstNonNull(tipBefore, ObjectId.zeroId()), tipAfter, getDest().get());
+        new ReceiveCommand(
+            firstNonNull(tipBefore, ObjectId.zeroId()), tipAfter, getDest().branch());
     ctx.addRefUpdate(command);
     args.submoduleOp.addBranchTip(getDest(), tipAfter);
   }
 
   private void checkProjectConfig(RepoContext ctx, CodeReviewCommit commit)
       throws IntegrationException {
-    String refName = getDest().get();
+    String refName = getDest().branch();
     if (RefNames.REFS_CONFIG.equals(refName)) {
       logger.atFine().log("Loading new configuration from %s", RefNames.REFS_CONFIG);
       try {
@@ -251,7 +251,7 @@
                 args.psUtil.get(ctx.getNotes(), oldPsId),
                 () -> String.format("missing old patch set %s", oldPsId));
       } else {
-        PatchSet.Id n = newPatchSet.getId();
+        PatchSet.Id n = newPatchSet.id();
         checkState(
             !n.equals(oldPsId) && n.equals(newPsId),
             "current patch was %s and is now %s, but updateChangeImpl returned"
@@ -294,6 +294,16 @@
     return true;
   }
 
+  /**
+   * Returns the updated change after this op has been executed.
+   *
+   * @return the updated change after this op has been executed, {@link Optional#empty()} if the op
+   *     was not executed yet, or if the execution has failed
+   */
+  public Optional<Change> getUpdatedChange() {
+    return Optional.ofNullable(updatedChange);
+  }
+
   private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx) throws IOException {
     PatchSet.Id psId = alreadyMergedCommit.getPatchsetId();
     logger.atFine().log("Fixing up already-merged patch set %s", psId);
@@ -311,7 +321,7 @@
     // a patch set ref. Fix up the database. Note that this uses the current
     // user as the uploader, which is as good a guess as any.
     List<String> groups =
-        prevPs != null ? prevPs.getGroups() : GroupCollector.getDefaultGroups(alreadyMergedCommit);
+        prevPs != null ? prevPs.groups() : GroupCollector.getDefaultGroups(alreadyMergedCommit);
     return args.psUtil.insert(
         ctx.getRevWalk(), ctx.getUpdate(psId), psId, alreadyMergedCommit, groups, null, null);
   }
@@ -333,7 +343,7 @@
     // approvals as well.
     if (!newPsId.equals(oldPsId)) {
       saveApprovals(normalized, newPsUpdate, true);
-      submitter = convertPatchSet(newPsId).apply(submitter);
+      submitter = submitter.copyWithPatchSet(newPsId);
     }
   }
 
@@ -344,12 +354,13 @@
     for (PatchSetApproval psa :
         args.approvalsUtil.byPatchSet(
             ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
-      byKey.put(psa.getKey(), psa);
+      byKey.put(psa.key(), psa);
     }
 
     submitter =
-        ApprovalsUtil.newApproval(psId, ctx.getUser(), LabelId.legacySubmit(), 1, ctx.getWhen());
-    byKey.put(submitter.getKey(), submitter);
+        ApprovalsUtil.newApproval(psId, ctx.getUser(), LabelId.legacySubmit(), 1, ctx.getWhen())
+            .build();
+    byKey.put(submitter.key(), submitter);
 
     // Flatten out existing approvals for this patch set based upon the current
     // permissions. Once the change is closed the approvals are not updated at
@@ -358,7 +369,7 @@
     // permissions get modified in the future, historical records stay accurate.
     LabelNormalizer.Result normalized =
         args.labelNormalizer.normalize(ctx.getNotes(), byKey.values());
-    update.putApproval(submitter.getLabel(), submitter.getValue());
+    update.putApproval(submitter.label(), submitter.value());
     saveApprovals(normalized, update, false);
     return normalized;
   }
@@ -366,10 +377,10 @@
   private void saveApprovals(
       LabelNormalizer.Result normalized, ChangeUpdate update, boolean includeUnchanged) {
     for (PatchSetApproval psa : normalized.updated()) {
-      update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
+      update.putApprovalFor(psa.accountId(), psa.label(), psa.value());
     }
     for (PatchSetApproval psa : normalized.deleted()) {
-      update.removeApprovalFor(psa.getAccountId(), psa.getLabel());
+      update.removeApprovalFor(psa.accountId(), psa.label());
     }
 
     // TODO(dborowitz): Don't use a label in NoteDb; just check when status
@@ -377,27 +388,17 @@
     for (PatchSetApproval psa : normalized.unchanged()) {
       if (includeUnchanged || psa.isLegacySubmit()) {
         logger.atFine().log("Adding submit label %s", psa);
-        update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
+        update.putApprovalFor(psa.accountId(), psa.label(), psa.value());
       }
     }
   }
 
-  private static Function<PatchSetApproval, PatchSetApproval> convertPatchSet(
-      final PatchSet.Id psId) {
-    return psa -> {
-      if (psa.getPatchSetId().equals(psId)) {
-        return psa;
-      }
-      return new PatchSetApproval(psId, psa);
-    };
-  }
-
   private String getByAccountName() {
     requireNonNull(submitter, "getByAccountName called before submitter populated");
     Optional<Account> account =
-        args.accountCache.get(submitter.getAccountId()).map(AccountState::getAccount);
-    if (account.isPresent() && account.get().getFullName() != null) {
-      return " by " + account.get().getFullName();
+        args.accountCache.get(submitter.accountId()).map(AccountState::account);
+    if (account.isPresent() && account.get().fullName() != null) {
+      return " by " + account.get().fullName();
     }
     return "";
   }
@@ -483,7 +484,7 @@
           getProject(), command.getRefName(), command.getOldId(), command.getNewId());
       // TODO(dborowitz): Move to BatchUpdate? Would also allow us to run once
       // per project even if multiple changes to refs/meta/config are submitted.
-      if (RefNames.REFS_CONFIG.equals(getDest().get())) {
+      if (RefNames.REFS_CONFIG.equals(getDest().branch())) {
         args.projectCache.evict(getProject());
         ProjectState p = args.projectCache.get(getProject());
         try (Repository git = args.repoManager.openRepository(getProject())) {
@@ -498,7 +499,7 @@
     // have failed fast in one of the other steps.
     try {
       args.mergedSenderFactory
-          .create(ctx.getProject(), getId(), submitter.getAccountId(), ctx.getNotify(getId()))
+          .create(ctx.getProject(), getId(), submitter.accountId(), ctx.getNotify(getId()))
           .sendAsync();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email merged notification for %s", getId());
@@ -507,7 +508,7 @@
       args.changeMerged.fire(
           updatedChange,
           mergedPatchSet,
-          args.accountCache.get(submitter.getAccountId()).orElse(null),
+          args.accountCache.get(submitter.accountId()).orElse(null),
           args.mergeTip.getCurrentTip().name(),
           ctx.getWhen());
     }
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index afcf9c5..8ab99dd 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -23,11 +23,11 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmoduleSubscription;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.VerboseSuperprojectUpdate;
@@ -79,9 +79,9 @@
 
   /** Only used for branches without code review changes */
   public class GitlinkOp implements RepoOnlyOp {
-    private final Branch.NameKey branch;
+    private final BranchNameKey branch;
 
-    GitlinkOp(Branch.NameKey branch) {
+    GitlinkOp(BranchNameKey branch) {
       this.branch = branch;
     }
 
@@ -89,7 +89,7 @@
     public void updateRepo(RepoContext ctx) throws Exception {
       CodeReviewCommit c = composeGitlinksCommit(branch);
       if (c != null) {
-        ctx.addRefUpdate(c.getParent(0), c, branch.get());
+        ctx.addRefUpdate(c.getParent(0), c, branch.branch());
         addBranchTip(branch, c);
       }
     }
@@ -114,7 +114,7 @@
       this.projectCache = projectCache;
     }
 
-    public SubmoduleOp create(Set<Branch.NameKey> updatedBranches, MergeOpRepoManager orm)
+    public SubmoduleOp create(Set<BranchNameKey> updatedBranches, MergeOpRepoManager orm)
         throws SubmoduleException {
       return new SubmoduleOp(
           gitmodulesFactory, serverIdent.get(), cfg, projectCache, updatedBranches, orm);
@@ -129,41 +129,41 @@
   private final long maxCombinedCommitMessageSize;
   private final long maxCommitMessages;
   private final MergeOpRepoManager orm;
-  private final Map<Branch.NameKey, GitModules> branchGitModules;
+  private final Map<BranchNameKey, GitModules> branchGitModules;
 
   /** Branches updated as part of the enclosing submit or push batch. */
-  private final ImmutableSet<Branch.NameKey> updatedBranches;
+  private final ImmutableSet<BranchNameKey> updatedBranches;
 
   /**
    * Current branch tips, taking into account commits created during the submit process as well as
    * submodule updates produced by this class.
    */
-  private final Map<Branch.NameKey, CodeReviewCommit> branchTips;
+  private final Map<BranchNameKey, CodeReviewCommit> branchTips;
 
   /**
    * Branches in a superproject that contain submodule subscriptions, plus branches in submodules
    * which are subscribed to by some superproject.
    */
-  private final Set<Branch.NameKey> affectedBranches;
+  private final Set<BranchNameKey> affectedBranches;
 
   /** Copy of {@link #affectedBranches}, sorted by submodule traversal order. */
-  private final ImmutableSet<Branch.NameKey> sortedBranches;
+  private final ImmutableSet<BranchNameKey> sortedBranches;
 
   /** Multimap of superproject branch to submodule subscriptions contained in that branch. */
-  private final SetMultimap<Branch.NameKey, SubmoduleSubscription> targets;
+  private final SetMultimap<BranchNameKey, SubmoduleSubscription> targets;
 
   /**
    * Multimap of superproject name to all branch names within that superproject which have submodule
    * subscriptions.
    */
-  private final SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject;
+  private final SetMultimap<Project.NameKey, BranchNameKey> branchesByProject;
 
   private SubmoduleOp(
       GitModules.Factory gitmodulesFactory,
       PersonIdent myIdent,
       Config cfg,
       ProjectCache projectCache,
-      Set<Branch.NameKey> updatedBranches,
+      Set<BranchNameKey> updatedBranches,
       MergeOpRepoManager orm)
       throws SubmoduleException {
     this.gitmodulesFactory = gitmodulesFactory;
@@ -214,15 +214,15 @@
   //
   // In addition to improving readability, this approach has the advantage of making (1) and (2)
   // testable using small tests.
-  private ImmutableSet<Branch.NameKey> calculateSubscriptionMaps() throws SubmoduleException {
+  private ImmutableSet<BranchNameKey> calculateSubscriptionMaps() throws SubmoduleException {
     if (!enableSuperProjectSubscriptions) {
       logger.atFine().log("Updating superprojects disabled");
       return null;
     }
 
     logger.atFine().log("Calculating superprojects - submodules map");
-    LinkedHashSet<Branch.NameKey> allVisited = new LinkedHashSet<>();
-    for (Branch.NameKey updatedBranch : updatedBranches) {
+    LinkedHashSet<BranchNameKey> allVisited = new LinkedHashSet<>();
+    for (BranchNameKey updatedBranch : updatedBranches) {
       if (allVisited.contains(updatedBranch)) {
         continue;
       }
@@ -240,9 +240,9 @@
   }
 
   private void searchForSuperprojects(
-      Branch.NameKey current,
-      LinkedHashSet<Branch.NameKey> currentVisited,
-      LinkedHashSet<Branch.NameKey> allVisited)
+      BranchNameKey current,
+      LinkedHashSet<BranchNameKey> currentVisited,
+      LinkedHashSet<BranchNameKey> allVisited)
       throws SubmoduleException {
     logger.atFine().log("Now processing %s", current);
 
@@ -261,10 +261,10 @@
       Collection<SubmoduleSubscription> subscriptions =
           superProjectSubscriptionsForSubmoduleBranch(current);
       for (SubmoduleSubscription sub : subscriptions) {
-        Branch.NameKey superBranch = sub.getSuperProject();
+        BranchNameKey superBranch = sub.getSuperProject();
         searchForSuperprojects(superBranch, currentVisited, allVisited);
         targets.put(superBranch, sub);
-        branchesByProject.put(superBranch.getParentKey(), superBranch);
+        branchesByProject.put(superBranch.project(), superBranch);
         affectedBranches.add(superBranch);
         affectedBranches.add(sub.getSubmodule());
       }
@@ -303,31 +303,33 @@
     return sb.toString();
   }
 
-  private Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src, SubscribeSection s)
+  private Collection<BranchNameKey> getDestinationBranches(BranchNameKey src, SubscribeSection s)
       throws IOException {
-    Collection<Branch.NameKey> ret = new HashSet<>();
+    Collection<BranchNameKey> ret = new HashSet<>();
     logger.atFine().log("Inspecting SubscribeSection %s", s);
     for (RefSpec r : s.getMatchingRefSpecs()) {
       logger.atFine().log("Inspecting [matching] ref %s", r);
-      if (!r.matchSource(src.get())) {
+      if (!r.matchSource(src.branch())) {
         continue;
       }
       if (r.isWildcard()) {
         // refs/heads/*[:refs/somewhere/*]
-        ret.add(new Branch.NameKey(s.getProject(), r.expandFromSource(src.get()).getDestination()));
+        ret.add(
+            BranchNameKey.create(
+                s.getProject(), r.expandFromSource(src.branch()).getDestination()));
       } else {
         // e.g. refs/heads/master[:refs/heads/stable]
         String dest = r.getDestination();
         if (dest == null) {
           dest = r.getSource();
         }
-        ret.add(new Branch.NameKey(s.getProject(), dest));
+        ret.add(BranchNameKey.create(s.getProject(), dest));
       }
     }
 
     for (RefSpec r : s.getMultiMatchRefSpecs()) {
       logger.atFine().log("Inspecting [all] ref %s", r);
-      if (!r.matchSource(src.get())) {
+      if (!r.matchSource(src.branch())) {
         continue;
       }
       OpenRepo or;
@@ -344,7 +346,7 @@
         if (r.getDestination() != null && !r.matchDestination(ref.getName())) {
           continue;
         }
-        Branch.NameKey b = new Branch.NameKey(s.getProject(), ref.getName());
+        BranchNameKey b = BranchNameKey.create(s.getProject(), ref.getName());
         if (!ret.contains(b)) {
           ret.add(b);
         }
@@ -356,18 +358,18 @@
 
   @UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT)
   public Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
-      Branch.NameKey srcBranch) throws IOException {
+      BranchNameKey srcBranch) throws IOException {
     logger.atFine().log("Calculating possible superprojects for %s", srcBranch);
     Collection<SubmoduleSubscription> ret = new ArrayList<>();
-    Project.NameKey srcProject = srcBranch.getParentKey();
+    Project.NameKey srcProject = srcBranch.project();
     for (SubscribeSection s : projectCache.get(srcProject).getSubscribeSections(srcBranch)) {
       logger.atFine().log("Checking subscribe section %s", s);
-      Collection<Branch.NameKey> branches = getDestinationBranches(srcBranch, s);
-      for (Branch.NameKey targetBranch : branches) {
-        Project.NameKey targetProject = targetBranch.getParentKey();
+      Collection<BranchNameKey> branches = getDestinationBranches(srcBranch, s);
+      for (BranchNameKey targetBranch : branches) {
+        Project.NameKey targetProject = targetBranch.project();
         try {
           OpenRepo or = orm.getRepo(targetProject);
-          ObjectId id = or.repo.resolve(targetBranch.get());
+          ObjectId id = or.repo.resolve(targetBranch.branch());
           if (id == null) {
             logger.atFine().log("The branch %s doesn't exist.", targetBranch);
             continue;
@@ -403,7 +405,7 @@
           superProjects.add(project);
           // get a new BatchUpdate for the super project
           OpenRepo or = orm.getRepo(project);
-          for (Branch.NameKey branch : branchesByProject.get(project)) {
+          for (BranchNameKey branch : branchesByProject.get(project)) {
             addOp(or.getUpdate(), branch);
           }
         }
@@ -415,11 +417,11 @@
   }
 
   /** Create a separate gitlink commit */
-  private CodeReviewCommit composeGitlinksCommit(Branch.NameKey subscriber)
+  private CodeReviewCommit composeGitlinksCommit(BranchNameKey subscriber)
       throws IOException, SubmoduleException {
     OpenRepo or;
     try {
-      or = orm.getRepo(subscriber.getParentKey());
+      or = orm.getRepo(subscriber.project());
     } catch (NoSuchProjectException | IOException e) {
       throw new SubmoduleException("Cannot access superproject", e);
     }
@@ -428,7 +430,7 @@
     if (branchTips.containsKey(subscriber)) {
       currentCommit = branchTips.get(subscriber);
     } else {
-      Ref r = or.repo.exactRef(subscriber.get());
+      Ref r = or.repo.exactRef(subscriber.branch());
       if (r == null) {
         throw new SubmoduleException(
             "The branch was probably deleted from the subscriber repository");
@@ -485,11 +487,11 @@
   }
 
   /** Amend an existing commit with gitlink updates */
-  CodeReviewCommit composeGitlinksCommit(Branch.NameKey subscriber, CodeReviewCommit currentCommit)
+  CodeReviewCommit composeGitlinksCommit(BranchNameKey subscriber, CodeReviewCommit currentCommit)
       throws IOException, SubmoduleException {
     OpenRepo or;
     try {
-      or = orm.getRepo(subscriber.getParentKey());
+      or = orm.getRepo(subscriber.project());
     } catch (NoSuchProjectException | IOException e) {
       throw new SubmoduleException("Cannot access superproject", e);
     }
@@ -531,7 +533,7 @@
     logger.atFine().log("Updating gitlink for %s", s);
     OpenRepo subOr;
     try {
-      subOr = orm.getRepo(s.getSubmodule().getParentKey());
+      subOr = orm.getRepo(s.getSubmodule().project());
     } catch (NoSuchProjectException | IOException e) {
       throw new SubmoduleException("Cannot access submodule", e);
     }
@@ -544,7 +546,7 @@
             "Requested to update gitlink "
                 + s.getPath()
                 + " in "
-                + s.getSubmodule().getParentKey().get()
+                + s.getSubmodule().project().get()
                 + " but entry "
                 + "doesn't have gitlink file mode.";
         throw new SubmoduleException(errMsg);
@@ -576,7 +578,7 @@
       // superproject is still subscribed to this branch. Re-read the ref to see if anything has
       // changed since the last time the gitlink was updated, and roll that update into the same
       // commit as all other submodule updates.
-      Ref ref = subOr.repo.getRefDatabase().exactRef(s.getSubmodule().get());
+      Ref ref = subOr.repo.getRefDatabase().exactRef(s.getSubmodule().branch());
       if (ref == null) {
         ed.add(new DeletePath(s.getPath()));
         return null;
@@ -615,7 +617,7 @@
     msgbuf.append("* Update ");
     msgbuf.append(s.getPath());
     msgbuf.append(" from branch '");
-    msgbuf.append(s.getSubmodule().getShortName());
+    msgbuf.append(s.getSubmodule().shortName());
     msgbuf.append("'");
     msgbuf.append("\n  to ");
     msgbuf.append(newCommit.getName());
@@ -675,8 +677,8 @@
       addAllSubmoduleProjects(project, new LinkedHashSet<>(), projects);
     }
 
-    for (Branch.NameKey branch : updatedBranches) {
-      projects.add(branch.getParentKey());
+    for (BranchNameKey branch : updatedBranches) {
+      projects.add(branch.project());
     }
     return ImmutableSet.copyOf(projects);
   }
@@ -697,10 +699,10 @@
 
     current.add(project);
     Set<Project.NameKey> subprojects = new HashSet<>();
-    for (Branch.NameKey branch : branchesByProject.get(project)) {
+    for (BranchNameKey branch : branchesByProject.get(project)) {
       Collection<SubmoduleSubscription> subscriptions = targets.get(branch);
       for (SubmoduleSubscription s : subscriptions) {
-        subprojects.add(s.getSubmodule().getParentKey());
+        subprojects.add(s.getSubmodule().project());
       }
     }
 
@@ -712,8 +714,8 @@
     projects.add(project);
   }
 
-  ImmutableSet<Branch.NameKey> getBranchesInOrder() {
-    LinkedHashSet<Branch.NameKey> branches = new LinkedHashSet<>();
+  ImmutableSet<BranchNameKey> getBranchesInOrder() {
+    LinkedHashSet<BranchNameKey> branches = new LinkedHashSet<>();
     if (sortedBranches != null) {
       branches.addAll(sortedBranches);
     }
@@ -721,15 +723,15 @@
     return ImmutableSet.copyOf(branches);
   }
 
-  boolean hasSubscription(Branch.NameKey branch) {
+  boolean hasSubscription(BranchNameKey branch) {
     return targets.containsKey(branch);
   }
 
-  void addBranchTip(Branch.NameKey branch, CodeReviewCommit tip) {
+  void addBranchTip(BranchNameKey branch, CodeReviewCommit tip) {
     branchTips.put(branch, tip);
   }
 
-  void addOp(BatchUpdate bu, Branch.NameKey branch) {
+  void addOp(BatchUpdate bu, BranchNameKey branch) {
     bu.addRepoOnlyOp(new GitlinkOp(branch));
   }
 }
diff --git a/java/com/google/gerrit/server/submit/TestHelperOp.java b/java/com/google/gerrit/server/submit/TestHelperOp.java
index bbb198a..7763e2f 100644
--- a/java/com/google/gerrit/server/submit/TestHelperOp.java
+++ b/java/com/google/gerrit/server/submit/TestHelperOp.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.submit;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.RepoContext;
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 0ca9f5e..ce16706 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -32,14 +32,15 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountState;
@@ -52,6 +53,7 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager;
+import com.google.gerrit.server.notedb.TooManyUpdatesException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -110,7 +112,6 @@
     };
   }
 
-  // TODO(dborowitz): Make this package-private to force all callers to use RetryHelper.
   public interface Factory {
     BatchUpdate create(Project.NameKey project, CurrentUser user, Timestamp when);
   }
@@ -178,6 +179,20 @@
   }
 
   private static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
+    // Convert common non-REST exception types with user-visible messages to corresponding REST
+    // exception types.
+    if (e instanceof InvalidChangeOperationException || e instanceof TooManyUpdatesException) {
+      throw new ResourceConflictException(e.getMessage(), e);
+    } else if (e instanceof NoSuchChangeException
+        || e instanceof NoSuchRefException
+        || e instanceof NoSuchProjectException) {
+      throw new ResourceNotFoundException(e.getMessage(), e);
+    } else if (e instanceof CommentsRejectedException) {
+      // SC_BAD_REQUEST is not ideal because it's not a syntactic error, but there is no better
+      // status code and it's isolated in monitoring.
+      throw new BadRequestException(e.getMessage(), e);
+    }
+
     Throwables.throwIfUnchecked(e);
 
     // Propagate REST API exceptions thrown by operations; they commonly throw exceptions like
@@ -185,16 +200,6 @@
     Throwables.throwIfInstanceOf(e, UpdateException.class);
     Throwables.throwIfInstanceOf(e, RestApiException.class);
 
-    // Convert other common non-REST exception types with user-visible messages to corresponding
-    // REST exception types
-    if (e instanceof InvalidChangeOperationException) {
-      throw new ResourceConflictException(e.getMessage(), e);
-    } else if (e instanceof NoSuchChangeException
-        || e instanceof NoSuchRefException
-        || e instanceof NoSuchProjectException) {
-      throw new ResourceNotFoundException(e.getMessage(), e);
-    }
-
     // Otherwise, wrap in a generic UpdateException, which does not include a user-visible message.
     throw new UpdateException(e);
   }
@@ -289,7 +294,7 @@
   private enum ChangeResult {
     SKIPPED,
     UPSERTED,
-    DELETED;
+    DELETED
   }
 
   private final GitRepositoryManager repoManager;
@@ -566,9 +571,7 @@
         handle.setResult(id, ChangeResult.SKIPPED);
         continue;
       }
-      for (ChangeUpdate u : ctx.updates.values()) {
-        handle.manager.add(u);
-      }
+      ctx.updates.values().forEach(handle.manager::add);
       if (ctx.deleted) {
         logDebug("Change %s was deleted", id);
         handle.manager.deleteChange(id);
diff --git a/java/com/google/gerrit/server/update/BatchUpdateOp.java b/java/com/google/gerrit/server/update/BatchUpdateOp.java
index 87a43a3..a2c2394 100644
--- a/java/com/google/gerrit/server/update/BatchUpdateOp.java
+++ b/java/com/google/gerrit/server/update/BatchUpdateOp.java
@@ -19,7 +19,7 @@
  *
  * <p>Each operation has {@link #updateChange(ChangeContext)} called once the change is read in a
  * transaction. Ops are associated with updates via {@link
- * BatchUpdate#addOp(com.google.gerrit.reviewdb.client.Change.Id, BatchUpdateOp)}.
+ * BatchUpdate#addOp(com.google.gerrit.entities.Change.Id, BatchUpdateOp)}.
  *
  * <p>Usually, a single {@code BatchUpdateOp} instance is only associated with a single change, i.e.
  * {@code addOp} is only called once with that instance. Additionally, each method in {@code
diff --git a/java/com/google/gerrit/server/update/ChangeContext.java b/java/com/google/gerrit/server/update/ChangeContext.java
index 28674fc..bd6d90b 100644
--- a/java/com/google/gerrit/server/update/ChangeContext.java
+++ b/java/com/google/gerrit/server/update/ChangeContext.java
@@ -16,8 +16,8 @@
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 
diff --git a/java/com/google/gerrit/server/update/CommentsRejectedException.java b/java/com/google/gerrit/server/update/CommentsRejectedException.java
new file mode 100644
index 0000000..6b0c04d
--- /dev/null
+++ b/java/com/google/gerrit/server/update/CommentsRejectedException.java
@@ -0,0 +1,47 @@
+// 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.update;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.validators.CommentValidationFailure;
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+/** Thrown when comment validation rejected a comment, preventing it from being published. */
+public class CommentsRejectedException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  private final ImmutableList<CommentValidationFailure> commentValidationFailures;
+
+  public CommentsRejectedException(Collection<CommentValidationFailure> commentValidationFailures) {
+    this.commentValidationFailures = ImmutableList.copyOf(commentValidationFailures);
+  }
+
+  @Override
+  public String getMessage() {
+    return "One or more comments were rejected in validation: "
+        + commentValidationFailures.stream()
+            .map(CommentValidationFailure::getMessage)
+            .collect(Collectors.joining("; "));
+  }
+
+  /**
+   * Returns the validation failures that caused this exception. By contract this list is never
+   * empty.
+   */
+  public ImmutableList<CommentValidationFailure> getCommentValidationFailures() {
+    return commentValidationFailures;
+  }
+}
diff --git a/java/com/google/gerrit/server/update/Context.java b/java/com/google/gerrit/server/update/Context.java
index 8704cf0..9947168 100644
--- a/java/com/google/gerrit/server/update/Context.java
+++ b/java/com/google/gerrit/server/update/Context.java
@@ -16,9 +16,9 @@
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
@@ -114,7 +114,7 @@
   /**
    * Get the account of the user performing the update.
    *
-   * <p>Convenience method for {@code getIdentifiedUser().getAccount()}.
+   * <p>Convenience method for {@code getIdentifiedUser().account()}.
    *
    * @see CurrentUser#asIdentifiedUser()
    * @return account.
diff --git a/java/com/google/gerrit/server/update/InsertChangeOp.java b/java/com/google/gerrit/server/update/InsertChangeOp.java
index 7060059..2676494 100644
--- a/java/com/google/gerrit/server/update/InsertChangeOp.java
+++ b/java/com/google/gerrit/server/update/InsertChangeOp.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.update;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import java.io.IOException;
 
 /**
diff --git a/java/com/google/gerrit/server/update/RepoView.java b/java/com/google/gerrit/server/update/RepoView.java
index 73dd12f..52467a4 100644
--- a/java/com/google/gerrit/server/update/RepoView.java
+++ b/java/com/google/gerrit/server/update/RepoView.java
@@ -18,7 +18,7 @@
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toMap;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import java.io.IOException;
 import java.util.Map;
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index ae8ba53..bea3867 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -28,7 +28,6 @@
 import com.github.rholder.retry.WaitStrategy;
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Predicate;
 import com.google.common.base.Throwables;
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
@@ -36,18 +35,25 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
-import com.google.gerrit.metrics.Histogram1;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.RequestId;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
@@ -94,12 +100,24 @@
     @Nullable
     abstract Duration timeout();
 
+    abstract Optional<Class<?>> caller();
+
+    abstract Optional<Predicate<Throwable>> retryWithTrace();
+
+    abstract Optional<Consumer<String>> onAutoTrace();
+
     @AutoValue.Builder
     public abstract static class Builder {
       public abstract Builder listener(RetryListener listener);
 
       public abstract Builder timeout(Duration timeout);
 
+      public abstract Builder caller(Class<?> caller);
+
+      public abstract Builder retryWithTrace(Predicate<Throwable> exceptionPredicate);
+
+      public abstract Builder onAutoTrace(Consumer<String> traceIdConsumer);
+
       public abstract Options build();
     }
   }
@@ -107,21 +125,24 @@
   @VisibleForTesting
   @Singleton
   public static class Metrics {
-    final Histogram1<ActionType> attemptCounts;
+    final Counter1<ActionType> attemptCounts;
     final Counter1<ActionType> timeoutCount;
+    final Counter2<ActionType, String> autoRetryCount;
+    final Counter2<ActionType, String> failuresOnAutoRetryCount;
 
     @Inject
     Metrics(MetricMaker metricMaker) {
-      Field<ActionType> view = Field.ofEnum(ActionType.class, "action_type");
+      Field<ActionType> actionTypeField =
+          Field.ofEnum(ActionType.class, "action_type", Metadata.Builder::actionType).build();
       attemptCounts =
-          metricMaker.newHistogram(
-              "action/retry_attempt_counts",
+          metricMaker.newCounter(
+              "action/retry_attempt_count",
               new Description(
-                      "Distribution of number of attempts made by RetryHelper to execute an action"
-                          + " (1 == single attempt, no retry)")
+                      "Number of retry attempts made by RetryHelper to execute an action"
+                          + " (0 == single attempt, no retry)")
                   .setCumulative()
                   .setUnit("attempts"),
-              view);
+              actionTypeField);
       timeoutCount =
           metricMaker.newCounter(
               "action/retry_timeout_count",
@@ -129,7 +150,27 @@
                       "Number of action executions of RetryHelper that ultimately timed out")
                   .setCumulative()
                   .setUnit("timeouts"),
-              view);
+              actionTypeField);
+      autoRetryCount =
+          metricMaker.newCounter(
+              "action/auto_retry_count",
+              new Description("Number of automatic retries with tracing")
+                  .setCumulative()
+                  .setUnit("retries"),
+              actionTypeField,
+              Field.ofString("operation_name", Metadata.Builder::operationName)
+                  .description("The name of the operation that was retried.")
+                  .build());
+      failuresOnAutoRetryCount =
+          metricMaker.newCounter(
+              "action/failures_on_auto_retry_count",
+              new Description("Number of failures on auto retry")
+                  .setCumulative()
+                  .setUnit("failures"),
+              actionTypeField,
+              Field.ofString("operation_name", Metadata.Builder::operationName)
+                  .description("The name of the operation that was retried.")
+                  .build());
     }
   }
 
@@ -143,13 +184,19 @@
 
   private final Metrics metrics;
   private final BatchUpdate.Factory updateFactory;
+  private final PluginSetContext<ExceptionHook> exceptionHooks;
   private final Map<ActionType, Duration> defaultTimeouts;
   private final WaitStrategy waitStrategy;
   @Nullable private final Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup;
+  private final boolean retryWithTraceOnFailure;
 
   @Inject
-  RetryHelper(@GerritServerConfig Config cfg, Metrics metrics, BatchUpdate.Factory updateFactory) {
-    this(cfg, metrics, updateFactory, null);
+  RetryHelper(
+      @GerritServerConfig Config cfg,
+      Metrics metrics,
+      PluginSetContext<ExceptionHook> exceptionHooks,
+      BatchUpdate.Factory updateFactory) {
+    this(cfg, metrics, updateFactory, exceptionHooks, null);
   }
 
   @VisibleForTesting
@@ -157,9 +204,11 @@
       @GerritServerConfig Config cfg,
       Metrics metrics,
       BatchUpdate.Factory updateFactory,
+      PluginSetContext<ExceptionHook> exceptionHooks,
       @Nullable Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup) {
     this.metrics = metrics;
     this.updateFactory = updateFactory;
+    this.exceptionHooks = exceptionHooks;
 
     Duration defaultTimeout =
         Duration.ofMillis(
@@ -185,6 +234,7 @@
                 MILLISECONDS),
             WaitStrategies.randomWait(50, MILLISECONDS));
     this.overwriteDefaultRetryerStrategySetup = overwriteDefaultRetryerStrategySetup;
+    this.retryWithTraceOnFailure = cfg.getBoolean("retry", "retryWithTraceOnFailure", false);
   }
 
   public Duration getDefaultTimeout(ActionType actionType) {
@@ -255,15 +305,57 @@
       Predicate<Throwable> exceptionPredicate)
       throws Throwable {
     MetricListener listener = new MetricListener();
-    try {
-      RetryerBuilder<T> retryerBuilder = createRetryerBuilder(actionType, opts, exceptionPredicate);
+    try (TraceContext traceContext = TraceContext.open()) {
+      RetryerBuilder<T> retryerBuilder =
+          createRetryerBuilder(
+              actionType,
+              opts,
+              t -> {
+                // exceptionPredicate checks for temporary errors for which the operation should be
+                // retried (e.g. LockFailure). The retry has good chances to succeed.
+                if (exceptionPredicate.test(t)) {
+                  return true;
+                }
+
+                // Exception hooks may identify additional exceptions for retry.
+                if (exceptionHooks.stream().anyMatch(h -> h.shouldRetry(t))) {
+                  return true;
+                }
+
+                // A non-recoverable failure occurred. Check if we should retry to capture a trace
+                // of the failure. If a trace was already done there is no need to retry.
+                if (retryWithTraceOnFailure
+                    && opts.retryWithTrace().isPresent()
+                    && opts.retryWithTrace().get().test(t)) {
+                  String caller = opts.caller().map(Class::getSimpleName).orElse("N/A");
+                  if (!traceContext.isTracing()) {
+                    String traceId = "retry-on-failure-" + new RequestId();
+                    traceContext.addTag(RequestId.Type.TRACE_ID, traceId).forceLogging();
+                    opts.onAutoTrace().ifPresent(c -> c.accept(traceId));
+                    logger.atFine().withCause(t).log(
+                        "AutoRetry: %s failed, retry with tracing enabled", caller);
+                    metrics.autoRetryCount.increment(actionType, caller);
+                    return true;
+                  }
+
+                  // A non-recoverable failure occurred. We retried the operation with tracing
+                  // enabled and it failed again. Log the failure so that admin can see if it
+                  // differs from the failure that triggered the retry.
+                  logger.atFine().withCause(t).log(
+                      "AutoRetry: auto-retry of %s has failed", caller);
+                  metrics.failuresOnAutoRetryCount.increment(actionType, caller);
+                  return false;
+                }
+
+                return false;
+              });
       retryerBuilder.withRetryListener(listener);
       return executeWithTimeoutCount(actionType, action, retryerBuilder.build());
     } finally {
       if (listener.getAttemptCount() > 1) {
         logger.atFine().log("%s was attempted %d times", actionType, listener.getAttemptCount());
+        metrics.attemptCounts.incrementBy(actionType, listener.getAttemptCount() - 1);
       }
-      metrics.attemptCounts.record(actionType, listener.getAttemptCount());
     }
   }
 
@@ -295,7 +387,7 @@
   private <O> RetryerBuilder<O> createRetryerBuilder(
       ActionType actionType, Options opts, Predicate<Throwable> exceptionPredicate) {
     RetryerBuilder<O> retryerBuilder =
-        RetryerBuilder.<O>newBuilder().retryIfException(exceptionPredicate);
+        RetryerBuilder.<O>newBuilder().retryIfException(exceptionPredicate::test);
     if (opts.listener() != null) {
       retryerBuilder.withRetryListener(opts.listener());
     }
diff --git a/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java b/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
index cd4df45..bce1209 100644
--- a/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
+++ b/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
@@ -14,11 +14,15 @@
 
 package com.google.gerrit.server.update;
 
+import com.google.common.base.Throwables;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
 import com.google.gerrit.extensions.restapi.RestResource;
+import java.util.concurrent.atomic.AtomicReference;
 
 public abstract class RetryingRestCollectionModifyView<
         P extends RestResource, C extends RestResource, I, O>
@@ -30,11 +34,25 @@
   }
 
   @Override
-  public final O apply(P parentResource, I input)
+  public final Response<O> apply(P parentResource, I input)
       throws AuthException, BadRequestException, ResourceConflictException, Exception {
-    return retryHelper.execute(updateFactory -> applyImpl(updateFactory, parentResource, input));
+    AtomicReference<String> traceId = new AtomicReference<>(null);
+    try {
+      RetryHelper.Options retryOptions =
+          RetryHelper.options()
+              .caller(getClass())
+              .retryWithTrace(t -> !(t instanceof RestApiException))
+              .onAutoTrace(traceId::set)
+              .build();
+      return retryHelper
+          .execute(updateFactory -> applyImpl(updateFactory, parentResource, input), retryOptions)
+          .traceId(traceId.get());
+    } catch (Exception e) {
+      Throwables.throwIfInstanceOf(e, RestApiException.class);
+      return Response.<O>internalServerError(e).traceId(traceId.get());
+    }
   }
 
-  protected abstract O applyImpl(BatchUpdate.Factory updateFactory, P parentResource, I input)
-      throws Exception;
+  protected abstract Response<O> applyImpl(
+      BatchUpdate.Factory updateFactory, P parentResource, I input) throws Exception;
 }
diff --git a/java/com/google/gerrit/server/update/RetryingRestModifyView.java b/java/com/google/gerrit/server/update/RetryingRestModifyView.java
index 1b0b1f4..56c3eec 100644
--- a/java/com/google/gerrit/server/update/RetryingRestModifyView.java
+++ b/java/com/google/gerrit/server/update/RetryingRestModifyView.java
@@ -14,8 +14,12 @@
 
 package com.google.gerrit.server.update;
 
+import com.google.common.base.Throwables;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestResource;
+import java.util.concurrent.atomic.AtomicReference;
 
 public abstract class RetryingRestModifyView<R extends RestResource, I, O>
     implements RestModifyView<R, I> {
@@ -26,10 +30,24 @@
   }
 
   @Override
-  public final O apply(R resource, I input) throws Exception {
-    return retryHelper.execute(updateFactory -> applyImpl(updateFactory, resource, input));
+  public final Response<O> apply(R resource, I input) throws RestApiException {
+    AtomicReference<String> traceId = new AtomicReference<>(null);
+    try {
+      RetryHelper.Options retryOptions =
+          RetryHelper.options()
+              .caller(getClass())
+              .retryWithTrace(t -> !(t instanceof RestApiException))
+              .onAutoTrace(traceId::set)
+              .build();
+      return retryHelper
+          .execute(updateFactory -> applyImpl(updateFactory, resource, input), retryOptions)
+          .traceId(traceId.get());
+    } catch (Exception e) {
+      Throwables.throwIfInstanceOf(e, RestApiException.class);
+      return Response.<O>internalServerError(e).traceId(traceId.get());
+    }
   }
 
-  protected abstract O applyImpl(BatchUpdate.Factory updateFactory, R resource, I input)
+  protected abstract Response<O> applyImpl(BatchUpdate.Factory updateFactory, R resource, I input)
       throws Exception;
 }
diff --git a/java/com/google/gerrit/server/util/CommitMessageUtil.java b/java/com/google/gerrit/server/util/CommitMessageUtil.java
index 4ad226b..1c8ce0c 100644
--- a/java/com/google/gerrit/server/util/CommitMessageUtil.java
+++ b/java/com/google/gerrit/server/util/CommitMessageUtil.java
@@ -18,8 +18,8 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.reviewdb.client.Change;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import org.eclipse.jgit.lib.Constants;
@@ -34,7 +34,7 @@
     try {
       rng = SecureRandom.getInstance("SHA1PRNG");
     } catch (NoSuchAlgorithmException e) {
-      throw new RuntimeException("Cannot create RNG for Change-Id generator", e);
+      throw new IllegalStateException("Cannot create RNG for Change-Id generator", e);
     }
   }
 
@@ -71,6 +71,6 @@
   }
 
   public static Change.Key generateKey() {
-    return new Change.Key("I" + generateChangeId().name());
+    return Change.key("I" + generateChangeId().name());
   }
 }
diff --git a/java/com/google/gerrit/server/util/IdGenerator.java b/java/com/google/gerrit/server/util/IdGenerator.java
index 276df06..d4c2dc4 100644
--- a/java/com/google/gerrit/server/util/IdGenerator.java
+++ b/java/com/google/gerrit/server/util/IdGenerator.java
@@ -45,8 +45,8 @@
   public static int mix(int salt, int in) {
     short v0 = hi16(in);
     short v1 = lo16(in);
-    v0 += ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1;
-    v1 += ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3;
+    v0 += (short) (((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1);
+    v1 += (short) (((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3);
     return result(v0, v1);
   }
 
@@ -54,8 +54,8 @@
   static int unmix(int in) {
     short v0 = hi16(in);
     short v1 = lo16(in);
-    v1 -= ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3;
-    v0 -= ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1;
+    v1 -= (short) (((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3);
+    v0 -= (short) (((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1);
     return result(v0, v1);
   }
 
diff --git a/java/com/google/gerrit/server/util/MagicBranch.java b/java/com/google/gerrit/server/util/MagicBranch.java
index 4e41be0..924c288 100644
--- a/java/com/google/gerrit/server/util/MagicBranch.java
+++ b/java/com/google/gerrit/server/util/MagicBranch.java
@@ -16,7 +16,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.Capable;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import java.io.IOException;
 import java.util.List;
 import org.eclipse.jgit.lib.Ref;
@@ -26,28 +26,19 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String NEW_CHANGE = "refs/for/";
-  // TODO(xchangcheng): remove after 'repo' supports private/wip changes.
-  public static final String NEW_DRAFT_CHANGE = "refs/drafts/";
 
   /** Extracts the destination from a ref name */
   public static String getDestBranchName(String refName) {
-    String magicBranch = NEW_CHANGE;
-    if (refName.startsWith(NEW_DRAFT_CHANGE)) {
-      magicBranch = NEW_DRAFT_CHANGE;
-    }
-    return refName.substring(magicBranch.length());
+    return refName.substring(NEW_CHANGE.length());
   }
 
   /** Checks if the supplied ref name is a magic branch */
   public static boolean isMagicBranch(String refName) {
-    return refName.startsWith(NEW_DRAFT_CHANGE) || refName.startsWith(NEW_CHANGE);
+    return refName.startsWith(NEW_CHANGE);
   }
 
   /** Returns the ref name prefix for a magic branch, {@code null} if the branch is not magic */
   public static String getMagicRefNamePrefix(String refName) {
-    if (refName.startsWith(NEW_DRAFT_CHANGE)) {
-      return NEW_DRAFT_CHANGE;
-    }
     if (refName.startsWith(NEW_CHANGE)) {
       return NEW_CHANGE;
     }
@@ -63,15 +54,7 @@
    * branch.
    */
   public static Capable checkMagicBranchRefs(Repository repo, Project project) {
-    Capable result = checkMagicBranchRef(NEW_CHANGE, repo, project);
-    if (result != Capable.OK) {
-      return result;
-    }
-    result = checkMagicBranchRef(NEW_DRAFT_CHANGE, repo, project);
-    if (result != Capable.OK) {
-      return result;
-    }
-    return Capable.OK;
+    return checkMagicBranchRef(NEW_CHANGE, repo, project);
   }
 
   private static Capable checkMagicBranchRef(String branchName, Repository repo, Project project) {
diff --git a/java/com/google/gerrit/server/util/OneOffRequestContext.java b/java/com/google/gerrit/server/util/OneOffRequestContext.java
index 1788343..62683f0 100644
--- a/java/com/google/gerrit/server/util/OneOffRequestContext.java
+++ b/java/com/google/gerrit/server/util/OneOffRequestContext.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/util/ReplicaUtil.java b/java/com/google/gerrit/server/util/ReplicaUtil.java
new file mode 100644
index 0000000..bf6111a
--- /dev/null
+++ b/java/com/google/gerrit/server/util/ReplicaUtil.java
@@ -0,0 +1,25 @@
+// 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.util;
+
+import org.eclipse.jgit.lib.Config;
+
+public class ReplicaUtil {
+  /** Provides backward compatibility for container.slave property. */
+  public static boolean isReplica(Config cfg) {
+    return cfg.getBoolean("container", "slave", false)
+        || cfg.getBoolean("container", "replica", false);
+  }
+}
diff --git a/java/com/google/gerrit/server/util/RequestScopePropagator.java b/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 789b9b1..dc8a136 100644
--- a/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -17,7 +17,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Throwables;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.RequestCleanup;
 import com.google.gerrit.server.git.ProjectRunnable;
 import com.google.inject.Key;
diff --git a/java/com/google/gerrit/server/util/git/BUILD b/java/com/google/gerrit/server/util/git/BUILD
index a8ae918..4f4ba83 100644
--- a/java/com/google/gerrit/server/util/git/BUILD
+++ b/java/com/google/gerrit/server/util/git/BUILD
@@ -5,7 +5,7 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
-        "//java/com/google/gerrit/reviewdb:server",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//java/com/google/gerrit/entities",
+        "//lib:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/server/util/git/DelegateSystemReader.java b/java/com/google/gerrit/server/util/git/DelegateSystemReader.java
new file mode 100644
index 0000000..279bb95
--- /dev/null
+++ b/java/com/google/gerrit/server/util/git/DelegateSystemReader.java
@@ -0,0 +1,68 @@
+// 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.util.git;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
+
+public class DelegateSystemReader extends SystemReader {
+  private final SystemReader delegate;
+
+  public DelegateSystemReader(SystemReader delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public String getHostname() {
+    return delegate.getHostname();
+  }
+
+  @Override
+  public String getenv(String variable) {
+    return delegate.getenv(variable);
+  }
+
+  @Override
+  public String getProperty(String key) {
+    return delegate.getProperty(key);
+  }
+
+  @Override
+  public FileBasedConfig openUserConfig(Config parent, FS fs) {
+    return delegate.openUserConfig(parent, fs);
+  }
+
+  @Override
+  public FileBasedConfig openSystemConfig(Config parent, FS fs) {
+    return delegate.openSystemConfig(parent, fs);
+  }
+
+  @Override
+  public FileBasedConfig openJGitConfig(Config parent, FS fs) {
+    return delegate.openJGitConfig(parent, fs);
+  }
+
+  @Override
+  public long getCurrentTime() {
+    return delegate.getCurrentTime();
+  }
+
+  @Override
+  public int getTimezone(long when) {
+    return delegate.getTimezone(when);
+  }
+}
diff --git a/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java b/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
index f05d1d7..97132a3 100644
--- a/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
+++ b/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.util.git;
 
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.SubmoduleSubscription;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.HashSet;
@@ -45,10 +45,10 @@
 
   private final Config config;
   private final String canonicalWebUrl;
-  private final Branch.NameKey superProjectBranch;
+  private final BranchNameKey superProjectBranch;
 
   public SubmoduleSectionParser(
-      Config config, String canonicalWebUrl, Branch.NameKey superProjectBranch) {
+      Config config, String canonicalWebUrl, BranchNameKey superProjectBranch) {
     this.config = config;
     this.canonicalWebUrl = canonicalWebUrl;
     this.superProjectBranch = superProjectBranch;
@@ -81,13 +81,13 @@
         String project;
 
         if (branch.equals(".")) {
-          branch = superProjectBranch.get();
+          branch = superProjectBranch.branch();
         }
 
         // relative URL
         if (url.startsWith("../")) {
           // prefix with a slash for easier relative path walks
-          project = '/' + superProjectBranch.getParentKey().get();
+          project = '/' + superProjectBranch.project().get();
           String hostPart = url;
           while (hostPart.startsWith("../")) {
             int lastSlash = project.lastIndexOf('/');
@@ -133,9 +133,9 @@
                   0, //
                   project.length() - Constants.DOT_GIT_EXT.length());
         }
-        Project.NameKey projectKey = new Project.NameKey(project);
+        Project.NameKey projectKey = Project.nameKey(project);
         return new SubmoduleSubscription(
-            superProjectBranch, new Branch.NameKey(projectKey, branch), path);
+            superProjectBranch, BranchNameKey.create(projectKey, branch), path);
       }
     } catch (URISyntaxException e) {
       // Error in url syntax (in fact it is uri syntax)
diff --git a/java/com/google/gerrit/server/util/time/BUILD b/java/com/google/gerrit/server/util/time/BUILD
index ea39efe..b1126f0 100644
--- a/java/com/google/gerrit/server/util/time/BUILD
+++ b/java/com/google/gerrit/server/util/time/BUILD
@@ -5,7 +5,9 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/server/util/git",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/server/util/time/TimeUtil.java b/java/com/google/gerrit/server/util/time/TimeUtil.java
index 645dbb9..639d0a6 100644
--- a/java/com/google/gerrit/server/util/time/TimeUtil.java
+++ b/java/com/google/gerrit/server/util/time/TimeUtil.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.util.time;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
+import com.google.gerrit.server.util.git.DelegateSystemReader;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.function.LongSupplier;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.SystemReader;
 
 /** Static utility methods for dealing with dates and times. */
@@ -43,6 +43,17 @@
     return new Timestamp(nowMs());
   }
 
+  /**
+   * Returns the magic timestamp representing no specific time.
+   *
+   * <p>This "null object" is helpful in contexts where using {@code null} directly is not possible.
+   */
+  @UsedAt(Project.PLUGIN_CHECKS)
+  public static Timestamp never() {
+    // Always create a new object as timestamps are mutable.
+    return new Timestamp(0);
+  }
+
   public static Timestamp truncateToSecond(Timestamp t) {
     return new Timestamp((t.getTime() / 1000) * 1000);
   }
@@ -63,47 +74,15 @@
     SystemReader.setInstance(null);
   }
 
-  private static class GerritSystemReader extends SystemReader {
-    SystemReader delegate;
-
-    GerritSystemReader(SystemReader delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public String getHostname() {
-      return delegate.getHostname();
-    }
-
-    @Override
-    public String getenv(String variable) {
-      return delegate.getenv(variable);
-    }
-
-    @Override
-    public String getProperty(String key) {
-      return delegate.getProperty(key);
-    }
-
-    @Override
-    public FileBasedConfig openUserConfig(Config parent, FS fs) {
-      return delegate.openUserConfig(parent, fs);
-    }
-
-    @Override
-    public FileBasedConfig openSystemConfig(Config parent, FS fs) {
-      return delegate.openSystemConfig(parent, fs);
+  static class GerritSystemReader extends DelegateSystemReader {
+    GerritSystemReader(SystemReader reader) {
+      super(reader);
     }
 
     @Override
     public long getCurrentTime() {
       return currentMillisSupplier.getAsLong();
     }
-
-    @Override
-    public int getTimezone(long when) {
-      return delegate.getTimezone(when);
-    }
   }
 
   private TimeUtil() {}
diff --git a/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java b/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java
index bc52308..9fdc9e6 100644
--- a/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java
+++ b/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java
@@ -26,6 +26,9 @@
   /**
    * Called when an account should be activated to allow validation of the account activation.
    *
+   * <p>See {@link com.google.gerrit.extensions.events.AccountActivationListener} for a listener
+   * that's run after the account got activated.
+   *
    * @param account the account that should be activated
    * @throws ValidationException if validation fails
    */
@@ -34,6 +37,9 @@
   /**
    * Called when an account should be deactivated to allow validation of the account deactivation.
    *
+   * <p>See {@link com.google.gerrit.extensions.events.AccountActivationListener} for a listener
+   * that's run after the account got deactivated.
+   *
    * @param account the account that should be deactivated
    * @throws ValidationException if validation fails
    */
diff --git a/java/com/google/gerrit/server/validators/AssigneeValidationListener.java b/java/com/google/gerrit/server/validators/AssigneeValidationListener.java
index a97ce0b..514125f 100644
--- a/java/com/google/gerrit/server/validators/AssigneeValidationListener.java
+++ b/java/com/google/gerrit/server/validators/AssigneeValidationListener.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.validators;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
 
 /** Listener to provide validation of assignees. */
 @ExtensionPoint
diff --git a/java/com/google/gerrit/server/validators/HashtagValidationListener.java b/java/com/google/gerrit/server/validators/HashtagValidationListener.java
index fbf8e76..5ea6bfa 100644
--- a/java/com/google/gerrit/server/validators/HashtagValidationListener.java
+++ b/java/com/google/gerrit/server/validators/HashtagValidationListener.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.validators;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.reviewdb.client.Change;
 import java.util.Set;
 
 /** Listener to provide validation of hashtag changes. */
diff --git a/java/com/google/gerrit/sshd/AbstractGitCommand.java b/java/com/google/gerrit/sshd/AbstractGitCommand.java
index 90f0c43..9efcff2 100644
--- a/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -24,11 +24,14 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
 import org.kohsuke.args4j.Argument;
 
 public abstract class AbstractGitCommand extends BaseCommand {
+  private static final String GIT_PROTOCOL = "GIT_PROTOCOL";
+
   @Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name")
   protected ProjectState projectState;
 
@@ -45,10 +48,16 @@
   protected Repository repo;
   protected Project.NameKey projectName;
   protected Project project;
+  protected String[] extraParameters;
 
   @Override
-  public void start(Environment env) {
+  public void start(ChannelSession channel, Environment env) {
     enableGracefulStop();
+    String gitProtocol = env.getEnv().get(GIT_PROTOCOL);
+    if (gitProtocol != null) {
+      extraParameters = gitProtocol.split(":");
+    }
+
     Context ctx = context.subContext(newSession(), context.getCommandLine());
     final Context old = sshScope.set(ctx);
     try {
diff --git a/java/com/google/gerrit/sshd/AliasCommand.java b/java/com/google/gerrit/sshd/AliasCommand.java
index 567cf00..bf0dd91 100644
--- a/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/java/com/google/gerrit/sshd/AliasCommand.java
@@ -27,6 +27,7 @@
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.apache.sshd.server.command.Command;
 
 /** Command that executes some other command. */
@@ -47,9 +48,9 @@
   }
 
   @Override
-  public void start(Environment env) throws IOException {
+  public void start(ChannelSession channel, Environment env) throws IOException {
     try {
-      begin(env);
+      begin(channel, env);
     } catch (Failure e) {
       String msg = e.getMessage();
       if (!msg.endsWith("\n")) {
@@ -61,7 +62,7 @@
     }
   }
 
-  private void begin(Environment env) throws IOException, Failure {
+  private void begin(ChannelSession channel, Environment env) throws IOException, Failure {
     Map<String, CommandProvider> map = root.getMap();
     for (String name : chain(command)) {
       CommandProvider p = map.get(name);
@@ -90,15 +91,15 @@
     }
     provideStateTo(cmd);
     atomicCmd.set(cmd);
-    cmd.start(env);
+    cmd.start(channel, env);
   }
 
   @Override
-  public void destroy() {
+  public void destroy(ChannelSession channel) {
     Command cmd = atomicCmd.getAndSet(null);
     if (cmd != null) {
       try {
-        cmd.destroy();
+        cmd.destroy(channel);
       } catch (Exception e) {
         Throwables.throwIfUnchecked(e);
         throw new RuntimeException(e);
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index f6f9b52..689c567 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -7,25 +7,27 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/json",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/audit",
         "//java/com/google/gerrit/server/git/receive",
         "//java/com/google/gerrit/server/ioutil",
         "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/restapi",
-        "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/logging",
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib:jgit-archive",
         "//lib:jsch",
         "//lib:servlet-api",
         "//lib/auto:auto-value",
@@ -37,8 +39,6 @@
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",  # SSH should not depend on servlet
-        "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/log:log4j",
         "//lib/mina:core",
         "//lib/mina:sshd",
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index 1d9635f..85d9eb2 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -20,10 +20,10 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.DynamicOptions;
@@ -51,12 +51,14 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.nio.charset.Charset;
+import java.util.Arrays;
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.apache.sshd.server.command.Command;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.CmdLineException;
@@ -182,7 +184,7 @@
   }
 
   @Override
-  public void destroy() {
+  public void destroy(ChannelSession channel) {
     Future<?> future = task.getAndSet(null);
     if (future != null && !future.isDone()) {
       future.cancel(true);
@@ -264,7 +266,8 @@
   /**
    * Spawn a function into its own thread.
    *
-   * <p>Typically this should be invoked within {@link Command#start(Environment)}, such as:
+   * <p>Typically this should be invoked within {@link Command#start(ChannelSession, Environment)},
+   * such as:
    *
    * <pre>
    * startThread(new CommandRunnable() {
@@ -354,7 +357,7 @@
       }
       m.append(" during ");
       m.append(context.getCommandLine());
-      logger.atSevere().withCause(e).log(m.toString());
+      logCauseIfRelevant(e, m);
     }
 
     if (e instanceof Failure) {
@@ -381,6 +384,20 @@
     return 128;
   }
 
+  private void logCauseIfRelevant(Throwable e, StringBuilder message) {
+    String zeroLength = "length=0";
+    String streamAlreadyClosed = "stream is already closed";
+    boolean isZeroLength = false;
+
+    if (streamAlreadyClosed.equals(e.getMessage())) {
+      StackTraceElement[] stackTrace = e.getStackTrace();
+      isZeroLength = Arrays.stream(stackTrace).anyMatch(s -> s.toString().contains(zeroLength));
+    }
+    if (!isZeroLength) {
+      logger.atSevere().withCause(e).log(message.toString());
+    }
+  }
+
   protected UnloggedFailure die(String msg) {
     return new UnloggedFailure(1, "fatal: " + msg);
   }
diff --git a/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index ed6f87f..491bcb8 100644
--- a/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -122,7 +122,7 @@
 
   private List<Change.Id> parseId(String id) throws UnloggedFailure {
     try {
-      return Arrays.asList(new Change.Id(Integer.parseInt(id)));
+      return Arrays.asList(Change.id(Integer.parseInt(id)));
     } catch (NumberFormatException e) {
       throw new UnloggedFailure(2, "Invalid change ID " + id, e);
     }
diff --git a/java/com/google/gerrit/sshd/ChannelIdTrackingUnknownChannelReferenceHandler.java b/java/com/google/gerrit/sshd/ChannelIdTrackingUnknownChannelReferenceHandler.java
new file mode 100644
index 0000000..f8ab90e
--- /dev/null
+++ b/java/com/google/gerrit/sshd/ChannelIdTrackingUnknownChannelReferenceHandler.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 file is based on sshd-contrib Apache SSHD Mina project. Original commit:
+ * https://github.com/apache/mina-sshd/commit/11b33dee37b5b9c71a40a8a98a42007e3687131e
+ */
+package com.google.gerrit.sshd;
+
+import com.google.common.flogger.FluentLogger;
+import java.io.IOException;
+import org.apache.sshd.common.AttributeRepository.AttributeKey;
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.channel.Channel;
+import org.apache.sshd.common.channel.ChannelListener;
+import org.apache.sshd.common.channel.exception.SshChannelNotFoundException;
+import org.apache.sshd.common.session.ConnectionService;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.helpers.DefaultUnknownChannelReferenceHandler;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * Makes sure that the referenced &quot;unknown&quot; channel identifier is one that was assigned in
+ * the past. <B>Note:</B> it relies on the fact that the default {@code ConnectionService}
+ * implementation assigns channels identifiers in ascending order.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ChannelIdTrackingUnknownChannelReferenceHandler
+    extends DefaultUnknownChannelReferenceHandler implements ChannelListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  public static final AttributeKey<Integer> LAST_CHANNEL_ID_KEY = new AttributeKey<>();
+
+  public static final ChannelIdTrackingUnknownChannelReferenceHandler TRACKER =
+      new ChannelIdTrackingUnknownChannelReferenceHandler();
+
+  public ChannelIdTrackingUnknownChannelReferenceHandler() {
+    super();
+  }
+
+  @Override
+  public void channelInitialized(Channel channel) {
+    int channelId = channel.getId();
+    Session session = channel.getSession();
+    Integer lastTracked = session.setAttribute(LAST_CHANNEL_ID_KEY, channelId);
+    logger.atFine().log(
+        "channelInitialized(%s) updated last tracked channel ID %s => %s",
+        channel, lastTracked, channelId);
+  }
+
+  @Override
+  public Channel handleUnknownChannelCommand(
+      ConnectionService service, byte cmd, int channelId, Buffer buffer) throws IOException {
+    Session session = service.getSession();
+    Integer lastTracked = session.getAttribute(LAST_CHANNEL_ID_KEY);
+    if ((lastTracked != null) && (channelId <= lastTracked.intValue())) {
+      // Use TRACE level in order to avoid messages flooding
+      logger.atFinest().log(
+          "handleUnknownChannelCommand(%s) apply default handling for %s on channel=%s (lastTracked=%s)",
+          session, SshConstants.getCommandMessageName(cmd), channelId, lastTracked);
+      return super.handleUnknownChannelCommand(service, cmd, channelId, buffer);
+    }
+
+    throw new SshChannelNotFoundException(
+        channelId,
+        "Received "
+            + SshConstants.getCommandMessageName(cmd)
+            + " on unassigned channel "
+            + channelId
+            + " (last assigned="
+            + lastTracked
+            + ")");
+  }
+}
diff --git a/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index c003b46..38ac26d 100644
--- a/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -40,6 +40,7 @@
 import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
 import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.apache.sshd.server.command.Command;
 import org.apache.sshd.server.command.CommandFactory;
 import org.apache.sshd.server.session.ServerSession;
@@ -91,13 +92,13 @@
 
   @Override
   public CommandFactory get() {
-    return requestCommand -> {
-      String c = requestCommand;
+    return (channelSession, requestCommand) -> {
+      String command = requestCommand;
       SshCreateCommandInterceptor interceptor = createCommandInterceptor.get();
       if (interceptor != null) {
-        c = interceptor.intercept(c);
+        command = interceptor.intercept(command);
       }
-      return new Trampoline(c);
+      return new Trampoline(command);
     };
   }
 
@@ -148,7 +149,7 @@
     }
 
     @Override
-    public void start(Environment env) throws IOException {
+    public void start(ChannelSession channel, Environment env) throws IOException {
       this.env = env;
       final Context ctx = this.ctx;
       task.set(
@@ -157,7 +158,7 @@
                 @Override
                 public void run() {
                   try {
-                    onStart();
+                    onStart(channel);
                   } catch (Exception e) {
                     logger.atWarning().withCause(e).log(
                         "Cannot start command \"%s\" for user %s",
@@ -172,7 +173,7 @@
               }));
     }
 
-    private void onStart() throws IOException {
+    private void onStart(ChannelSession channel) throws IOException {
       synchronized (this) {
         final Context old = sshScope.set(ctx);
         try {
@@ -195,7 +196,7 @@
                   log(rc);
                 }
               });
-          cmd.start(env);
+          cmd.start(channel, env);
         } finally {
           sshScope.set(old);
         }
@@ -231,20 +232,20 @@
     }
 
     @Override
-    public void destroy() {
+    public void destroy(ChannelSession channel) {
       Future<?> future = task.getAndSet(null);
       if (future != null) {
         future.cancel(true);
-        destroyExecutor.execute(this::onDestroy);
+        destroyExecutor.execute(() -> onDestroy(channel));
       }
     }
 
-    private void onDestroy() {
+    private void onDestroy(ChannelSession channel) {
       synchronized (this) {
         if (cmd != null) {
           final Context old = sshScope.set(ctx);
           try {
-            cmd.destroy();
+            cmd.destroy(channel);
             log(BaseCommand.STATUS_CANCEL);
           } finally {
             ctx = null;
diff --git a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 1e32e1b..6c0f3af 100644
--- a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -31,6 +31,7 @@
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
+import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.PublicKey;
 import java.util.Collection;
@@ -80,19 +81,24 @@
   }
 
   private static Set<PublicKey> myHostKeys(KeyPairProvider p) {
-    final Set<PublicKey> keys = new HashSet<>(6);
-    addPublicKey(keys, p, KeyPairProvider.SSH_ED25519);
-    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP256);
-    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP384);
-    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP521);
-    addPublicKey(keys, p, KeyPairProvider.SSH_RSA);
-    addPublicKey(keys, p, KeyPairProvider.SSH_DSS);
+    Set<PublicKey> keys = new HashSet<>(6);
+    try {
+      addPublicKey(keys, p, KeyPairProvider.SSH_ED25519);
+      addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP256);
+      addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP384);
+      addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP521);
+      addPublicKey(keys, p, KeyPairProvider.SSH_RSA);
+      addPublicKey(keys, p, KeyPairProvider.SSH_DSS);
+    } catch (IOException | GeneralSecurityException e) {
+      throw new IllegalStateException("Cannot load SSHD host key", e);
+    }
+
     return keys;
   }
 
-  private static void addPublicKey(
-      final Collection<PublicKey> out, KeyPairProvider p, String type) {
-    final KeyPair pair = p.loadKey(type);
+  private static void addPublicKey(Collection<PublicKey> out, KeyPairProvider p, String type)
+      throws IOException, GeneralSecurityException {
+    KeyPair pair = p.loadKey(null, type);
     if (pair != null && pair.getPublic() != null) {
       out.add(pair.getPublic());
     }
diff --git a/java/com/google/gerrit/sshd/DispatchCommand.java b/java/com/google/gerrit/sshd/DispatchCommand.java
index 68962db..7db65bd 100644
--- a/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -33,6 +33,7 @@
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.apache.sshd.server.command.Command;
 import org.kohsuke.args4j.Argument;
 
@@ -69,7 +70,7 @@
   }
 
   @Override
-  public void start(Environment env) throws IOException {
+  public void start(ChannelSession channel, Environment env) throws IOException {
     try {
       parseCommandLine();
       if (Strings.isNullOrEmpty(commandName)) {
@@ -115,7 +116,7 @@
 
       provideStateTo(cmd);
       atomicCmd.set(cmd);
-      cmd.start(env);
+      cmd.start(channel, env);
 
     } catch (UnloggedFailure e) {
       String msg = e.getMessage();
@@ -145,11 +146,11 @@
   }
 
   @Override
-  public void destroy() {
+  public void destroy(ChannelSession channel) {
     Command cmd = atomicCmd.getAndSet(null);
     if (cmd != null) {
       try {
-        cmd.destroy();
+        cmd.destroy(channel);
       } catch (Exception e) {
         Throwables.throwIfUnchecked(e);
         throw new RuntimeException(e);
diff --git a/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java b/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java
index adb5085..6759275 100644
--- a/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java
+++ b/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.account.AccountCache;
@@ -66,7 +66,7 @@
     }
 
     Optional<Account> account =
-        accounts.getByUsername(username).map(AccountState::getAccount).filter(Account::isActive);
+        accounts.getByUsername(username).map(AccountState::account).filter(Account::isActive);
     if (!account.isPresent()) {
       return false;
     }
@@ -77,6 +77,6 @@
         sshScope,
         sshLog,
         sd,
-        SshUtil.createUser(sd, userFactory, account.get().getId()));
+        SshUtil.createUser(sd, userFactory, account.get().id()));
   }
 }
diff --git a/java/com/google/gerrit/sshd/HostKeyProvider.java b/java/com/google/gerrit/sshd/HostKeyProvider.java
index bffcfcd..3578fb9 100644
--- a/java/com/google/gerrit/sshd/HostKeyProvider.java
+++ b/java/com/google/gerrit/sshd/HostKeyProvider.java
@@ -18,7 +18,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
-import java.io.File;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -44,21 +43,21 @@
     Path ecdsaKey_521 = site.ssh_ecdsa_521;
     Path ed25519Key = site.ssh_ed25519;
 
-    final List<File> stdKeys = new ArrayList<>(6);
+    final List<Path> stdKeys = new ArrayList<>(6);
     if (Files.exists(rsaKey)) {
-      stdKeys.add(rsaKey.toAbsolutePath().toFile());
+      stdKeys.add(rsaKey);
     }
     if (Files.exists(ecdsaKey_256)) {
-      stdKeys.add(ecdsaKey_256.toAbsolutePath().toFile());
+      stdKeys.add(ecdsaKey_256);
     }
     if (Files.exists(ecdsaKey_384)) {
-      stdKeys.add(ecdsaKey_384.toAbsolutePath().toFile());
+      stdKeys.add(ecdsaKey_384);
     }
     if (Files.exists(ecdsaKey_521)) {
-      stdKeys.add(ecdsaKey_521.toAbsolutePath().toFile());
+      stdKeys.add(ecdsaKey_521);
     }
     if (Files.exists(ed25519Key)) {
-      stdKeys.add(ed25519Key.toAbsolutePath().toFile());
+      stdKeys.add(ed25519Key);
     }
 
     if (Files.exists(objKey)) {
@@ -70,14 +69,14 @@
       // Both formats of host key exist, we don't know which format
       // should be authoritative. Complain and abort.
       //
-      stdKeys.add(objKey.toAbsolutePath().toFile());
+      stdKeys.add(objKey);
       throw new ProvisionException("Multiple host keys exist: " + stdKeys);
     }
     if (stdKeys.isEmpty()) {
       throw new ProvisionException("No SSH keys under " + site.etc_dir);
     }
     FileKeyPairProvider kp = new FileKeyPairProvider();
-    kp.setFiles(stdKeys);
+    kp.setPaths(stdKeys);
     return kp;
   }
 }
diff --git a/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java b/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
new file mode 100644
index 0000000..1086626
--- /dev/null
+++ b/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
@@ -0,0 +1,60 @@
+// 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.sshd;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.events.AccountActivationListener;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.sshd.BaseCommand.Failure;
+import com.google.inject.Inject;
+import java.io.IOException;
+
+/** Closes open SSH connections upon account deactivation. */
+public class InactiveAccountDisconnector implements AccountActivationListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final SshDaemon sshDaemon;
+
+  @Inject
+  InactiveAccountDisconnector(SshDaemon sshDaemon) {
+    this.sshDaemon = sshDaemon;
+  }
+
+  @Override
+  public void onAccountDeactivated(int id) {
+    try {
+      SshUtil.forEachSshSession(
+          sshDaemon,
+          (sshId, sshSession, abstractSession, ioSession) -> {
+            CurrentUser sessionUser = sshSession.getUser();
+            if (sessionUser.isIdentifiedUser() && sessionUser.getAccountId().get() == id) {
+              logger.atInfo().log(
+                  "Disconnecting SSH session %s because user %s(%d) got deactivated",
+                  abstractSession, sessionUser.getLoggableName(), id);
+              try {
+                abstractSession.disconnect(-1, "user deactivated");
+              } catch (IOException e) {
+                logger.atWarning().withCause(e).log(
+                    "Failure while deactivating session %s", abstractSession);
+              }
+            }
+          });
+    } catch (Failure e) {
+      // Ssh Daemon no longer running. Since we're only disconnecting connections anyways, this is
+      // most likely ok, so we log only at info level.
+      logger.atInfo().withCause(e).log("Failure while disconnecting deactivated account %d", id);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/sshd/LogMaxConnectionsPerUserExceeded.java b/java/com/google/gerrit/sshd/LogMaxConnectionsPerUserExceeded.java
new file mode 100644
index 0000000..6f568b1
--- /dev/null
+++ b/java/com/google/gerrit/sshd/LogMaxConnectionsPerUserExceeded.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.apache.sshd.common.Service;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionDisconnectHandler;
+
+@Singleton
+public class LogMaxConnectionsPerUserExceeded implements SessionDisconnectHandler {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Override
+  public boolean handleSessionsCountDisconnectReason(
+      Session session,
+      Service service,
+      String username,
+      int currentSessionCount,
+      int maxSessionCount)
+      throws IOException {
+    logger.atWarning().log(
+        "Max connection count for user %s exceeded, rejecting new connection."
+            + " currentSessionCount = %d, maxSessionCount = %d",
+        username, currentSessionCount, maxSessionCount);
+    return false;
+  }
+}
diff --git a/java/com/google/gerrit/sshd/NoShell.java b/java/com/google/gerrit/sshd/NoShell.java
index 5aaa647..2a29a62 100644
--- a/java/com/google/gerrit/sshd/NoShell.java
+++ b/java/com/google/gerrit/sshd/NoShell.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -27,12 +27,17 @@
 import java.io.OutputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
-import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.io.IoInputStream;
+import org.apache.sshd.common.io.IoOutputStream;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
 import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.command.AsyncCommand;
 import org.apache.sshd.server.command.Command;
 import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.shell.ShellFactory;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.util.SystemReader;
 
@@ -42,7 +47,7 @@
  * <p>This implementation is used to ensure clients who try to SSH directly to this server without
  * supplying a command will get a reasonable error message, but cannot continue further.
  */
-class NoShell implements Factory<Command> {
+class NoShell implements ShellFactory {
   private final Provider<SendMessage> shell;
 
   @Inject
@@ -51,17 +56,23 @@
   }
 
   @Override
-  public Command create() {
+  public Command createShell(ChannelSession channel) {
     return shell.get();
   }
 
-  static class SendMessage implements Command, SessionAware {
+  /**
+   * When AsyncCommand is implemented by a command as below, the usual blocking streams aren't set.
+   *
+   * @see org.apache.sshd.server.command.AsyncCommand
+   */
+  static class SendMessage implements AsyncCommand, SessionAware {
     private final Provider<MessageFactory> messageFactory;
     private final SshScope sshScope;
 
-    private InputStream in;
-    private OutputStream out;
-    private OutputStream err;
+    private IoInputStream in;
+    private IoOutputStream out;
+    private IoOutputStream err;
+
     private ExitCallback exit;
     private Context context;
 
@@ -72,21 +83,36 @@
     }
 
     @Override
-    public void setInputStream(InputStream in) {
+    public void setIoInputStream(IoInputStream in) {
       this.in = in;
     }
 
     @Override
-    public void setOutputStream(OutputStream out) {
+    public void setIoOutputStream(IoOutputStream out) {
       this.out = out;
     }
 
     @Override
-    public void setErrorStream(OutputStream err) {
+    public void setIoErrorStream(IoOutputStream err) {
       this.err = err;
     }
 
     @Override
+    public void setInputStream(InputStream in) {
+      // ignored
+    }
+
+    @Override
+    public void setOutputStream(OutputStream out) {
+      // ignore
+    }
+
+    @Override
+    public void setErrorStream(OutputStream err) {
+      // ignore
+    }
+
+    @Override
     public void setExitCallback(ExitCallback callback) {
       this.exit = callback;
     }
@@ -98,7 +124,7 @@
     }
 
     @Override
-    public void start(Environment env) throws IOException {
+    public void start(ChannelSession channel, Environment env) throws IOException {
       Context old = sshScope.set(context);
       String message;
       try {
@@ -106,8 +132,7 @@
       } finally {
         sshScope.set(old);
       }
-      err.write(Constants.encode(message));
-      err.flush();
+      err.writePacket(new ByteArrayBuffer(Constants.encode(message)));
 
       in.close();
       out.close();
@@ -116,7 +141,7 @@
     }
 
     @Override
-    public void destroy() {}
+    public void destroy(ChannelSession channel) {}
   }
 
   static class MessageFactory {
@@ -145,7 +170,7 @@
       msg.append("\r\n");
 
       Account account = user.getAccount();
-      String name = account.getFullName();
+      String name = account.fullName();
       if (name == null || name.isEmpty()) {
         name = user.getUserName().orElse(anonymousCowardName);
       }
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index 9828957..e60ba6d 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -14,14 +14,28 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.RequestListener;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.PerformanceLogContext;
+import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.inject.Inject;
 import java.io.IOException;
 import java.io.PrintWriter;
 import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 
 public abstract class SshCommand extends BaseCommand {
+  @Inject private DynamicSet<PerformanceLogger> performanceLoggers;
+  @Inject private PluginSetContext<RequestListener> requestListeners;
+  @Inject @GerritServerConfig private Config config;
+
   @Option(name = "--trace", usage = "enable request tracing")
   private boolean trace;
 
@@ -32,13 +46,18 @@
   protected PrintWriter stderr;
 
   @Override
-  public void start(Environment env) throws IOException {
+  public void start(ChannelSession channel, Environment env) throws IOException {
     startThread(
         () -> {
           parseCommandLine();
           stdout = toPrintWriter(out);
           stderr = toPrintWriter(err);
-          try (TraceContext traceContext = enableTracing()) {
+          try (TraceContext traceContext = enableTracing();
+              PerformanceLogContext performanceLogContext =
+                  new PerformanceLogContext(config, performanceLoggers)) {
+            RequestInfo requestInfo =
+                RequestInfo.builder(RequestInfo.RequestType.SSH, user, traceContext).build();
+            requestListeners.runEach(l -> l.onRequest(requestInfo));
             SshCommand.this.run();
           } finally {
             stdout.flush();
diff --git a/java/com/google/gerrit/sshd/SshDaemon.java b/java/com/google/gerrit/sshd/SshDaemon.java
index 8265413..f04ff7e 100644
--- a/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/java/com/google/gerrit/sshd/SshDaemon.java
@@ -51,6 +51,7 @@
 import java.nio.file.WatchService;
 import java.nio.file.attribute.UserPrincipalLookupService;
 import java.nio.file.spi.FileSystemProvider;
+import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import java.security.KeyPair;
 import java.security.PublicKey;
@@ -159,7 +160,8 @@
       SshLog sshLog,
       @SshListenAddresses List<SocketAddress> listen,
       @SshAdvertisedAddresses List<String> advertised,
-      MetricMaker metricMaker) {
+      MetricMaker metricMaker,
+      LogMaxConnectionsPerUserExceeded logMaxConnectionsPerUserExceeded) {
     setPort(IANA_SSH_PORT /* never used */);
 
     this.cfg = cfg;
@@ -214,6 +216,7 @@
     final boolean enableCompression = cfg.getBoolean("sshd", "enableCompression", false);
 
     SshSessionBackend backend = cfg.getEnum("sshd", null, "backend", SshSessionBackend.NIO2);
+    boolean channelIdTracking = cfg.getBoolean("sshd", "enableChannelIdTracking", true);
 
     gracefulStopTimeout = cfg.getTimeUnit("sshd", null, "gracefulStopTimeout", 0, TimeUnit.SECONDS);
 
@@ -229,7 +232,7 @@
     initMacs(cfg);
     initSignatures();
     initChannels();
-    initUnknownChannelReferenceHandler();
+    initUnknownChannelReferenceHandler(channelIdTracking);
     initForwarding();
     initFileSystemFactory();
     initSubsystems();
@@ -238,6 +241,7 @@
     setKeyPairProvider(hostKeyProvider);
     setCommandFactory(commandFactory);
     setShellFactory(noShell);
+    setSessionDisconnectHandler(logMaxConnectionsPerUserExceeded);
 
     final AtomicInteger connected = new AtomicInteger();
     metricMaker.newCallbackMetric(
@@ -429,12 +433,12 @@
       return Collections.emptyList();
     }
 
-    final List<PublicKey> keys = myHostKeys();
-    final List<HostKey> r = new ArrayList<>();
+    List<HostKey> r = new ArrayList<>();
+    List<PublicKey> keys = myHostKeys();
     for (PublicKey pub : keys) {
-      final Buffer buf = new ByteArrayBuffer();
+      Buffer buf = new ByteArrayBuffer();
       buf.putRawPublicKey(pub);
-      final byte[] keyBin = buf.getCompactData();
+      byte[] keyBin = buf.getCompactData();
 
       for (String addr : advertised) {
         try {
@@ -445,24 +449,29 @@
         }
       }
     }
+
     return Collections.unmodifiableList(r);
   }
 
   private List<PublicKey> myHostKeys() {
-    final KeyPairProvider p = getKeyPairProvider();
-    final List<PublicKey> keys = new ArrayList<>(6);
-    addPublicKey(keys, p, KeyPairProvider.SSH_ED25519);
-    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP256);
-    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP384);
-    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP521);
-    addPublicKey(keys, p, KeyPairProvider.SSH_RSA);
-    addPublicKey(keys, p, KeyPairProvider.SSH_DSS);
+    KeyPairProvider p = getKeyPairProvider();
+    List<PublicKey> keys = new ArrayList<>(6);
+    try {
+      addPublicKey(keys, p, KeyPairProvider.SSH_ED25519);
+      addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP256);
+      addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP384);
+      addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP521);
+      addPublicKey(keys, p, KeyPairProvider.SSH_RSA);
+      addPublicKey(keys, p, KeyPairProvider.SSH_DSS);
+    } catch (IOException | GeneralSecurityException e) {
+      throw new IllegalStateException("Cannot load SSHD host key", e);
+    }
     return keys;
   }
 
-  private static void addPublicKey(
-      final Collection<PublicKey> out, KeyPairProvider p, String type) {
-    final KeyPair pair = p.loadKey(type);
+  private static void addPublicKey(final Collection<PublicKey> out, KeyPairProvider p, String type)
+      throws IOException, GeneralSecurityException {
+    final KeyPair pair = p.loadKey(null, type);
     if (pair != null && pair.getPublic() != null) {
       out.add(pair.getPublic());
     }
@@ -562,14 +571,14 @@
 
   @SuppressWarnings("unchecked")
   private void initCiphers(Config cfg) {
-    final List<NamedFactory<Cipher>> a = BaseBuilder.setUpDefaultCiphers(true);
+    List<NamedFactory<Cipher>> a = BaseBuilder.setUpDefaultCiphers(true);
 
     for (Iterator<NamedFactory<Cipher>> i = a.iterator(); i.hasNext(); ) {
-      final NamedFactory<Cipher> f = i.next();
+      NamedFactory<Cipher> f = i.next();
       try {
-        final Cipher c = f.create();
-        final byte[] key = new byte[c.getBlockSize()];
-        final byte[] iv = new byte[c.getIVSize()];
+        Cipher c = f.create();
+        byte[] key = new byte[c.getKdfSize()];
+        byte[] iv = new byte[c.getIVSize()];
         c.init(Cipher.Mode.Encrypt, key, iv);
       } catch (InvalidKeyException e) {
         logger.atWarning().log(
@@ -662,7 +671,8 @@
   }
 
   private void initSignatures() {
-    setSignatureFactories(BaseBuilder.setUpDefaultSignatures(true));
+    setSignatureFactories(
+        NamedFactory.setUpBuiltinFactories(false, ServerBuilder.DEFAULT_SIGNATURE_PREFERENCE));
   }
 
   private void initCompression(boolean enableCompression) {
@@ -678,10 +688,9 @@
     // However, if there are CPU in abundance and the server is reachable through
     // slow networks, gits with huge amount of refs can benefit from SSH-compression
     // since git does not compress the ref announcement during the handshake.
-    //
-    // Compression can be especially useful when Gerrit slaves are being used
-    // for the larger clones and fetches and the master server mostly takes small
-    // receive-packs.
+    // Compression can be especially useful when Gerrit replica are being used
+    // for the larger clones and fetches and the primary server handling write
+    // operations mostly takes small receive-packs.
 
     if (enableCompression) {
       compressionFactories.add(BuiltinCompressions.zlib);
@@ -694,8 +703,11 @@
     setChannelFactories(ServerBuilder.DEFAULT_CHANNEL_FACTORIES);
   }
 
-  private void initUnknownChannelReferenceHandler() {
-    setUnknownChannelReferenceHandler(DefaultUnknownChannelReferenceHandler.INSTANCE);
+  private void initUnknownChannelReferenceHandler(boolean enableChannelIdTracking) {
+    setUnknownChannelReferenceHandler(
+        enableChannelIdTracking
+            ? ChannelIdTrackingUnknownChannelReferenceHandler.TRACKER
+            : DefaultUnknownChannelReferenceHandler.INSTANCE);
   }
 
   private void initSubsystems() {
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheEntry.java b/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
index 8e962e3..78fd7da 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import java.security.PublicKey;
 
 class SshKeyCacheEntry {
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index fb0b8f6..773c25b 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.ssh.SshKeyCache;
@@ -106,7 +107,9 @@
     @Override
     public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
       try (TraceTimer timer =
-          TraceContext.newTimer("Loading SSH keys for account with username %s", username)) {
+          TraceContext.newTimer(
+              "Loading SSH keys for account with username",
+              Metadata.builder().username(username).build())) {
         Optional<ExternalId> user =
             externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, username));
         if (!user.isPresent()) {
diff --git a/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java b/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
index b2853b9..02a9e48 100644
--- a/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.sshd;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.ssh.SshKeyCreator;
 import java.security.NoSuchAlgorithmException;
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index e4aa14c..9301f8a 100644
--- a/java/com/google/gerrit/sshd/SshModule.java
+++ b/java/com/google/gerrit/sshd/SshModule.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Splitter;
+import com.google.gerrit.extensions.events.AccountActivationListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -103,6 +104,9 @@
     DynamicItem.itemOf(binder(), SshCreateCommandInterceptor.class);
     DynamicSet.setOf(binder(), SshExecuteCommandInterceptor.class);
 
+    DynamicSet.bind(binder(), AccountActivationListener.class)
+        .to(InactiveAccountDisconnector.class);
+
     listener().toInstance(registerInParentInjectors());
     listener().to(SshLog.class);
     listener().to(SshDaemon.class);
diff --git a/java/com/google/gerrit/sshd/SshSession.java b/java/com/google/gerrit/sshd/SshSession.java
index a2c3d93..b39eaed 100644
--- a/java/com/google/gerrit/sshd/SshSession.java
+++ b/java/com/google/gerrit/sshd/SshSession.java
@@ -19,7 +19,7 @@
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
-import org.apache.sshd.common.AttributeStore.AttributeKey;
+import org.apache.sshd.common.AttributeRepository.AttributeKey;
 
 /** Global data related to an active SSH connection. */
 public class SshSession {
diff --git a/java/com/google/gerrit/sshd/SshUtil.java b/java/com/google/gerrit/sshd/SshUtil.java
index 670d64c..6176628 100644
--- a/java/com/google/gerrit/sshd/SshUtil.java
+++ b/java/com/google/gerrit/sshd/SshUtil.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountSshKey;
+import com.google.gerrit.sshd.BaseCommand.Failure;
 import com.google.gerrit.sshd.SshScope.Context;
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -30,7 +31,10 @@
 import java.security.spec.InvalidKeySpecException;
 import org.apache.commons.codec.binary.Base64;
 import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.io.IoAcceptor;
+import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.server.session.ServerSession;
 import org.eclipse.jgit.lib.Constants;
@@ -154,4 +158,30 @@
       final Account.Id account) {
     return userFactory.create(sd.getRemoteAddress(), account);
   }
+
+  public static void forEachSshSession(SshDaemon sshDaemon, SessionConsumer consumer)
+      throws Failure {
+    IoAcceptor ioAcceptor = sshDaemon.getIoAcceptor();
+    if (ioAcceptor == null) {
+      throw new Failure(1, "fatal: sshd no longer running");
+    }
+    ioAcceptor
+        .getManagedSessions()
+        .forEach(
+            (id, ioSession) -> {
+              AbstractSession abstractSession = AbstractSession.getSession(ioSession, true);
+              if (abstractSession != null) {
+                SshSession sshSession = abstractSession.getAttribute(SshSession.KEY);
+                if (sshSession != null) {
+                  consumer.accept(id, sshSession, abstractSession, ioSession);
+                }
+              }
+            });
+  }
+
+  @FunctionalInterface
+  public interface SessionConsumer {
+    public void accept(
+        Long id, SshSession sshSession, AbstractSession abstractSession, IoSession ioSession);
+  }
 }
diff --git a/java/com/google/gerrit/sshd/SuExec.java b/java/com/google/gerrit/sshd/SuExec.java
index 6d0bf2b..ea163d5 100644
--- a/java/com/google/gerrit/sshd/SuExec.java
+++ b/java/com/google/gerrit/sshd/SuExec.java
@@ -18,8 +18,8 @@
 
 import com.google.common.base.Throwables;
 import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
@@ -35,6 +35,7 @@
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.apache.sshd.server.command.Command;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
@@ -90,7 +91,7 @@
   }
 
   @Override
-  public void start(Environment env) throws IOException {
+  public void start(ChannelSession channel, Environment env) throws IOException {
     try {
       checkCanRunAs();
       parseCommandLine();
@@ -102,7 +103,7 @@
         cmd.setArguments(args.toArray(new String[args.size()]));
         provideStateTo(cmd);
         atomicCmd.set(cmd);
-        cmd.start(env);
+        cmd.start(channel, env);
       } finally {
         sshScope.set(old);
       }
@@ -158,11 +159,11 @@
   }
 
   @Override
-  public void destroy() {
+  public void destroy(ChannelSession channel) {
     Command cmd = atomicCmd.getAndSet(null);
     if (cmd != null) {
       try {
-        cmd.destroy();
+        cmd.destroy(channel);
       } catch (Exception e) {
         Throwables.throwIfUnchecked(e);
         throw new RuntimeException(e);
diff --git a/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index 42e5624..134fb03 100644
--- a/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -69,7 +69,7 @@
           BanCommitInput.fromCommits(Lists.transform(commitsToBan, ObjectId::getName));
       input.reason = reason;
 
-      BanResultInfo r = banCommit.apply(new ProjectResource(projectState, user), input);
+      BanResultInfo r = banCommit.apply(new ProjectResource(projectState, user), input).value();
       printCommits(r.newlyBanned, "The following commits were banned");
       printCommits(r.alreadyBanned, "The following commits were already banned");
       printCommits(r.ignored, "The following ids do not represent commits and were ignored");
diff --git a/java/com/google/gerrit/sshd/commands/CloseConnection.java b/java/com/google/gerrit/sshd/commands/CloseConnection.java
index 6cd009b..e0b87f8 100644
--- a/java/com/google/gerrit/sshd/commands/CloseConnection.java
+++ b/java/com/google/gerrit/sshd/commands/CloseConnection.java
@@ -23,15 +23,12 @@
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
-import com.google.gerrit.sshd.SshSession;
+import com.google.gerrit.sshd.SshUtil;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.sshd.common.future.CloseFuture;
-import org.apache.sshd.common.io.IoAcceptor;
-import org.apache.sshd.common.io.IoSession;
-import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -61,36 +58,26 @@
   @Override
   protected void run() throws Failure {
     enableGracefulStop();
-    IoAcceptor acceptor = sshDaemon.getIoAcceptor();
-    if (acceptor == null) {
-      throw new Failure(1, "fatal: sshd no longer running");
-    }
-    for (String sessionId : sessionIds) {
-      boolean connectionFound = false;
-      int id = (int) Long.parseLong(sessionId, 16);
-      for (IoSession io : acceptor.getManagedSessions().values()) {
-        AbstractSession serverSession = AbstractSession.getSession(io, true);
-        SshSession sshSession =
-            serverSession != null ? serverSession.getAttribute(SshSession.KEY) : null;
-        if (sshSession != null && sshSession.getSessionId() == id) {
-          connectionFound = true;
-          stdout.println("closing connection " + sessionId + "...");
-          CloseFuture future = io.close(true);
-          if (wait) {
-            try {
-              future.await();
-              stdout.println("closed connection " + sessionId);
-            } catch (IOException e) {
-              logger.atWarning().log(
-                  "Wait for connection to close interrupted: %s", e.getMessage());
+    SshUtil.forEachSshSession(
+        sshDaemon,
+        (k, sshSession, abstractSession, ioSession) -> {
+          String sessionId = String.format("%08x", sshSession.getSessionId());
+          if (sessionIds.remove(sessionId)) {
+            stdout.println("closing connection " + sessionId + "...");
+            CloseFuture future = ioSession.close(true);
+            if (wait) {
+              try {
+                future.await();
+                stdout.println("closed connection " + sessionId);
+              } catch (IOException e) {
+                logger.atWarning().log(
+                    "Wait for connection to close interrupted: %s", e.getMessage());
+              }
             }
           }
-          break;
-        }
-      }
-      if (!connectionFound) {
-        stderr.print("close connection " + sessionId + ": no such connection\n");
-      }
+        });
+    for (String sessionId : sessionIds) {
+      stderr.print("close connection " + sessionId + ": no such connection\n");
     }
   }
 }
diff --git a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 00fc20f..4da55e2 100644
--- a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -18,12 +18,12 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.account.CreateAccount;
 import com.google.gerrit.sshd.CommandMetaData;
diff --git a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 8169901..b243748 100644
--- a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -17,14 +17,14 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.AddMembers;
@@ -115,11 +115,12 @@
       }
     } catch (RestApiException e) {
       throw die(e);
+    } catch (Exception e) {
+      throw new Failure(1, "unavailable", e);
     }
   }
 
-  private GroupResource createGroup()
-      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
+  private GroupResource createGroup() throws Exception {
     GroupInput input = new GroupInput();
     input.description = groupDescription;
     input.visibleToAll = visibleToAll;
@@ -129,7 +130,9 @@
     }
 
     GroupInfo group =
-        createGroup.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(groupName), input);
+        createGroup
+            .apply(TopLevelResource.INSTANCE, IdString.fromDecoded(groupName), input)
+            .value();
     return groups.parse(TopLevelResource.INSTANCE, IdString.fromUrl(group.id));
   }
 
diff --git a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index ad7021b..f2ab4e8 100644
--- a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -18,6 +18,8 @@
 import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.projects.ConfigValue;
@@ -25,8 +27,6 @@
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SuggestParentCandidates;
diff --git a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 87b6f02..905ba9c 100644
--- a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
+import com.google.gerrit.entities.CoreDownloadSchemes;
 import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.sshd.CommandModule;
 import com.google.gerrit.sshd.CommandName;
diff --git a/java/com/google/gerrit/sshd/commands/FlushCaches.java b/java/com/google/gerrit/sshd/commands/FlushCaches.java
index e048eae..fe2a897 100644
--- a/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.config.ListCaches;
 import com.google.gerrit.server.restapi.config.ListCaches.OutputFormat;
 import com.google.gerrit.server.restapi.config.PostCaches;
@@ -82,15 +81,16 @@
       }
     } catch (RestApiException e) {
       throw die(e.getMessage());
-    } catch (PermissionBackendException e) {
+    } catch (Exception e) {
       throw new Failure(1, "unavailable", e);
     }
   }
 
   @SuppressWarnings("unchecked")
-  private void doList() {
+  private void doList() throws Exception {
     for (String name :
-        (List<String>) listCaches.setFormat(OutputFormat.LIST).apply(new ConfigResource())) {
+        (List<String>)
+            listCaches.setFormat(OutputFormat.LIST).apply(new ConfigResource()).value()) {
       stderr.print(name);
       stderr.print('\n');
     }
diff --git a/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index 97737b0..28a7804 100644
--- a/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -21,8 +21,8 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GarbageCollectionResult;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
diff --git a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index 376b837..041c85c 100644
--- a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.change.Index;
diff --git a/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index a6fe833..7bf42eb 100644
--- a/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -18,9 +18,9 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.ioutil.ColumnFormatter;
@@ -63,7 +63,7 @@
       if (verboseOutput) {
         Optional<InternalGroup> group =
             info.ownerId != null
-                ? groupCache.get(new AccountGroup.UUID(Url.decode(info.ownerId)))
+                ? groupCache.get(AccountGroup.uuid(Url.decode(info.ownerId)))
                 : Optional.empty();
 
         formatter.addColumn(Url.decode(info.id));
diff --git a/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
index 53d4ac6..3269c2b 100644
--- a/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -18,8 +18,8 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
@@ -70,7 +70,7 @@
     }
 
     void display(PrintWriter writer) throws PermissionBackendException {
-      Optional<InternalGroup> group = groupCache.get(new AccountGroup.NameKey(name));
+      Optional<InternalGroup> group = groupCache.get(AccountGroup.nameKey(name));
       String errorText = "Group not found or not visible\n";
 
       if (!group.isPresent()) {
diff --git a/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index a1af9a0..b8fc55a0 100644
--- a/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -17,12 +17,12 @@
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -77,7 +77,7 @@
     enableGracefulStop();
     Account.Id userAccountId;
     try {
-      userAccountId = accountResolver.resolve(userName).asUnique().getAccount().getId();
+      userAccountId = accountResolver.resolve(userName).asUnique().account().id();
     } catch (UnprocessableEntityException e) {
       stdout.println(e.getMessage());
       stdout.flush();
diff --git a/java/com/google/gerrit/sshd/commands/PatchSetParser.java b/java/com/google/gerrit/sshd/commands/PatchSetParser.java
index 45210f6..f804c2d 100644
--- a/java/com/google/gerrit/sshd/commands/PatchSetParser.java
+++ b/java/com/google/gerrit/sshd/commands/PatchSetParser.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -56,7 +56,7 @@
       throws UnloggedFailure {
     // By commit?
     //
-    if (token.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) {
+    if (token.matches("^([0-9a-fA-F]{4," + ObjectIds.STR_LEN + "})$")) {
       InternalChangeQuery query = queryProvider.get();
       List<ChangeData> cds;
       if (projectState != null) {
@@ -76,7 +76,7 @@
           continue;
         }
         for (PatchSet ps : cd.patchSets()) {
-          if (ps.getRevision().matches(token)) {
+          if (ObjectIds.matchesAbbreviation(ps.commitId(), token)) {
             matches.add(ps);
           }
         }
@@ -101,7 +101,7 @@
       } catch (IllegalArgumentException e) {
         throw error("\"" + token + "\" is not a valid patch set", e);
       }
-      ChangeNotes notes = getNotes(projectState, patchSetId.getParentKey());
+      ChangeNotes notes = getNotes(projectState, patchSetId.changeId());
       PatchSet patchSet = psUtil.get(notes, patchSetId);
       if (patchSet == null) {
         throw error("\"" + token + "\" no such patch set");
@@ -147,7 +147,7 @@
       // No --branch option, so they want every branch.
       return true;
     }
-    return change.getDest().get().equals(branch);
+    return change.getDest().branch().equals(branch);
   }
 
   public static UnloggedFailure error(String msg) {
diff --git a/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index 7216ed9..504b239 100644
--- a/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -42,7 +42,7 @@
   @Override
   public void run() throws Exception {
     enableGracefulStop();
-    Map<String, PluginInfo> output = list.apply(TopLevelResource.INSTANCE);
+    Map<String, PluginInfo> output = list.apply(TopLevelResource.INSTANCE).value();
 
     if (format.isJson()) {
       format
diff --git a/java/com/google/gerrit/sshd/commands/Receive.java b/java/com/google/gerrit/sshd/commands/Receive.java
index 0b089b6..92666f3 100644
--- a/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/java/com/google/gerrit/sshd/commands/Receive.java
@@ -18,10 +18,9 @@
 import com.google.common.collect.SetMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -31,12 +30,8 @@
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
 import org.eclipse.jgit.errors.TooLargeObjectInPackException;
 import org.eclipse.jgit.errors.UnpackException;
-import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.kohsuke.args4j.Option;
@@ -118,52 +113,17 @@
         logger.atInfo().log(msg.toString());
         throw new UnloggedFailure(128, "error: " + badStream.getCause().getMessage());
       }
-
-      // This may have been triggered by branch level access controls.
-      // Log what the heck is going on, as detailed as we can.
-      //
       StringBuilder msg = new StringBuilder();
       msg.append("Unpack error on project \"").append(projectState.getName()).append("\":\n");
 
       msg.append("  AdvertiseRefsHook: ").append(rp.getAdvertiseRefsHook());
       if (rp.getAdvertiseRefsHook() == AdvertiseRefsHook.DEFAULT) {
         msg.append("DEFAULT");
-      } else if (rp.getAdvertiseRefsHook() instanceof DefaultAdvertiseRefsHook) {
-        msg.append("DefaultAdvertiseRefsHook");
       } else {
         msg.append(rp.getAdvertiseRefsHook().getClass());
       }
       msg.append("\n");
 
-      if (rp.getAdvertiseRefsHook() instanceof DefaultAdvertiseRefsHook) {
-        Map<String, Ref> adv = rp.getAdvertisedRefs();
-        msg.append("  Visible references (").append(adv.size()).append("):\n");
-        for (Ref ref : adv.values()) {
-          msg.append("  - ")
-              .append(ref.getObjectId().abbreviate(8).name())
-              .append(" ")
-              .append(ref.getName())
-              .append("\n");
-        }
-
-        List<Ref> allRefs = rp.getRepository().getRefDatabase().getRefs();
-        List<Ref> hidden = new ArrayList<>();
-        for (Ref ref : allRefs) {
-          if (!adv.containsKey(ref.getName())) {
-            hidden.add(ref);
-          }
-        }
-
-        msg.append("  Hidden references (").append(hidden.size()).append("):\n");
-        for (Ref ref : hidden) {
-          msg.append("  - ")
-              .append(ref.getObjectId().abbreviate(8).name())
-              .append(" ")
-              .append(ref.getName())
-              .append("\n");
-        }
-      }
-
       IOException detail = new IOException(msg.toString(), badStream);
       throw new Failure(128, "fatal: Unpack error, check server log", detail);
     }
diff --git a/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 4202aac..61e529f 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -19,10 +19,12 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 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.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
@@ -34,7 +36,6 @@
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -228,11 +229,11 @@
         writeError("error", e.getMessage() + "\n");
       } catch (NoSuchChangeException e) {
         ok = false;
-        writeError("error", "no such change " + patchSet.getId().getParentKey().get());
+        writeError("error", "no such change " + patchSet.id().changeId().get());
       } catch (Exception e) {
         ok = false;
-        writeError("fatal", "internal server error while reviewing " + patchSet.getId() + "\n");
-        logger.atSevere().withCause(e).log("internal error while reviewing %s", patchSet.getId());
+        writeError("fatal", "internal server error while reviewing " + patchSet.id() + "\n");
+        logger.atSevere().withCause(e).log("internal error while reviewing %s", patchSet.id());
       }
     }
 
@@ -243,8 +244,8 @@
 
   private void applyReview(PatchSet patchSet, ReviewInput review) throws RestApiException {
     gApi.changes()
-        .id(patchSet.getId().getParentKey().get())
-        .revision(patchSet.getRevision().get())
+        .id(patchSet.id().changeId().get())
+        .revision(patchSet.commitId().name())
         .review(review);
   }
 
@@ -311,11 +312,11 @@
   }
 
   private ChangeApi changeApi(PatchSet patchSet) throws RestApiException {
-    return gApi.changes().id(patchSet.getId().getParentKey().get());
+    return gApi.changes().id(patchSet.id().changeId().get());
   }
 
   private RevisionApi revisionApi(PatchSet patchSet) throws RestApiException {
-    return changeApi(patchSet).revision(patchSet.getRevision().get());
+    return changeApi(patchSet).revision(patchSet.commitId().name());
   }
 
   @Override
@@ -350,15 +351,15 @@
   private static Option newApproveOption(LabelType type, String usage) {
     return OptionUtil.newOption(
         asOptionName(type),
-        new String[0],
+        ImmutableList.of(),
         usage,
         "N",
         false,
         false,
         false,
         LabelHandler.class,
-        new String[0],
-        new String[0]);
+        ImmutableList.of(),
+        ImmutableList.of());
   }
 
   private static class LabelSetter implements Setter<Short> {
diff --git a/java/com/google/gerrit/sshd/commands/ScpCommand.java b/java/com/google/gerrit/sshd/commands/ScpCommand.java
index 5122b35..6912795 100644
--- a/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -34,6 +34,7 @@
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.channel.ChannelSession;
 
 final class ScpCommand extends BaseCommand {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -81,7 +82,7 @@
   }
 
   @Override
-  public void start(Environment env) {
+  public void start(ChannelSession channel, Environment env) {
     startThread(this::runImp, AccessPath.SSH_COMMAND);
   }
 
diff --git a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 1ecb591..43a1670 100644
--- a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.accounts.SshKeyInput;
@@ -31,7 +32,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
@@ -212,8 +212,7 @@
     }
   }
 
-  private void setAccount()
-      throws IOException, UnloggedFailure, ConfigInvalidException, PermissionBackendException {
+  private void setAccount() throws Failure {
     user = genericUserFactory.create(id);
     rsrc = new AccountResource(user.asIdentifiedUser());
     try {
@@ -268,6 +267,8 @@
       }
     } catch (RestApiException e) {
       throw die(e.getMessage());
+    } catch (Exception e) {
+      throw new Failure(1, "unavailable", e);
     }
   }
 
@@ -280,10 +281,8 @@
     }
   }
 
-  private void deleteSshKeys(List<String> sshKeys)
-      throws RestApiException, RepositoryNotFoundException, IOException, ConfigInvalidException,
-          PermissionBackendException {
-    List<SshKeyInfo> infos = getSshKeys.apply(rsrc);
+  private void deleteSshKeys(List<String> sshKeys) throws Exception {
+    List<SshKeyInfo> infos = getSshKeys.apply(rsrc).value();
     if (sshKeys.contains("ALL")) {
       for (SshKeyInfo i : infos) {
         deleteSshKey(i);
@@ -319,10 +318,9 @@
     }
   }
 
-  private void deleteEmail(String email)
-      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
+  private void deleteEmail(String email) throws Exception {
     if (email.equals("ALL")) {
-      List<EmailInfo> emails = getEmails.apply(rsrc);
+      List<EmailInfo> emails = getEmails.apply(rsrc).value();
       for (EmailInfo e : emails) {
         deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), e.email), new Input());
       }
@@ -331,9 +329,8 @@
     }
   }
 
-  private void putPreferred(String email)
-      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
-    for (EmailInfo e : getEmails.apply(rsrc)) {
+  private void putPreferred(String email) throws Exception {
+    for (EmailInfo e : getEmails.apply(rsrc).value()) {
       if (e.email.equals(email)) {
         putPreferred.apply(new AccountResource.Email(user.asIdentifiedUser(), email), null);
         return;
diff --git a/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
index 5557b16..db8e42a 100644
--- a/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -19,11 +19,11 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.Streams;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.GroupCache;
@@ -141,7 +141,7 @@
                     return "n/a";
                   }
                   return MoreObjects.firstNonNull(
-                      accountState.get().getAccount().getPreferredEmail(), "n/a");
+                      accountState.get().account().preferredEmail(), "n/a");
                 })
             .collect(joining(", "));
     out.write(
diff --git a/java/com/google/gerrit/sshd/commands/SetParentCommand.java b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
index 87722d9..bfdbff9 100644
--- a/java/com/google/gerrit/sshd/commands/SetParentCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
@@ -16,16 +16,14 @@
 
 import static java.util.stream.Collectors.toList;
 
-import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.ParentInput;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectResource;
@@ -113,7 +111,7 @@
         childProjects.addAll(getChildrenForReparenting(oldParent));
       } catch (PermissionBackendException e) {
         throw new Failure(1, "permissions unavailable", e);
-      } catch (StorageException | RestApiException e) {
+      } catch (Exception e) {
         throw new Failure(1, "failure in request", e);
       }
     }
@@ -149,8 +147,7 @@
    * list of child projects does not contain projects that were specified to be excluded from
    * reparenting.
    */
-  private List<Project.NameKey> getChildrenForReparenting(ProjectState parent)
-      throws PermissionBackendException, RestApiException {
+  private List<Project.NameKey> getChildrenForReparenting(ProjectState parent) throws Exception {
     final List<Project.NameKey> childProjects = new ArrayList<>();
     final List<Project.NameKey> excluded = new ArrayList<>(excludedChildren.size());
     for (ProjectState excludedChild : excludedChildren) {
@@ -160,8 +157,8 @@
     if (newParentKey != null) {
       automaticallyExcluded.addAll(getAllParents(newParentKey));
     }
-    for (ProjectInfo child : listChildProjects.apply(new ProjectResource(parent, user))) {
-      final Project.NameKey childName = new Project.NameKey(child.name);
+    for (ProjectInfo child : listChildProjects.apply(new ProjectResource(parent, user)).value()) {
+      final Project.NameKey childName = Project.nameKey(child.name);
       if (!excluded.contains(childName)) {
         if (!automaticallyExcluded.contains(childName)) {
           childProjects.add(childName);
diff --git a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 5a04026..0e6fa65 100644
--- a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -142,7 +142,7 @@
       input.confirmed = true;
       String error;
       try {
-        error = postReviewers.apply(changeRsrc, input).error;
+        error = postReviewers.apply(changeRsrc, input).value().error;
       } catch (Exception e) {
         error = String.format("could not add %s: %s", reviewer, e.getMessage());
       }
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 039b74e..63a0dda 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -51,6 +51,7 @@
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.io.mina.MinaSession;
 import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.kohsuke.args4j.Option;
 
 /** Show the current cache states. */
@@ -97,7 +98,7 @@
   private int nw;
 
   @Override
-  public void start(Environment env) throws IOException {
+  public void start(ChannelSession channel, Environment env) throws IOException {
     String s = env.getEnv().get(Environment.ENV_COLUMNS);
     if (s != null && !s.isEmpty()) {
       try {
@@ -106,11 +107,11 @@
         columns = 80;
       }
     }
-    super.start(env);
+    super.start(channel, env);
   }
 
   @Override
-  protected void run() throws UnloggedFailure {
+  protected void run() throws Failure {
     enableGracefulStop();
     nw = columns - 50;
     Date now = new Date();
@@ -162,39 +163,45 @@
     }
     stdout.print("+---------------------+---------+---------+\n");
 
-    Collection<CacheInfo> caches = getCaches();
-    printMemoryCoreCaches(caches);
-    printMemoryPluginCaches(caches);
-    printDiskCaches(caches);
-    stdout.print('\n');
-
-    boolean showJvm;
     try {
-      permissionBackend.user(self).check(GlobalPermission.MAINTAIN_SERVER);
-      showJvm = true;
-    } catch (AuthException | PermissionBackendException e) {
-      // Silently ignore and do not display detailed JVM information.
-      showJvm = false;
-    }
-    if (showJvm) {
-      sshSummary();
+      Collection<CacheInfo> caches = getCaches();
+      printMemoryCoreCaches(caches);
+      printMemoryPluginCaches(caches);
+      printDiskCaches(caches);
+      stdout.print('\n');
 
-      SummaryInfo summary = getSummary.setGc(gc).setJvm(showJVM).apply(new ConfigResource());
-      taskSummary(summary.taskSummary);
-      memSummary(summary.memSummary);
-      threadSummary(summary.threadSummary);
-
-      if (showJVM && summary.jvmSummary != null) {
-        jvmSummary(summary.jvmSummary);
+      boolean showJvm;
+      try {
+        permissionBackend.user(self).check(GlobalPermission.MAINTAIN_SERVER);
+        showJvm = true;
+      } catch (AuthException | PermissionBackendException e) {
+        // Silently ignore and do not display detailed JVM information.
+        showJvm = false;
       }
+      if (showJvm) {
+        sshSummary();
+
+        SummaryInfo summary =
+            getSummary.setGc(gc).setJvm(showJVM).apply(new ConfigResource()).value();
+        taskSummary(summary.taskSummary);
+        memSummary(summary.memSummary);
+        threadSummary(summary.threadSummary);
+
+        if (showJVM && summary.jvmSummary != null) {
+          jvmSummary(summary.jvmSummary);
+        }
+      }
+    } catch (Exception e) {
+      throw new Failure(1, "unavailable", e);
     }
 
     stdout.flush();
   }
 
-  private Collection<CacheInfo> getCaches() {
+  private Collection<CacheInfo> getCaches() throws Exception {
     @SuppressWarnings("unchecked")
-    Map<String, CacheInfo> caches = (Map<String, CacheInfo>) listCaches.apply(new ConfigResource());
+    Map<String, CacheInfo> caches =
+        (Map<String, CacheInfo>) listCaches.apply(new ConfigResource()).value();
     for (Map.Entry<String, CacheInfo> entry : caches.entrySet()) {
       CacheInfo cache = entry.getValue();
       cache.name = entry.getKey();
diff --git a/java/com/google/gerrit/sshd/commands/ShowConnections.java b/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 52e824b..d271364 100644
--- a/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -44,6 +44,7 @@
 import org.apache.sshd.common.io.nio2.Nio2Acceptor;
 import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.kohsuke.args4j.Option;
 
 /** Show the current SSH connections. */
@@ -71,7 +72,7 @@
   private int columns = 80;
 
   @Override
-  public void start(Environment env) throws IOException {
+  public void start(ChannelSession channel, Environment env) throws IOException {
     String s = env.getEnv().get(Environment.ENV_COLUMNS);
     if (s != null && !s.isEmpty()) {
       try {
@@ -80,7 +81,7 @@
         columns = 80;
       }
     }
-    super.start(env);
+    super.start(channel, env);
   }
 
   @Override
@@ -149,7 +150,7 @@
     }
 
     stdout.print("--\n");
-    stdout.print("SSHD Backend: " + getBackend() + "\n");
+    stdout.print(String.format(" %d connections; SSHD Backend: %s\n", list.size(), getBackend()));
   }
 
   private String getBackend() {
diff --git a/java/com/google/gerrit/sshd/commands/ShowQueue.java b/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 04d06ee..779f2df 100644
--- a/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -40,6 +40,7 @@
 import java.util.List;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.kohsuke.args4j.Option;
 
 /** Display the current work queue. */
@@ -70,7 +71,7 @@
   private int maxCommandWidth;
 
   @Override
-  public void start(Environment env) throws IOException {
+  public void start(ChannelSession channel, Environment env) throws IOException {
     String s = env.getEnv().get(Environment.ENV_COLUMNS);
     if (s != null && !s.isEmpty()) {
       try {
@@ -79,7 +80,7 @@
         columns = 80;
       }
     }
-    super.start(env);
+    super.start(channel, env);
   }
 
   @Override
@@ -95,11 +96,13 @@
 
     List<TaskInfo> tasks;
     try {
-      tasks = listTasks.apply(new ConfigResource());
+      tasks = listTasks.apply(new ConfigResource()).value();
     } catch (AuthException e) {
       throw die(e);
     } catch (PermissionBackendException e) {
       throw new Failure(1, "permission backend unavailable", e);
+    } catch (Exception e) {
+      throw new Failure(1, "unavailable", e);
     }
 
     boolean viewAll = permissionBackend.user(currentUser).testOrFalse(GlobalPermission.VIEW_QUEUE);
diff --git a/java/com/google/gerrit/sshd/commands/StreamEvents.java b/java/com/google/gerrit/sshd/commands/StreamEvents.java
index ffd98d5..45540a0 100644
--- a/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -16,26 +16,22 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.base.Supplier;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventGson;
 import com.google.gerrit.server.events.EventTypes;
-import com.google.gerrit.server.events.ProjectNameKeySerializer;
-import com.google.gerrit.server.events.SupplierSerializer;
 import com.google.gerrit.server.events.UserScopedEventListener;
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.StreamCommandExecutor;
 import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -45,11 +41,12 @@
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.channel.ChannelSession;
 import org.kohsuke.args4j.Option;
 
 @RequiresCapability(GlobalCapability.STREAM_EVENTS)
 @CommandMetaData(name = "stream-events", description = "Monitor events occurring in real time")
-final class StreamEvents extends BaseCommand {
+public final class StreamEvents extends BaseCommand {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /** Maximum number of events that may be queued up for each connection. */
@@ -71,11 +68,11 @@
 
   @Inject @StreamCommandExecutor private ScheduledThreadPoolExecutor pool;
 
+  @Inject @EventGson private Gson gson;
+
   /** Queue of events to stream to the connected user. */
   private final LinkedBlockingQueue<Event> queue = new LinkedBlockingQueue<>(MAX_EVENTS);
 
-  private Gson gson;
-
   private RegistrationHandle eventListenerRegistration;
 
   /** Special event to notify clients they missed other events. */
@@ -91,29 +88,6 @@
     EventTypes.register(DroppedOutputEvent.TYPE, DroppedOutputEvent.class);
   }
 
-  private final CancelableRunnable writer =
-      new CancelableRunnable() {
-        @Override
-        public void run() {
-          writeEvents();
-        }
-
-        @Override
-        public void cancel() {
-          onExit(0);
-        }
-
-        @Override
-        public String toString() {
-          StringBuilder b = new StringBuilder();
-          b.append("Stream Events");
-          if (currentUser.getUserName().isPresent()) {
-            b.append(" (").append(currentUser.getUserName().get()).append(")");
-          }
-          return b.toString();
-        }
-      };
-
   /** True if {@link DroppedOutputEvent} needs to be sent. */
   private volatile boolean dropped;
 
@@ -131,10 +105,8 @@
    */
   private Future<?> task;
 
-  private PrintWriter stdout;
-
   @Override
-  public void start(Environment env) throws IOException {
+  public void start(ChannelSession channel, Environment env) throws IOException {
     try {
       parseCommandLine();
     } catch (UnloggedFailure e) {
@@ -148,7 +120,30 @@
       return;
     }
 
-    stdout = toPrintWriter(out);
+    PrintWriter stdout = toPrintWriter(out);
+    CancelableRunnable writer =
+        new CancelableRunnable() {
+          @Override
+          public void run() {
+            writeEvents(this, stdout);
+          }
+
+          @Override
+          public void cancel() {
+            onExit(0);
+          }
+
+          @Override
+          public String toString() {
+            StringBuilder b = new StringBuilder();
+            b.append("Stream Events");
+            if (currentUser.getUserName().isPresent()) {
+              b.append(" (").append(currentUser.getUserName().get()).append(")");
+            }
+            return b.toString();
+          }
+        };
+
     eventListenerRegistration =
         eventListeners.add(
             "gerrit",
@@ -156,7 +151,7 @@
               @Override
               public void onEvent(Event event) {
                 if (subscribedToEvents.isEmpty() || subscribedToEvents.contains(event.getType())) {
-                  offer(event);
+                  offer(writer, event);
                 }
               }
 
@@ -165,12 +160,6 @@
                 return currentUser;
               }
             });
-
-    gson =
-        new GsonBuilder()
-            .registerTypeAdapter(Supplier.class, new SupplierSerializer())
-            .registerTypeAdapter(Project.NameKey.class, new ProjectNameKeySerializer())
-            .create();
   }
 
   private void removeEventListenerRegistration() {
@@ -191,7 +180,7 @@
   }
 
   @Override
-  public void destroy() {
+  public void destroy(ChannelSession channel) {
     removeEventListenerRegistration();
 
     final boolean exit;
@@ -209,7 +198,7 @@
     }
   }
 
-  private void offer(Event event) {
+  private void offer(CancelableRunnable writer, Event event) {
     synchronized (taskLock) {
       if (!queue.offer(event)) {
         dropped = true;
@@ -231,7 +220,7 @@
     }
   }
 
-  private void writeEvents() {
+  private void writeEvents(CancelableRunnable writer, PrintWriter stdout) {
     int processed = 0;
 
     while (processed < BATCH_SIZE) {
@@ -241,13 +230,13 @@
         // accepting output. Either way terminate this instance.
         //
         removeEventListenerRegistration();
-        flush();
+        flush(stdout);
         onExit(0);
         return;
       }
 
       if (dropped) {
-        write(new DroppedOutputEvent());
+        write(stdout, new DroppedOutputEvent());
         dropped = false;
       }
 
@@ -256,11 +245,11 @@
         break;
       }
 
-      write(event);
+      write(stdout, event);
       processed++;
     }
 
-    flush();
+    flush(stdout);
 
     if (BATCH_SIZE <= processed) {
       // We processed the limit, but more might remain in the queue.
@@ -273,7 +262,7 @@
     }
   }
 
-  private void write(Object message) {
+  private void write(PrintWriter stdout, Object message) {
     String msg = null;
     try {
       msg = gson.toJson(message) + "\n";
@@ -287,7 +276,7 @@
     }
   }
 
-  private void flush() {
+  private void flush(PrintWriter stdout) {
     synchronized (stdout) {
       stdout.flush();
     }
diff --git a/java/com/google/gerrit/sshd/commands/Upload.java b/java/com/google/gerrit/sshd/commands/Upload.java
index e195098..b80b879 100644
--- a/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/java/com/google/gerrit/sshd/commands/Upload.java
@@ -14,22 +14,29 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.RequestListener;
+import com.google.gerrit.server.git.PermissionAwareRepositoryManager;
+import com.google.gerrit.server.git.TracingHook;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.UploadPackInitializer;
+import com.google.gerrit.server.git.UsersSelfAdvertiseRefsHook;
 import com.google.gerrit.server.git.validators.UploadValidationException;
 import com.google.gerrit.server.git.validators.UploadValidators;
+import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.sshd.AbstractGitCommand;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.List;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.storage.pack.PackStatistics;
 import org.eclipse.jgit.transport.PostUploadHook;
 import org.eclipse.jgit.transport.PostUploadHookChain;
@@ -43,8 +50,10 @@
   @Inject private DynamicSet<PreUploadHook> preUploadHooks;
   @Inject private DynamicSet<PostUploadHook> postUploadHooks;
   @Inject private DynamicSet<UploadPackInitializer> uploadPackInitializers;
+  @Inject private PluginSetContext<RequestListener> requestListeners;
   @Inject private UploadValidators.Factory uploadValidatorsFactory;
   @Inject private PermissionBackend permissionBackend;
+  @Inject private UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook;
 
   private PackStatistics stats;
 
@@ -53,7 +62,6 @@
     PermissionBackend.ForProject perm =
         permissionBackend.user(user).project(projectState.getNameKey());
     try {
-
       perm.check(ProjectPermission.RUN_UPLOAD_PACK);
     } catch (AuthException e) {
       throw new Failure(1, "fatal: upload-pack not permitted on this server", e);
@@ -61,20 +69,35 @@
       throw new Failure(1, "fatal: unable to check permissions ", e);
     }
 
-    final UploadPack up = new UploadPack(repo);
-    up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
+    Repository permissionAwareRepo = PermissionAwareRepositoryManager.wrap(repo, perm);
+    UploadPack up = new UploadPack(permissionAwareRepo);
+
     up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
     up.setPostUploadHook(PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
+    if (projectState.isAllUsers()) {
+      up.setAdvertiseRefsHook(usersSelfAdvertiseRefsHook);
+    }
+    if (extraParameters != null) {
+      up.setExtraParameters(ImmutableList.copyOf(extraParameters));
+    }
 
     List<PreUploadHook> allPreUploadHooks = Lists.newArrayList(preUploadHooks);
     allPreUploadHooks.add(
-        uploadValidatorsFactory.create(project, repo, session.getRemoteAddressAsString()));
+        uploadValidatorsFactory.create(
+            project, permissionAwareRepo, session.getRemoteAddressAsString()));
     up.setPreUploadHook(PreUploadHookChain.newChain(allPreUploadHooks));
     for (UploadPackInitializer initializer : uploadPackInitializers) {
       initializer.init(projectState.getNameKey(), up);
     }
-    try {
+    try (TraceContext traceContext = TraceContext.open();
+        TracingHook tracingHook = new TracingHook()) {
+      RequestInfo requestInfo =
+          RequestInfo.builder(RequestInfo.RequestType.GIT_UPLOAD, user, traceContext)
+              .project(projectState.getNameKey())
+              .build();
+      requestListeners.runEach(l -> l.onRequest(requestInfo));
+      up.setProtocolV2Hook(tracingHook);
       up.upload(in, out, err);
       session.setPeerAgent(up.getPeerUserAgent());
       stats = up.getStatistics();
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 24f82a7..c25a1a8 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -139,7 +139,7 @@
     PacketLineIn packetIn = new PacketLineIn(in);
     for (; ; ) {
       String s = packetIn.readString();
-      if (s == PacketLineIn.END) {
+      if (PacketLineIn.isEnd(s)) {
         break;
       }
       if (!s.startsWith(argCmd)) {
diff --git a/java/com/google/gerrit/testing/AssertableExecutorService.java b/java/com/google/gerrit/testing/AssertableExecutorService.java
new file mode 100644
index 0000000..18ac2e9
--- /dev/null
+++ b/java/com/google/gerrit/testing/AssertableExecutorService.java
@@ -0,0 +1,65 @@
+// 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.testing;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.common.util.concurrent.ForwardingExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Forwards all calls to a direct executor making it so that the submitted {@link Runnable}s run
+ * synchronously. Holds a count of the number of tasks that were executed.
+ */
+public class AssertableExecutorService extends ForwardingExecutorService {
+
+  private final ExecutorService delegate = MoreExecutors.newDirectExecutorService();
+  private final AtomicInteger numInteractions = new AtomicInteger();
+
+  @Override
+  protected ExecutorService delegate() {
+    return delegate;
+  }
+
+  @Override
+  public <T> Future<T> submit(Callable<T> task) {
+    numInteractions.incrementAndGet();
+    return super.submit(task);
+  }
+
+  @Override
+  public Future<?> submit(Runnable task) {
+    numInteractions.incrementAndGet();
+    return super.submit(task);
+  }
+
+  @Override
+  public <T> Future<T> submit(Runnable task, T result) {
+    numInteractions.incrementAndGet();
+    return super.submit(task, result);
+  }
+
+  /** Asserts and resets the number of executions this executor observed. */
+  public void assertInteractions(int expectedNumInteractions) {
+    assertWithMessage("expectedRunnablesSubmittedOnExecutor")
+        .that(numInteractions.get())
+        .isEqualTo(expectedNumInteractions);
+    numInteractions.set(0);
+  }
+}
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index dc1c150..c610d07 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -3,19 +3,19 @@
 java_library(
     name = "gerrit-test-util",
     testonly = True,
-    srcs = glob(["**/*.java"]),
+    srcs = glob(
+        ["**/*.java"],
+        exclude = ["AssertableExecutorService.java"],
+    ),
     visibility = ["//visibility:public"],
     exports = [
-        "//lib/easymock",
-        "//lib/powermock:powermock-api-easymock",
-        "//lib/powermock:powermock-api-support",
-        "//lib/powermock:powermock-core",
-        "//lib/powermock:powermock-module-junit4",
-        "//lib/powermock:powermock-module-junit4-common",
+        "//lib:junit",
+        "//lib/mockito",
     ],
     deps = [
+        "//java/com/google/gerrit/acceptance/testsuite/project",
         "//java/com/google/gerrit/common:annotations",
-        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/gpg",
@@ -24,8 +24,6 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
-        "//java/com/google/gerrit/pgm/init",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server:module",
         "//java/com/google/gerrit/server/api",
@@ -38,16 +36,28 @@
         "//java/com/google/gerrit/server/util/time",
         "//lib:guava",
         "//lib:h2",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib:junit",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/log:impl-log4j",
         "//lib/log:log4j",
         "//lib/truth",
     ],
 )
+
+java_library(
+    # This can't be part of gerrit-test-util because of https://github.com/google/guava/issues/2837
+    name = "assertable-executor",
+    testonly = True,
+    srcs = ["AssertableExecutorService.java"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//lib:guava",
+        "//lib/truth",
+    ],
+)
diff --git a/java/com/google/gerrit/testing/ConfigSuite.java b/java/com/google/gerrit/testing/ConfigSuite.java
index 0fcca24..d7ae397 100644
--- a/java/com/google/gerrit/testing/ConfigSuite.java
+++ b/java/com/google/gerrit/testing/ConfigSuite.java
@@ -57,6 +57,7 @@
  *   public static Config firstConfig() {
  *     Config cfg = new Config();
  *     cfg.setString("gerrit", null, "testValue", "a");
+ *     return cfg;
  *   }
  * }
  *
@@ -65,6 +66,7 @@
  *   public static Config secondConfig() {
  *     Config cfg = new Config();
  *     cfg.setString("gerrit", null, "testValue", "b");
+ *     return cfg;
  *   }
  *
  *   {@literal @}Test
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index b99a32d..0178c72 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -17,11 +17,9 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.util.time.TimeUtil;
 import java.util.HashMap;
 import java.util.Map;
@@ -42,7 +40,10 @@
     if (state != null) {
       return state;
     }
-    return newState(new Account(accountId, TimeUtil.nowTs()));
+    return newState(
+        Account.builder(accountId, TimeUtil.nowTs())
+            .setMetaId("1234567812345678123456781234567812345678")
+            .build());
   }
 
   @Override
@@ -74,10 +75,10 @@
 
   public synchronized void put(Account account) {
     AccountState state = newState(account);
-    byId.put(account.getId(), state);
+    byId.put(account.id(), state);
   }
 
   private static AccountState newState(Account account) {
-    return AccountState.forAccount(new AllUsersName(AllUsersNameProvider.DEFAULT), account);
+    return AccountState.forAccount(account);
   }
 }
diff --git a/java/com/google/gerrit/testing/GerritServerTests.java b/java/com/google/gerrit/testing/GerritServerTests.java
index 9a922d6..ad985b6 100644
--- a/java/com/google/gerrit/testing/GerritServerTests.java
+++ b/java/com/google/gerrit/testing/GerritServerTests.java
@@ -21,7 +21,7 @@
 import org.junit.runners.model.Statement;
 
 @RunWith(ConfigSuite.class)
-public class GerritServerTests extends GerritBaseTests {
+public class GerritServerTests {
   @ConfigSuite.Parameter public Config config;
 
   @ConfigSuite.Name private String configName;
diff --git a/java/com/google/gerrit/testing/GerritBaseTests.java b/java/com/google/gerrit/testing/GerritTestName.java
similarity index 68%
rename from java/com/google/gerrit/testing/GerritBaseTests.java
rename to java/com/google/gerrit/testing/GerritTestName.java
index 500d791..d287837 100644
--- a/java/com/google/gerrit/testing/GerritBaseTests.java
+++ b/java/com/google/gerrit/testing/GerritTestName.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// 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.
@@ -16,23 +16,21 @@
 
 import com.google.common.base.CharMatcher;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.rules.ExpectedException;
 import org.junit.rules.TestName;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
 
-@Ignore
-public abstract class GerritBaseTests {
-  @Rule public ExpectedException exception = ExpectedException.none();
-  @Rule public final TestName testName = new TestName();
+public class GerritTestName implements TestRule {
+  private final TestName delegate = new TestName();
 
   @BeforeClass
   public static void beforeClassTest() {
     TestLoggingActivator.configureLogging();
   }
 
-  protected String getSanitizedMethodName() {
-    String name = testName.getMethodName().toLowerCase();
+  public String getSanitizedMethodName() {
+    String name = delegate.getMethodName().toLowerCase();
     name =
         CharMatcher.inRange('a', 'z')
             .or(CharMatcher.inRange('A', 'Z'))
@@ -42,4 +40,9 @@
     name = CharMatcher.is('_').trimTrailingFrom(name);
     return name;
   }
+
+  @Override
+  public Statement apply(Statement base, Description description) {
+    return delegate.apply(base, description);
+  }
 }
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 83b9e2b..abb5955 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -19,10 +19,13 @@
 
 import com.google.common.base.Strings;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.gpg.GpgModule;
+import com.google.gerrit.index.IndexType;
 import com.google.gerrit.index.SchemaDefinitions;
 import com.google.gerrit.index.project.ProjectSchemaDefinitions;
 import com.google.gerrit.metrics.DisabledMetricMaker;
@@ -61,7 +64,6 @@
 import com.google.gerrit.server.git.PerThreadRequestScope;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
 import com.google.gerrit.server.index.account.AllAccountsIndexer;
 import com.google.gerrit.server.index.change.AllChangesIndexer;
@@ -82,6 +84,7 @@
 import com.google.gerrit.server.securestore.SecureStore;
 import com.google.gerrit.server.ssh.NoSshKeyCache;
 import com.google.gerrit.server.submit.LocalMergeSuperSetComputation;
+import com.google.gerrit.server.util.ReplicaUtil;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -172,8 +175,8 @@
 
     bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
 
-    // TODO(dborowitz): Use Jimfs. The biggest blocker is that JGit does not support Path-based
-    // Configs, only FileBasedConfig.
+    // It would be nice to use Jimfs for the SitePath, but the biggest blocker is that JGit does not
+    // support Path-based Configs, only FileBasedConfig.
     bind(Path.class).annotatedWith(SitePath.class).toInstance(Paths.get("."));
     bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
     bind(GerritOptions.class).toInstance(new GerritOptions(false, false, false));
@@ -218,28 +221,19 @@
     bind(AllChangesIndexer.class).toProvider(Providers.of(null));
     bind(AllGroupsIndexer.class).toProvider(Providers.of(null));
 
-    IndexType indexType = null;
-    try {
-      indexType = cfg.getEnum("index", null, "type", IndexType.LUCENE);
-    } catch (IllegalArgumentException e) {
-      // Custom index type, caller must provide their own module.
-    }
-    if (indexType != null) {
-      switch (indexType) {
-        case LUCENE:
-          install(luceneIndexModule());
-          break;
-        case ELASTICSEARCH:
-          install(elasticIndexModule());
-          break;
-        default:
-          throw new ProvisionException("index type unsupported in tests: " + indexType);
-      }
+    IndexType indexType = new IndexType(cfg.getString("index", null, "type"));
+    // For custom index types, callers must provide their own module.
+    if (indexType.isLucene()) {
+      install(luceneIndexModule());
+    } else if (indexType.isElasticsearch()) {
+      install(elasticIndexModule());
     }
     bind(ServerInformationImpl.class);
     bind(ServerInformation.class).to(ServerInformationImpl.class);
     install(new RestApiModule());
     install(new DefaultProjectNameLockManager.Module());
+
+    bind(ProjectOperations.class).to(ProjectOperationsImpl.class);
   }
 
   /** Copy of SchemaModule with a slightly different server ID provider. */
@@ -301,11 +295,10 @@
 
   private Module indexModule(String moduleClassName) {
     try {
-      boolean slave = cfg.getBoolean("container", "slave", false);
       Class<?> clazz = Class.forName(moduleClassName);
       Method m =
           clazz.getMethod("singleVersionWithExplicitVersions", Map.class, int.class, boolean.class);
-      return (Module) m.invoke(null, getSingleSchemaVersions(), 0, slave);
+      return (Module) m.invoke(null, getSingleSchemaVersions(), 0, ReplicaUtil.isReplica(cfg));
     } catch (ClassNotFoundException
         | SecurityException
         | NoSuchMethodException
diff --git a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
index e44d8d38..09ae115 100644
--- a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
+++ b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
@@ -16,7 +16,7 @@
 
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.RepositoryCaseMismatchException;
 import com.google.inject.Inject;
@@ -103,7 +103,7 @@
   public synchronized SortedSet<Project.NameKey> list() {
     SortedSet<Project.NameKey> names = Sets.newTreeSet();
     for (DfsRepository repo : repos.values()) {
-      names.add(new Project.NameKey(repo.getDescription().getRepositoryName()));
+      names.add(Project.nameKey(repo.getDescription().getRepositoryName()));
     }
     return ImmutableSortedSet.copyOf(names);
   }
diff --git a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
index 05672aa..44d5cea 100644
--- a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
+++ b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.testing;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
diff --git a/java/com/google/gerrit/testing/IndexConfig.java b/java/com/google/gerrit/testing/IndexConfig.java
index 9cace88..21c49dd 100644
--- a/java/com/google/gerrit/testing/IndexConfig.java
+++ b/java/com/google/gerrit/testing/IndexConfig.java
@@ -23,6 +23,7 @@
 
   public static Config createFromExistingConfig(Config cfg) {
     cfg.setInt("index", null, "maxPages", 10);
+    cfg.setBoolean("index", null, "reindexAfterRefUpdate", false);
     cfg.setString("trackingid", "query-bug", "footer", "Bug:");
     cfg.setString("trackingid", "query-bug", "match", "QUERY\\d{2,8}");
     cfg.setString("trackingid", "query-bug", "system", "querytests");
diff --git a/java/com/google/gerrit/testing/TestChanges.java b/java/com/google/gerrit/testing/TestChanges.java
index d0a939d..b795c5b 100644
--- a/java/com/google/gerrit/testing/TestChanges.java
+++ b/java/com/google/gerrit/testing/TestChanges.java
@@ -17,14 +17,13 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 
 import com.google.common.collect.Ordering;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetInfo;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.AbstractChangeNotes;
@@ -52,13 +51,13 @@
   }
 
   public static Change newChange(Project.NameKey project, Account.Id userId, int id) {
-    Change.Id changeId = new Change.Id(id);
+    Change.Id changeId = Change.id(id);
     Change c =
         new Change(
-            new Change.Key("Iabcd1234abcd1234abcd1234abcd1234abcd1234"),
+            Change.key("Iabcd1234abcd1234abcd1234abcd1234abcd1234"),
             changeId,
             userId,
-            new Branch.NameKey(project, "master"),
+            BranchNameKey.create(project, "master"),
             TimeUtil.nowTs());
     incrementPatchSet(c);
     return c;
@@ -69,11 +68,12 @@
   }
 
   public static PatchSet newPatchSet(PatchSet.Id id, String revision, Account.Id userId) {
-    PatchSet ps = new PatchSet(id);
-    ps.setRevision(new RevId(revision));
-    ps.setUploader(userId);
-    ps.setCreatedOn(TimeUtil.nowTs());
-    return ps;
+    return PatchSet.builder()
+        .id(id)
+        .commitId(ObjectId.fromString(revision))
+        .uploader(userId)
+        .createdOn(TimeUtil.nowTs())
+        .build();
   }
 
   public static ChangeUpdate newUpdate(
@@ -115,11 +115,11 @@
               .author(ident)
               .committer(ident)
               .message(firstNonNull(c.getSubject(), "Test change"));
-      Ref parent = repo.exactRef(c.getDest().get());
+      Ref parent = repo.exactRef(c.getDest().branch());
       if (parent != null) {
         cb.parent(tr.getRevWalk().parseCommit(parent.getObjectId()));
       }
-      update.setBranch(c.getDest().get());
+      update.setBranch(c.getDest().branch());
       update.setChangeId(c.getKey().get());
       update.setCommit(tr.getRevWalk(), cb.create());
       return update;
@@ -129,7 +129,7 @@
   public static void incrementPatchSet(Change change) {
     PatchSet.Id curr = change.currentPatchSetId();
     PatchSetInfo ps =
-        new PatchSetInfo(new PatchSet.Id(change.getId(), curr != null ? curr.get() + 1 : 1));
+        new PatchSetInfo(PatchSet.id(change.getId(), curr != null ? curr.get() + 1 : 1));
     ps.setSubject("Change subject");
     change.setCurrentPatchSet(ps);
   }
diff --git a/java/com/google/gerrit/testing/TestCommentHelper.java b/java/com/google/gerrit/testing/TestCommentHelper.java
new file mode 100644
index 0000000..b72cca7
--- /dev/null
+++ b/java/com/google/gerrit/testing/TestCommentHelper.java
@@ -0,0 +1,107 @@
+// 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.testing;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.Comment.Range;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.inject.Inject;
+import java.util.Collection;
+
+/** Test helper for dealing with comments/drafts. */
+public class TestCommentHelper {
+  private final GerritApi gApi;
+
+  @Inject
+  public TestCommentHelper(GerritApi gerritApi) {
+    gApi = gerritApi;
+  }
+
+  public DraftInput newDraft(String message) {
+    return populate(new DraftInput(), "file", message);
+  }
+
+  public DraftInput newDraft(String path, Side side, int line, String message) {
+    DraftInput d = new DraftInput();
+    return populate(d, path, side, line, message);
+  }
+
+  public void addDraft(String changeId, String revId, DraftInput in) throws Exception {
+    gApi.changes().id(changeId).revision(revId).createDraft(in).get();
+  }
+
+  public Collection<CommentInfo> getPublishedComments(String changeId) throws Exception {
+    return gApi.changes().id(changeId).comments().values().stream()
+        .flatMap(Collection::stream)
+        .collect(toList());
+  }
+
+  public static <C extends Comment> C populate(C c, String path, String message) {
+    return populate(c, path, createLineRange(), message);
+  }
+
+  private static <C extends Comment> C populate(C c, String path, Range range, String message) {
+    int line = range.startLine;
+    c.path = path;
+    c.side = Side.REVISION;
+    c.parent = null;
+    c.line = line != 0 ? line : null;
+    c.message = message;
+    c.unresolved = false;
+    if (line != 0) c.range = range;
+    return c;
+  }
+
+  private static <C extends Comment> C populate(
+      C c, String path, Side side, Range range, String message) {
+    int line = range.startLine;
+    c.path = path;
+    c.side = side;
+    c.parent = null;
+    c.line = line != 0 ? line : null;
+    c.message = message;
+    c.unresolved = false;
+    if (line != 0) c.range = range;
+    return c;
+  }
+
+  private static <C extends Comment> C populate(
+      C c, String path, Side side, int line, String message) {
+    return populate(c, path, side, createLineRange(line), message);
+  }
+
+  private static Range createLineRange() {
+    Range range = new Range();
+    range.startLine = 0;
+    range.startCharacter = 1;
+    range.endLine = 0;
+    range.endCharacter = 5;
+    return range;
+  }
+
+  private static Range createLineRange(int line) {
+    Range range = new Range();
+    range.startLine = line;
+    range.startCharacter = 1;
+    range.endLine = line;
+    range.endCharacter = 5;
+    return range;
+  }
+}
diff --git a/java/com/google/gerrit/truth/BUILD b/java/com/google/gerrit/truth/BUILD
index f21e3c9..b0dfef0 100644
--- a/java/com/google/gerrit/truth/BUILD
+++ b/java/com/google/gerrit/truth/BUILD
@@ -8,6 +8,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/truth/CacheStatsSubject.java b/java/com/google/gerrit/truth/CacheStatsSubject.java
index f1a9393..ff94334 100644
--- a/java/com/google/gerrit/truth/CacheStatsSubject.java
+++ b/java/com/google/gerrit/truth/CacheStatsSubject.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.common.UsedAt.Project;
 
 @UsedAt(Project.PLUGINS_ALL)
-public class CacheStatsSubject extends Subject<CacheStatsSubject, CacheStats> {
+public class CacheStatsSubject extends Subject {
   public static CacheStatsSubject assertThat(CacheStats stats) {
     return assertAbout(CacheStatsSubject::new).that(stats);
   }
@@ -39,10 +39,12 @@
         other.evictionCount());
   }
 
+  private final CacheStats stats;
   private CacheStats start = new CacheStats(0, 0, 0, 0, 0, 0);
 
   private CacheStatsSubject(FailureMetadata failureMetadata, CacheStats stats) {
     super(failureMetadata, stats);
+    this.stats = stats;
   }
 
   public CacheStatsSubject since(CacheStats start) {
@@ -52,11 +54,11 @@
 
   public void hasHitCount(int expectedHitCount) {
     isNotNull();
-    check("hitCount()").that(actual().minus(start).hitCount()).isEqualTo(expectedHitCount);
+    check("hitCount()").that(stats.minus(start).hitCount()).isEqualTo(expectedHitCount);
   }
 
   public void hasMissCount(int expectedMissCount) {
     isNotNull();
-    check("missCount()").that(actual().minus(start).missCount()).isEqualTo(expectedMissCount);
+    check("missCount()").that(stats.minus(start).missCount()).isEqualTo(expectedMissCount);
   }
 }
diff --git a/java/com/google/gerrit/truth/ConfigSubject.java b/java/com/google/gerrit/truth/ConfigSubject.java
new file mode 100644
index 0000000..dd55b71
--- /dev/null
+++ b/java/com/google/gerrit/truth/ConfigSubject.java
@@ -0,0 +1,129 @@
+// 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.truth;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.truth.BooleanSubject;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IntegerSubject;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.LongSubject;
+import com.google.common.truth.MultimapSubject;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.common.Nullable;
+import java.util.Arrays;
+import org.eclipse.jgit.lib.Config;
+
+public class ConfigSubject extends Subject {
+  public static ConfigSubject assertThat(Config config) {
+    return assertAbout(ConfigSubject::new).that(config);
+  }
+
+  private final Config config;
+
+  private ConfigSubject(FailureMetadata metadata, Config actual) {
+    super(metadata, actual);
+    this.config = actual;
+  }
+
+  public IterableSubject sections() {
+    isNotNull();
+    return check("getSections()").that(config.getSections());
+  }
+
+  public IterableSubject subsections(String section) {
+    requireNonNull(section);
+    isNotNull();
+    return check("getSubsections(%s)", section).that(config.getSubsections(section));
+  }
+
+  public MultimapSubject sectionValues(String section) {
+    requireNonNull(section);
+    return sectionValuesImpl(section, null);
+  }
+
+  public MultimapSubject subsectionValues(String section, String subsection) {
+    requireNonNull(section);
+    requireNonNull(subsection);
+    return sectionValuesImpl(section, subsection);
+  }
+
+  private MultimapSubject sectionValuesImpl(String section, @Nullable String subsection) {
+    isNotNull();
+    ImmutableListMultimap.Builder<String, String> b = ImmutableListMultimap.builder();
+    config
+        .getNames(section, subsection, true)
+        .forEach(
+            n ->
+                Arrays.stream(config.getStringList(section, subsection, n))
+                    .forEach(v -> b.put(n, v)));
+    return check("getSection(%s, %s)", section, subsection).that(b.build());
+  }
+
+  public void isEmpty() {
+    sections().isEmpty();
+  }
+
+  public StringSubject text() {
+    isNotNull();
+    return check("toText()").that(config.toText());
+  }
+
+  public IterableSubject stringValues(String section, @Nullable String subsection, String name) {
+    requireNonNull(section);
+    requireNonNull(name);
+    isNotNull();
+    return check("getStringList(%s, %s, %s)", section, subsection, name)
+        .that(Arrays.asList(config.getStringList(section, subsection, name)));
+  }
+
+  public StringSubject stringValue(String section, @Nullable String subsection, String name) {
+    requireNonNull(section);
+    requireNonNull(name);
+    isNotNull();
+    return check("getString(%s, %s, %s)", section, subsection, name)
+        .that(config.getString(section, subsection, name));
+  }
+
+  public IntegerSubject intValue(
+      String section, @Nullable String subsection, String name, int defaultValue) {
+    requireNonNull(section);
+    requireNonNull(name);
+    isNotNull();
+    return check("getInt(%s, %s, %s, %s)", section, subsection, name, defaultValue)
+        .that(config.getInt(section, subsection, name, defaultValue));
+  }
+
+  public LongSubject longValue(String section, String subsection, String name, long defaultValue) {
+    requireNonNull(section);
+    requireNonNull(name);
+    isNotNull();
+    return check("getLong(%s, %s, %s, %s)", section, subsection, name, defaultValue)
+        .that(config.getLong(section, subsection, name, defaultValue));
+  }
+
+  public BooleanSubject booleanValue(
+      String section, String subsection, String name, boolean defaultValue) {
+    requireNonNull(section);
+    requireNonNull(name);
+    isNotNull();
+    return check("getBoolean(%s, %s, %s, %s)", section, subsection, name, defaultValue)
+        .that(config.getBoolean(section, subsection, name, defaultValue));
+  }
+}
diff --git a/java/com/google/gerrit/truth/ListSubject.java b/java/com/google/gerrit/truth/ListSubject.java
index 9a839dd..9f93964 100644
--- a/java/com/google/gerrit/truth/ListSubject.java
+++ b/java/com/google/gerrit/truth/ListSubject.java
@@ -27,12 +27,13 @@
 import java.util.List;
 import java.util.function.BiFunction;
 
-public class ListSubject<S extends Subject<S, E>, E> extends IterableSubject {
+public class ListSubject<S extends Subject, E> extends IterableSubject {
 
-  private final BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator;
+  private final List<E> list;
+  private final BiFunction<StandardSubjectBuilder, ? super E, ? extends S> elementSubjectCreator;
 
-  public static <S extends Subject<S, E>, E> ListSubject<S, E> assertThat(
-      List<E> list, Subject.Factory<S, E> subjectFactory) {
+  public static <S extends Subject, E> ListSubject<S, E> assertThat(
+      List<E> list, Subject.Factory<? extends S, ? super E> subjectFactory) {
     return assertAbout(elements()).thatCustom(list, subjectFactory);
   }
 
@@ -43,15 +44,15 @@
   private ListSubject(
       FailureMetadata failureMetadata,
       List<E> list,
-      BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator) {
+      BiFunction<StandardSubjectBuilder, ? super E, ? extends S> elementSubjectCreator) {
     super(failureMetadata, list);
+    this.list = list;
     this.elementSubjectCreator = elementSubjectCreator;
   }
 
   public S element(int index) {
     checkArgument(index >= 0, "index(%s) must be >= 0", index);
     isNotNull();
-    List<E> list = getActualList();
     if (index >= list.size()) {
       failWithoutActual(fact("expected to have element at index", index));
     }
@@ -61,43 +62,29 @@
   public S onlyElement() {
     isNotNull();
     hasSize(1);
-    List<E> list = getActualList();
     return elementSubjectCreator.apply(check("onlyElement()"), Iterables.getOnlyElement(list));
   }
 
   public S lastElement() {
     isNotNull();
     isNotEmpty();
-    List<E> list = getActualList();
     return elementSubjectCreator.apply(check("lastElement()"), Iterables.getLast(list));
   }
 
-  @SuppressWarnings("unchecked")
-  private List<E> getActualList() {
-    // The constructor only accepts lists. -> Casting is appropriate.
-    return (List<E>) actual();
-  }
-
-  @SuppressWarnings("unchecked")
-  @Override
-  public ListSubject<S, E> named(String s, Object... objects) {
-    // This object is returned which is of type ListSubject. -> Casting is appropriate.
-    return (ListSubject<S, E>) super.named(s, objects);
-  }
-
   public static class ListSubjectBuilder extends CustomSubjectBuilder {
 
     ListSubjectBuilder(FailureMetadata failureMetadata) {
       super(failureMetadata);
     }
 
-    public <S extends Subject<S, E>, E> ListSubject<S, E> thatCustom(
-        List<E> list, Subject.Factory<S, E> subjectFactory) {
+    public <S extends Subject, E> ListSubject<S, E> thatCustom(
+        List<E> list, Subject.Factory<? extends S, ? super E> subjectFactory) {
       return that(list, (builder, element) -> builder.about(subjectFactory).that(element));
     }
 
-    public <S extends Subject<S, E>, E> ListSubject<S, E> that(
-        List<E> list, BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator) {
+    public <S extends Subject, E> ListSubject<S, E> that(
+        List<E> list,
+        BiFunction<StandardSubjectBuilder, ? super E, ? extends S> elementSubjectCreator) {
       return new ListSubject<>(metadata(), list, elementSubjectCreator);
     }
   }
diff --git a/java/com/google/gerrit/truth/NullAwareCorrespondence.java b/java/com/google/gerrit/truth/NullAwareCorrespondence.java
new file mode 100644
index 0000000..5b107a6
--- /dev/null
+++ b/java/com/google/gerrit/truth/NullAwareCorrespondence.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.truth;
+
+import com.google.common.base.Function;
+import com.google.common.truth.Correspondence;
+import java.util.Optional;
+
+/** Utility class for constructing null aware {@link Correspondence}s. */
+public class NullAwareCorrespondence {
+  /**
+   * Constructs a {@link Correspondence} that compares elements by transforming the actual elements
+   * using the given function and testing for equality with the expected elements.
+   *
+   * <p>If the actual element is null, it will correspond to a null expected element. This is
+   * different to {@link Correspondence#transforming(Function, String)} which would invoke the
+   * function with a {@code null} argument, requiring the function being able to handle {@code
+   * null}.
+   *
+   * @param actualTransform a {@link Function} taking an actual value and returning a new value
+   *     which will be compared with an expected value to determine whether they correspond
+   * @param description should fill the gap in a failure message of the form {@code "not true that
+   *     <some actual element> is an element that <description> <some expected element>"}, e.g.
+   *     {@code "has an ID of"}
+   */
+  public static <A, E> Correspondence<A, E> transforming(
+      Function<A, ? extends E> actualTransform, String description) {
+    return Correspondence.transforming(
+        actualValue -> Optional.ofNullable(actualValue).map(actualTransform).orElse(null),
+        description);
+  }
+
+  /**
+   * Constructs a {@link Correspondence} that compares elements by transforming the actual elements
+   * using the given function and testing for equality with the expected elements.
+   *
+   * <p>If the actual element is null, it will correspond to a null expected element. This is
+   * different to {@link Correspondence#transforming(Function, Function, String)} which would invoke
+   * the function with a {@code null} argument, requiring the function being able to handle {@code
+   * null}.
+   *
+   * <p>If the expected element is null, it will correspond to a new null expected element. This is
+   * different to {@link Correspondence#transforming(Function, Function, String)} which would invoke
+   * the function with a {@code null} argument, requiring the function being able to handle {@code
+   * null}.
+   *
+   * @param actualTransform a {@link Function} taking an actual value and returning a new value
+   *     which will be compared with an expected value to determine whether they correspond
+   * @param expectedTransform a {@link Function} taking an expected value and returning a new value
+   *     which will be compared with a transformed actual value
+   * @param description should fill the gap in a failure message of the form {@code "not true that
+   *     <some actual element> is an element that <description> <some expected element>"}, e.g.
+   *     {@code "has an ID of"}
+   */
+  public static <A, E> Correspondence<A, E> transforming(
+      Function<A, ? extends E> actualTransform,
+      Function<E, ?> expectedTransform,
+      String description) {
+    return Correspondence.transforming(
+        actualValue -> Optional.ofNullable(actualValue).map(actualTransform).orElse(null),
+        expectedValue -> Optional.ofNullable(expectedValue).map(expectedTransform).orElse(null),
+        description);
+  }
+
+  /**
+   * Private constructor to prevent instantiation of this class.
+   *
+   * <p>This class contains only static method and hence never needs to be instantiated.
+   */
+  private NullAwareCorrespondence() {}
+}
diff --git a/java/com/google/gerrit/truth/OptionalSubject.java b/java/com/google/gerrit/truth/OptionalSubject.java
index b5fc5d0..2023765 100644
--- a/java/com/google/gerrit/truth/OptionalSubject.java
+++ b/java/com/google/gerrit/truth/OptionalSubject.java
@@ -18,39 +18,24 @@
 import static com.google.common.truth.Truth.assertAbout;
 
 import com.google.common.truth.CustomSubjectBuilder;
-import com.google.common.truth.DefaultSubject;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StandardSubjectBuilder;
 import com.google.common.truth.Subject;
 import java.util.Optional;
 import java.util.function.BiFunction;
-import java.util.function.Function;
 
-public class OptionalSubject<S extends Subject<S, ? super T>, T>
-    extends Subject<OptionalSubject<S, T>, Optional<T>> {
+public class OptionalSubject<S extends Subject, T> extends Subject {
 
+  private final Optional<T> optional;
   private final BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator;
 
-  // TODO(aliceks): Remove when all relevant usages are adapted to new check()/factory approach.
-  public static <S extends Subject<S, T>, T> OptionalSubject<S, T> assertThat(
-      Optional<T> optional, Function<? super T, ? extends S> elementAssertThatFunction) {
-    Subject.Factory<S, T> valueSubjectFactory =
-        (metadata, value) -> elementAssertThatFunction.apply(value);
-    return assertThat(optional, valueSubjectFactory);
-  }
-
-  public static <S extends Subject<S, T>, T> OptionalSubject<S, T> assertThat(
-      Optional<T> optional, Subject.Factory<S, T> valueSubjectFactory) {
+  public static <S extends Subject, T> OptionalSubject<S, T> assertThat(
+      Optional<T> optional, Subject.Factory<? extends S, ? super T> valueSubjectFactory) {
     return assertAbout(optionals()).thatCustom(optional, valueSubjectFactory);
   }
 
-  public static OptionalSubject<DefaultSubject, ?> assertThat(Optional<?> optional) {
-    // Unfortunately, we need to cast to DefaultSubject as StandardSubjectBuilder#that
-    // only returns Subject<DefaultSubject, Object>. There shouldn't be a way
-    // for that method not to return a DefaultSubject because the generic type
-    // definitions of a Subject are quite strict.
-    return assertAbout(optionals())
-        .that(optional, (builder, value) -> (DefaultSubject) builder.that(value));
+  public static OptionalSubject<Subject, ?> assertThat(Optional<?> optional) {
+    return assertAbout(optionals()).that(optional);
   }
 
   public static CustomSubjectBuilder.Factory<OptionalSubjectBuilder> optionals() {
@@ -62,12 +47,12 @@
       Optional<T> optional,
       BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator) {
     super(failureMetadata, optional);
+    this.optional = optional;
     this.valueSubjectCreator = valueSubjectCreator;
   }
 
   public void isPresent() {
     isNotNull();
-    Optional<T> optional = actual();
     if (!optional.isPresent()) {
       failWithoutActual(fact("expected to have", "value"));
     }
@@ -75,7 +60,6 @@
 
   public void isAbsent() {
     isNotNull();
-    Optional<T> optional = actual();
     if (optional.isPresent()) {
       failWithoutActual(fact("expected not to have", "value"));
     }
@@ -88,7 +72,6 @@
   public S value() {
     isNotNull();
     isPresent();
-    Optional<T> optional = actual();
     return valueSubjectCreator.apply(check("value()"), optional.get());
   }
 
@@ -98,16 +81,16 @@
       super(failureMetadata);
     }
 
-    public <S extends Subject<S, T>, T> OptionalSubject<S, T> thatCustom(
-        Optional<T> optional, Subject.Factory<S, T> valueSubjectFactory) {
+    public <S extends Subject, T> OptionalSubject<S, T> thatCustom(
+        Optional<T> optional, Subject.Factory<? extends S, ? super T> valueSubjectFactory) {
       return that(optional, (builder, value) -> builder.about(valueSubjectFactory).that(value));
     }
 
-    public OptionalSubject<DefaultSubject, ?> that(Optional<?> optional) {
-      return that(optional, (builder, value) -> (DefaultSubject) builder.that(value));
+    public OptionalSubject<Subject, ?> that(Optional<?> optional) {
+      return that(optional, StandardSubjectBuilder::that);
     }
 
-    public <S extends Subject<S, ? super T>, T> OptionalSubject<S, T> that(
+    public <S extends Subject, T> OptionalSubject<S, T> that(
         Optional<T> optional,
         BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator) {
       return new OptionalSubject<>(metadata(), optional, valueSubjectCreator);
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index 2917257..e002eeb 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -35,13 +35,14 @@
 package com.google.gerrit.util.cli;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.util.cli.Localizable.localizable;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.MultimapBuilder;
 import com.google.common.flogger.FluentLogger;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -313,23 +314,13 @@
     parser.parseArgument(tmp.toArray(new String[tmp.size()]));
   }
 
-  public void parseOptionMap(Map<String, String[]> parameters) throws CmdLineException {
-    ListMultimap<String, String> map = MultimapBuilder.hashKeys().arrayListValues().build();
-    for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
-      for (String val : ent.getValue()) {
-        map.put(ent.getKey(), val);
-      }
-    }
-    parseOptionMap(map);
-  }
-
   public void parseOptionMap(ListMultimap<String, String> params) throws CmdLineException {
     logger.atFinest().log("Command-line parameters: %s", params.keySet());
     List<String> tmp = Lists.newArrayListWithCapacity(2 * params.size());
     for (String key : params.keySet()) {
       String name = makeOption(key);
 
-      if (isBoolean(name)) {
+      if (isBooleanOption(name)) {
         boolean on = false;
         for (String value : params.get(key)) {
           on = toBoolean(key, value);
@@ -347,10 +338,6 @@
     parser.parseArgument(tmp.toArray(new String[tmp.size()]));
   }
 
-  public boolean isBoolean(String name) {
-    return findHandler(makeOption(name)) instanceof BooleanOptionHandler;
-  }
-
   public void parseWithPrefix(String prefix, Object bean) {
     parser.parseWithPrefix(prefix, bean);
   }
@@ -359,6 +346,10 @@
     parser.addOptionsWithMetRequirements();
   }
 
+  private boolean isBooleanOption(String name) {
+    return findHandler(makeOption(name)) instanceof BooleanOptionHandler;
+  }
+
   private String makeOption(String name) {
     if (!name.startsWith("-")) {
       if (name.length() == 1) {
@@ -422,7 +413,8 @@
   private static Option newPrefixedOption(String prefix, Option o) {
     requireNonNull(prefix);
     checkArgument(o.name().startsWith("-"), "Option name must start with '-': %s", o);
-    String[] aliases = Arrays.stream(o.aliases()).map(prefix::concat).toArray(String[]::new);
+    ImmutableList<String> aliases =
+        Arrays.stream(o.aliases()).map(prefix::concat).collect(toImmutableList());
     return OptionUtil.newOption(
         prefix + o.name(),
         aliases,
@@ -432,8 +424,8 @@
         false,
         o.hidden(),
         o.handler(),
-        o.depends(),
-        new String[0]);
+        ImmutableList.copyOf(o.depends()),
+        ImmutableList.of());
   }
 
   public class MyParser extends org.kohsuke.args4j.CmdLineParser {
@@ -626,15 +618,15 @@
     private Option newHelpOption() {
       return OptionUtil.newOption(
           "--help",
-          new String[] {"-h"},
+          ImmutableList.of("-h"),
           "display this help text",
           "",
           false,
           false,
           false,
           BooleanOptionHandler.class,
-          new String[0],
-          new String[0]);
+          ImmutableList.of(),
+          ImmutableList.of());
     }
 
     private boolean isHandlerSpecified(OptionDef option) {
diff --git a/java/com/google/gerrit/util/cli/OptionUtil.java b/java/com/google/gerrit/util/cli/OptionUtil.java
index 1125a0d..68cd717 100644
--- a/java/com/google/gerrit/util/cli/OptionUtil.java
+++ b/java/com/google/gerrit/util/cli/OptionUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.util.cli;
 
 import com.google.auto.value.AutoAnnotation;
+import com.google.common.collect.ImmutableList;
 import org.kohsuke.args4j.Option;
 import org.kohsuke.args4j.spi.OptionHandler;
 
@@ -24,15 +25,15 @@
   @SuppressWarnings("rawtypes")
   public static Option newOption(
       String name,
-      String[] aliases,
+      ImmutableList<String> aliases,
       String usage,
       String metaVar,
       boolean required,
       boolean help,
       boolean hidden,
       Class<? extends OptionHandler> handler,
-      String[] depends,
-      String[] forbids) {
+      ImmutableList<String> depends,
+      ImmutableList<String> forbids) {
     return new AutoAnnotation_OptionUtil_newOption(
         name, aliases, usage, metaVar, required, help, hidden, handler, depends, forbids);
   }
diff --git a/java/com/google/gwtorm/BUILD b/java/com/google/gwtorm/BUILD
deleted file mode 100644
index baf7a8cb..0000000
--- a/java/com/google/gwtorm/BUILD
+++ /dev/null
@@ -1,7 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-
-java_library(
-    name = "gwtorm",
-    srcs = glob(["**/*.java"]),
-    visibility = ["//visibility:public"],
-)
diff --git a/java/com/google/gwtorm/client/CompoundKey.java b/java/com/google/gwtorm/client/CompoundKey.java
deleted file mode 100644
index 1c66d18..0000000
--- a/java/com/google/gwtorm/client/CompoundKey.java
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright 2008 Google Inc.
-//
-// 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.gwtorm.client;
-
-import java.io.Serializable;
-
-/**
- * Abstract key type composed of other keys.
- *
- * <p>Applications should subclass this type to create their own entity-specific key classes.
- *
- * @param <P> the parent key type. Use {@link Key} if no parent key is needed.
- */
-@SuppressWarnings("serial")
-public abstract class CompoundKey<P extends Key<?>> implements Key<P>, Serializable {
-  /** @return the member key components, minus the parent key. */
-  public abstract Key<?>[] members();
-
-  /** @return the parent key instance; null if this is a root level key. */
-  @Override
-  public P getParentKey() {
-    return null;
-  }
-
-  @Override
-  public int hashCode() {
-    int hc = 0;
-    if (getParentKey() != null) {
-      hc = getParentKey().hashCode();
-    }
-    for (final Key<?> k : members()) {
-      hc *= 31;
-      hc += k.hashCode();
-    }
-    return hc;
-  }
-
-  @Override
-  public boolean equals(final Object b) {
-    if (b == null || b.getClass() != getClass()) {
-      return false;
-    }
-
-    final CompoundKey<P> q = cast(b);
-    if (getParentKey() != null && !getParentKey().equals(q.getParentKey())) {
-      return false;
-    }
-
-    final Key<?>[] aMembers = members();
-    final Key<?>[] bMembers = q.members();
-    if (aMembers.length != bMembers.length) {
-      return false;
-    }
-    for (int i = 0; i < aMembers.length; i++) {
-      if (!aMembers[i].equals(bMembers[i])) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  @Override
-  public String toString() {
-    final StringBuffer r = new StringBuffer();
-    boolean first = true;
-    if (getParentKey() != null) {
-      r.append(KeyUtil.encode(getParentKey().toString()));
-      first = false;
-    }
-    for (final Key<?> k : members()) {
-      if (!first) {
-        r.append(',');
-      }
-      r.append(KeyUtil.encode(k.toString()));
-      first = false;
-    }
-    return r.toString();
-  }
-
-  @Override
-  public void fromString(final String in) {
-    final String[] parts = in.split(",");
-    int p = 0;
-    if (getParentKey() != null) {
-      getParentKey().fromString(KeyUtil.decode(parts[p++]));
-    }
-    for (final Key<?> k : members()) {
-      k.fromString(KeyUtil.decode(parts[p++]));
-    }
-  }
-
-  @SuppressWarnings("unchecked")
-  private static <A extends Key<?>> CompoundKey<A> cast(final Object b) {
-    return (CompoundKey<A>) b;
-  }
-}
diff --git a/java/com/google/gwtorm/client/IntKey.java b/java/com/google/gwtorm/client/IntKey.java
deleted file mode 100644
index 08c90e0..0000000
--- a/java/com/google/gwtorm/client/IntKey.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright 2008 Google Inc.
-//
-// 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.gwtorm.client;
-
-import java.io.Serializable;
-
-/**
- * Abstract key type using a single integer value.
- *
- * <p>Applications should subclass this type to create their own entity-specific key classes.
- *
- * @param <P> the parent key type. Use {@link Key} if no parent key is needed.
- */
-@SuppressWarnings("serial")
-public abstract class IntKey<P extends Key<?>> implements Key<P>, Serializable {
-  /** @return id of the entity instance. */
-  public abstract int get();
-
-  /** @param newValue the new value of this key. */
-  protected abstract void set(int newValue);
-
-  /** @return the parent key instance; null if this is a root level key. */
-  @Override
-  public P getParentKey() {
-    return null;
-  }
-
-  @Override
-  public int hashCode() {
-    int hc = get();
-    if (getParentKey() != null) {
-      hc *= 31;
-      hc += getParentKey().hashCode();
-    }
-    return hc;
-  }
-
-  @Override
-  public boolean equals(final Object b) {
-    if (b == null || b.getClass() != getClass()) {
-      return false;
-    }
-
-    final IntKey<P> q = cast(b);
-    return get() == q.get() && KeyUtil.eq(getParentKey(), q.getParentKey());
-  }
-
-  @Override
-  public String toString() {
-    final StringBuffer r = new StringBuffer();
-    if (getParentKey() != null) {
-      r.append(getParentKey().toString());
-      r.append(',');
-    }
-    r.append(get());
-    return r.toString();
-  }
-
-  @Override
-  public void fromString(final String in) {
-    set(Integer.parseInt(KeyUtil.parseFromString(getParentKey(), in)));
-  }
-
-  @SuppressWarnings("unchecked")
-  private static <A extends Key<?>> IntKey<A> cast(final Object b) {
-    return (IntKey<A>) b;
-  }
-}
diff --git a/java/com/google/gwtorm/client/Key.java b/java/com/google/gwtorm/client/Key.java
deleted file mode 100644
index 69a2248..0000000
--- a/java/com/google/gwtorm/client/Key.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2008 Google Inc.
-//
-// 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.gwtorm.client;
-
-/**
- * Generic type for an entity key.
- *
- * <p>Although not required, entities should make their primary key type implement this interface,
- * permitting traversal up through the containment hierarchy of the entity keys.
- *
- * @param <P> type of the parent key. If no parent, use {@link Key} itself.
- */
-public interface Key<P extends Key<?>> {
-  /**
-   * Get the parent key instance.
-   *
-   * @return the parent key; null if this entity key is a root-level key.
-   */
-  public P getParentKey();
-
-  @Override
-  public int hashCode();
-
-  @Override
-  public boolean equals(Object o);
-
-  /** @return the key, encoded in a string format . */
-  @Override
-  public String toString();
-
-  /** Reset this key instance to represent the data in the supplied string. */
-  public void fromString(String in);
-}
diff --git a/java/com/google/gwtorm/client/KeyUtil.java b/java/com/google/gwtorm/client/KeyUtil.java
deleted file mode 100644
index e236d37..0000000
--- a/java/com/google/gwtorm/client/KeyUtil.java
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright 2008 Google Inc.
-//
-// 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.gwtorm.client;
-
-/** Common utility functions for {@link Key} implementors. */
-public class KeyUtil {
-  private static Encoder ENCODER_IMPL = new StandardKeyEncoder();
-
-  /**
-   * Determine if two keys are equal, supporting null references.
-   *
-   * @param <T> type of the key entity.
-   * @param a first key to test; may be null.
-   * @param b second key to test; may be null.
-   * @return true if both <code>a</code> and <code>b</code> are null, or if both are not-null and
-   *     <code>a.equals(b)</code> is true. Otherwise false.
-   */
-  public static <T extends Key<?>> boolean eq(final T a, final T b) {
-    if (a == b) {
-      return true;
-    }
-    if (a == null || b == null) {
-      return false;
-    }
-    return a.equals(b);
-  }
-
-  /**
-   * Encode a string to be safe for use within a URL like string.
-   *
-   * <p>The returned encoded string has URL component characters escaped with hex escapes (e.g. ' '
-   * is '+' and '%' is '%25'). The special character '/' is left literal. The comma character (',')
-   * is always encoded, permitting multiple encoded string values to be joined together safely.
-   *
-   * @param e the string to encode, must not be null.
-   * @return the encoded string.
-   */
-  public static String encode(final String e) {
-    return ENCODER_IMPL.encode(e);
-  }
-
-  /**
-   * Decode a string previously encoded by {@link #encode(String)}.
-   *
-   * @param e the string to decode, must not be null.
-   * @return the decoded string.
-   */
-  public static String decode(final String e) {
-    return ENCODER_IMPL.decode(e);
-  }
-
-  /**
-   * Split a string along the last comma and parse into the parent.
-   *
-   * @param parent parent key; <code>parent.fromString(in[0..comma])</code>.
-   * @param in the input string.
-   * @return text (if any) after the last comma in the input.
-   */
-  public static String parseFromString(final Key<?> parent, final String in) {
-    final int comma = in.lastIndexOf(',');
-    if (comma < 0 && parent == null) {
-      return decode(in);
-    }
-    if (comma < 0 && parent != null) {
-      throw new IllegalArgumentException("Not enough components: " + in);
-    }
-    assert (parent != null);
-    parent.fromString(in.substring(0, comma));
-    return decode(in.substring(comma + 1));
-  }
-
-  public abstract static class Encoder {
-    public abstract String encode(String e);
-
-    public abstract String decode(String e);
-  }
-
-  private KeyUtil() {}
-}
diff --git a/java/com/google/gwtorm/client/StringKey.java b/java/com/google/gwtorm/client/StringKey.java
deleted file mode 100644
index e56661f..0000000
--- a/java/com/google/gwtorm/client/StringKey.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright 2008 Google Inc.
-//
-// 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.gwtorm.client;
-
-import java.io.Serializable;
-
-/**
- * Abstract key type using a single string value.
- *
- * <p>Applications should subclass this type to create their own entity-specific key classes.
- *
- * @param <P> the parent key type. Use {@link Key} if no parent key is needed.
- */
-@SuppressWarnings("serial")
-public abstract class StringKey<P extends Key<?>>
-    implements Key<P>, Serializable, Comparable<StringKey<?>> {
-  /** @return name of the entity instance. */
-  public abstract String get();
-
-  /** @param newValue the new value of this key. */
-  protected abstract void set(String newValue);
-
-  /** @return the parent key instance; null if this is a root level key. */
-  @Override
-  public P getParentKey() {
-    return null;
-  }
-
-  @Override
-  public int hashCode() {
-    int hc = get() != null ? get().hashCode() : 0;
-    if (getParentKey() != null) {
-      hc *= 31;
-      hc += getParentKey().hashCode();
-    }
-    return hc;
-  }
-
-  @Override
-  public boolean equals(final Object b) {
-    if (b == null || get() == null || b.getClass() != getClass()) {
-      return false;
-    }
-
-    final StringKey<P> q = cast(b);
-    return get().equals(q.get()) && KeyUtil.eq(getParentKey(), q.getParentKey());
-  }
-
-  @Override
-  public int compareTo(final StringKey<?> other) {
-    return get().compareTo(other.get());
-  }
-
-  @Override
-  public String toString() {
-    final StringBuffer r = new StringBuffer();
-    if (getParentKey() != null) {
-      r.append(getParentKey().toString());
-      r.append(',');
-    }
-    r.append(KeyUtil.encode(get()));
-    return r.toString();
-  }
-
-  @Override
-  public void fromString(final String in) {
-    set(KeyUtil.parseFromString(getParentKey(), in));
-  }
-
-  @SuppressWarnings("unchecked")
-  private static <A extends Key<?>> StringKey<A> cast(final Object b) {
-    return (StringKey<A>) b;
-  }
-}
diff --git a/java/gerrit/AbstractCommitUserIdentityPredicate.java b/java/gerrit/AbstractCommitUserIdentityPredicate.java
index 1bfc95c..bd8cf1a 100644
--- a/java/gerrit/AbstractCommitUserIdentityPredicate.java
+++ b/java/gerrit/AbstractCommitUserIdentityPredicate.java
@@ -15,7 +15,7 @@
 package gerrit;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.rules.PrologEnvironment;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
diff --git a/java/gerrit/BUILD b/java/gerrit/BUILD
index d7e2306..7dbf751 100644
--- a/java/gerrit/BUILD
+++ b/java/gerrit/BUILD
@@ -6,12 +6,11 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/exceptions",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//lib:jgit",
         "//lib/flogger:api",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/prolog:runtime",
         "@guava//jar",
     ],
diff --git a/java/gerrit/PRED__load_commit_labels_1.java b/java/gerrit/PRED__load_commit_labels_1.java
index 693c89e..90a2cbf 100644
--- a/java/gerrit/PRED__load_commit_labels_1.java
+++ b/java/gerrit/PRED__load_commit_labels_1.java
@@ -4,7 +4,7 @@
 
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
@@ -38,16 +38,15 @@
     LabelTypes types = cd.getLabelTypes();
 
     for (PatchSetApproval a : cd.currentApprovals()) {
-      LabelType t = types.byLabel(a.getLabelId());
+      LabelType t = types.byLabel(a.labelId());
       if (t == null) {
         continue;
       }
 
       StructureTerm labelTerm =
-          new StructureTerm(
-              sym_label, SymbolTerm.intern(t.getName()), new IntegerTerm(a.getValue()));
+          new StructureTerm(sym_label, SymbolTerm.intern(t.getName()), new IntegerTerm(a.value()));
 
-      StructureTerm userTerm = new StructureTerm(sym_user, new IntegerTerm(a.getAccountId().get()));
+      StructureTerm userTerm = new StructureTerm(sym_user, new IntegerTerm(a.accountId().get()));
 
       listHead = new ListTerm(new StructureTerm(sym_commit_label, labelTerm, userTerm), listHead);
     }
diff --git a/java/gerrit/PRED_change_branch_1.java b/java/gerrit/PRED_change_branch_1.java
index 0a7bb74..4501169 100644
--- a/java/gerrit/PRED_change_branch_1.java
+++ b/java/gerrit/PRED_change_branch_1.java
@@ -14,7 +14,7 @@
 
 package gerrit;
 
-import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
@@ -34,9 +34,9 @@
     engine.setB0();
     Term a1 = arg1.dereference();
 
-    Branch.NameKey name = StoredValues.getChange(engine).getDest();
+    BranchNameKey name = StoredValues.getChange(engine).getDest();
 
-    if (!a1.unify(SymbolTerm.create(name.get()), engine.trail)) {
+    if (!a1.unify(SymbolTerm.create(name.branch()), engine.trail)) {
       return engine.fail();
     }
     return cont;
diff --git a/java/gerrit/PRED_change_owner_1.java b/java/gerrit/PRED_change_owner_1.java
index 937b761..d42c0e1 100644
--- a/java/gerrit/PRED_change_owner_1.java
+++ b/java/gerrit/PRED_change_owner_1.java
@@ -14,7 +14,7 @@
 
 package gerrit;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
diff --git a/java/gerrit/PRED_change_project_1.java b/java/gerrit/PRED_change_project_1.java
index 28e637a..a973e1c 100644
--- a/java/gerrit/PRED_change_project_1.java
+++ b/java/gerrit/PRED_change_project_1.java
@@ -14,7 +14,7 @@
 
 package gerrit;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
diff --git a/java/gerrit/PRED_change_topic_1.java b/java/gerrit/PRED_change_topic_1.java
index 564878f..11d737a 100644
--- a/java/gerrit/PRED_change_topic_1.java
+++ b/java/gerrit/PRED_change_topic_1.java
@@ -14,7 +14,7 @@
 
 package gerrit;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
diff --git a/java/gerrit/PRED_commit_delta_4.java b/java/gerrit/PRED_commit_delta_4.java
index d2634ea..6e971fc 100644
--- a/java/gerrit/PRED_commit_delta_4.java
+++ b/java/gerrit/PRED_commit_delta_4.java
@@ -14,7 +14,7 @@
 
 package gerrit;
 
-import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.rules.StoredValues;
diff --git a/java/gerrit/PRED_commit_edits_2.java b/java/gerrit/PRED_commit_edits_2.java
index 6ca5338..12e7086 100644
--- a/java/gerrit/PRED_commit_edits_2.java
+++ b/java/gerrit/PRED_commit_edits_2.java
@@ -14,7 +14,7 @@
 
 package gerrit;
 
-import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.Text;
diff --git a/java/gerrit/PRED_commit_stats_3.java b/java/gerrit/PRED_commit_stats_3.java
index c1666d8..286bc2c 100644
--- a/java/gerrit/PRED_commit_stats_3.java
+++ b/java/gerrit/PRED_commit_stats_3.java
@@ -14,7 +14,7 @@
 
 package gerrit;
 
-import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.rules.StoredValues;
diff --git a/java/gerrit/PRED_uploader_1.java b/java/gerrit/PRED_uploader_1.java
index 029b84a..681d86c 100644
--- a/java/gerrit/PRED_uploader_1.java
+++ b/java/gerrit/PRED_uploader_1.java
@@ -15,8 +15,8 @@
 package gerrit;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
@@ -50,7 +50,7 @@
       return engine.fail();
     }
 
-    Account.Id uploaderId = patchSet.getUploader();
+    Account.Id uploaderId = patchSet.uploader();
 
     if (!a1.unify(new StructureTerm(user, new IntegerTerm(uploaderId.get())), engine.trail)) {
       return engine.fail();
diff --git a/javatests/com/google/gerrit/acceptance/BUILD b/javatests/com/google/gerrit/acceptance/BUILD
index 54b3626..75c90f2 100644
--- a/javatests/com/google/gerrit/acceptance/BUILD
+++ b/javatests/com/google/gerrit/acceptance/BUILD
@@ -7,8 +7,9 @@
         "//java/com/google/gerrit/acceptance:lib",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/truth",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java b/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java
index 69fdc7e..3d17de0 100644
--- a/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java
+++ b/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java
@@ -15,17 +15,17 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.File;
 import java.nio.file.Files;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 
-public class MergeableFileBasedConfigTest extends GerritBaseTests {
+public class MergeableFileBasedConfigTest {
   @Test
   public void mergeNull() throws Exception {
     MergeableFileBasedConfig cfg = newConfig();
@@ -112,7 +112,7 @@
   }
 
   private void assertConfig(MergeableFileBasedConfig cfg, String expected) throws Exception {
-    assertThat(cfg.toText()).isEqualTo(expected);
+    assertThat(cfg).text().isEqualTo(expected);
     cfg.save();
     assertThat(new String(Files.readAllBytes(cfg.getFile().toPath()), UTF_8)).isEqualTo(expected);
   }
diff --git a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
index 53d8ef8..1a09aa1 100644
--- a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
+++ b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
@@ -15,13 +15,17 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupIncludeCache;
@@ -31,12 +35,10 @@
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.TestTimeUtil;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
-import org.easymock.EasyMock;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -50,7 +52,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class ProjectResetterTest extends GerritBaseTests {
+public class ProjectResetterTest {
   private InMemoryRepositoryManager repoManager;
   private Project.NameKey project;
   private Repository repo;
@@ -58,7 +60,7 @@
   @Before
   public void setUp() throws Exception {
     repoManager = new InMemoryRepositoryManager();
-    project = new Project.NameKey("foo");
+    project = Project.nameKey("foo");
     repo = repoManager.createRepository(project);
   }
 
@@ -135,7 +137,7 @@
 
   @Test
   public void onlyResetMatchingRefsMultipleProjects() throws Exception {
-    Project.NameKey project2 = new Project.NameKey("bar");
+    Project.NameKey project2 = Project.nameKey("bar");
     Repository repo2 = repoManager.createRepository(project2);
 
     Ref matchingRefProject1 = createRef("refs/foo/test");
@@ -170,7 +172,7 @@
 
   @Test
   public void onlyDeleteNewlyCreatedMatchingRefsMultipleProjects() throws Exception {
-    Project.NameKey project2 = new Project.NameKey("bar");
+    Project.NameKey project2 = Project.nameKey("bar");
     Repository repo2 = repoManager.createRepository(project2);
 
     Ref matchingRefProject1;
@@ -216,14 +218,11 @@
 
   @Test
   public void projectEvictionIfRefsMetaConfigIsReset() throws Exception {
-    Project.NameKey project2 = new Project.NameKey("bar");
+    Project.NameKey project2 = Project.nameKey("bar");
     Repository repo2 = repoManager.createRepository(project2);
     Ref metaConfig = createRef(repo2, RefNames.REFS_CONFIG);
 
-    ProjectCache projectCache = EasyMock.createNiceMock(ProjectCache.class);
-    projectCache.evict(project2);
-    EasyMock.expectLastCall();
-    EasyMock.replay(projectCache);
+    ProjectCache projectCache = mock(ProjectCache.class);
 
     Ref nonMetaConfig = createRef("refs/heads/master");
 
@@ -234,18 +233,15 @@
       updateRef(repo2, metaConfig);
     }
 
-    EasyMock.verify(projectCache);
+    verify(projectCache, only()).evict(project2);
   }
 
   @Test
   public void projectEvictionIfRefsMetaConfigIsDeleted() throws Exception {
-    Project.NameKey project2 = new Project.NameKey("bar");
+    Project.NameKey project2 = Project.nameKey("bar");
     Repository repo2 = repoManager.createRepository(project2);
 
-    ProjectCache projectCache = EasyMock.createNiceMock(ProjectCache.class);
-    projectCache.evict(project2);
-    EasyMock.expectLastCall();
-    EasyMock.replay(projectCache);
+    ProjectCache projectCache = mock(ProjectCache.class);
 
     try (ProjectResetter resetProject =
         builder(null, null, null, null, null, null, projectCache)
@@ -254,28 +250,21 @@
       createRef(repo2, RefNames.REFS_CONFIG);
     }
 
-    EasyMock.verify(projectCache);
+    verify(projectCache, only()).evict(project2);
   }
 
   @Test
   public void accountEvictionIfUserBranchIsReset() throws Exception {
-    Account.Id accountId = new Account.Id(1);
-    Project.NameKey allUsers = new Project.NameKey(AllUsersNameProvider.DEFAULT);
+    Account.Id accountId = Account.id(1);
+    Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
     Repository allUsersRepo = repoManager.createRepository(allUsers);
     Ref userBranch = createRef(allUsersRepo, RefNames.refsUsers(accountId));
 
-    AccountCache accountCache = EasyMock.createNiceMock(AccountCache.class);
-    accountCache.evict(accountId);
-    EasyMock.expectLastCall();
-    EasyMock.replay(accountCache);
-
-    AccountIndexer accountIndexer = EasyMock.createNiceMock(AccountIndexer.class);
-    accountIndexer.index(accountId);
-    EasyMock.expectLastCall();
-    EasyMock.replay(accountIndexer);
+    AccountCache accountCache = mock(AccountCache.class);
+    AccountIndexer accountIndexer = mock(AccountIndexer.class);
 
     // Non-user branch because it's not in All-Users.
-    Ref nonUserBranch = createRef(RefNames.refsUsers(new Account.Id(2)));
+    Ref nonUserBranch = createRef(RefNames.refsUsers(Account.id(2)));
 
     try (ProjectResetter resetProject =
         builder(null, accountCache, accountIndexer, null, null, null, null)
@@ -284,63 +273,47 @@
       updateRef(allUsersRepo, userBranch);
     }
 
-    EasyMock.verify(accountCache, accountIndexer);
+    verify(accountCache, only()).evict(accountId);
+    verify(accountIndexer, only()).index(accountId);
   }
 
   @Test
   public void accountEvictionIfUserBranchIsDeleted() throws Exception {
-    Account.Id accountId = new Account.Id(1);
-    Project.NameKey allUsers = new Project.NameKey(AllUsersNameProvider.DEFAULT);
+    Account.Id accountId = Account.id(1);
+    Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
     Repository allUsersRepo = repoManager.createRepository(allUsers);
 
-    AccountCache accountCache = EasyMock.createNiceMock(AccountCache.class);
-    accountCache.evict(accountId);
-    EasyMock.expectLastCall();
-    EasyMock.replay(accountCache);
-
-    AccountIndexer accountIndexer = EasyMock.createNiceMock(AccountIndexer.class);
-    accountIndexer.index(accountId);
-    EasyMock.expectLastCall();
-    EasyMock.replay(accountIndexer);
+    AccountCache accountCache = mock(AccountCache.class);
+    AccountIndexer accountIndexer = mock(AccountIndexer.class);
 
     try (ProjectResetter resetProject =
         builder(null, accountCache, accountIndexer, null, null, null, null)
             .build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
       // Non-user branch because it's not in All-Users.
-      createRef(RefNames.refsUsers(new Account.Id(2)));
+      createRef(RefNames.refsUsers(Account.id(2)));
 
       createRef(allUsersRepo, RefNames.refsUsers(accountId));
     }
 
-    EasyMock.verify(accountCache, accountIndexer);
+    verify(accountCache, only()).evict(accountId);
+    verify(accountIndexer, only()).index(accountId);
   }
 
   @Test
   public void accountEvictionIfExternalIdsBranchIsReset() throws Exception {
-    Account.Id accountId = new Account.Id(1);
-    Project.NameKey allUsers = new Project.NameKey(AllUsersNameProvider.DEFAULT);
+    Account.Id accountId = Account.id(1);
+    Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
     Repository allUsersRepo = repoManager.createRepository(allUsers);
     Ref externalIds = createRef(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
     createRef(allUsersRepo, RefNames.refsUsers(accountId));
 
-    Account.Id accountId2 = new Account.Id(2);
+    Account.Id accountId2 = Account.id(2);
 
-    AccountCache accountCache = EasyMock.createNiceMock(AccountCache.class);
-    accountCache.evict(accountId);
-    EasyMock.expectLastCall();
-    accountCache.evict(accountId2);
-    EasyMock.expectLastCall();
-    EasyMock.replay(accountCache);
-
-    AccountIndexer accountIndexer = EasyMock.createNiceMock(AccountIndexer.class);
-    accountIndexer.index(accountId);
-    EasyMock.expectLastCall();
-    accountIndexer.index(accountId2);
-    EasyMock.expectLastCall();
-    EasyMock.replay(accountIndexer);
+    AccountCache accountCache = mock(AccountCache.class);
+    AccountIndexer accountIndexer = mock(AccountIndexer.class);
 
     // Non-user branch because it's not in All-Users.
-    Ref nonUserBranch = createRef(RefNames.refsUsers(new Account.Id(3)));
+    Ref nonUserBranch = createRef(RefNames.refsUsers(Account.id(3)));
 
     try (ProjectResetter resetProject =
         builder(null, accountCache, accountIndexer, null, null, null, null)
@@ -350,34 +323,27 @@
       createRef(allUsersRepo, RefNames.refsUsers(accountId2));
     }
 
-    EasyMock.verify(accountCache, accountIndexer);
+    verify(accountCache).evict(accountId);
+    verify(accountCache).evict(accountId2);
+    verify(accountIndexer).index(accountId);
+    verify(accountIndexer).index(accountId2);
+    verifyNoMoreInteractions(accountCache, accountIndexer);
   }
 
   @Test
   public void accountEvictionIfExternalIdsBranchIsDeleted() throws Exception {
-    Account.Id accountId = new Account.Id(1);
-    Project.NameKey allUsers = new Project.NameKey(AllUsersNameProvider.DEFAULT);
+    Account.Id accountId = Account.id(1);
+    Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
     Repository allUsersRepo = repoManager.createRepository(allUsers);
     createRef(allUsersRepo, RefNames.refsUsers(accountId));
 
-    Account.Id accountId2 = new Account.Id(2);
+    Account.Id accountId2 = Account.id(2);
 
-    AccountCache accountCache = EasyMock.createNiceMock(AccountCache.class);
-    accountCache.evict(accountId);
-    EasyMock.expectLastCall();
-    accountCache.evict(accountId2);
-    EasyMock.expectLastCall();
-    EasyMock.replay(accountCache);
-
-    AccountIndexer accountIndexer = EasyMock.createNiceMock(AccountIndexer.class);
-    accountIndexer.index(accountId);
-    EasyMock.expectLastCall();
-    accountIndexer.index(accountId2);
-    EasyMock.expectLastCall();
-    EasyMock.replay(accountIndexer);
+    AccountCache accountCache = mock(AccountCache.class);
+    AccountIndexer accountIndexer = mock(AccountIndexer.class);
 
     // Non-user branch because it's not in All-Users.
-    Ref nonUserBranch = createRef(RefNames.refsUsers(new Account.Id(3)));
+    Ref nonUserBranch = createRef(RefNames.refsUsers(Account.id(3)));
 
     try (ProjectResetter resetProject =
         builder(null, accountCache, accountIndexer, null, null, null, null)
@@ -387,19 +353,20 @@
       createRef(allUsersRepo, RefNames.refsUsers(accountId2));
     }
 
-    EasyMock.verify(accountCache, accountIndexer);
+    verify(accountCache).evict(accountId);
+    verify(accountCache).evict(accountId2);
+    verify(accountIndexer).index(accountId);
+    verify(accountIndexer).index(accountId2);
+    verifyNoMoreInteractions(accountCache, accountIndexer);
   }
 
   @Test
   public void accountEvictionFromAccountCreatorIfUserBranchIsDeleted() throws Exception {
-    Account.Id accountId = new Account.Id(1);
-    Project.NameKey allUsers = new Project.NameKey(AllUsersNameProvider.DEFAULT);
+    Account.Id accountId = Account.id(1);
+    Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
     Repository allUsersRepo = repoManager.createRepository(allUsers);
 
-    AccountCreator accountCreator = EasyMock.createNiceMock(AccountCreator.class);
-    accountCreator.evict(ImmutableSet.of(accountId));
-    EasyMock.expectLastCall();
-    EasyMock.replay(accountCreator);
+    AccountCreator accountCreator = mock(AccountCreator.class);
 
     try (ProjectResetter resetProject =
         builder(accountCreator, null, null, null, null, null, null)
@@ -407,29 +374,20 @@
       createRef(allUsersRepo, RefNames.refsUsers(accountId));
     }
 
-    EasyMock.verify(accountCreator);
+    verify(accountCreator, only()).evict(ImmutableSet.of(accountId));
   }
 
   @Test
   public void groupEviction() throws Exception {
-    AccountGroup.UUID uuid1 = new AccountGroup.UUID("abcd1");
-    AccountGroup.UUID uuid2 = new AccountGroup.UUID("abcd2");
-    AccountGroup.UUID uuid3 = new AccountGroup.UUID("abcd3");
-    Project.NameKey allUsers = new Project.NameKey(AllUsersNameProvider.DEFAULT);
+    AccountGroup.UUID uuid1 = AccountGroup.uuid("abcd1");
+    AccountGroup.UUID uuid2 = AccountGroup.uuid("abcd2");
+    AccountGroup.UUID uuid3 = AccountGroup.uuid("abcd3");
+    Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
     Repository allUsersRepo = repoManager.createRepository(allUsers);
 
-    GroupCache cache = EasyMock.createNiceMock(GroupCache.class);
-    GroupIndexer indexer = EasyMock.createNiceMock(GroupIndexer.class);
-    GroupIncludeCache includeCache = EasyMock.createNiceMock(GroupIncludeCache.class);
-    cache.evict(uuid2);
-    indexer.index(uuid2);
-    includeCache.evictParentGroupsOf(uuid2);
-    cache.evict(uuid3);
-    indexer.index(uuid3);
-    includeCache.evictParentGroupsOf(uuid3);
-    EasyMock.expectLastCall();
-
-    EasyMock.replay(cache, indexer);
+    GroupCache cache = mock(GroupCache.class);
+    GroupIndexer indexer = mock(GroupIndexer.class);
+    GroupIncludeCache includeCache = mock(GroupIncludeCache.class);
 
     createRef(allUsersRepo, RefNames.refsGroups(uuid1));
     Ref ref2 = createRef(allUsersRepo, RefNames.refsGroups(uuid2));
@@ -440,7 +398,13 @@
       createRef(allUsersRepo, RefNames.refsGroups(uuid3));
     }
 
-    EasyMock.verify(cache, indexer);
+    verify(cache).evict(uuid2);
+    verify(indexer).index(uuid2);
+    verify(includeCache).evictParentGroupsOf(uuid2);
+    verify(cache).evict(uuid3);
+    verify(indexer).index(uuid3);
+    verify(includeCache).evictParentGroupsOf(uuid3);
+    verifyNoMoreInteractions(cache, indexer, includeCache);
   }
 
   private Ref createRef(String ref) throws IOException {
diff --git a/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java b/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java
index bf387fd..448629c 100644
--- a/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java
+++ b/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java
@@ -16,21 +16,19 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.account.UniversalGroupBackend;
 import com.google.gerrit.server.group.testing.TestGroupBackend;
 import com.google.inject.Inject;
 import org.junit.Test;
 
 public class TestGroupBackendTest extends AbstractDaemonTest {
-  @Inject private DynamicSet<GroupBackend> groupBackends;
   @Inject private UniversalGroupBackend universalGroupBackend;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   private final TestGroupBackend testGroupBackend = new TestGroupBackend();
-  private final AccountGroup.UUID testUUID = new AccountGroup.UUID("testbackend:test");
+  private final AccountGroup.UUID testUUID = AccountGroup.uuid("testbackend:test");
 
   @Test
   public void handlesTestGroup() throws Exception {
@@ -39,17 +37,14 @@
 
   @Test
   public void universalGroupBackendHandlesTestGroup() throws Exception {
-    RegistrationHandle registrationHandle = groupBackends.add("gerrit", testGroupBackend);
-    try {
+    try (Registration registration = extensionRegistry.newRegistration().add(testGroupBackend)) {
       assertThat(universalGroupBackend.handles(testUUID)).isTrue();
-    } finally {
-      registrationHandle.remove();
     }
   }
 
   @Test
   public void doesNotHandleLDAP() throws Exception {
-    assertThat(testGroupBackend.handles(new AccountGroup.UUID("ldap:1234"))).isFalse();
+    assertThat(testGroupBackend.handles(AccountGroup.uuid("ldap:1234"))).isFalse();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/annotation/BUILD b/javatests/com/google/gerrit/acceptance/annotation/BUILD
index 5476bb6..8dd99c9 100644
--- a/javatests/com/google/gerrit/acceptance/annotation/BUILD
+++ b/javatests/com/google/gerrit/acceptance/annotation/BUILD
@@ -4,4 +4,5 @@
     srcs = glob(["*.java"]),
     group = "annotation",
     labels = ["annotation"],
+    deps = ["//java/com/google/gerrit/server/util/time"],
 )
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java b/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java
new file mode 100644
index 0000000..ecfe3f5
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java
@@ -0,0 +1,57 @@
+// 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.annotation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+public class UseClockStepTest extends AbstractDaemonTest {
+  @Test
+  @UseClockStep
+  public void useClockStepWithDefaults() {
+    long firstTimestamp = TimeUtil.nowMs();
+    long secondTimestamp = TimeUtil.nowMs();
+    assertThat(secondTimestamp - firstTimestamp).isEqualTo(1000);
+  }
+
+  @Test
+  @UseClockStep(clockStepUnit = TimeUnit.MINUTES)
+  public void useClockStepWithTimeUnit() {
+    long firstTimestamp = TimeUtil.nowMs();
+    long secondTimestamp = TimeUtil.nowMs();
+    assertThat(secondTimestamp - firstTimestamp).isEqualTo(60 * 1000);
+  }
+
+  @Test
+  @UseClockStep(clockStep = 5)
+  public void useClockStepWithClockStep() {
+    long firstTimestamp = TimeUtil.nowMs();
+    long secondTimestamp = TimeUtil.nowMs();
+    assertThat(secondTimestamp - firstTimestamp).isEqualTo(5 * 1000);
+  }
+
+  @Test
+  @UseClockStep(startAtEpoch = true)
+  public void useClockStepWithStartAtEpoch() {
+    assertThat(TimeUtil.nowTs()).isEqualTo(Timestamp.from(Instant.EPOCH));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseSystemTimeTest.java b/javatests/com/google/gerrit/acceptance/annotation/UseSystemTimeTest.java
new file mode 100644
index 0000000..7480200
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/annotation/UseSystemTimeTest.java
@@ -0,0 +1,34 @@
+// 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.annotation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseSystemTime;
+import com.google.gerrit.server.util.time.TimeUtil;
+import org.junit.Test;
+
+@UseClockStep
+public class UseSystemTimeTest extends AbstractDaemonTest {
+  @Test
+  @UseSystemTime
+  public void useSystemTimeAlthoughClassIsAnnotatedWithUseClockStep() {
+    long firstTimestamp = TimeUtil.nowMs();
+    long secondTimestamp = TimeUtil.nowMs();
+    assertThat(secondTimestamp - firstTimestamp).isLessThan(1000);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseTimezoneTest.java b/javatests/com/google/gerrit/acceptance/annotation/UseTimezoneTest.java
new file mode 100644
index 0000000..abf5eda
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/annotation/UseTimezoneTest.java
@@ -0,0 +1,35 @@
+// 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.annotation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.UseTimezone;
+import org.junit.Test;
+
+public class UseTimezoneTest extends AbstractDaemonTest {
+  @Test
+  @UseTimezone(timezone = "US/Eastern")
+  public void usEastern() {
+    assertThat(System.getProperty("user.timezone")).isEqualTo("US/Eastern");
+  }
+
+  @Test
+  @UseTimezone(timezone = "UTC")
+  public void utc() {
+    assertThat(System.getProperty("user.timezone")).isEqualTo("UTC");
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 7dcff20..5719c7f 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -17,10 +17,14 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.deleteRef;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
 import static com.google.gerrit.gpg.testing.TestKeys.allValidKeys;
@@ -33,11 +37,17 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
 
 import com.github.rholder.retry.StopStrategies;
 import com.google.common.cache.LoadingCache;
@@ -48,14 +58,17 @@
 import com.google.common.collect.Iterables;
 import com.google.common.io.BaseEncoding;
 import com.google.common.truth.Correspondence;
-import com.google.common.truth.Correspondence.BinaryPredicate;
 import com.google.common.util.concurrent.AtomicLongMap;
 import com.google.common.util.concurrent.Runnables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountIndexedCounter;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
@@ -68,6 +81,12 @@
 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.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
@@ -90,10 +109,9 @@
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
-import com.google.gerrit.extensions.events.AccountIndexedListener;
+import com.google.gerrit.extensions.events.AccountActivationListener;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -106,19 +124,11 @@
 import com.google.gerrit.gpg.testing.TestKey;
 import com.google.gerrit.httpd.CacheBasedWebSession;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountProperties;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.Emails;
-import com.google.gerrit.server.account.ProjectWatches;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
@@ -128,17 +138,17 @@
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.index.account.StalenessChecker;
 import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.server.validators.AccountActivationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
@@ -147,7 +157,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -170,6 +179,7 @@
 import org.bouncycastle.bcpg.ArmoredOutputStream;
 import org.bouncycastle.openpgp.PGPPublicKey;
 import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.TransportException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -208,8 +218,6 @@
 
   @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
   @Inject private AccountIndexer accountIndexer;
-  @Inject private DynamicSet<AccountIndexedListener> accountIndexedListeners;
-  @Inject private DynamicSet<GitReferenceUpdatedListener> refUpdateListeners;
   @Inject private ExternalIdNotes.Factory extIdNotesFactory;
   @Inject private ExternalIds externalIds;
   @Inject private GitReferenceUpdated gitReferenceUpdated;
@@ -222,53 +230,21 @@
   @Inject private Sequences seq;
   @Inject private StalenessChecker stalenessChecker;
   @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   @Inject protected Emails emails;
 
   @Inject
   @Named("accounts")
-  private LoadingCache<Account.Id, Optional<AccountState>> accountsCache;
+  private LoadingCache<Account.Id, AccountState> accountsCache;
 
   @Inject private AccountOperations accountOperations;
 
-  @Inject
-  private DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners;
-
   @Inject protected GroupOperations groupOperations;
 
-  private AccountIndexedCounter accountIndexedCounter;
-  private RegistrationHandle accountIndexEventCounterHandle;
-  private RefUpdateCounter refUpdateCounter;
-  private RegistrationHandle refUpdateCounterHandle;
   private BasicCookieStore httpCookieStore;
   private CloseableHttpClient httpclient;
 
-  @Before
-  public void addAccountIndexEventCounter() {
-    accountIndexedCounter = new AccountIndexedCounter();
-    accountIndexEventCounterHandle = accountIndexedListeners.add("gerrit", accountIndexedCounter);
-  }
-
-  @After
-  public void removeAccountIndexEventCounter() {
-    if (accountIndexEventCounterHandle != null) {
-      accountIndexEventCounterHandle.remove();
-    }
-  }
-
-  @Before
-  public void addRefUpdateCounter() {
-    refUpdateCounter = new RefUpdateCounter();
-    refUpdateCounterHandle = refUpdateListeners.add("gerrit", refUpdateCounter);
-  }
-
-  @After
-  public void removeRefUpdateCounter() {
-    if (refUpdateCounterHandle != null) {
-      refUpdateCounterHandle.remove();
-    }
-  }
-
   @After
   public void clearPublicKeyStore() throws Exception {
     try (Repository repo = repoManager.openRepository(allUsers)) {
@@ -327,11 +303,14 @@
 
   @Test
   public void createByAccountCreator() throws Exception {
-    Account.Id accountId = createByAccountCreator(1);
-    refUpdateCounter.assertRefUpdateFor(
-        RefUpdateCounter.projectRef(allUsers, RefNames.refsUsers(accountId)),
-        RefUpdateCounter.projectRef(allUsers, RefNames.REFS_EXTERNAL_IDS),
-        RefUpdateCounter.projectRef(allUsers, RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS));
+    RefUpdateCounter refUpdateCounter = new RefUpdateCounter();
+    try (Registration registration = extensionRegistry.newRegistration().add(refUpdateCounter)) {
+      Account.Id accountId = createByAccountCreator(1);
+      refUpdateCounter.assertRefUpdateFor(
+          RefUpdateCounter.projectRef(allUsers, RefNames.refsUsers(accountId)),
+          RefUpdateCounter.projectRef(allUsers, RefNames.REFS_EXTERNAL_IDS),
+          RefUpdateCounter.projectRef(allUsers, RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS));
+    }
   }
 
   @Test
@@ -350,42 +329,54 @@
   }
 
   private Account.Id createByAccountCreator(int expectedAccountReindexCalls) throws Exception {
-    String name = "foo";
-    TestAccount foo = accountCreator.create(name);
-    AccountInfo info = gApi.accounts().id(foo.id().get()).get();
-    assertThat(info.username).isEqualTo(name);
-    assertThat(info.name).isEqualTo(name);
-    accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls);
-    assertUserBranch(foo.id(), name, null);
-    return foo.id();
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String name = "foo";
+      TestAccount foo = accountCreator.create(name);
+      AccountInfo info = gApi.accounts().id(foo.id().get()).get();
+      assertThat(info.username).isEqualTo(name);
+      assertThat(info.name).isEqualTo(name);
+      accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls);
+      assertUserBranch(foo.id(), name, null);
+      return foo.id();
+    }
   }
 
   @Test
   public void createAnonymousCowardByAccountCreator() throws Exception {
-    TestAccount anonymousCoward = accountCreator.create();
-    accountIndexedCounter.assertReindexOf(anonymousCoward);
-    assertUserBranchWithoutAccountConfig(anonymousCoward.id());
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestAccount anonymousCoward = accountCreator.create();
+      accountIndexedCounter.assertReindexOf(anonymousCoward);
+      assertUserBranchWithoutAccountConfig(anonymousCoward.id());
+    }
   }
 
   @Test
   public void create() throws Exception {
-    AccountInput input = new AccountInput();
-    input.username = "foo";
-    input.name = "Foo";
-    input.email = "foo@example.com";
-    AccountInfo accountInfo = gApi.accounts().create(input).get();
-    assertThat(accountInfo._accountId).isNotNull();
-    assertThat(accountInfo.username).isEqualTo(input.username);
-    assertThat(accountInfo.name).isEqualTo(input.name);
-    assertThat(accountInfo.email).isEqualTo(input.email);
-    assertThat(accountInfo.status).isNull();
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      AccountInput input = new AccountInput();
+      input.username = "foo";
+      input.name = "Foo";
+      input.email = "foo@example.com";
+      AccountInfo accountInfo = gApi.accounts().create(input).get();
+      assertThat(accountInfo._accountId).isNotNull();
+      assertThat(accountInfo.username).isEqualTo(input.username);
+      assertThat(accountInfo.name).isEqualTo(input.name);
+      assertThat(accountInfo.email).isEqualTo(input.email);
+      assertThat(accountInfo.status).isNull();
 
-    Account.Id accountId = new Account.Id(accountInfo._accountId);
-    accountIndexedCounter.assertReindexOf(accountId, 1);
-    assertThat(externalIds.byAccount(accountId))
-        .containsExactly(
-            ExternalId.createUsername(input.username, accountId, null),
-            ExternalId.createEmail(accountId, input.email));
+      Account.Id accountId = Account.id(accountInfo._accountId);
+      accountIndexedCounter.assertReindexOf(accountId, 1);
+      assertThat(externalIds.byAccount(accountId))
+          .containsExactly(
+              ExternalId.createUsername(input.username, accountId, null),
+              ExternalId.createEmail(accountId, input.email));
+    }
   }
 
   @Test
@@ -393,9 +384,11 @@
     AccountInput input = new AccountInput();
     input.username = admin.username();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("username '" + admin.username() + "' already exists");
-    gApi.accounts().create(input);
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.accounts().create(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("username '" + admin.username() + "' already exists");
   }
 
   @Test
@@ -404,15 +397,15 @@
     input.username = "foo";
     input.email = admin.email();
 
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("email '" + admin.email() + "' already exists");
-    gApi.accounts().create(input);
+    UnprocessableEntityException thrown =
+        assertThrows(UnprocessableEntityException.class, () -> gApi.accounts().create(input));
+    assertThat(thrown).hasMessageThat().contains("email '" + admin.email() + "' already exists");
   }
 
   @Test
   public void commitMessageOnAccountUpdates() throws Exception {
     AccountsUpdate au = accountsUpdateProvider.get();
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     au.insert("Create Test Account", accountId, u -> {});
     assertLastCommitMessageOfUserBranch(accountId, "Create Test Account");
 
@@ -431,39 +424,37 @@
   }
 
   @Test
+  @UseClockStep
   public void createAtomically() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    try {
-      Account.Id accountId = new Account.Id(seq.nextAccountId());
-      String fullName = "Foo";
-      ExternalId extId = ExternalId.createEmail(accountId, "foo@example.com");
-      AccountState accountState =
-          accountsUpdateProvider
-              .get()
-              .insert(
-                  "Create Account Atomically",
-                  accountId,
-                  u -> u.setFullName(fullName).addExternalId(extId));
-      assertThat(accountState.getAccount().getFullName()).isEqualTo(fullName);
+    Account.Id accountId = Account.id(seq.nextAccountId());
+    String fullName = "Foo";
+    ExternalId extId = ExternalId.createEmail(accountId, "foo@example.com");
+    AccountState accountState =
+        accountsUpdateProvider
+            .get()
+            .insert(
+                "Create Account Atomically",
+                accountId,
+                u -> u.setFullName(fullName).addExternalId(extId));
+    assertThat(accountState.account().fullName()).isEqualTo(fullName);
 
-      AccountInfo info = gApi.accounts().id(accountId.get()).get();
-      assertThat(info.name).isEqualTo(fullName);
+    AccountInfo info = gApi.accounts().id(accountId.get()).get();
+    assertThat(info.name).isEqualTo(fullName);
 
-      List<EmailInfo> emails = gApi.accounts().id(accountId.get()).getEmails();
-      assertThat(emails.stream().map(e -> e.email).collect(toSet())).containsExactly(extId.email());
+    List<EmailInfo> emails = gApi.accounts().id(accountId.get()).getEmails();
+    assertThat(emails.stream().map(e -> e.email).collect(toSet())).containsExactly(extId.email());
 
-      RevCommit commitUserBranch = getRemoteHead(allUsers, RefNames.refsUsers(accountId));
-      RevCommit commitRefsMetaExternalIds = getRemoteHead(allUsers, RefNames.REFS_EXTERNAL_IDS);
-      assertThat(commitUserBranch.getCommitTime())
-          .isEqualTo(commitRefsMetaExternalIds.getCommitTime());
-    } finally {
-      TestTimeUtil.useSystemTime();
-    }
+    RevCommit commitUserBranch =
+        projectOperations.project(allUsers).getHead(RefNames.refsUsers(accountId));
+    RevCommit commitRefsMetaExternalIds =
+        projectOperations.project(allUsers).getHead(RefNames.REFS_EXTERNAL_IDS);
+    assertThat(commitUserBranch.getCommitTime())
+        .isEqualTo(commitRefsMetaExternalIds.getCommitTime());
   }
 
   @Test
   public void updateNonExistingAccount() throws Exception {
-    Account.Id nonExistingAccountId = new Account.Id(999999);
+    Account.Id nonExistingAccountId = Account.id(999999);
     AtomicBoolean consumerCalled = new AtomicBoolean();
     Optional<AccountState> accountState =
         accountsUpdateProvider
@@ -485,9 +476,9 @@
             .get()
             .update("Set status", anonymousCoward.id(), u -> u.setStatus(status));
     assertThat(accountState).isPresent();
-    Account account = accountState.get().getAccount();
-    assertThat(account.getFullName()).isNull();
-    assertThat(account.getStatus()).isEqualTo(status);
+    Account account = accountState.get().account();
+    assertThat(account.fullName()).isNull();
+    assertThat(account.status()).isEqualTo(status);
     assertUserBranch(anonymousCoward.id(), null, status);
   }
 
@@ -504,7 +495,7 @@
       assertThat(ref).isNotNull();
       RevCommit c = rw.parseCommit(ref.getObjectId());
       long timestampDiffMs =
-          Math.abs(c.getCommitTime() * 1000L - getAccount(accountId).getRegisteredOn().getTime());
+          Math.abs(c.getCommitTime() * 1000L - getAccount(accountId).registeredOn().getTime());
       assertThat(timestampDiffMs).isAtMost(SECONDS.toMillis(1));
 
       // Check the 'account.config' file.
@@ -513,10 +504,11 @@
           assertThat(tw).isNotNull();
           Config cfg = new Config();
           cfg.fromText(new String(or.open(tw.getObjectId(0), OBJ_BLOB).getBytes(), UTF_8));
-          assertThat(
-                  cfg.getString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_FULL_NAME))
+          assertThat(cfg)
+              .stringValue(AccountProperties.ACCOUNT, null, AccountProperties.KEY_FULL_NAME)
               .isEqualTo(name);
-          assertThat(cfg.getString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS))
+          assertThat(cfg)
+              .stringValue(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS)
               .isEqualTo(status);
         } else {
           // No account properties were set, hence an 'account.config' file was not created.
@@ -528,76 +520,98 @@
 
   @Test
   public void get() throws Exception {
-    AccountInfo info = gApi.accounts().id("admin").get();
-    assertThat(info.name).isEqualTo("Administrator");
-    assertThat(info.email).isEqualTo("admin@example.com");
-    assertThat(info.username).isEqualTo("admin");
-    accountIndexedCounter.assertNoReindex();
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      AccountInfo info = gApi.accounts().id("admin").get();
+      assertThat(info.name).isEqualTo("Administrator");
+      assertThat(info.email).isEqualTo("admin@example.com");
+      assertThat(info.username).isEqualTo("admin");
+      accountIndexedCounter.assertNoReindex();
+    }
   }
 
   @Test
   public void getByIntId() throws Exception {
-    AccountInfo info = gApi.accounts().id("admin").get();
-    AccountInfo infoByIntId = gApi.accounts().id(info._accountId).get();
-    assertThat(info.name).isEqualTo(infoByIntId.name);
-    accountIndexedCounter.assertNoReindex();
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      AccountInfo info = gApi.accounts().id("admin").get();
+      AccountInfo infoByIntId = gApi.accounts().id(info._accountId).get();
+      assertThat(info.name).isEqualTo(infoByIntId.name);
+      accountIndexedCounter.assertNoReindex();
+    }
   }
 
   @Test
   public void self() throws Exception {
-    AccountInfo info = gApi.accounts().self().get();
-    assertUser(info, admin);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      AccountInfo info = gApi.accounts().self().get();
+      assertUser(info, admin);
 
-    info = gApi.accounts().id("self").get();
-    assertUser(info, admin);
-    accountIndexedCounter.assertNoReindex();
+      info = gApi.accounts().id("self").get();
+      assertUser(info, admin);
+      accountIndexedCounter.assertNoReindex();
+    }
   }
 
   @Test
   public void active() throws Exception {
-    int id = gApi.accounts().id("user").get()._accountId;
-    assertThat(gApi.accounts().id("user").getActive()).isTrue();
-    gApi.accounts().id("user").setActive(false);
-    accountIndexedCounter.assertReindexOf(user);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      int id = gApi.accounts().id("user").get()._accountId;
+      assertThat(gApi.accounts().id("user").getActive()).isTrue();
+      gApi.accounts().id("user").setActive(false);
+      accountIndexedCounter.assertReindexOf(user);
 
-    // Inactive users may only be resolved by ID.
-    try {
-      gApi.accounts().id("user");
-      assert_().fail("expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e)
+      // Inactive users may only be resolved by ID.
+      ResourceNotFoundException thrown =
+          assertThrows(ResourceNotFoundException.class, () -> gApi.accounts().id("user"));
+      assertThat(thrown)
           .hasMessageThat()
           .isEqualTo(
               "Account 'user' only matches inactive accounts. To use an inactive account, retry"
                   + " with one of the following exact account IDs:\n"
                   + id
                   + ": User <user@example.com>");
-    }
-    assertThat(gApi.accounts().id(id).getActive()).isFalse();
+      assertThat(gApi.accounts().id(id).getActive()).isFalse();
 
-    gApi.accounts().id(id).setActive(true);
-    assertThat(gApi.accounts().id("user").getActive()).isTrue();
-    accountIndexedCounter.assertReindexOf(user);
+      gApi.accounts().id(id).setActive(true);
+      assertThat(gApi.accounts().id("user").getActive()).isTrue();
+      accountIndexedCounter.assertReindexOf(user);
+    }
   }
 
   @Test
   public void shouldAllowQueryByEmailForInactiveUser() throws Exception {
-    Account.Id activatableAccountId =
-        accountOperations.newAccount().inactive().preferredEmail("foo@activatable.com").create();
-    accountIndexedCounter.assertReindexOf(activatableAccountId, 1);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      Account.Id activatableAccountId =
+          accountOperations.newAccount().inactive().preferredEmail("foo@activatable.com").create();
+      accountIndexedCounter.assertReindexOf(activatableAccountId, 1);
+    }
 
     gApi.changes().query("owner:foo@activatable.com").get();
   }
 
   @Test
   public void shouldAllowQueryByUserNameForInactiveUser() throws Exception {
-    Account.Id activatableAccountId =
-        accountOperations.newAccount().inactive().username("foo").create();
-    accountIndexedCounter.assertReindexOf(activatableAccountId, 1);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      Account.Id activatableAccountId =
+          accountOperations.newAccount().inactive().username("foo").create();
+      accountIndexedCounter.assertReindexOf(activatableAccountId, 1);
+    }
 
     gApi.changes().query("owner:foo").get();
   }
 
+  @Test
   @GerritConfig(name = "auth.type", value = "DEVELOPMENT_BECOME_ANY_ACCOUNT")
   public void activeUserGetSessionCookieOnLogin() throws Exception {
     Integer accountId = accountIdApi().get()._accountId;
@@ -640,90 +654,98 @@
         accountOperations.newAccount().inactive().preferredEmail("foo@activatable.com").create();
     Account.Id deactivatableAccountId =
         accountOperations.newAccount().preferredEmail("foo@deactivatable.com").create();
-    RegistrationHandle registrationHandle =
-        accountActivationValidationListeners.add(
-            "gerrit",
-            new AccountActivationValidationListener() {
-              @Override
-              public void validateActivation(AccountState account) throws ValidationException {
-                String preferredEmail = account.getAccount().getPreferredEmail();
-                if (preferredEmail == null || !preferredEmail.endsWith("@activatable.com")) {
-                  throw new ValidationException("not allowed to active account");
-                }
-              }
 
-              @Override
-              public void validateDeactivation(AccountState account) throws ValidationException {
-                String preferredEmail = account.getAccount().getPreferredEmail();
-                if (preferredEmail == null || !preferredEmail.endsWith("@deactivatable.com")) {
-                  throw new ValidationException("not allowed to deactive account");
-                }
-              }
-            });
-    try {
+    AccountActivationValidationListener validationListener =
+        new AccountActivationValidationListener() {
+          @Override
+          public void validateActivation(AccountState account) throws ValidationException {
+            String preferredEmail = account.account().preferredEmail();
+            if (preferredEmail == null || !preferredEmail.endsWith("@activatable.com")) {
+              throw new ValidationException("not allowed to active account");
+            }
+          }
+
+          @Override
+          public void validateDeactivation(AccountState account) throws ValidationException {
+            String preferredEmail = account.account().preferredEmail();
+            if (preferredEmail == null || !preferredEmail.endsWith("@deactivatable.com")) {
+              throw new ValidationException("not allowed to deactive account");
+            }
+          }
+        };
+
+    AccountActivationListener listener = mock(AccountActivationListener.class);
+
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(validationListener).add(listener)) {
       /* Test account that can be activated, but not deactivated */
       // Deactivate account that is already inactive
-      try {
-        gApi.accounts().id(activatableAccountId.get()).setActive(false);
-        fail("Expected exception");
-      } catch (ResourceConflictException e) {
-        assertThat(e.getMessage()).isEqualTo("account not active");
-      }
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.accounts().id(activatableAccountId.get()).setActive(false));
+      assertThat(thrown).hasMessageThat().isEqualTo("account not active");
       assertThat(accountOperations.account(activatableAccountId).get().active()).isFalse();
+      verifyZeroInteractions(listener);
 
       // Activate account that can be activated
       gApi.accounts().id(activatableAccountId.get()).setActive(true);
       assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
+      verify(listener).onAccountActivated(activatableAccountId.get());
+      verifyNoMoreInteractions(listener);
 
       // Activate account that is already active
       gApi.accounts().id(activatableAccountId.get()).setActive(true);
       assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
+      verifyZeroInteractions(listener);
 
       // Try deactivating account that cannot be deactivated
-      try {
-        gApi.accounts().id(activatableAccountId.get()).setActive(false);
-        fail("Expected exception");
-      } catch (ResourceConflictException e) {
-        assertThat(e.getMessage()).isEqualTo("not allowed to deactive account");
-      }
+      thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.accounts().id(activatableAccountId.get()).setActive(false));
+      assertThat(thrown).hasMessageThat().isEqualTo("not allowed to deactive account");
       assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
+      verifyZeroInteractions(listener);
 
       /* Test account that can be deactivated, but not activated */
       // Activate account that is already inactive
       gApi.accounts().id(deactivatableAccountId.get()).setActive(true);
       assertThat(accountOperations.account(deactivatableAccountId).get().active()).isTrue();
+      verifyZeroInteractions(listener);
 
       // Deactivate account that can be deactivated
       gApi.accounts().id(deactivatableAccountId.get()).setActive(false);
       assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
+      verify(listener).onAccountDeactivated(deactivatableAccountId.get());
+      verifyNoMoreInteractions(listener);
 
       // Deactivate account that is already inactive
-      try {
-        gApi.accounts().id(deactivatableAccountId.get()).setActive(false);
-        fail("Expected exception");
-      } catch (ResourceConflictException e) {
-        assertThat(e.getMessage()).isEqualTo("account not active");
-      }
+      thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.accounts().id(deactivatableAccountId.get()).setActive(false));
+      assertThat(thrown).hasMessageThat().isEqualTo("account not active");
       assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
+      verifyZeroInteractions(listener);
 
       // Try activating account that cannot be activated
-      try {
-        gApi.accounts().id(deactivatableAccountId.get()).setActive(true);
-        fail("Expected exception");
-      } catch (ResourceConflictException e) {
-        assertThat(e.getMessage()).isEqualTo("not allowed to active account");
-      }
+      thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.accounts().id(deactivatableAccountId.get()).setActive(true));
+      assertThat(thrown).hasMessageThat().isEqualTo("not allowed to active account");
       assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
-    } finally {
-      registrationHandle.remove();
+      verifyZeroInteractions(listener);
     }
   }
 
   @Test
   public void deactivateSelf() throws Exception {
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("cannot deactivate own account");
-    gApi.accounts().self().setActive(false);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.accounts().self().setActive(false));
+    assertThat(thrown).hasMessageThat().contains("cannot deactivate own account");
   }
 
   @Test
@@ -732,107 +754,125 @@
     assertThat(gApi.accounts().id("user").getActive()).isTrue();
     gApi.accounts().id("user").setActive(false);
     assertThat(gApi.accounts().id(id).getActive()).isFalse();
-    try {
-      gApi.accounts().id(id).setActive(false);
-      fail("Expected exception");
-    } catch (ResourceConflictException e) {
-      assertThat(e.getMessage()).isEqualTo("account not active");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.accounts().id(id).setActive(false));
+    assertThat(thrown).hasMessageThat().isEqualTo("account not active");
     gApi.accounts().id(id).setActive(true);
   }
 
   @Test
   public void starUnstarChange() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String triplet = project.get() + "~master~" + r.getChangeId();
-    refUpdateCounter.clear();
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    RefUpdateCounter refUpdateCounter = new RefUpdateCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter).add(refUpdateCounter)) {
+      PushOneCommit.Result r = createChange();
+      String triplet = project.get() + "~master~" + r.getChangeId();
+      refUpdateCounter.clear();
 
-    gApi.accounts().self().starChange(triplet);
-    ChangeInfo change = info(triplet);
-    assertThat(change.starred).isTrue();
-    assertThat(change.stars).contains(DEFAULT_LABEL);
-    refUpdateCounter.assertRefUpdateFor(
-        RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
+      gApi.accounts().self().starChange(triplet);
+      ChangeInfo change = info(triplet);
+      assertThat(change.starred).isTrue();
+      assertThat(change.stars).contains(DEFAULT_LABEL);
+      refUpdateCounter.assertRefUpdateFor(
+          RefUpdateCounter.projectRef(
+              allUsers, RefNames.refsStarredChanges(Change.id(change._number), admin.id())));
 
-    gApi.accounts().self().unstarChange(triplet);
-    change = info(triplet);
-    assertThat(change.starred).isNull();
-    assertThat(change.stars).isNull();
-    refUpdateCounter.assertRefUpdateFor(
-        RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
+      gApi.accounts().self().unstarChange(triplet);
+      change = info(triplet);
+      assertThat(change.starred).isNull();
+      assertThat(change.stars).isNull();
+      refUpdateCounter.assertRefUpdateFor(
+          RefUpdateCounter.projectRef(
+              allUsers, RefNames.refsStarredChanges(Change.id(change._number), admin.id())));
 
-    accountIndexedCounter.assertNoReindex();
+      accountIndexedCounter.assertNoReindex();
+    }
   }
 
   @Test
   public void starUnstarChangeWithLabels() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String triplet = project.get() + "~master~" + r.getChangeId();
-    refUpdateCounter.clear();
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    RefUpdateCounter refUpdateCounter = new RefUpdateCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter).add(refUpdateCounter)) {
+      PushOneCommit.Result r = createChange();
+      String triplet = project.get() + "~master~" + r.getChangeId();
+      refUpdateCounter.clear();
 
-    assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
-    assertThat(gApi.accounts().self().getStarredChanges()).isEmpty();
+      assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
+      assertThat(gApi.accounts().self().getStarredChanges()).isEmpty();
 
-    gApi.accounts()
-        .self()
-        .setStars(triplet, new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "red", "blue")));
-    ChangeInfo change = info(triplet);
-    assertThat(change.starred).isTrue();
-    assertThat(change.stars).containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
-    assertThat(gApi.accounts().self().getStars(triplet))
-        .containsExactly("blue", "red", DEFAULT_LABEL)
-        .inOrder();
-    List<ChangeInfo> starredChanges = gApi.accounts().self().getStarredChanges();
-    assertThat(starredChanges).hasSize(1);
-    ChangeInfo starredChange = starredChanges.get(0);
-    assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
-    assertThat(starredChange.starred).isTrue();
-    assertThat(starredChange.stars).containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
-    refUpdateCounter.assertRefUpdateFor(
-        RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
+      gApi.accounts()
+          .self()
+          .setStars(triplet, new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "red", "blue")));
+      ChangeInfo change = info(triplet);
+      assertThat(change.starred).isTrue();
+      assertThat(change.stars).containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
+      assertThat(gApi.accounts().self().getStars(triplet))
+          .containsExactly("blue", "red", DEFAULT_LABEL)
+          .inOrder();
+      List<ChangeInfo> starredChanges = gApi.accounts().self().getStarredChanges();
+      assertThat(starredChanges).hasSize(1);
+      ChangeInfo starredChange = starredChanges.get(0);
+      assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
+      assertThat(starredChange.starred).isTrue();
+      assertThat(starredChange.stars).containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
+      refUpdateCounter.assertRefUpdateFor(
+          RefUpdateCounter.projectRef(
+              allUsers, RefNames.refsStarredChanges(Change.id(change._number), admin.id())));
 
-    gApi.accounts()
-        .self()
-        .setStars(
-            triplet,
-            new StarsInput(ImmutableSet.of("yellow"), ImmutableSet.of(DEFAULT_LABEL, "blue")));
-    change = info(triplet);
-    assertThat(change.starred).isNull();
-    assertThat(change.stars).containsExactly("red", "yellow").inOrder();
-    assertThat(gApi.accounts().self().getStars(triplet)).containsExactly("red", "yellow").inOrder();
-    starredChanges = gApi.accounts().self().getStarredChanges();
-    assertThat(starredChanges).hasSize(1);
-    starredChange = starredChanges.get(0);
-    assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
-    assertThat(starredChange.starred).isNull();
-    assertThat(starredChange.stars).containsExactly("red", "yellow").inOrder();
-    refUpdateCounter.assertRefUpdateFor(
-        RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
+      gApi.accounts()
+          .self()
+          .setStars(
+              triplet,
+              new StarsInput(ImmutableSet.of("yellow"), ImmutableSet.of(DEFAULT_LABEL, "blue")));
+      change = info(triplet);
+      assertThat(change.starred).isNull();
+      assertThat(change.stars).containsExactly("red", "yellow").inOrder();
+      assertThat(gApi.accounts().self().getStars(triplet))
+          .containsExactly("red", "yellow")
+          .inOrder();
+      starredChanges = gApi.accounts().self().getStarredChanges();
+      assertThat(starredChanges).hasSize(1);
+      starredChange = starredChanges.get(0);
+      assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
+      assertThat(starredChange.starred).isNull();
+      assertThat(starredChange.stars).containsExactly("red", "yellow").inOrder();
+      refUpdateCounter.assertRefUpdateFor(
+          RefUpdateCounter.projectRef(
+              allUsers, RefNames.refsStarredChanges(Change.id(change._number), admin.id())));
 
-    accountIndexedCounter.assertNoReindex();
+      accountIndexedCounter.assertNoReindex();
 
-    requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to get stars of another account");
-    gApi.accounts().id(Integer.toString((admin.id().get()))).getStars(triplet);
+      requestScopeOperations.setApiUser(user.id());
+      AuthException thrown =
+          assertThrows(
+              AuthException.class,
+              () -> gApi.accounts().id(Integer.toString((admin.id().get()))).getStars(triplet));
+      assertThat(thrown).hasMessageThat().contains("not allowed to get stars of another account");
+    }
   }
 
   @Test
   public void starWithInvalidLabels() throws Exception {
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("invalid labels: another invalid label, invalid label");
-    gApi.accounts()
-        .self()
-        .setStars(
-            triplet,
-            new StarsInput(
-                ImmutableSet.of(DEFAULT_LABEL, "invalid label", "blue", "another invalid label")));
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.accounts()
+                    .self()
+                    .setStars(
+                        triplet,
+                        new StarsInput(
+                            ImmutableSet.of(
+                                DEFAULT_LABEL, "invalid label", "blue", "another invalid label"))));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("invalid labels: another invalid label, invalid label");
   }
 
   @Test
@@ -850,65 +890,84 @@
   public void starWithDefaultAndIgnoreLabel() throws Exception {
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        "The labels "
-            + DEFAULT_LABEL
-            + " and "
-            + IGNORE_LABEL
-            + " are mutually exclusive."
-            + " Only one of them can be set.");
-    gApi.accounts()
-        .self()
-        .setStars(triplet, new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "blue", IGNORE_LABEL)));
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.accounts()
+                    .self()
+                    .setStars(
+                        triplet,
+                        new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "blue", IGNORE_LABEL))));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "The labels "
+                + DEFAULT_LABEL
+                + " and "
+                + IGNORE_LABEL
+                + " are mutually exclusive."
+                + " Only one of them can be set.");
   }
 
   @Test
   public void ignoreChangeBySetStars() throws Exception {
-    TestAccount user2 = accountCreator.user2();
-    accountIndexedCounter.clear();
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestAccount user2 = accountCreator.user2();
+      accountIndexedCounter.clear();
 
-    PushOneCommit.Result r = createChange();
+      PushOneCommit.Result r = createChange();
 
-    AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email();
-    gApi.changes().id(r.getChangeId()).addReviewer(in);
+      AddReviewerInput in = new AddReviewerInput();
+      in.reviewer = user.email();
+      gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    in = new AddReviewerInput();
-    in.reviewer = user2.email();
-    gApi.changes().id(r.getChangeId()).addReviewer(in);
+      in = new AddReviewerInput();
+      in.reviewer = user2.email();
+      gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    requestScopeOperations.setApiUser(user.id());
-    gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
+      requestScopeOperations.setApiUser(user.id());
+      gApi.accounts()
+          .self()
+          .setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
-    sender.clear();
-    requestScopeOperations.setApiUser(admin.id());
-    gApi.changes().id(r.getChangeId()).abandon();
-    List<Message> messages = sender.getMessages();
-    assertThat(messages).hasSize(1);
-    assertThat(messages.get(0).rcpt()).containsExactly(user2.getEmailAddress());
-    accountIndexedCounter.assertNoReindex();
+      sender.clear();
+      requestScopeOperations.setApiUser(admin.id());
+      gApi.changes().id(r.getChangeId()).abandon();
+      List<Message> messages = sender.getMessages();
+      assertThat(messages).hasSize(1);
+      assertThat(messages.get(0).rcpt()).containsExactly(user2.getEmailAddress());
+      accountIndexedCounter.assertNoReindex();
+    }
   }
 
   @Test
   public void addReviewerToIgnoredChange() throws Exception {
-    PushOneCommit.Result r = createChange();
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      PushOneCommit.Result r = createChange();
 
-    requestScopeOperations.setApiUser(user.id());
-    gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
+      requestScopeOperations.setApiUser(user.id());
+      gApi.accounts()
+          .self()
+          .setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
-    sender.clear();
-    requestScopeOperations.setApiUser(admin.id());
+      sender.clear();
+      requestScopeOperations.setApiUser(admin.id());
 
-    AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email();
-    gApi.changes().id(r.getChangeId()).addReviewer(in);
-    List<Message> messages = sender.getMessages();
-    assertThat(messages).hasSize(1);
-    Message message = messages.get(0);
-    assertThat(message.rcpt()).containsExactly(user.getEmailAddress());
-    assertMailReplyTo(message, admin.email());
-    accountIndexedCounter.assertNoReindex();
+      AddReviewerInput in = new AddReviewerInput();
+      in.reviewer = user.email();
+      gApi.changes().id(r.getChangeId()).addReviewer(in);
+      List<Message> messages = sender.getMessages();
+      assertThat(messages).hasSize(1);
+      Message message = messages.get(0);
+      assertThat(message.rcpt()).containsExactly(user.getEmailAddress());
+      assertMailReplyTo(message, admin.email());
+      accountIndexedCounter.assertNoReindex();
+    }
   }
 
   @Test
@@ -1006,17 +1065,21 @@
 
   @Test
   public void suggestAccounts() throws Exception {
-    String adminUsername = "admin";
-    List<AccountInfo> result = gApi.accounts().suggestAccounts().withQuery(adminUsername).get();
-    assertThat(result).hasSize(1);
-    assertThat(result.get(0).username).isEqualTo(adminUsername);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String adminUsername = "admin";
+      List<AccountInfo> result = gApi.accounts().suggestAccounts().withQuery(adminUsername).get();
+      assertThat(result).hasSize(1);
+      assertThat(result.get(0).username).isEqualTo(adminUsername);
 
-    List<AccountInfo> resultShortcutApi = gApi.accounts().suggestAccounts(adminUsername).get();
-    assertThat(resultShortcutApi).hasSize(result.size());
+      List<AccountInfo> resultShortcutApi = gApi.accounts().suggestAccounts(adminUsername).get();
+      assertThat(resultShortcutApi).hasSize(result.size());
 
-    List<AccountInfo> emptyResult = gApi.accounts().suggestAccounts("unknown").get();
-    assertThat(emptyResult).isEmpty();
-    accountIndexedCounter.assertNoReindex();
+      List<AccountInfo> emptyResult = gApi.accounts().suggestAccounts("unknown").get();
+      assertThat(emptyResult).isEmpty();
+      accountIndexedCounter.assertNoReindex();
+    }
   }
 
   @Test
@@ -1040,7 +1103,7 @@
     assertThat(detail.email).isEqualTo(email);
     assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
     assertThat(detail.status).isEqualTo(status);
-    assertThat(detail.registeredOn).isEqualTo(getAccount(foo.id()).getRegisteredOn());
+    assertThat(detail.registeredOn).isEqualTo(getAccount(foo.id()).registeredOn());
     assertThat(detail.inactive).isNull();
     assertThat(detail._moreAccounts).isNull();
   }
@@ -1082,7 +1145,7 @@
     requestScopeOperations.setApiUser(admin.id());
     String secondaryEmail = "secondary@example.com";
     EmailInput input = newEmailInput(secondaryEmail);
-    gApi.accounts().id(foo.id().hashCode()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
 
     requestScopeOperations.setApiUser(foo.id());
     assertThat(getEmails()).containsExactly(email, secondaryEmail);
@@ -1094,9 +1157,9 @@
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("modify account not permitted");
-    gApi.accounts().id(foo.id().get()).getEmails();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.accounts().id(foo.id().get()).getEmails());
+    assertThat(thrown).hasMessageThat().contains("modify account not permitted");
   }
 
   @Test
@@ -1105,7 +1168,7 @@
     String secondaryEmail = "secondary3@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
     EmailInput input = newEmailInput(secondaryEmail);
-    gApi.accounts().id(foo.id().hashCode()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
 
     assertThat(
             gApi.accounts().id(foo.id().get()).getEmails().stream()
@@ -1116,17 +1179,21 @@
 
   @Test
   public void addEmail() throws Exception {
-    List<String> emails = ImmutableList.of("new.email@example.com", "new.email@example.systems");
-    Set<String> currentEmails = getEmails();
-    for (String email : emails) {
-      assertThat(currentEmails).doesNotContain(email);
-      EmailInput input = newEmailInput(email);
-      gApi.accounts().self().addEmail(input);
-      accountIndexedCounter.assertReindexOf(admin);
-    }
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      List<String> emails = ImmutableList.of("new.email@example.com", "new.email@example.systems");
+      Set<String> currentEmails = getEmails();
+      for (String email : emails) {
+        assertThat(currentEmails).doesNotContain(email);
+        EmailInput input = newEmailInput(email);
+        gApi.accounts().self().addEmail(input);
+        accountIndexedCounter.assertReindexOf(admin);
+      }
 
-    requestScopeOperations.resetCurrentApiUser();
-    assertThat(getEmails()).containsAtLeastElementsIn(emails);
+      requestScopeOperations.resetCurrentApiUser();
+      assertThat(getEmails()).containsAtLeastElementsIn(emails);
+    }
   }
 
   @Test
@@ -1144,16 +1211,17 @@
 
             // Non-supported TLD  (see tlds-alpha-by-domain.txt)
             "new.email@example.africa");
-    for (String email : emails) {
-      EmailInput input = newEmailInput(email);
-      try {
-        gApi.accounts().self().addEmail(input);
-        fail("Expected BadRequestException for invalid email address: " + email);
-      } catch (BadRequestException e) {
-        assertThat(e).hasMessageThat().isEqualTo("invalid email address");
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      for (String email : emails) {
+        EmailInput input = newEmailInput(email);
+        BadRequestException thrown =
+            assertThrows(BadRequestException.class, () -> gApi.accounts().self().addEmail(input));
+        assertWithMessage(email).that(thrown).hasMessageThat().isEqualTo("invalid email address");
       }
+      accountIndexedCounter.assertNoReindex();
     }
-    accountIndexedCounter.assertNoReindex();
   }
 
   @Test
@@ -1161,8 +1229,7 @@
     TestAccount account = accountCreator.create(name("user"));
     EmailInput input = newEmailInput("test@test.com");
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.accounts().id(account.username()).addEmail(input);
+    assertThrows(AuthException.class, () -> gApi.accounts().id(account.username()).addEmail(input));
   }
 
   @Test
@@ -1170,9 +1237,13 @@
     String email = "new.email@example.com";
     EmailInput input = newEmailInput(email);
     gApi.accounts().self().addEmail(input);
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Identity 'mailto:" + email + "' in use by another account");
-    gApi.accounts().id(user.username()).addEmail(input);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.accounts().id(user.username()).addEmail(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Identity 'mailto:" + email + "' in use by another account");
   }
 
   @Test
@@ -1208,9 +1279,14 @@
     TestAccount user = accountCreator.create();
     requestScopeOperations.setApiUser(user.id());
 
-    exception.expect(AuthException.class);
-    exception.expectMessage("modify account not permitted");
-    gApi.accounts().id(admin.id().get()).addEmail(newEmailInput("foo@example.com", false));
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () ->
+                gApi.accounts()
+                    .id(admin.id().get())
+                    .addEmail(newEmailInput("foo@example.com", false)));
+    assertThat(thrown).hasMessageThat().contains("modify account not permitted");
   }
 
   @Test
@@ -1225,62 +1301,74 @@
 
   @Test
   public void addEmailAndSetPreferred() throws Exception {
-    String email = "foo.bar@example.com";
-    EmailInput input = new EmailInput();
-    input.email = email;
-    input.noConfirmation = true;
-    input.preferred = true;
-    gApi.accounts().self().addEmail(input);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String email = "foo.bar@example.com";
+      EmailInput input = new EmailInput();
+      input.email = email;
+      input.noConfirmation = true;
+      input.preferred = true;
+      gApi.accounts().self().addEmail(input);
 
-    // Account is reindexed twice; once on adding the new email,
-    // and then again on setting the email preferred.
-    accountIndexedCounter.assertReindexOf(admin, 2);
+      // Account is reindexed twice; once on adding the new email,
+      // and then again on setting the email preferred.
+      accountIndexedCounter.assertReindexOf(admin, 2);
 
-    String preferred = gApi.accounts().self().get().email;
-    assertThat(preferred).isEqualTo(email);
+      String preferred = gApi.accounts().self().get().email;
+      assertThat(preferred).isEqualTo(email);
+    }
   }
 
   @Test
   public void deleteEmail() throws Exception {
-    String email = "foo.bar@example.com";
-    EmailInput input = newEmailInput(email);
-    gApi.accounts().self().addEmail(input);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String email = "foo.bar@example.com";
+      EmailInput input = newEmailInput(email);
+      gApi.accounts().self().addEmail(input);
 
-    requestScopeOperations.resetCurrentApiUser();
-    assertThat(getEmails()).contains(email);
+      requestScopeOperations.resetCurrentApiUser();
+      assertThat(getEmails()).contains(email);
 
-    accountIndexedCounter.clear();
-    gApi.accounts().self().deleteEmail(input.email);
-    accountIndexedCounter.assertReindexOf(admin);
+      accountIndexedCounter.clear();
+      gApi.accounts().self().deleteEmail(input.email);
+      accountIndexedCounter.assertReindexOf(admin);
 
-    requestScopeOperations.resetCurrentApiUser();
-    assertThat(getEmails()).doesNotContain(email);
+      requestScopeOperations.resetCurrentApiUser();
+      assertThat(getEmails()).doesNotContain(email);
+    }
   }
 
   @Test
   public void deletePreferredEmail() throws Exception {
-    String previous = gApi.accounts().self().get().email;
-    String email = "foo.bar.baz@example.com";
-    EmailInput input = new EmailInput();
-    input.email = email;
-    input.noConfirmation = true;
-    input.preferred = true;
-    gApi.accounts().self().addEmail(input);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String previous = gApi.accounts().self().get().email;
+      String email = "foo.bar.baz@example.com";
+      EmailInput input = new EmailInput();
+      input.email = email;
+      input.noConfirmation = true;
+      input.preferred = true;
+      gApi.accounts().self().addEmail(input);
 
-    // Account is reindexed twice; once on adding the new email,
-    // and then again on setting the email preferred.
-    accountIndexedCounter.assertReindexOf(admin, 2);
+      // Account is reindexed twice; once on adding the new email,
+      // and then again on setting the email preferred.
+      accountIndexedCounter.assertReindexOf(admin, 2);
 
-    // The new preferred email is set
-    assertThat(gApi.accounts().self().get().email).isEqualTo(email);
+      // The new preferred email is set
+      assertThat(gApi.accounts().self().get().email).isEqualTo(email);
 
-    accountIndexedCounter.clear();
-    gApi.accounts().self().deleteEmail(input.email);
-    accountIndexedCounter.assertReindexOf(admin);
+      accountIndexedCounter.clear();
+      gApi.accounts().self().deleteEmail(input.email);
+      accountIndexedCounter.assertReindexOf(admin);
 
-    requestScopeOperations.resetCurrentApiUser();
-    assertThat(getEmails()).containsExactly(previous);
-    assertThat(gApi.accounts().self().get().email).isNull();
+      requestScopeOperations.resetCurrentApiUser();
+      assertThat(getEmails()).containsExactly(previous);
+      assertThat(gApi.accounts().self().get().email).isNull();
+    }
   }
 
   @Test
@@ -1306,36 +1394,45 @@
 
   @Test
   public void deleteEmailFromCustomExternalIdSchemes() throws Exception {
-    String email = "foo.bar@example.com";
-    String extId1 = "foo:bar";
-    String extId2 = "foo:baz";
-    accountsUpdateProvider
-        .get()
-        .update(
-            "Add External IDs",
-            admin.id(),
-            u ->
-                u.addExternalId(
-                        ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id(), email))
-                    .addExternalId(
-                        ExternalId.createWithEmail(
-                            ExternalId.Key.parse(extId2), admin.id(), email)));
-    accountIndexedCounter.assertReindexOf(admin);
-    assertThat(
-            gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
-        .containsAtLeast(extId1, extId2);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String email = "foo.bar@example.com";
+      String extId1 = "foo:bar";
+      String extId2 = "foo:baz";
+      accountsUpdateProvider
+          .get()
+          .update(
+              "Add External IDs",
+              admin.id(),
+              u ->
+                  u.addExternalId(
+                          ExternalId.createWithEmail(
+                              ExternalId.Key.parse(extId1), admin.id(), email))
+                      .addExternalId(
+                          ExternalId.createWithEmail(
+                              ExternalId.Key.parse(extId2), admin.id(), email)));
+      accountIndexedCounter.assertReindexOf(admin);
+      assertThat(
+              gApi.accounts().self().getExternalIds().stream()
+                  .map(e -> e.identity)
+                  .collect(toSet()))
+          .containsAtLeast(extId1, extId2);
 
-    requestScopeOperations.resetCurrentApiUser();
-    assertThat(getEmails()).contains(email);
+      requestScopeOperations.resetCurrentApiUser();
+      assertThat(getEmails()).contains(email);
 
-    gApi.accounts().self().deleteEmail(email);
-    accountIndexedCounter.assertReindexOf(admin);
+      gApi.accounts().self().deleteEmail(email);
+      accountIndexedCounter.assertReindexOf(admin);
 
-    requestScopeOperations.resetCurrentApiUser();
-    assertThat(getEmails()).doesNotContain(email);
-    assertThat(
-            gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
-        .containsNoneOf(extId1, extId2);
+      requestScopeOperations.resetCurrentApiUser();
+      assertThat(getEmails()).doesNotContain(email);
+      assertThat(
+              gApi.accounts().self().getExternalIds().stream()
+                  .map(e -> e.identity)
+                  .collect(toSet()))
+          .containsNoneOf(extId1, extId2);
+    }
   }
 
   @Test
@@ -1352,7 +1449,6 @@
                 u.addExternalId(
                     ExternalId.createWithEmail(
                         ExternalId.Key.parse(ldapExternalId), admin.id(), ldapEmail)));
-    accountIndexedCounter.assertReindexOf(admin);
     assertThat(
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
         .contains(ldapExternalId);
@@ -1391,7 +1487,6 @@
                     .addExternalId(
                         ExternalId.createWithEmail(
                             ExternalId.Key.parse(ldapExternalId), admin.id(), ldapEmail)));
-    accountIndexedCounter.assertReindexOf(admin);
     assertThat(
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
         .containsAtLeast(ldapExternalId, nonLdapExternalId);
@@ -1410,28 +1505,34 @@
 
   @Test
   public void deleteEmailOfOtherUser() throws Exception {
-    String email = "foo.bar@example.com";
-    EmailInput input = new EmailInput();
-    input.email = email;
-    input.noConfirmation = true;
-    gApi.accounts().id(user.id().get()).addEmail(input);
-    accountIndexedCounter.assertReindexOf(user);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String email = "foo.bar@example.com";
+      EmailInput input = new EmailInput();
+      input.email = email;
+      input.noConfirmation = true;
+      gApi.accounts().id(user.id().get()).addEmail(input);
+      accountIndexedCounter.assertReindexOf(user);
 
-    requestScopeOperations.setApiUser(user.id());
-    assertThat(getEmails()).contains(email);
+      requestScopeOperations.setApiUser(user.id());
+      assertThat(getEmails()).contains(email);
 
-    // admin can delete email of user
-    requestScopeOperations.setApiUser(admin.id());
-    gApi.accounts().id(user.id().get()).deleteEmail(email);
-    accountIndexedCounter.assertReindexOf(user);
+      // admin can delete email of user
+      requestScopeOperations.setApiUser(admin.id());
+      gApi.accounts().id(user.id().get()).deleteEmail(email);
+      accountIndexedCounter.assertReindexOf(user);
 
-    requestScopeOperations.setApiUser(user.id());
-    assertThat(getEmails()).doesNotContain(email);
+      requestScopeOperations.setApiUser(user.id());
+      assertThat(getEmails()).doesNotContain(email);
 
-    // user cannot delete email of admin
-    exception.expect(AuthException.class);
-    exception.expectMessage("modify account not permitted");
-    gApi.accounts().id(admin.id().get()).deleteEmail(admin.email());
+      // user cannot delete email of admin
+      AuthException thrown =
+          assertThrows(
+              AuthException.class,
+              () -> gApi.accounts().id(admin.id().get()).deleteEmail(admin.email()));
+      assertThat(thrown).hasMessageThat().contains("modify account not permitted");
+    }
   }
 
   @Test
@@ -1497,17 +1598,21 @@
   public void putStatus() throws Exception {
     List<String> statuses = ImmutableList.of("OOO", "Busy");
     AccountInfo info;
-    for (String status : statuses) {
-      gApi.accounts().self().setStatus(status);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      for (String status : statuses) {
+        gApi.accounts().self().setStatus(status);
+        info = gApi.accounts().self().get();
+        assertUser(info, admin, status);
+        accountIndexedCounter.assertReindexOf(admin);
+      }
+
+      gApi.accounts().self().setStatus(null);
       info = gApi.accounts().self().get();
-      assertUser(info, admin, status);
+      assertUser(info, admin);
       accountIndexedCounter.assertReindexOf(admin);
     }
-
-    gApi.accounts().self().setStatus(null);
-    info = gApi.accounts().self().get();
-    assertUser(info, admin);
-    accountIndexedCounter.assertReindexOf(admin);
   }
 
   @Test
@@ -1525,617 +1630,92 @@
   @Test
   public void userCannotSetNameOfOtherUser() throws Exception {
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username()).setName("Admin McAdminface");
+    assertThrows(
+        AuthException.class,
+        () -> gApi.accounts().id(admin.username()).setName("Admin McAdminface"));
   }
 
   @Test
   @Sandboxed
   public void userCanSetNameOfOtherUserWithModifyAccountPermission() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.MODIFY_ACCOUNT);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.MODIFY_ACCOUNT).group(REGISTERED_USERS))
+        .update();
     gApi.accounts().id(admin.username()).setName("Admin McAdminface");
     assertThat(gApi.accounts().id(admin.username()).get().name).isEqualTo("Admin McAdminface");
   }
 
   @Test
   public void fetchUserBranch() throws Exception {
-    requestScopeOperations.setApiUser(user.id());
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      requestScopeOperations.setApiUser(user.id());
 
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
-    String userRefName = RefNames.refsUsers(user.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
+      String userRefName = RefNames.refsUsers(user.id());
 
-    // remove default READ permissions
-    try (ProjectConfigUpdate u = updateProject(allUsers)) {
-      u.getConfig()
-          .getAccessSection(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", true)
-          .remove(new Permission(Permission.READ));
-      u.save();
-    }
+      // remove default READ permissions
+      try (ProjectConfigUpdate u = updateProject(allUsers)) {
+        u.getConfig()
+            .getAccessSection(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", true)
+            .remove(new Permission(Permission.READ));
+        u.save();
+      }
 
-    // deny READ permission that is inherited from All-Projects
-    deny(allUsers, RefNames.REFS + "*", Permission.READ, ANONYMOUS_USERS);
+      // deny READ permission that is inherited from All-Projects
+      projectOperations
+          .project(allUsers)
+          .forUpdate()
+          .add(deny(Permission.READ).ref(RefNames.REFS + "*").group(ANONYMOUS_USERS))
+          .update();
 
-    // fetching user branch without READ permission fails
-    try {
+      // fetching user branch without READ permission fails
+      assertThrows(TransportException.class, () -> fetch(allUsersRepo, userRefName + ":userRef"));
+
+      // allow each user to read its own user branch
+      projectOperations
+          .project(allUsers)
+          .forUpdate()
+          .add(
+              allow(Permission.READ)
+                  .ref(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}")
+                  .group(REGISTERED_USERS))
+          .update();
+
+      // fetch user branch using refs/users/YY/XXXXXXX
       fetch(allUsersRepo, userRefName + ":userRef");
-      fail("user branch is visible although no READ permission is granted");
-    } catch (TransportException e) {
-      // expected because no READ granted on user branch
-    }
+      Ref userRef = allUsersRepo.getRepository().exactRef("userRef");
+      assertThat(userRef).isNotNull();
 
-    // allow each user to read its own user branch
-    grant(
-        allUsers,
-        RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
-        Permission.READ,
-        false,
-        REGISTERED_USERS);
+      // fetch user branch using refs/users/self
+      fetch(allUsersRepo, RefNames.REFS_USERS_SELF + ":userSelfRef");
+      Ref userSelfRef = allUsersRepo.getRepository().getRefDatabase().exactRef("userSelfRef");
+      assertThat(userSelfRef).isNotNull();
+      assertThat(userSelfRef.getObjectId()).isEqualTo(userRef.getObjectId());
 
-    // fetch user branch using refs/users/YY/XXXXXXX
-    fetch(allUsersRepo, userRefName + ":userRef");
-    Ref userRef = allUsersRepo.getRepository().exactRef("userRef");
-    assertThat(userRef).isNotNull();
+      accountIndexedCounter.assertNoReindex();
 
-    // fetch user branch using refs/users/self
-    fetch(allUsersRepo, RefNames.REFS_USERS_SELF + ":userSelfRef");
-    Ref userSelfRef = allUsersRepo.getRepository().getRefDatabase().exactRef("userSelfRef");
-    assertThat(userSelfRef).isNotNull();
-    assertThat(userSelfRef.getObjectId()).isEqualTo(userRef.getObjectId());
-
-    accountIndexedCounter.assertNoReindex();
-
-    // fetching user branch of another user fails
-    String otherUserRefName = RefNames.refsUsers(admin.id());
-    exception.expect(TransportException.class);
-    exception.expectMessage("Remote does not have " + otherUserRefName + " available for fetch.");
-    fetch(allUsersRepo, otherUserRefName + ":otherUserRef");
-  }
-
-  @Test
-  public void pushToUserBranch() throws Exception {
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
-    allUsersRepo.reset("userRef");
-    PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
-    push.to(RefNames.refsUsers(admin.id())).assertOkStatus();
-    accountIndexedCounter.assertReindexOf(admin);
-
-    push = pushFactory.create(admin.newIdent(), allUsersRepo);
-    push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
-    accountIndexedCounter.assertReindexOf(admin);
-  }
-
-  @Test
-  public void pushToUserBranchForReview() throws Exception {
-    String userRefName = RefNames.refsUsers(admin.id());
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, userRefName + ":userRef");
-    allUsersRepo.reset("userRef");
-    PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
-    PushOneCommit.Result r = push.to(MagicBranch.NEW_CHANGE + userRefName);
-    r.assertOkStatus();
-    accountIndexedCounter.assertNoReindex();
-    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName);
-    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).current().submit();
-    accountIndexedCounter.assertReindexOf(admin);
-
-    push = pushFactory.create(admin.newIdent(), allUsersRepo);
-    r = push.to(MagicBranch.NEW_CHANGE + RefNames.REFS_USERS_SELF);
-    r.assertOkStatus();
-    accountIndexedCounter.assertNoReindex();
-    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName);
-    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).current().submit();
-    accountIndexedCounter.assertReindexOf(admin);
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchForReviewAndSubmit() throws Exception {
-    String userRef = RefNames.refsUsers(admin.id());
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, userRef + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, "out-of-office");
-
-    PushOneCommit.Result r =
-        pushFactory
-            .create(
-                admin.newIdent(),
-                allUsersRepo,
-                "Update account config",
-                AccountProperties.ACCOUNT_CONFIG,
-                ac.toText())
-            .to(MagicBranch.NEW_CHANGE + userRef);
-    r.assertOkStatus();
-    accountIndexedCounter.assertNoReindex();
-    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
-
-    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).current().submit();
-    accountIndexedCounter.assertReindexOf(admin);
-
-    AccountInfo info = gApi.accounts().self().get();
-    assertThat(info.email).isEqualTo(admin.email());
-    assertThat(info.name).isEqualTo(admin.fullName());
-    assertThat(info.status).isEqualTo("out-of-office");
-  }
-
-  @Test
-  public void pushAccountConfigWithPrefEmailThatDoesNotExistAsExtIdToUserBranchForReviewAndSubmit()
-      throws Exception {
-    TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
-    String userRef = RefNames.refsUsers(foo.id());
-    accountIndexedCounter.clear();
-
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
-    fetch(allUsersRepo, userRef + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    String email = "some.email@example.com";
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, email);
-
-    PushOneCommit.Result r =
-        pushFactory
-            .create(
-                foo.newIdent(),
-                allUsersRepo,
-                "Update account config",
-                AccountProperties.ACCOUNT_CONFIG,
-                ac.toText())
-            .to(MagicBranch.NEW_CHANGE + userRef);
-    r.assertOkStatus();
-    accountIndexedCounter.assertNoReindex();
-    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
-
-    requestScopeOperations.setApiUser(foo.id());
-    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).current().submit();
-
-    accountIndexedCounter.assertReindexOf(foo);
-
-    AccountInfo info = gApi.accounts().self().get();
-    assertThat(info.email).isEqualTo(email);
-    assertThat(info.name).isEqualTo(foo.fullName());
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfConfigIsInvalid()
-      throws Exception {
-    String userRef = RefNames.refsUsers(admin.id());
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, userRef + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    PushOneCommit.Result r =
-        pushFactory
-            .create(
-                admin.newIdent(),
-                allUsersRepo,
-                "Update account config",
-                AccountProperties.ACCOUNT_CONFIG,
-                "invalid config")
-            .to(MagicBranch.NEW_CHANGE + userRef);
-    r.assertOkStatus();
-    accountIndexedCounter.assertNoReindex();
-    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
-
-    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        String.format(
-            "invalid account configuration: commit '%s' has an invalid '%s' file for account '%s':"
-                + " Invalid config file %s in commit %s",
-            r.getCommit().name(),
-            AccountProperties.ACCOUNT_CONFIG,
-            admin.id(),
-            AccountProperties.ACCOUNT_CONFIG,
-            r.getCommit().name()));
-    gApi.changes().id(r.getChangeId()).current().submit();
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfPreferredEmailIsInvalid()
-      throws Exception {
-    String userRef = RefNames.refsUsers(admin.id());
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, userRef + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    String noEmail = "no.email";
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, noEmail);
-
-    PushOneCommit.Result r =
-        pushFactory
-            .create(
-                admin.newIdent(),
-                allUsersRepo,
-                "Update account config",
-                AccountProperties.ACCOUNT_CONFIG,
-                ac.toText())
-            .to(MagicBranch.NEW_CHANGE + userRef);
-    r.assertOkStatus();
-    accountIndexedCounter.assertNoReindex();
-    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
-
-    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        String.format(
-            "invalid account configuration: invalid preferred email '%s' for account '%s'",
-            noEmail, admin.id()));
-    gApi.changes().id(r.getChangeId()).current().submit();
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfOwnAccountIsDeactivated()
-      throws Exception {
-    String userRef = RefNames.refsUsers(admin.id());
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, userRef + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
-
-    PushOneCommit.Result r =
-        pushFactory
-            .create(
-                admin.newIdent(),
-                allUsersRepo,
-                "Update account config",
-                AccountProperties.ACCOUNT_CONFIG,
-                ac.toText())
-            .to(MagicBranch.NEW_CHANGE + userRef);
-    r.assertOkStatus();
-    accountIndexedCounter.assertNoReindex();
-    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
-
-    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("invalid account configuration: cannot deactivate own account");
-    gApi.changes().id(r.getChangeId()).current().submit();
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchForReviewDeactivateOtherAccount() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-
-    TestAccount foo = accountCreator.create(name("foo"));
-    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
-    String userRef = RefNames.refsUsers(foo.id());
-    accountIndexedCounter.clear();
-
-    grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
-    grantLabel("Code-Review", -2, 2, allUsers, userRef, false, adminGroupUuid(), false);
-    grant(allUsers, userRef, Permission.SUBMIT, false, adminGroupUuid());
-
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, userRef + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
-
-    PushOneCommit.Result r =
-        pushFactory
-            .create(
-                admin.newIdent(),
-                allUsersRepo,
-                "Update account config",
-                AccountProperties.ACCOUNT_CONFIG,
-                ac.toText())
-            .to(MagicBranch.NEW_CHANGE + userRef);
-    r.assertOkStatus();
-    accountIndexedCounter.assertNoReindex();
-    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
-
-    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).current().submit();
-    accountIndexedCounter.assertReindexOf(foo);
-
-    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
-  }
-
-  @Test
-  public void pushWatchConfigToUserBranch() throws Exception {
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    Config wc = new Config();
-    wc.setString(
-        ProjectWatches.PROJECT,
-        project.get(),
-        ProjectWatches.KEY_NOTIFY,
-        ProjectWatches.NotifyValue.create(null, EnumSet.of(NotifyType.ALL_COMMENTS)).toString());
-    PushOneCommit push =
-        pushFactory.create(
-            admin.newIdent(),
-            allUsersRepo,
-            "Add project watch",
-            ProjectWatches.WATCH_CONFIG,
-            wc.toText());
-    push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
-    accountIndexedCounter.assertReindexOf(admin);
-
-    String invalidNotifyValue = "]invalid[";
-    wc.setString(
-        ProjectWatches.PROJECT, project.get(), ProjectWatches.KEY_NOTIFY, invalidNotifyValue);
-    push =
-        pushFactory.create(
-            admin.newIdent(),
-            allUsersRepo,
-            "Add invalid project watch",
-            ProjectWatches.WATCH_CONFIG,
-            wc.toText());
-    PushOneCommit.Result r = push.to(RefNames.REFS_USERS_SELF);
-    r.assertErrorStatus("invalid account configuration");
-    r.assertMessage(
-        String.format(
-            "%s: Invalid project watch of account %d for project %s: %s",
-            ProjectWatches.WATCH_CONFIG, admin.id().get(), project.get(), invalidNotifyValue));
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranch() throws Exception {
-    TestAccount oooUser = accountCreator.create("away", "away@mail.invalid", "Ambrose Way");
-    requestScopeOperations.setApiUser(oooUser.id());
-
-    // Must clone as oooUser to ensure the push is allowed.
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, oooUser);
-    fetch(allUsersRepo, RefNames.refsUsers(oooUser.id()) + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, "out-of-office");
-
-    accountIndexedCounter.clear();
-    pushFactory
-        .create(
-            oooUser.newIdent(),
-            allUsersRepo,
-            "Update account config",
-            AccountProperties.ACCOUNT_CONFIG,
-            ac.toText())
-        .to(RefNames.refsUsers(oooUser.id()))
-        .assertOkStatus();
-
-    accountIndexedCounter.assertReindexOf(oooUser);
-
-    AccountInfo info = gApi.accounts().self().get();
-    assertThat(info.email).isEqualTo(oooUser.email());
-    assertThat(info.name).isEqualTo(oooUser.fullName());
-    assertThat(info.status).isEqualTo("out-of-office");
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchIsRejectedIfConfigIsInvalid() throws Exception {
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    PushOneCommit.Result r =
-        pushFactory
-            .create(
-                admin.newIdent(),
-                allUsersRepo,
-                "Update account config",
-                AccountProperties.ACCOUNT_CONFIG,
-                "invalid config")
-            .to(RefNames.REFS_USERS_SELF);
-    r.assertErrorStatus("invalid account configuration");
-    r.assertMessage(
-        String.format(
-            "commit '%s' has an invalid '%s' file for account '%s':"
-                + " Invalid config file %s in commit %s",
-            r.getCommit().name(),
-            AccountProperties.ACCOUNT_CONFIG,
-            admin.id(),
-            AccountProperties.ACCOUNT_CONFIG,
-            r.getCommit().name()));
-    accountIndexedCounter.assertNoReindex();
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchIsRejectedIfPreferredEmailIsInvalid() throws Exception {
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    String noEmail = "no.email";
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, noEmail);
-
-    PushOneCommit.Result r =
-        pushFactory
-            .create(
-                admin.newIdent(),
-                allUsersRepo,
-                "Update account config",
-                AccountProperties.ACCOUNT_CONFIG,
-                ac.toText())
-            .to(RefNames.REFS_USERS_SELF);
-    r.assertErrorStatus("invalid account configuration");
-    r.assertMessage(
-        String.format("invalid preferred email '%s' for account '%s'", noEmail, admin.id()));
-    accountIndexedCounter.assertNoReindex();
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchInvalidPreferredEmailButNotChanged() throws Exception {
-    TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
-    String userRef = RefNames.refsUsers(foo.id());
-
-    String noEmail = "no.email";
-    accountsUpdateProvider
-        .get()
-        .update("Set Preferred Email", foo.id(), u -> u.setPreferredEmail(noEmail));
-    accountIndexedCounter.clear();
-
-    grant(allUsers, userRef, Permission.PUSH, false, REGISTERED_USERS);
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
-    fetch(allUsersRepo, userRef + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    String status = "in vacation";
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, status);
-
-    pushFactory
-        .create(
-            foo.newIdent(),
-            allUsersRepo,
-            "Update account config",
-            AccountProperties.ACCOUNT_CONFIG,
-            ac.toText())
-        .to(userRef)
-        .assertOkStatus();
-    accountIndexedCounter.assertReindexOf(foo);
-
-    AccountInfo info = gApi.accounts().id(foo.id().get()).get();
-    assertThat(info.email).isEqualTo(noEmail);
-    assertThat(info.name).isEqualTo(foo.fullName());
-    assertThat(info.status).isEqualTo(status);
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchIfPreferredEmailDoesNotExistAsExtId() throws Exception {
-    TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
-    String userRef = RefNames.refsUsers(foo.id());
-    accountIndexedCounter.clear();
-
-    grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
-
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
-    fetch(allUsersRepo, userRef + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    String email = "some.email@example.com";
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, email);
-
-    pushFactory
-        .create(
-            foo.newIdent(),
-            allUsersRepo,
-            "Update account config",
-            AccountProperties.ACCOUNT_CONFIG,
-            ac.toText())
-        .to(userRef)
-        .assertOkStatus();
-    accountIndexedCounter.assertReindexOf(foo);
-
-    AccountInfo info = gApi.accounts().id(foo.id().get()).get();
-    assertThat(info.email).isEqualTo(email);
-    assertThat(info.name).isEqualTo(foo.fullName());
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchIsRejectedIfOwnAccountIsDeactivated() throws Exception {
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
-
-    PushOneCommit.Result r =
-        pushFactory
-            .create(
-                admin.newIdent(),
-                allUsersRepo,
-                "Update account config",
-                AccountProperties.ACCOUNT_CONFIG,
-                ac.toText())
-            .to(RefNames.REFS_USERS_SELF);
-    r.assertErrorStatus("invalid account configuration");
-    r.assertMessage("cannot deactivate own account");
-    accountIndexedCounter.assertNoReindex();
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchDeactivateOtherAccount() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-
-    TestAccount foo = accountCreator.create(name("foo"));
-    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
-    String userRef = RefNames.refsUsers(foo.id());
-    accountIndexedCounter.clear();
-
-    grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
-
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, userRef + ":userRef");
-    allUsersRepo.reset("userRef");
-
-    Config ac = getAccountConfig(allUsersRepo);
-    ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
-
-    pushFactory
-        .create(
-            admin.newIdent(),
-            allUsersRepo,
-            "Update account config",
-            AccountProperties.ACCOUNT_CONFIG,
-            ac.toText())
-        .to(userRef)
-        .assertOkStatus();
-    accountIndexedCounter.assertReindexOf(foo);
-
-    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
-  }
-
-  @Test
-  public void cannotCreateUserBranch() throws Exception {
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
-
-    String userRef = RefNames.refsUsers(new Account.Id(seq.nextAccountId()));
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
-    r.assertErrorStatus();
-    assertThat(r.getMessage()).contains("Not allowed to create user branch.");
-
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      assertThat(repo.exactRef(userRef)).isNull();
+      // fetching user branch of another user fails
+      String otherUserRefName = RefNames.refsUsers(admin.id());
+      TransportException thrown =
+          assertThrows(
+              TransportException.class,
+              () -> fetch(allUsersRepo, otherUserRefName + ":otherUserRef"));
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains("Remote does not have " + otherUserRefName + " available for fetch.");
     }
   }
 
   @Test
-  public void createUserBranchWithAccessDatabaseCapability() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
-
-    String userRef = RefNames.refsUsers(new Account.Id(seq.nextAccountId()));
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef).assertOkStatus();
-
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      assertThat(repo.exactRef(userRef)).isNotNull();
-    }
-  }
-
-  @Test
-  public void cannotCreateNonUserBranchUnderRefsUsersWithAccessDatabaseCapability()
-      throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
-
-    String userRef = RefNames.REFS_USERS + "foo";
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
-    r.assertErrorStatus();
-    assertThat(r.getMessage()).contains("Not allowed to create non-user branch under refs/users/.");
-
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      assertThat(repo.exactRef(userRef)).isNull();
+  public void refsUsersSelfIsAdvertised() throws Exception {
+    TestRepository<?> testRepository = cloneProject(allUsers, user);
+    try (Git git = testRepository.git()) {
+      List<String> advertisedRefs =
+          git.lsRemote().call().stream().map(Ref::getName).collect(toList());
+      assertThat(advertisedRefs).contains(RefNames.REFS_USERS_SELF);
     }
   }
 
@@ -2145,8 +1725,12 @@
       assertThat(repo.exactRef(RefNames.REFS_USERS_DEFAULT)).isNull();
     }
 
-    grant(allUsers, RefNames.REFS_USERS_DEFAULT, Permission.CREATE);
-    grant(allUsers, RefNames.REFS_USERS_DEFAULT, Permission.PUSH);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS_DEFAULT).group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS_DEFAULT).group(adminGroupUuid()))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     pushFactory
@@ -2161,12 +1745,15 @@
 
   @Test
   public void cannotDeleteUserBranch() throws Exception {
-    grant(
-        allUsers,
-        RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
-        Permission.DELETE,
-        true,
-        REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(
+            allow(Permission.DELETE)
+                .ref(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}")
+                .group(REGISTERED_USERS)
+                .force(true))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     String userRef = RefNames.refsUsers(admin.id());
@@ -2182,13 +1769,19 @@
 
   @Test
   public void deleteUserBranchWithAccessDatabaseCapability() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    grant(
-        allUsers,
-        RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
-        Permission.DELETE,
-        true,
-        REGISTERED_USERS);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(
+            allow(Permission.DELETE)
+                .ref(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}")
+                .group(REGISTERED_USERS)
+                .force(true))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     String userRef = RefNames.refsUsers(admin.id());
@@ -2217,9 +1810,10 @@
     assertThat(sender.getMessages().get(0).body()).contains("new GPG keys have been added");
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage(id);
-    gApi.accounts().self().gpgKey(id).get();
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class, () -> gApi.accounts().self().gpgKey(id).get());
+    assertThat(thrown).hasMessageThat().contains(id);
   }
 
   @Test
@@ -2229,8 +1823,7 @@
 
     sender.clear();
     requestScopeOperations.setApiUser(admin.id());
-    exception.expect(ResourceNotFoundException.class);
-    addGpgKey(user, key.getPublicKeyArmored());
+    assertThrows(ResourceNotFoundException.class, () -> addGpgKey(user, key.getPublicKeyArmored()));
   }
 
   @Test
@@ -2259,212 +1852,251 @@
 
   @Test
   public void addOtherUsersGpgKey_Conflict() throws Exception {
-    // Both users have a matching external ID for this key.
-    addExternalIdEmail(admin, "test5@example.com");
-    accountsUpdateProvider
-        .get()
-        .update(
-            "Add External ID",
-            user.id(),
-            u -> u.addExternalId(ExternalId.create("foo", "myId", user.id())));
-    accountIndexedCounter.assertReindexOf(user);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      // Both users have a matching external ID for this key.
+      addExternalIdEmail(admin, "test5@example.com");
+      accountIndexedCounter.clear();
+      accountsUpdateProvider
+          .get()
+          .update(
+              "Add External ID",
+              user.id(),
+              u -> u.addExternalId(ExternalId.create("foo", "myId", user.id())));
+      accountIndexedCounter.assertReindexOf(user);
 
-    TestKey key = validKeyWithSecondUserId();
-    addGpgKey(key.getPublicKeyArmored());
-    requestScopeOperations.setApiUser(user.id());
+      TestKey key = validKeyWithSecondUserId();
+      addGpgKey(key.getPublicKeyArmored());
+      requestScopeOperations.setApiUser(user.id());
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("GPG key already associated with another account");
-    addGpgKey(user, key.getPublicKeyArmored());
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class, () -> addGpgKey(user, key.getPublicKeyArmored()));
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains("GPG key already associated with another account");
+    }
   }
 
   @Test
   public void listGpgKeys() throws Exception {
-    List<TestKey> keys = allValidKeys();
-    List<String> toAdd = new ArrayList<>(keys.size());
-    for (TestKey key : keys) {
-      addExternalIdEmail(admin, PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
-      toAdd.add(key.getPublicKeyArmored());
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      List<TestKey> keys = allValidKeys();
+      List<String> toAdd = new ArrayList<>(keys.size());
+      for (TestKey key : keys) {
+        addExternalIdEmail(
+            admin, PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
+        toAdd.add(key.getPublicKeyArmored());
+      }
+      accountIndexedCounter.clear();
+      gApi.accounts().self().putGpgKeys(toAdd, ImmutableList.of());
+      assertKeys(keys);
+      accountIndexedCounter.assertReindexOf(admin);
     }
-    gApi.accounts().self().putGpgKeys(toAdd, ImmutableList.of());
-    assertKeys(keys);
-    accountIndexedCounter.assertReindexOf(admin);
   }
 
   @Test
   public void deleteGpgKey() throws Exception {
-    TestKey key = validKeyWithoutExpiration();
-    String id = key.getKeyIdString();
-    addExternalIdEmail(admin, "test1@example.com");
-    addGpgKey(key.getPublicKeyArmored());
-    assertKeys(key);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestKey key = validKeyWithoutExpiration();
+      String id = key.getKeyIdString();
+      addExternalIdEmail(admin, "test1@example.com");
+      addGpgKey(key.getPublicKeyArmored());
+      assertKeys(key);
+      accountIndexedCounter.clear();
 
-    sender.clear();
-    gApi.accounts().self().gpgKey(id).delete();
-    accountIndexedCounter.assertReindexOf(admin);
-    assertKeys();
-    assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).body()).contains("GPG keys have been deleted");
+      sender.clear();
+      gApi.accounts().self().gpgKey(id).delete();
+      accountIndexedCounter.assertReindexOf(admin);
+      assertKeys();
+      assertThat(sender.getMessages()).hasSize(1);
+      assertThat(sender.getMessages().get(0).body()).contains("GPG keys have been deleted");
 
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage(id);
-    gApi.accounts().self().gpgKey(id).get();
+      ResourceNotFoundException thrown =
+          assertThrows(
+              ResourceNotFoundException.class, () -> gApi.accounts().self().gpgKey(id).get());
+      assertThat(thrown).hasMessageThat().contains(id);
+    }
   }
 
   @Test
   public void addAndRemoveGpgKeys() throws Exception {
-    for (TestKey key : allValidKeys()) {
-      addExternalIdEmail(admin, PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      for (TestKey key : allValidKeys()) {
+        addExternalIdEmail(
+            admin, PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
+      }
+      accountIndexedCounter.clear();
+      TestKey key1 = validKeyWithoutExpiration();
+      TestKey key2 = validKeyWithExpiration();
+      TestKey key5 = validKeyWithSecondUserId();
+
+      Map<String, GpgKeyInfo> infos =
+          gApi.accounts()
+              .self()
+              .putGpgKeys(
+                  ImmutableList.of(key1.getPublicKeyArmored(), key2.getPublicKeyArmored()),
+                  ImmutableList.of(key5.getKeyIdString()));
+      assertThat(infos.keySet()).containsExactly(key1.getKeyIdString(), key2.getKeyIdString());
+      assertKeys(key1, key2);
+      accountIndexedCounter.assertReindexOf(admin);
+
+      infos =
+          gApi.accounts()
+              .self()
+              .putGpgKeys(
+                  ImmutableList.of(key5.getPublicKeyArmored()),
+                  ImmutableList.of(key1.getKeyIdString()));
+      assertThat(infos.keySet()).containsExactly(key1.getKeyIdString(), key5.getKeyIdString());
+      assertKeyMapContains(key5, infos);
+      assertThat(infos.get(key1.getKeyIdString()).key).isNull();
+      assertKeys(key2, key5);
+      accountIndexedCounter.assertReindexOf(admin);
+
+      BadRequestException thrown =
+          assertThrows(
+              BadRequestException.class,
+              () ->
+                  gApi.accounts()
+                      .self()
+                      .putGpgKeys(
+                          ImmutableList.of(key2.getPublicKeyArmored()),
+                          ImmutableList.of(key2.getKeyIdString())));
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains("Cannot both add and delete key: " + keyToString(key2.getPublicKey()));
     }
-    TestKey key1 = validKeyWithoutExpiration();
-    TestKey key2 = validKeyWithExpiration();
-    TestKey key5 = validKeyWithSecondUserId();
-
-    Map<String, GpgKeyInfo> infos =
-        gApi.accounts()
-            .self()
-            .putGpgKeys(
-                ImmutableList.of(key1.getPublicKeyArmored(), key2.getPublicKeyArmored()),
-                ImmutableList.of(key5.getKeyIdString()));
-    assertThat(infos.keySet()).containsExactly(key1.getKeyIdString(), key2.getKeyIdString());
-    assertKeys(key1, key2);
-    accountIndexedCounter.assertReindexOf(admin);
-
-    infos =
-        gApi.accounts()
-            .self()
-            .putGpgKeys(
-                ImmutableList.of(key5.getPublicKeyArmored()),
-                ImmutableList.of(key1.getKeyIdString()));
-    assertThat(infos.keySet()).containsExactly(key1.getKeyIdString(), key5.getKeyIdString());
-    assertKeyMapContains(key5, infos);
-    assertThat(infos.get(key1.getKeyIdString()).key).isNull();
-    assertKeys(key2, key5);
-    accountIndexedCounter.assertReindexOf(admin);
-
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("Cannot both add and delete key: " + keyToString(key2.getPublicKey()));
-    gApi.accounts()
-        .self()
-        .putGpgKeys(
-            ImmutableList.of(key2.getPublicKeyArmored()), ImmutableList.of(key2.getKeyIdString()));
   }
 
   @Test
   public void addMalformedGpgKey() throws Exception {
     String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\ntest\n-----END PGP PUBLIC KEY BLOCK-----";
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("Failed to parse GPG keys");
-    addGpgKey(key);
+    BadRequestException thrown = assertThrows(BadRequestException.class, () -> addGpgKey(key));
+    assertThat(thrown).hasMessageThat().contains("Failed to parse GPG keys");
   }
 
   @Test
   @UseSsh
   public void sshKeys() throws Exception {
-    // The test account should initially have exactly one ssh key
-    List<SshKeyInfo> info = gApi.accounts().self().listSshKeys();
-    assertThat(info).hasSize(1);
-    assertSequenceNumbers(info);
-    SshKeyInfo key = info.get(0);
-    KeyPair keyPair = sshKeys.getKeyPair(admin);
-    String initial = TestSshKeys.publicKey(keyPair, admin.email());
-    assertThat(key.sshPublicKey).isEqualTo(initial);
-    accountIndexedCounter.assertNoReindex();
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      // The test account should initially have exactly one ssh key
+      List<SshKeyInfo> info = gApi.accounts().self().listSshKeys();
+      assertThat(info).hasSize(1);
+      assertSequenceNumbers(info);
+      SshKeyInfo key = info.get(0);
+      KeyPair keyPair = sshKeys.getKeyPair(admin);
+      String initial = TestSshKeys.publicKey(keyPair, admin.email());
+      assertThat(key.sshPublicKey).isEqualTo(initial);
+      accountIndexedCounter.assertNoReindex();
 
-    // Add a new key
-    sender.clear();
-    String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
-    gApi.accounts().self().addSshKey(newKey);
-    info = gApi.accounts().self().listSshKeys();
-    assertThat(info).hasSize(2);
-    assertSequenceNumbers(info);
-    accountIndexedCounter.assertReindexOf(admin);
-    assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
+      // Add a new key
+      sender.clear();
+      String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
+      gApi.accounts().self().addSshKey(newKey);
+      info = gApi.accounts().self().listSshKeys();
+      assertThat(info).hasSize(2);
+      assertSequenceNumbers(info);
+      accountIndexedCounter.assertReindexOf(admin);
+      assertThat(sender.getMessages()).hasSize(1);
+      assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
 
-    // Add an existing key (the request succeeds, but the key isn't added again)
-    sender.clear();
-    gApi.accounts().self().addSshKey(initial);
-    info = gApi.accounts().self().listSshKeys();
-    assertThat(info).hasSize(2);
-    assertSequenceNumbers(info);
-    accountIndexedCounter.assertNoReindex();
-    // TODO: Issue 10769: Adding an already existing key should not result in a notification email
-    assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
+      // Add an existing key (the request succeeds, but the key isn't added again)
+      sender.clear();
+      gApi.accounts().self().addSshKey(initial);
+      info = gApi.accounts().self().listSshKeys();
+      assertThat(info).hasSize(2);
+      assertSequenceNumbers(info);
+      accountIndexedCounter.assertNoReindex();
+      // TODO: Issue 10769: Adding an already existing key should not result in a notification email
+      assertThat(sender.getMessages()).hasSize(1);
+      assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
 
-    // Add another new key
-    sender.clear();
-    String newKey2 = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
-    gApi.accounts().self().addSshKey(newKey2);
-    info = gApi.accounts().self().listSshKeys();
-    assertThat(info).hasSize(3);
-    assertSequenceNumbers(info);
-    accountIndexedCounter.assertReindexOf(admin);
-    assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
+      // Add another new key
+      sender.clear();
+      String newKey2 = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
+      gApi.accounts().self().addSshKey(newKey2);
+      info = gApi.accounts().self().listSshKeys();
+      assertThat(info).hasSize(3);
+      assertSequenceNumbers(info);
+      accountIndexedCounter.assertReindexOf(admin);
+      assertThat(sender.getMessages()).hasSize(1);
+      assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
 
-    // Delete second key
-    sender.clear();
-    gApi.accounts().self().deleteSshKey(2);
-    info = gApi.accounts().self().listSshKeys();
-    assertThat(info).hasSize(2);
-    assertThat(info.get(0).seq).isEqualTo(1);
-    assertThat(info.get(1).seq).isEqualTo(3);
-    accountIndexedCounter.assertReindexOf(admin);
+      // Delete second key
+      sender.clear();
+      gApi.accounts().self().deleteSshKey(2);
+      info = gApi.accounts().self().listSshKeys();
+      assertThat(info).hasSize(2);
+      assertThat(info.get(0).seq).isEqualTo(1);
+      assertThat(info.get(1).seq).isEqualTo(3);
+      accountIndexedCounter.assertReindexOf(admin);
 
-    assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).body()).contains("SSH keys have been deleted");
+      assertThat(sender.getMessages()).hasSize(1);
+      assertThat(sender.getMessages().get(0).body()).contains("SSH keys have been deleted");
 
-    // Mark first key as invalid
-    assertThat(info.get(0).valid).isTrue();
-    authorizedKeys.markKeyInvalid(admin.id(), 1);
-    info = gApi.accounts().self().listSshKeys();
-    assertThat(info).hasSize(2);
-    assertThat(info.get(0).seq).isEqualTo(1);
-    assertThat(info.get(0).valid).isFalse();
-    assertThat(info.get(1).seq).isEqualTo(3);
-    accountIndexedCounter.assertReindexOf(admin);
+      // Mark first key as invalid
+      assertThat(info.get(0).valid).isTrue();
+      authorizedKeys.markKeyInvalid(admin.id(), 1);
+      info = gApi.accounts().self().listSshKeys();
+      assertThat(info).hasSize(2);
+      assertThat(info.get(0).seq).isEqualTo(1);
+      assertThat(info.get(0).valid).isFalse();
+      assertThat(info.get(1).seq).isEqualTo(3);
+      accountIndexedCounter.assertReindexOf(admin);
+    }
   }
 
   @Test
   @UseSsh
   public void adminCanAddOrRemoveSshKeyOnOtherAccount() throws Exception {
-    // The test account should initially have exactly one ssh key
-    List<SshKeyInfo> info = gApi.accounts().self().listSshKeys();
-    assertThat(info).hasSize(1);
-    assertSequenceNumbers(info);
-    SshKeyInfo key = info.get(0);
-    KeyPair keyPair = sshKeys.getKeyPair(admin);
-    String initial = TestSshKeys.publicKey(keyPair, admin.email());
-    assertThat(key.sshPublicKey).isEqualTo(initial);
-    accountIndexedCounter.assertNoReindex();
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      // The test account should initially have exactly one ssh key
+      List<SshKeyInfo> info = gApi.accounts().self().listSshKeys();
+      assertThat(info).hasSize(1);
+      assertSequenceNumbers(info);
+      SshKeyInfo key = info.get(0);
+      KeyPair keyPair = sshKeys.getKeyPair(admin);
+      String initial = TestSshKeys.publicKey(keyPair, admin.email());
+      assertThat(key.sshPublicKey).isEqualTo(initial);
+      accountIndexedCounter.assertNoReindex();
 
-    // Add a new key
-    sender.clear();
-    String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), user.email());
-    gApi.accounts().id(user.username()).addSshKey(newKey);
-    info = gApi.accounts().id(user.username()).listSshKeys();
-    assertThat(info).hasSize(2);
-    assertSequenceNumbers(info);
-    accountIndexedCounter.assertReindexOf(user);
+      // Add a new key
+      sender.clear();
+      String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), user.email());
+      gApi.accounts().id(user.username()).addSshKey(newKey);
+      info = gApi.accounts().id(user.username()).listSshKeys();
+      assertThat(info).hasSize(2);
+      assertSequenceNumbers(info);
+      accountIndexedCounter.assertReindexOf(user);
 
-    assertThat(sender.getMessages()).hasSize(1);
-    Message message = sender.getMessages().get(0);
-    assertThat(message.rcpt()).containsExactly(user.getEmailAddress());
-    assertThat(message.body()).contains("new SSH keys have been added");
+      assertThat(sender.getMessages()).hasSize(1);
+      Message message = sender.getMessages().get(0);
+      assertThat(message.rcpt()).containsExactly(user.getEmailAddress());
+      assertThat(message.body()).contains("new SSH keys have been added");
 
-    // Delete key
-    sender.clear();
-    gApi.accounts().id(user.username()).deleteSshKey(1);
-    info = gApi.accounts().id(user.username()).listSshKeys();
-    assertThat(info).hasSize(1);
-    accountIndexedCounter.assertReindexOf(user);
+      // Delete key
+      sender.clear();
+      gApi.accounts().id(user.username()).deleteSshKey(1);
+      info = gApi.accounts().id(user.username()).listSshKeys();
+      assertThat(info).hasSize(1);
+      accountIndexedCounter.assertReindexOf(user);
 
-    assertThat(sender.getMessages()).hasSize(1);
-    message = sender.getMessages().get(0);
-    assertThat(message.rcpt()).containsExactly(user.getEmailAddress());
-    assertThat(message.body()).contains("SSH keys have been deleted");
+      assertThat(sender.getMessages()).hasSize(1);
+      message = sender.getMessages().get(0);
+      assertThat(message.rcpt()).containsExactly(user.getEmailAddress());
+      assertThat(message.body()).contains("SSH keys have been deleted");
+    }
   }
 
   @Test
@@ -2472,40 +2104,47 @@
   public void userCannotAddSshKeyToOtherAccount() throws Exception {
     String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username()).addSshKey(newKey);
+    assertThrows(AuthException.class, () -> gApi.accounts().id(admin.username()).addSshKey(newKey));
   }
 
   @Test
   @UseSsh
   public void userCannotDeleteSshKeyOfOtherAccount() throws Exception {
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(ResourceNotFoundException.class);
-    gApi.accounts().id(admin.username()).deleteSshKey(0);
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.accounts().id(admin.username()).deleteSshKey(0));
   }
 
   // reindex is tested by {@link AbstractQueryAccountsTest#reindex}
   @Test
   public void reindexPermissions() throws Exception {
-    // admin can reindex any account
-    requestScopeOperations.setApiUser(admin.id());
-    gApi.accounts().id(user.username()).index();
-    accountIndexedCounter.assertReindexOf(user);
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      // admin can reindex any account
+      requestScopeOperations.setApiUser(admin.id());
+      gApi.accounts().id(user.username()).index();
+      accountIndexedCounter.assertReindexOf(user);
 
-    // user can reindex own account
-    requestScopeOperations.setApiUser(user.id());
-    gApi.accounts().self().index();
-    accountIndexedCounter.assertReindexOf(user);
+      // user can reindex own account
+      requestScopeOperations.setApiUser(user.id());
+      gApi.accounts().self().index();
+      accountIndexedCounter.assertReindexOf(user);
 
-    // user cannot reindex any account
-    exception.expect(AuthException.class);
-    exception.expectMessage("modify account not permitted");
-    gApi.accounts().id(admin.username()).index();
+      // user cannot reindex any account
+      AuthException thrown =
+          assertThrows(AuthException.class, () -> gApi.accounts().id(admin.username()).index());
+      assertThat(thrown).hasMessageThat().contains("modify account not permitted");
+    }
   }
 
   @Test
   public void checkConsistency() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.resetCurrentApiUser();
 
     // Create an account with a preferred email.
@@ -2560,22 +2199,21 @@
   @Test
   public void checkMetaId() throws Exception {
     // metaId is set when account is loaded
-    assertThat(accounts.get(admin.id()).get().getAccount().getMetaId())
-        .isEqualTo(getMetaId(admin.id()));
+    assertThat(accounts.get(admin.id()).get().account().metaId()).isEqualTo(getMetaId(admin.id()));
 
     // metaId is set when account is created
     AccountsUpdate au = accountsUpdateProvider.get();
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     AccountState accountState = au.insert("Create Test Account", accountId, u -> {});
-    assertThat(accountState.getAccount().getMetaId()).isEqualTo(getMetaId(accountId));
+    assertThat(accountState.account().metaId()).isEqualTo(getMetaId(accountId));
 
     // metaId is set when account is updated
     Optional<AccountState> updatedAccountState =
         au.update("Set Full Name", accountId, u -> u.setFullName("foo"));
     assertThat(updatedAccountState).isPresent();
-    Account updatedAccount = updatedAccountState.get().getAccount();
-    assertThat(accountState.getAccount().getMetaId()).isNotEqualTo(updatedAccount.getMetaId());
-    assertThat(updatedAccount.getMetaId()).isEqualTo(getMetaId(accountId));
+    Account updatedAccount = updatedAccountState.get().account();
+    assertThat(accountState.account().metaId()).isNotEqualTo(updatedAccount.metaId());
+    assertThat(updatedAccount.metaId()).isEqualTo(getMetaId(accountId));
   }
 
   private EmailInput newEmailInput(String email, boolean noConfirmation) {
@@ -2629,12 +2267,9 @@
             "@", "@foo", "-", "-foo", "_", "_foo", "!", "+", "{", "}", "*", "%", "#", "$", "&", "’",
             "^", "=", "~");
     for (String name : invalidNames) {
-      try {
-        gApi.accounts().create(name);
-        fail(String.format("Expected BadRequestException for username [%s]", name));
-      } catch (BadRequestException e) {
-        assertThat(e).hasMessageThat().isEqualTo(String.format("Invalid username '%s'", name));
-      }
+      BadRequestException thrown =
+          assertThrows(BadRequestException.class, () -> gApi.accounts().create(name));
+      assertThat(thrown).hasMessageThat().isEqualTo(String.format("Invalid username '%s'", name));
     }
   }
 
@@ -2693,7 +2328,11 @@
             externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
-                cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
+                cfg,
+                retryMetrics,
+                null,
+                new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
+                r -> r.withBlockStrategy(noSleepBlockStrategy)),
             extIdNotesFactory,
             ident,
             ident,
@@ -2719,9 +2358,9 @@
     assertThat(doneBgUpdate.get()).isTrue();
 
     assertThat(updatedAccountState).isPresent();
-    Account updatedAccount = updatedAccountState.get().getAccount();
-    assertThat(updatedAccount.getStatus()).isEqualTo(status);
-    assertThat(updatedAccount.getFullName()).isEqualTo(fullName);
+    Account updatedAccount = updatedAccountState.get().account();
+    assertThat(updatedAccount.status()).isEqualTo(status);
+    assertThat(updatedAccount.fullName()).isEqualTo(fullName);
 
     accountInfo = gApi.accounts().id(admin.id().get()).get();
     assertThat(accountInfo.status).isEqualTo(status);
@@ -2746,6 +2385,7 @@
                 cfg,
                 retryMetrics,
                 null,
+                new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
                 r ->
                     r.withStopStrategy(StopStrategies.stopAfterAttempt(status.size()))
                         .withBlockStrategy(noSleepBlockStrategy)),
@@ -2770,17 +2410,14 @@
     assertThat(accountInfo.status).isNull();
     assertThat(accountInfo.name).isNotEqualTo(fullName);
 
-    try {
-      update.update("Set Full Name", admin.id(), u -> u.setFullName(fullName));
-      fail("expected LockFailureException");
-    } catch (LockFailureException e) {
-      // Ignore, expected
-    }
+    assertThrows(
+        LockFailureException.class,
+        () -> update.update("Set Full Name", admin.id(), u -> u.setFullName(fullName)));
     assertThat(bgCounter.get()).isEqualTo(status.size());
 
-    Account updatedAccount = accounts.get(admin.id()).get().getAccount();
-    assertThat(updatedAccount.getStatus()).isEqualTo(Iterables.getLast(status));
-    assertThat(updatedAccount.getFullName()).isEqualTo(admin.fullName());
+    Account updatedAccount = accounts.get(admin.id()).get().account();
+    assertThat(updatedAccount.status()).isEqualTo(Iterables.getLast(status));
+    assertThat(updatedAccount.fullName()).isEqualTo(admin.fullName());
 
     accountInfo = gApi.accounts().id(admin.id().get()).get();
     assertThat(accountInfo.status).isEqualTo(Iterables.getLast(status));
@@ -2803,7 +2440,11 @@
             externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
-                cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
+                cfg,
+                retryMetrics,
+                null,
+                new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
+                r -> r.withBlockStrategy(noSleepBlockStrategy)),
             extIdNotesFactory,
             ident,
             ident,
@@ -2826,12 +2467,12 @@
             "Set Status",
             admin.id(),
             (a, u) -> {
-              if ("A-1".equals(a.getAccount().getStatus())) {
+              if ("A-1".equals(a.account().status())) {
                 bgCounterA1.getAndIncrement();
                 u.setStatus("B-1");
               }
 
-              if ("A-2".equals(a.getAccount().getStatus())) {
+              if ("A-2".equals(a.account().status())) {
                 bgCounterA2.getAndIncrement();
                 u.setStatus("B-2");
               }
@@ -2841,16 +2482,19 @@
     assertThat(bgCounterA2.get()).isEqualTo(1);
 
     assertThat(updatedAccountState).isPresent();
-    assertThat(updatedAccountState.get().getAccount().getStatus()).isEqualTo("B-2");
-    assertThat(accounts.get(admin.id()).get().getAccount().getStatus()).isEqualTo("B-2");
+    assertThat(updatedAccountState.get().account().status()).isEqualTo("B-2");
+    assertThat(accounts.get(admin.id()).get().account().status()).isEqualTo("B-2");
     assertThat(gApi.accounts().id(admin.id().get()).get().status).isEqualTo("B-2");
   }
 
   @Test
   public void atomicReadMofifyWriteExternalIds() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId extIdA1 = ExternalId.create("foo", "A-1", accountId);
     accountsUpdateProvider
         .get()
@@ -2869,7 +2513,11 @@
             externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
-                cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
+                cfg,
+                retryMetrics,
+                null,
+                new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
+                r -> r.withBlockStrategy(noSleepBlockStrategy)),
             extIdNotesFactory,
             ident,
             ident,
@@ -2901,12 +2549,12 @@
             "Update External ID",
             accountId,
             (a, u) -> {
-              if (a.getExternalIds().contains(extIdA1)) {
+              if (a.externalIds().contains(extIdA1)) {
                 bgCounterA1.getAndIncrement();
                 u.replaceExternalId(extIdA1, extIdB1);
               }
 
-              if (a.getExternalIds().contains(extIdA2)) {
+              if (a.externalIds().contains(extIdA2)) {
                 bgCounterA2.getAndIncrement();
                 u.replaceExternalId(extIdA2, extIdB2);
               }
@@ -2916,8 +2564,8 @@
     assertThat(bgCounterA2.get()).isEqualTo(1);
 
     assertThat(updatedAccount).isPresent();
-    assertThat(updatedAccount.get().getExternalIds()).containsExactly(extIdB2);
-    assertThat(accounts.get(accountId).get().getExternalIds()).containsExactly(extIdB2);
+    assertThat(updatedAccount.get().externalIds()).containsExactly(extIdB2);
+    assertThat(accounts.get(accountId).get().externalIds()).containsExactly(extIdB2);
     assertThat(
             gApi.accounts().id(accountId.get()).getExternalIds().stream()
                 .map(i -> i.identity)
@@ -2929,7 +2577,7 @@
   public void stalenessChecker() throws Exception {
     // Newly created account is not stale.
     AccountInfo accountInfo = gApi.accounts().create(name("foo")).get();
-    Account.Id accountId = new Account.Id(accountInfo._accountId);
+    Account.Id accountId = Account.id(accountInfo._accountId);
     assertThat(stalenessChecker.isStale(accountId)).isFalse();
 
     // Manually updating the user ref makes the index document stale.
@@ -3006,9 +2654,9 @@
   }
 
   @Test
+  @UseClockStep
   public void deleteAllDraftComments() throws Exception {
     try {
-      TestTimeUtil.resetWithClockStep(1, SECONDS);
       Project.NameKey project2 = projectOperations.newProject().create();
       PushOneCommit.Result r1 = createChange();
 
@@ -3093,12 +2741,14 @@
       requestScopeOperations.setApiUser(user.id());
       createDraft(r, PushOneCommit.FILE_NAME, "draft");
       requestScopeOperations.setApiUser(admin.id());
-      try {
-        gApi.accounts().id(user.id().get()).deleteDraftComments(new DeleteDraftCommentsInput());
-        assert_().fail("expected AuthException");
-      } catch (AuthException e) {
-        assertThat(e).hasMessageThat().isEqualTo("Cannot delete drafts of other user");
-      }
+      AuthException thrown =
+          assertThrows(
+              AuthException.class,
+              () ->
+                  gApi.accounts()
+                      .id(user.id().get())
+                      .deleteDraftComments(new DeleteDraftCommentsInput()));
+      assertThat(thrown).hasMessageThat().isEqualTo("Cannot delete drafts of other user");
     } finally {
       cleanUpDrafts();
     }
@@ -3107,7 +2757,7 @@
   @Test
   public void deleteDraftCommentsSkipsInvisibleChanges() throws Exception {
     try {
-      createBranch(new Branch.NameKey(project, "secret"));
+      createBranch(BranchNameKey.create(project, "secret"));
       PushOneCommit.Result r1 = createChange();
       PushOneCommit.Result r2 = createChange("refs/for/secret");
 
@@ -3117,14 +2767,22 @@
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
 
-      block(project, "refs/heads/secret", Permission.READ, REGISTERED_USERS);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .add(block(Permission.READ).ref("refs/heads/secret").group(REGISTERED_USERS))
+          .update();
       List<DeletedDraftCommentInfo> result =
           gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
       assertThat(result).hasSize(1);
       assertThat(result.get(0).change.changeId).isEqualTo(r1.getChangeId());
       assertThat(result.get(0).deleted.stream().map(c -> c.message)).containsExactly("draft a");
 
-      removePermission(project, "refs/heads/secret", Permission.READ);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .remove(permissionKey(Permission.READ).ref("refs/heads/secret"))
+          .update();
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
       // Draft still exists since change wasn't visible when drafts where deleted.
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
@@ -3155,22 +2813,23 @@
   @Test
   public void userCannotGenerateNewHttpPasswordForOtherUser() throws Exception {
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username()).generateHttpPassword();
+    assertThrows(
+        AuthException.class, () -> gApi.accounts().id(admin.username()).generateHttpPassword());
   }
 
   @Test
   public void userCannotExplicitlySetHttpPassword() throws Exception {
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.accounts().self().setHttpPassword("my-new-password");
+    assertThrows(
+        AuthException.class, () -> gApi.accounts().self().setHttpPassword("my-new-password"));
   }
 
   @Test
   public void userCannotExplicitlySetHttpPasswordForOtherUser() throws Exception {
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username()).setHttpPassword("my-new-password");
+    assertThrows(
+        AuthException.class,
+        () -> gApi.accounts().id(admin.username()).setHttpPassword("my-new-password"));
   }
 
   @Test
@@ -3185,8 +2844,8 @@
   @Test
   public void userCannotRemoveHttpPasswordForOtherUser() throws Exception {
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username()).setHttpPassword(null);
+    assertThrows(
+        AuthException.class, () -> gApi.accounts().id(admin.username()).setHttpPassword(null));
   }
 
   @Test
@@ -3214,9 +2873,11 @@
     requestScopeOperations.setApiUser(admin.id());
     int userId = accountCreator.create().id().get();
     assertThat(gApi.accounts().id(userId).get().username).isNull();
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("username");
-    gApi.accounts().id(userId).generateHttpPassword();
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.accounts().id(userId).generateHttpPassword());
+    assertThat(thrown).hasMessageThat().contains("username");
   }
 
   private void createDraft(PushOneCommit.Result r, String path, String message) throws Exception {
@@ -3243,12 +2904,9 @@
 
   private static Correspondence<GroupInfo, String> getGroupToNameCorrespondence() {
     return Correspondence.from(
-        new BinaryPredicate<GroupInfo, String>() {
-          @Override
-          public boolean apply(GroupInfo actualGroup, String expectedName) {
-            String groupName = actualGroup == null ? null : actualGroup.name;
-            return Objects.equals(groupName, expectedName);
-          }
+        (actualGroup, expectedName) -> {
+          String groupName = actualGroup == null ? null : actualGroup.name;
+          return Objects.equals(groupName, expectedName);
         },
         "has name");
   }
@@ -3297,8 +2955,8 @@
     // Check via API.
     FluentIterable<TestKey> expected = FluentIterable.from(expectedKeys);
     Map<String, GpgKeyInfo> keyMap = gApi.accounts().self().listGpgKeys();
-    assertThat(keyMap.keySet())
-        .named("keys returned by listGpgKeys()")
+    assertWithMessage("keys returned by listGpgKeys()")
+        .that(keyMap.keySet())
         .containsExactlyElementsIn(expected.transform(TestKey::getKeyIdString));
 
     for (TestKey key : expected) {
@@ -3320,7 +2978,9 @@
         externalIds.byAccount(currAccountId, SCHEME_GPGKEY).stream()
             .map(e -> e.key().id())
             .collect(toSet());
-    assertThat(actualFps).named("external IDs in database").containsExactlyElementsIn(expectedFps);
+    assertWithMessage("external IDs in database")
+        .that(actualFps)
+        .containsExactlyElementsIn(expectedFps);
 
     // Check raw stored keys.
     for (TestKey key : expected) {
@@ -3330,31 +2990,35 @@
 
   private static void assertKeyEquals(TestKey expected, GpgKeyInfo actual) {
     String id = expected.getKeyIdString();
-    assertThat(actual.id).named(id).isEqualTo(id);
-    assertThat(actual.fingerprint)
-        .named(id)
+    assertWithMessage(id).that(actual.id).isEqualTo(id);
+    assertWithMessage(id)
+        .that(actual.fingerprint)
         .isEqualTo(Fingerprint.toString(expected.getPublicKey().getFingerprint()));
     List<String> userIds = ImmutableList.copyOf(expected.getPublicKey().getUserIDs());
-    assertThat(actual.userIds).named(id).containsExactlyElementsIn(userIds);
+    assertWithMessage(id).that(actual.userIds).containsExactlyElementsIn(userIds);
     String key = actual.key;
-    assertThat(key).named(id).startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n");
-    assertThat(key).named(id).endsWith("-----END PGP PUBLIC KEY BLOCK-----\n");
+    assertWithMessage(id).that(key).startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n");
+    assertWithMessage(id).that(key).endsWith("-----END PGP PUBLIC KEY BLOCK-----\n");
     assertThat(actual.status).isEqualTo(GpgKeyInfo.Status.TRUSTED);
     assertThat(actual.problems).isEmpty();
   }
 
   private void addExternalIdEmail(TestAccount account, String email) throws Exception {
-    requireNonNull(email);
-    accountsUpdateProvider
-        .get()
-        .update(
-            "Add Email",
-            account.id(),
-            u ->
-                u.addExternalId(
-                    ExternalId.createWithEmail(name("test"), email, account.id(), email)));
-    accountIndexedCounter.assertReindexOf(account);
-    requestScopeOperations.setApiUser(account.id());
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      requireNonNull(email);
+      accountsUpdateProvider
+          .get()
+          .update(
+              "Add Email",
+              account.id(),
+              u ->
+                  u.addExternalId(
+                      ExternalId.createWithEmail(name("test"), email, account.id(), email)));
+      accountIndexedCounter.assertReindexOf(account);
+      requestScopeOperations.setApiUser(account.id());
+    }
   }
 
   private Map<String, GpgKeyInfo> addGpgKey(String armored) throws Exception {
@@ -3362,12 +3026,16 @@
   }
 
   private Map<String, GpgKeyInfo> addGpgKey(TestAccount account, String armored) throws Exception {
-    Map<String, GpgKeyInfo> gpgKeys =
-        gApi.accounts()
-            .id(account.username())
-            .putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
-    accountIndexedCounter.assertReindexOf(gApi.accounts().id(account.username()).get());
-    return gpgKeys;
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      Map<String, GpgKeyInfo> gpgKeys =
+          gApi.accounts()
+              .id(account.username())
+              .putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
+      accountIndexedCounter.assertReindexOf(gApi.accounts().id(account.username()).get());
+      return gpgKeys;
+    }
   }
 
   private Map<String, GpgKeyInfo> addGpgKeyNoReindex(String armored) throws Exception {
@@ -3395,26 +3063,6 @@
     assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.id());
   }
 
-  private Config getAccountConfig(TestRepository<?> allUsersRepo) throws Exception {
-    Config ac = new Config();
-    try (TreeWalk tw =
-        TreeWalk.forPath(
-            allUsersRepo.getRepository(),
-            AccountProperties.ACCOUNT_CONFIG,
-            getHead(allUsersRepo.getRepository(), "HEAD").getTree())) {
-      assertThat(tw).isNotNull();
-      ac.fromText(
-          new String(
-              allUsersRepo
-                  .getRevWalk()
-                  .getObjectReader()
-                  .open(tw.getObjectId(0), OBJ_BLOB)
-                  .getBytes(),
-              UTF_8));
-    }
-    return ac;
-  }
-
   private AccountApi accountIdApi() throws RestApiException {
     return gApi.accounts().id(user.id().get());
   }
@@ -3439,47 +3087,6 @@
     assertThat(loginResponse.getStatusLine().getStatusCode()).isEqualTo(expectedHttpStatus);
   }
 
-  /** Checks if an account is indexed the correct number of times. */
-  private static class AccountIndexedCounter implements AccountIndexedListener {
-    private final AtomicLongMap<Integer> countsByAccount = AtomicLongMap.create();
-
-    @Override
-    public void onAccountIndexed(int id) {
-      countsByAccount.incrementAndGet(id);
-    }
-
-    void clear() {
-      countsByAccount.clear();
-    }
-
-    long getCount(Account.Id accountId) {
-      return countsByAccount.get(accountId.get());
-    }
-
-    void assertReindexOf(TestAccount testAccount) {
-      assertReindexOf(testAccount, 1);
-    }
-
-    void assertReindexOf(AccountInfo accountInfo) {
-      assertReindexOf(new Account.Id(accountInfo._accountId), 1);
-    }
-
-    void assertReindexOf(TestAccount testAccount, int expectedCount) {
-      assertThat(getCount(testAccount.id())).isEqualTo(expectedCount);
-      assertThat(countsByAccount).hasSize(1);
-      clear();
-    }
-
-    void assertReindexOf(Account.Id accountId, int expectedCount) {
-      assertThat(getCount(accountId)).isEqualTo(expectedCount);
-      countsByAccount.remove(accountId.get());
-    }
-
-    void assertNoReindex() {
-      assertThat(countsByAccount).isEmpty();
-    }
-  }
-
   private static class RefUpdateCounter implements GitReferenceUpdatedListener {
     private final AtomicLongMap<String> countsByProjectRefs = AtomicLongMap.create();
 
@@ -3500,23 +3107,17 @@
       countsByProjectRefs.clear();
     }
 
-    long getCount(String projectRef) {
-      return countsByProjectRefs.get(projectRef);
-    }
-
     void assertRefUpdateFor(String... projectRefs) {
-      Map<String, Integer> expectedRefUpdateCounts = new HashMap<>();
+      Map<String, Long> expectedRefUpdateCounts = new HashMap<>();
       for (String projectRef : projectRefs) {
-        expectedRefUpdateCounts.put(projectRef, 1);
+        expectedRefUpdateCounts.put(projectRef, 1L);
       }
       assertRefUpdateFor(expectedRefUpdateCounts);
     }
 
-    void assertRefUpdateFor(Map<String, Integer> expectedProjectRefUpdateCounts) {
-      for (Map.Entry<String, Integer> e : expectedProjectRefUpdateCounts.entrySet()) {
-        assertThat(getCount(e.getKey())).isEqualTo(e.getValue());
-      }
-      assertThat(countsByProjectRefs).hasSize(expectedProjectRefUpdateCounts.size());
+    void assertRefUpdateFor(Map<String, Long> expectedProjectRefUpdateCounts) {
+      assertThat(countsByProjectRefs.asMap())
+          .containsExactlyEntriesIn(expectedProjectRefUpdateCounts);
       clear();
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
index 60a61d1..72a8264 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
@@ -17,10 +17,10 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountConfig;
@@ -66,7 +66,7 @@
     List<AccountState> matchedAccountStates =
         accountQueryProvider.get().byPreferredEmail(preferredEmail);
     assertThat(matchedAccountStates).hasSize(1);
-    assertThat(matchedAccountStates.get(0).getAccount().getId()).isEqualTo(accountId);
+    assertThat(matchedAccountStates.get(0).account().id()).isEqualTo(accountId);
   }
 
   @Test
@@ -82,7 +82,7 @@
     List<AccountState> matchedAccountStates =
         accountQueryProvider.get().byPreferredEmail(preferredEmail);
     assertThat(matchedAccountStates).hasSize(1);
-    assertThat(matchedAccountStates.get(0).getAccount().getId()).isEqualTo(accountId);
+    assertThat(matchedAccountStates.get(0).account().id()).isEqualTo(accountId);
   }
 
   @Test
@@ -91,10 +91,10 @@
     loadAccountToCache(accountId);
     String status = "ooo";
     updateAccountWithoutCacheOrIndex(accountId, newAccountUpdate().setStatus(status).build());
-    assertThat(accountCache.get(accountId).get().getAccount().getStatus()).isNull();
+    assertThat(accountCache.get(accountId).get().account().status()).isNull();
 
     accountIndexer.index(accountId);
-    assertThat(accountCache.get(accountId).get().getAccount().getStatus()).isEqualTo(status);
+    assertThat(accountCache.get(accountId).get().account().status()).isEqualTo(status);
   }
 
   @Test
@@ -109,7 +109,7 @@
     List<AccountState> matchedAccountStates =
         accountQueryProvider.get().byPreferredEmail(preferredEmail);
     assertThat(matchedAccountStates).hasSize(1);
-    assertThat(matchedAccountStates.get(0).getAccount().getId()).isEqualTo(accountId);
+    assertThat(matchedAccountStates.get(0).account().id()).isEqualTo(accountId);
   }
 
   @Test
@@ -136,7 +136,7 @@
 
   private Account.Id createAccount(String name) throws RestApiException {
     AccountInfo account = gApi.accounts().create(name).get();
-    return new Account.Id(account._accountId);
+    return Account.id(account._accountId);
   }
 
   private void reloadAccountToCache(Account.Id accountId) {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java
new file mode 100644
index 0000000..80eff96
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java
@@ -0,0 +1,212 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.accounts;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.extensions.events.AccountActivationListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.validators.AccountActivationValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests the wiring of a real plugin's account listeners
+ *
+ * <p>This test really puts focus on the wiring of the account listeners. Tests for the inner
+ * workings of account activation/deactivation can be found in {@link AccountIT}.
+ */
+@TestPlugin(
+    name = "account-listener-it-plugin",
+    sysModule = "com.google.gerrit.acceptance.api.accounts.AccountListenersIT$Module")
+public class AccountListenersIT extends LightweightPluginDaemonTest {
+  @Inject private AccountOperations accountOperations;
+
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      DynamicSet.bind(binder(), AccountActivationValidationListener.class).to(Validator.class);
+      DynamicSet.bind(binder(), AccountActivationListener.class).to(Listener.class);
+    }
+  }
+
+  Validator validator;
+  Listener listener;
+
+  @Before
+  public void setUp() {
+    validator = plugin.getSysInjector().getInstance(Validator.class);
+
+    listener = plugin.getSysInjector().getInstance(Listener.class);
+  }
+
+  @Test
+  public void testActivation() throws RestApiException {
+    int id = accountOperations.newAccount().inactive().create().get();
+
+    gApi.accounts().id(id).setActive(true);
+
+    validator.assertActivationValidation(id);
+    listener.assertActivated(id);
+    assertNoMoreEvents();
+    assertThat(gApi.accounts().id(id).getActive()).isTrue();
+  }
+
+  @Test
+  public void testActivationProhibited() throws RestApiException {
+    int id = accountOperations.newAccount().inactive().create().get();
+
+    validator.failActivationValidations();
+
+    assertThrows(
+        ResourceConflictException.class,
+        () -> {
+          gApi.accounts().id(id).setActive(true);
+        });
+
+    validator.assertActivationValidation(id);
+    // No call to activation listener as validation failed
+    assertNoMoreEvents();
+    assertThat(gApi.accounts().id(id).getActive()).isFalse();
+  }
+
+  @Test
+  public void testDeactivation() throws RestApiException {
+    int id = accountOperations.newAccount().active().create().get();
+
+    gApi.accounts().id(id).setActive(false);
+
+    validator.assertDeactivationValidation(id);
+    listener.assertDeactivated(id);
+    assertNoMoreEvents();
+    assertThat(gApi.accounts().id(id).getActive()).isFalse();
+  }
+
+  @Test
+  public void testDeactivationProhibited() throws RestApiException {
+    int id = accountOperations.newAccount().active().create().get();
+
+    validator.failDeactivationValidations();
+
+    assertThrows(
+        ResourceConflictException.class,
+        () -> {
+          gApi.accounts().id(id).setActive(false);
+        });
+
+    validator.assertDeactivationValidation(id);
+    // No call to activation listener as validation failed
+    assertNoMoreEvents();
+    assertThat(gApi.accounts().id(id).getActive()).isTrue();
+  }
+
+  private void assertNoMoreEvents() {
+    validator.assertNoMoreEvents();
+    listener.assertNoMoreEvents();
+  }
+
+  @Singleton
+  public static class Validator implements AccountActivationValidationListener {
+    private Integer lastIdActivationValidation;
+    private Integer lastIdDeactivationValidation;
+    private boolean failActivationValidations;
+    private boolean failDeactivationValidations;
+
+    @Override
+    public void validateActivation(AccountState account) throws ValidationException {
+      assertThat(lastIdActivationValidation).isNull();
+      lastIdActivationValidation = account.account().id().get();
+      if (failActivationValidations) {
+        throw new ValidationException("testing validation failure");
+      }
+    }
+
+    @Override
+    public void validateDeactivation(AccountState account) throws ValidationException {
+      assertThat(lastIdDeactivationValidation).isNull();
+      lastIdDeactivationValidation = account.account().id().get();
+      if (failDeactivationValidations) {
+        throw new ValidationException("testing validation failure");
+      }
+    }
+
+    public void failActivationValidations() {
+      failActivationValidations = true;
+    }
+
+    public void failDeactivationValidations() {
+      failDeactivationValidations = true;
+    }
+
+    private void assertNoMoreEvents() {
+      assertThat(lastIdActivationValidation).isNull();
+      assertThat(lastIdDeactivationValidation).isNull();
+    }
+
+    private void assertActivationValidation(int id) {
+      assertThat(lastIdActivationValidation).isEqualTo(id);
+      lastIdActivationValidation = null;
+    }
+
+    private void assertDeactivationValidation(int id) {
+      assertThat(lastIdDeactivationValidation).isEqualTo(id);
+      lastIdDeactivationValidation = null;
+    }
+  }
+
+  @Singleton
+  public static class Listener implements AccountActivationListener {
+    private Integer lastIdActivated;
+    private Integer lastIdDeactivated;
+
+    @Override
+    public void onAccountActivated(int id) {
+      assertThat(lastIdActivated).isNull();
+      lastIdActivated = id;
+    }
+
+    @Override
+    public void onAccountDeactivated(int id) {
+      assertThat(lastIdDeactivated).isNull();
+      lastIdDeactivated = id;
+    }
+
+    private void assertNoMoreEvents() {
+      assertThat(lastIdActivated).isNull();
+      assertThat(lastIdDeactivated).isNull();
+    }
+
+    private void assertDeactivated(int id) {
+      assertThat(lastIdDeactivated).isEqualTo(id);
+      lastIdDeactivated = null;
+    }
+
+    private void assertActivated(int id) {
+      assertThat(lastIdActivated).isEqualTo(id);
+      lastIdActivated = null;
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index 7fd6af9..5bc0473 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -14,16 +14,19 @@
 
 package com.google.gerrit.acceptance.api.accounts;
 
+import static com.google.common.truth.OptionalSubject.optionals;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountException;
@@ -137,7 +140,7 @@
   @Test
   public void authenticateWithEmail() throws Exception {
     String email = "foo@example.com";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key mailtoExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_MAILTO, email);
     accountsUpdate.insert(
         "Create Test Account",
@@ -152,7 +155,7 @@
   @Test
   public void authenticateWithUsername() throws Exception {
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -167,7 +170,7 @@
   @Test
   public void authenticateWithExternalUser() throws Exception {
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -183,7 +186,7 @@
   public void authenticateWithUsernameAndUpdateEmail() throws Exception {
     String username = "foo";
     String email = "foo@example.com";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -204,7 +207,7 @@
 
     Optional<AccountState> accountState = accounts.get(accountId);
     assertThat(accountState).isPresent();
-    assertThat(accountState.get().getAccount().getPreferredEmail()).isEqualTo(newEmail);
+    assertThat(accountState.get().account().preferredEmail()).isEqualTo(newEmail);
   }
 
   @Test
@@ -236,7 +239,7 @@
   private void authenticateWithUsernameAndUpdateDisplayName(AccountManager am) throws Exception {
     String username = "foo";
     String email = "foo@example.com";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -254,7 +257,7 @@
 
     Optional<AccountState> accountState = accounts.get(accountId);
     assertThat(accountState).isPresent();
-    assertThat(accountState.get().getAccount().getFullName()).isEqualTo(newName);
+    assertThat(accountState.get().account().fullName()).isEqualTo(newName);
   }
 
   @Test
@@ -264,7 +267,7 @@
     assertNoSuchExternalIds(gerritExtIdKey);
 
     // Create orphaned SCHEME_GERRIT external ID.
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId gerritExtId = ExternalId.create(gerritExtIdKey, accountId);
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
@@ -274,15 +277,15 @@
     }
 
     AuthRequest who = AuthRequest.forUser(username);
-    exception.expect(AccountException.class);
-    exception.expectMessage("Authentication error, account not found");
-    accountManager.authenticate(who);
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.authenticate(who));
+    assertThat(thrown).hasMessageThat().contains("Authentication error, account not found");
   }
 
   @Test
   public void cannotAuthenticateWithInactiveAccount() throws Exception {
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -290,16 +293,16 @@
         u -> u.setActive(false).addExternalId(ExternalId.create(gerritExtIdKey, accountId)));
 
     AuthRequest who = AuthRequest.forUser(username);
-    exception.expect(AccountException.class);
-    exception.expectMessage("Authentication error, account inactive");
-    accountManager.authenticate(who);
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.authenticate(who));
+    assertThat(thrown).hasMessageThat().contains("Authentication error, account inactive");
   }
 
   @Test
   public void cannotActivateAccountOnAuthenticationWhenAutoUpdateAccountActiveStatusIsDisabled()
       throws Exception {
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -309,9 +312,9 @@
     AuthRequest who = AuthRequest.forUser(username);
     who.setActive(true);
     who.setAuthProvidesAccountActiveStatus(true);
-    exception.expect(AccountException.class);
-    exception.expectMessage("Authentication error, account inactive");
-    accountManager.authenticate(who);
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.authenticate(who));
+    assertThat(thrown).hasMessageThat().contains("Authentication error, account inactive");
   }
 
   @Test
@@ -319,7 +322,7 @@
   public void activateAccountOnAuthenticationWhenAutoUpdateAccountActiveStatusIsEnabled()
       throws Exception {
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -333,14 +336,14 @@
     assertAuthResultForExistingAccount(authResult, accountId, gerritExtIdKey);
     Optional<AccountState> accountState = accounts.get(accountId);
     assertThat(accountState).isPresent();
-    assertThat(accountState.get().getAccount().isActive()).isTrue();
+    assertThat(accountState.get().account().isActive()).isTrue();
   }
 
   @Test
   public void cannotDeactivateAccountOnAuthenticationWhenAutoUpdateAccountActiveStatusIsDisabled()
       throws Exception {
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -354,7 +357,7 @@
     assertAuthResultForExistingAccount(authResult, accountId, gerritExtIdKey);
     Optional<AccountState> accountState = accounts.get(accountId);
     assertThat(accountState).isPresent();
-    assertThat(accountState.get().getAccount().isActive()).isTrue();
+    assertThat(accountState.get().account().isActive()).isTrue();
   }
 
   @Test
@@ -362,7 +365,7 @@
   public void deactivateAccountOnAuthenticationWhenAutoUpdateAccountActiveStatusIsEnabled()
       throws Exception {
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -372,16 +375,13 @@
     AuthRequest who = AuthRequest.forUser(username);
     who.setActive(false);
     who.setAuthProvidesAccountActiveStatus(true);
-    try {
-      accountManager.authenticate(who);
-      fail("Expected AccountException");
-    } catch (AccountException e) {
-      assertThat(e).hasMessageThat().isEqualTo("Authentication error, account inactive");
-    }
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.authenticate(who));
+    assertThat(thrown).hasMessageThat().isEqualTo("Authentication error, account inactive");
 
     Optional<AccountState> accountState = accounts.get(accountId);
     assertThat(accountState).isPresent();
-    assertThat(accountState.get().getAccount().isActive()).isFalse();
+    assertThat(accountState.get().account().isActive()).isFalse();
   }
 
   @Test
@@ -390,7 +390,7 @@
 
     // Create an account with an SCHEME_EXTERNAL external ID that occupies the email.
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -400,9 +400,11 @@
     // Try to authenticate with this email to create a new account with a SCHEME_MAILTO external ID.
     // Expect that this fails because the email is already assigned to the other account.
     AuthRequest who = AuthRequest.forEmail(email);
-    exception.expect(AccountException.class);
-    exception.expectMessage("Email 'foo@example.com' in use by another account");
-    accountManager.authenticate(who);
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.authenticate(who));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Email 'foo@example.com' in use by another account");
   }
 
   @Test
@@ -411,7 +413,7 @@
 
     // Create an account with an SCHEME_EXTERNAL external ID that occupies the email.
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -422,9 +424,11 @@
     // Expect that this fails because the email is already assigned to the other account.
     AuthRequest who = AuthRequest.forUser("bar");
     who.setEmailAddress(email);
-    exception.expect(AccountException.class);
-    exception.expectMessage("Email 'foo@example.com' in use by another account");
-    accountManager.authenticate(who);
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.authenticate(who));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Email 'foo@example.com' in use by another account");
   }
 
   @Test
@@ -434,7 +438,7 @@
 
     // Create an account with a SCHEME_GERRIT external ID and an email.
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -444,7 +448,7 @@
                 .addExternalId(ExternalId.createWithEmail(gerritExtIdKey, accountId, email)));
 
     // Create another account with an SCHEME_EXTERNAL external ID that occupies the new email.
-    Account.Id accountId2 = new Account.Id(seq.nextAccountId());
+    Account.Id accountId2 = Account.id(seq.nextAccountId());
     ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, "bar");
     accountsUpdate.insert(
         "Create Test Account",
@@ -455,12 +459,11 @@
     // Expect that this fails because the new email is already assigned to the other account.
     AuthRequest who = AuthRequest.forUser(username);
     who.setEmailAddress(newEmail);
-    try {
-      accountManager.authenticate(who);
-      fail("Expected AccountException");
-    } catch (AccountException e) {
-      assertThat(e).hasMessageThat().isEqualTo("Email 'bar@example.com' in use by another account");
-    }
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.authenticate(who));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Email 'bar@example.com' in use by another account");
 
     // Verify that the email in the external ID was not updated.
     Optional<ExternalId> gerritExtId = externalIds.get(gerritExtIdKey);
@@ -470,7 +473,7 @@
     // Verify that the preferred email was not updated.
     Optional<AccountState> accountState = accounts.get(accountId);
     assertThat(accountState).isPresent();
-    assertThat(accountState.get().getAccount().getPreferredEmail()).isEqualTo(email);
+    assertThat(accountState.get().account().preferredEmail()).isEqualTo(email);
   }
 
   @Test
@@ -480,7 +483,7 @@
     // Create an account with a SCHEME_GERRIT external ID
     String username = "foo";
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
@@ -502,20 +505,20 @@
 
     // Verify that the account external ids with scheme 'mailto:' contains the email
     AccountState account = accounts.get(authResult.getAccountId()).get();
-    ImmutableSet<ExternalId> accountExternalIds = account.getExternalIds(ExternalId.SCHEME_MAILTO);
+    ImmutableSet<ExternalId> accountExternalIds = account.externalIds();
     assertThat(accountExternalIds).isNotEmpty();
     Set<String> emails = ExternalId.getEmails(accountExternalIds).collect(toSet());
     assertThat(emails).contains(email);
 
     // Verify the preferred email
-    assertThat(account.getAccount().getPreferredEmail()).isEqualTo(email);
+    assertThat(account.account().preferredEmail()).isEqualTo(email);
   }
 
   @Test
   public void linkNewExternalId() throws Exception {
     // Create an account with a SCHEME_GERRIT external ID and no email
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -539,7 +542,7 @@
   public void updateExternalIdOnLink() throws Exception {
     // Create an account with a SCHEME_GERRIT external ID and no email
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -562,7 +565,7 @@
   public void cannotLinkExternalIdThatIsAlreadyUsed() throws Exception {
     // Create an account with a SCHEME_EXTERNAL external ID
     String username1 = "foo";
-    Account.Id accountId1 = new Account.Id(seq.nextAccountId());
+    Account.Id accountId1 = Account.id(seq.nextAccountId());
     ExternalId.Key externalExtIdKey1 = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username1);
     accountsUpdate.insert(
         "Create Test Account",
@@ -571,7 +574,7 @@
 
     // Create another account with a SCHEME_EXTERNAL external ID
     String username2 = "bar";
-    Account.Id accountId2 = new Account.Id(seq.nextAccountId());
+    Account.Id accountId2 = Account.id(seq.nextAccountId());
     ExternalId.Key externalExtIdKey2 = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username2);
     accountsUpdate.insert(
         "Create Test Account",
@@ -581,9 +584,11 @@
     // Try to link external ID of the first account to the second account.
     // Expect that this fails because the external ID is already assigned to the first account.
     AuthRequest who = AuthRequest.forExternalUser(username1);
-    exception.expect(AccountException.class);
-    exception.expectMessage("Identity 'external:foo' in use by another account");
-    accountManager.link(accountId2, who);
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.link(accountId2, who));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Identity 'external:foo' in use by another account");
   }
 
   @Test
@@ -592,7 +597,7 @@
 
     // Create an account with an SCHEME_EXTERNAL external ID that occupies the email.
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -601,7 +606,7 @@
 
     // Create another account with a SCHEME_GERRIT external ID and no email
     String username2 = "foo";
-    Account.Id accountId2 = new Account.Id(seq.nextAccountId());
+    Account.Id accountId2 = Account.id(seq.nextAccountId());
     ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username2);
     accountsUpdate.insert(
         "Create Test Account",
@@ -611,9 +616,11 @@
     // Try to link the email to the second account (via a new MAILTO external ID) and expect that
     // this fails because the email is already assigned to the first account.
     AuthRequest who = AuthRequest.forEmail(email);
-    exception.expect(AccountException.class);
-    exception.expectMessage("Email 'foo@example.com' in use by another account");
-    accountManager.link(accountId2, who);
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.link(accountId2, who));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Email 'foo@example.com' in use by another account");
   }
 
   @Test
@@ -622,7 +629,7 @@
 
     // Create an account with an SCHEME_EXTERNAL external ID that occupies the email.
     String username = "foo";
-    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
@@ -637,7 +644,10 @@
 
   private void assertNoSuchExternalIds(ExternalId.Key... extIdKeys) throws Exception {
     for (ExternalId.Key extIdKey : extIdKeys) {
-      assertThat(externalIds.get(extIdKey)).named(extIdKey.get()).isEmpty();
+      assertWithMessage(extIdKey.get())
+          .about(optionals())
+          .that(externalIds.get(extIdKey))
+          .isEmpty();
     }
   }
 
@@ -658,13 +668,15 @@
       @Nullable String expectedEmail)
       throws Exception {
     Optional<ExternalId> extId = externalIds.get(extIdKey);
-    assertThat(extId).named(extIdKey.get()).isPresent();
+    assertWithMessage(extIdKey.get()).about(optionals()).that(extId).isPresent();
     if (expectedAccountId != null) {
-      assertThat(extId.get().accountId())
-          .named("account ID of " + extIdKey.get())
+      assertWithMessage("account ID of " + extIdKey.get())
+          .that(extId.get().accountId())
           .isEqualTo(expectedAccountId);
     }
-    assertThat(extId.get().email()).named("email of " + extIdKey.get()).isEqualTo(expectedEmail);
+    assertWithMessage("email of " + extIdKey.get())
+        .that(extId.get().email())
+        .isEqualTo(expectedEmail);
   }
 
   private void assertAuthResultForNewAccount(
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index a4a5745..5550d98 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -16,19 +16,22 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 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.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -44,21 +47,17 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
+@UseClockStep
 public class AgreementsIT extends AbstractDaemonTest {
   private ContributorAgreement caAutoVerify;
   private ContributorAgreement caNoAutoVerify;
@@ -81,7 +80,7 @@
     AccountGroup.UUID g = groupOperations.newGroup().name(name).create();
     GroupApi groupApi = gApi.groups().id(g.get());
     groupApi.description("CLA test group");
-    InternalGroup caGroup = group(new AccountGroup.UUID(groupApi.detail().id));
+    InternalGroup caGroup = group(AccountGroup.uuid(groupApi.detail().id));
     GroupReference groupRef = new GroupReference(caGroup.getGroupUUID(), caGroup.getName());
     PermissionRule rule = new PermissionRule(groupRef);
     rule.setAction(PermissionRule.Action.ALLOW);
@@ -111,16 +110,6 @@
     return cfg;
   }
 
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Before
   public void setUp() throws Exception {
     caAutoVerify = configureContributorAgreement(true);
@@ -145,17 +134,21 @@
   @Test
   public void signNonExistingAgreement() throws Exception {
     assume().that(isContributorAgreementsEnabled()).isTrue();
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("contributor agreement not found");
-    gApi.accounts().self().signAgreement("does-not-exist");
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.accounts().self().signAgreement("does-not-exist"));
+    assertThat(thrown).hasMessageThat().contains("contributor agreement not found");
   }
 
   @Test
   public void signAgreementNoAutoVerify() throws Exception {
     assume().that(isContributorAgreementsEnabled()).isTrue();
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("cannot enter a non-autoVerify agreement");
-    gApi.accounts().self().signAgreement(caNoAutoVerify.getName());
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.accounts().self().signAgreement(caNoAutoVerify.getName()));
+    assertThat(thrown).hasMessageThat().contains("cannot enter a non-autoVerify agreement");
   }
 
   @Test
@@ -188,33 +181,40 @@
   public void signAgreementAsOtherUser() throws Exception {
     assume().that(isContributorAgreementsEnabled()).isTrue();
     assertThat(gApi.accounts().self().get().name).isNotEqualTo("admin");
-    exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to enter contributor agreement");
-    gApi.accounts().id("admin").signAgreement(caAutoVerify.getName());
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.accounts().id("admin").signAgreement(caAutoVerify.getName()));
+    assertThat(thrown).hasMessageThat().contains("not allowed to enter contributor agreement");
   }
 
   @Test
   public void signAgreementAnonymous() throws Exception {
     requestScopeOperations.setApiUserAnonymous();
-    exception.expect(AuthException.class);
-    exception.expectMessage("Authentication required");
-    gApi.accounts().self().signAgreement(caAutoVerify.getName());
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.accounts().self().signAgreement(caAutoVerify.getName()));
+    assertThat(thrown).hasMessageThat().contains("Authentication required");
   }
 
   @Test
   public void agreementsDisabledSign() throws Exception {
     assume().that(isContributorAgreementsEnabled()).isFalse();
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("contributor agreements disabled");
-    gApi.accounts().self().signAgreement(caAutoVerify.getName());
+    MethodNotAllowedException thrown =
+        assertThrows(
+            MethodNotAllowedException.class,
+            () -> gApi.accounts().self().signAgreement(caAutoVerify.getName()));
+    assertThat(thrown).hasMessageThat().contains("contributor agreements disabled");
   }
 
   @Test
   public void agreementsDisabledList() throws Exception {
     assume().that(isContributorAgreementsEnabled()).isFalse();
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("contributor agreements disabled");
-    gApi.accounts().self().listAgreements();
+    MethodNotAllowedException thrown =
+        assertThrows(
+            MethodNotAllowedException.class, () -> gApi.accounts().self().listAgreements());
+    assertThat(thrown).hasMessageThat().contains("contributor agreements disabled");
   }
 
   @Test
@@ -233,9 +233,9 @@
     // Revert is not allowed when CLA is required but not signed
     requestScopeOperations.setApiUser(user.id());
     setUseContributorAgreements(InheritableBoolean.TRUE);
-    exception.expect(AuthException.class);
-    exception.expectMessage("Contributor Agreement");
-    gApi.changes().id(change.changeId).revert();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(change.changeId).revert());
+    assertThat(thrown).hasMessageThat().contains("Contributor Agreement");
   }
 
   @Test
@@ -287,9 +287,10 @@
     CherryPickInput in = new CherryPickInput();
     in.destination = dest.ref;
     in.message = change.subject;
-    exception.expect(AuthException.class);
-    exception.expectMessage("Contributor Agreement");
-    gApi.changes().id(change.changeId).current().cherryPick(in);
+    AuthException thrown =
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(change.changeId).current().cherryPick(in));
+    assertThat(thrown).hasMessageThat().contains("Contributor Agreement");
   }
 
   @Test
@@ -302,12 +303,9 @@
 
     // Create a change is not allowed when CLA is required but not signed
     setUseContributorAgreements(InheritableBoolean.TRUE);
-    try {
-      gApi.changes().create(newChangeInput());
-      fail("Expected AuthException");
-    } catch (AuthException e) {
-      assertThat(e.getMessage()).contains("Contributor Agreement");
-    }
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().create(newChangeInput()));
+    assertThat(thrown).hasMessageThat().contains("Contributor Agreement");
 
     // Sign the agreement
     gApi.accounts().self().signAgreement(caAutoVerify.getName());
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index c905d3f..6717fb7 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -16,8 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.AssertUtil.assertPrefs;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -26,23 +29,18 @@
 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.ReviewCategoryStrategy;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.TimeFormat;
 import com.google.gerrit.extensions.client.MenuItem;
 import com.google.gerrit.extensions.config.DownloadScheme;
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.inject.Inject;
-import com.google.inject.util.Providers;
 import java.util.ArrayList;
 import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class GeneralPreferencesIT extends AbstractDaemonTest {
-  @Inject private DynamicMap<DownloadScheme> downloadSchemes;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   private TestAccount user42;
 
@@ -63,15 +61,13 @@
             new MenuItem("Edits", "#/q/has:edit", null),
             new MenuItem("Watched Changes", "#/q/is:watched+is:open", null),
             new MenuItem("Starred Changes", "#/q/is:starred", null),
-            new MenuItem("Groups", "#/groups/self", null));
+            new MenuItem("Groups", "#/settings/#Groups", null));
     assertThat(o.changeTable).isEmpty();
 
     GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults();
 
     // change all default values
     i.changesPerPage *= -1;
-    i.showSiteHeader ^= true;
-    i.useFlashClipboard ^= true;
     i.dateFormat = DateFormat.US;
     i.timeFormat = TimeFormat.HHMM_24;
     i.emailStrategy = EmailStrategy.DISABLED;
@@ -84,7 +80,6 @@
     i.legacycidInChangeTable ^= true;
     i.muteCommonPathPrefixes ^= true;
     i.signedOffBy ^= true;
-    i.reviewCategoryStrategy = ReviewCategoryStrategy.ABBREV;
     i.diffView = DiffView.UNIFIED_DIFF;
     i.my = new ArrayList<>();
     i.my.add(new MenuItem("name", "url"));
@@ -155,9 +150,11 @@
     i.my = new ArrayList<>();
     i.my.add(new MenuItem(null, "url"));
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("name for menu item is required");
-    gApi.accounts().id(user42.id().toString()).setPreferences(i);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.accounts().id(user42.id().toString()).setPreferences(i));
+    assertThat(thrown).hasMessageThat().contains("name for menu item is required");
   }
 
   @Test
@@ -166,9 +163,11 @@
     i.my = new ArrayList<>();
     i.my.add(new MenuItem("name", null));
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("URL for menu item is required");
-    gApi.accounts().id(user42.id().toString()).setPreferences(i);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.accounts().id(user42.id().toString()).setPreferences(i));
+    assertThat(thrown).hasMessageThat().contains("URL for menu item is required");
   }
 
   @Test
@@ -186,18 +185,20 @@
     GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults();
     i.downloadScheme = "foo";
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("Unsupported download scheme: " + i.downloadScheme);
-    gApi.accounts().id(user42.id().toString()).setPreferences(i);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.accounts().id(user42.id().toString()).setPreferences(i));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Unsupported download scheme: " + i.downloadScheme);
   }
 
   @Test
   public void setDownloadScheme() throws Exception {
     String schemeName = "foo";
-    RegistrationHandle registrationHandle =
-        ((PrivateInternals_DynamicMapImpl<DownloadScheme>) downloadSchemes)
-            .put("myPlugin", schemeName, Providers.of(new TestDownloadScheme()));
-    try {
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(new TestDownloadScheme(), schemeName)) {
       GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults();
       i.downloadScheme = schemeName;
 
@@ -206,8 +207,6 @@
 
       o = gApi.accounts().id(user42.id().toString()).getPreferences();
       assertThat(o.downloadScheme).isEqualTo(schemeName);
-    } finally {
-      registrationHandle.remove();
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index 925c66a..8aebc69 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -15,10 +15,11 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.concurrent.TimeUnit.HOURS;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
@@ -26,13 +27,15 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
+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.Project;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.AbandonUtil;
 import com.google.gerrit.server.config.ChangeCleanupConfig;
@@ -46,8 +49,9 @@
 
 public class AbandonIT extends AbstractDaemonTest {
   @Inject private AbandonUtil abandonUtil;
-  @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ChangeCleanupConfig cleanupConfig;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void abandon() throws Exception {
@@ -59,9 +63,9 @@
     assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
     assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("abandoned");
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("change is abandoned");
-    gApi.changes().id(changeId).abandon();
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.changes().id(changeId).abandon());
+    assertThat(thrown).hasMessageThat().contains("change is abandoned");
   }
 
   @Test
@@ -89,24 +93,29 @@
     String project2Name = name("Project2");
     gApi.projects().create(project1Name);
     gApi.projects().create(project2Name);
-    TestRepository<InMemoryRepository> project1 = cloneProject(new Project.NameKey(project1Name));
-    TestRepository<InMemoryRepository> project2 = cloneProject(new Project.NameKey(project2Name));
+    TestRepository<InMemoryRepository> project1 = cloneProject(Project.nameKey(project1Name));
+    TestRepository<InMemoryRepository> project2 = cloneProject(Project.nameKey(project2Name));
 
     CurrentUser user = atrScope.get().getUser();
     PushOneCommit.Result a = createChange(project1, "master", "x", "x", "x", "");
     PushOneCommit.Result b = createChange(project2, "master", "x", "x", "x", "");
     List<ChangeData> list = ImmutableList.of(a.getChange(), b.getChange());
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        String.format("Project name \"%s\" doesn't match \"%s\"", project2Name, project1Name));
-    batchAbandon.batchAbandon(batchUpdateFactory, new Project.NameKey(project1Name), user, list);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () ->
+                batchAbandon.batchAbandon(
+                    batchUpdateFactory, Project.nameKey(project1Name), user, list));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            String.format("Project name \"%s\" doesn't match \"%s\"", project2Name, project1Name));
   }
 
   @Test
+  @UseClockStep
   @GerritConfig(name = "changeCleanup.abandonAfter", value = "1w")
   public void abandonInactiveOpenChanges() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-
     // create 2 changes which will be abandoned ...
     int id1 = createChange().getChange().getId().get();
     int id2 = createChange().getChange().getId().get();
@@ -157,9 +166,9 @@
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("abandon not permitted");
-    gApi.changes().id(changeId).abandon();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).abandon());
+    assertThat(thrown).hasMessageThat().contains("abandon not permitted");
   }
 
   @Test
@@ -167,7 +176,11 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
-    grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.ABANDON).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).abandon();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
@@ -188,9 +201,9 @@
     assertThat(info.status).isEqualTo(ChangeStatus.NEW);
     assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("restored");
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("change is new");
-    gApi.changes().id(changeId).restore();
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.changes().id(changeId).restore());
+    assertThat(thrown).hasMessageThat().contains("change is new");
   }
 
   @Test
@@ -201,9 +214,9 @@
     gApi.changes().id(changeId).abandon();
     requestScopeOperations.setApiUser(user.id());
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
-    exception.expect(AuthException.class);
-    exception.expectMessage("restore not permitted");
-    gApi.changes().id(changeId).restore();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).restore());
+    assertThat(thrown).hasMessageThat().contains("restore not permitted");
   }
 
   private List<Integer> toChangeNumbers(List<ChangeInfo> changes) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index f59238c..6a360c9 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -15,12 +15,21 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
+import static com.google.gerrit.entities.RefNames.changeMetaRef;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
@@ -38,20 +47,23 @@
 import static com.google.gerrit.extensions.client.ReviewerState.CC;
 import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
 import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
-import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
 import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.CacheStatsSubject.assertThat;
+import static com.google.gerrit.truth.CacheStatsSubject.cloneStats;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheStats;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -60,20 +72,33 @@
 import com.google.common.truth.ThrowableSubject;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.ChangeIndexedCounter;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
@@ -87,7 +112,6 @@
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
-import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.ReviewResult;
@@ -98,7 +122,6 @@
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
-import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -119,12 +142,8 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.MergeInput;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
-import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.common.TrackingIdInfo;
-import com.google.gerrit.extensions.events.ChangeIndexedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -135,22 +154,21 @@
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.git.ChangeMessageModifier;
+import com.google.gerrit.server.change.testing.TestChangeETagComputation;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.patch.DiffSummary;
+import com.google.gerrit.server.patch.DiffSummaryKey;
+import com.google.gerrit.server.patch.IntraLineDiff;
+import com.google.gerrit.server.patch.IntraLineDiffKey;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
 import com.google.gerrit.server.restapi.change.PostReview;
@@ -159,9 +177,9 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.name.Named;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
@@ -184,49 +202,31 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushResult;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
+@UseTimezone(timezone = "US/Eastern")
 public class ChangeIT extends AbstractDaemonTest {
-  private String systemTimeZone;
 
   @Inject private AccountOperations accountOperations;
   @Inject private ChangeIndexCollection changeIndexCollection;
-  @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
-  @Inject private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
   @Inject private GroupOperations groupOperations;
   @Inject private IndexConfig indexConfig;
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
-  private ChangeIndexedCounter changeIndexedCounter;
-  private RegistrationHandle changeIndexedCounterHandle;
+  @Inject
+  @Named("diff")
+  private Cache<PatchListKey, PatchList> fileCache;
 
-  @Before
-  public void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-  }
+  @Inject
+  @Named("diff_intraline")
+  private Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
 
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
-
-  @Before
-  public void addChangeIndexedCounter() {
-    changeIndexedCounter = new ChangeIndexedCounter();
-    changeIndexedCounterHandle = changeIndexedListeners.add("gerrit", changeIndexedCounter);
-  }
-
-  @After
-  public void removeChangeIndexedCounter() {
-    if (changeIndexedCounterHandle != null) {
-      changeIndexedCounterHandle.remove();
-    }
-  }
+  @Inject
+  @Named("diff_summary")
+  private Cache<DiffSummaryKey, DiffSummary> diffSummaryCache;
 
   @Test
   public void get() throws Exception {
@@ -252,6 +252,48 @@
   }
 
   @Test
+  public void diffStatShouldComputeInsertionsAndDeletions() throws Exception {
+    String fileName = "a_new_file.txt";
+    String fileContent = "First line\nSecond line\n";
+    PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
+    String triplet = project.get() + "~master~" + result.getChangeId();
+    ChangeInfo change = gApi.changes().id(triplet).get();
+    assertThat(change.insertions).isNotNull();
+    assertThat(change.deletions).isNotNull();
+  }
+
+  @Test
+  public void diffStatShouldSkipInsertionsAndDeletions() throws Exception {
+    String fileName = "a_new_file.txt";
+    String fileContent = "First line\nSecond line\n";
+    PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
+    String triplet = project.get() + "~master~" + result.getChangeId();
+    ChangeInfo change =
+        gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
+    assertThat(change.insertions).isNull();
+    assertThat(change.deletions).isNull();
+  }
+
+  @Test
+  public void skipDiffstatOptionAvoidsAllDiffComputations() throws Exception {
+    String fileName = "a_new_file.txt";
+    String fileContent = "First line\nSecond line\n";
+    PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
+    String triplet = project.get() + "~master~" + result.getChangeId();
+    CacheStats startPatch = cloneStats(fileCache.stats());
+    CacheStats startIntra = cloneStats(intraCache.stats());
+    CacheStats startSummary = cloneStats(diffSummaryCache.stats());
+    gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
+
+    assertThat(fileCache.stats()).since(startPatch).hasMissCount(0);
+    assertThat(fileCache.stats()).since(startPatch).hasHitCount(0);
+    assertThat(intraCache.stats()).since(startIntra).hasMissCount(0);
+    assertThat(intraCache.stats()).since(startIntra).hasHitCount(0);
+    assertThat(diffSummaryCache.stats()).since(startSummary).hasMissCount(0);
+    assertThat(diffSummaryCache.stats()).since(startSummary).hasHitCount(0);
+  }
+
+  @Test
   public void skipMergeable() throws Exception {
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
@@ -277,9 +319,9 @@
     String changeId = rwip.getChangeId();
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("toggle work in progress state not permitted");
-    gApi.changes().id(changeId).setWorkInProgress();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).setWorkInProgress());
+    assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
   }
 
   @Test
@@ -300,7 +342,11 @@
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
 
     com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
-    grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user2.id());
     gApi.changes().id(changeId).setWorkInProgress();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
@@ -323,9 +369,9 @@
     gApi.changes().id(changeId).setWorkInProgress();
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("toggle work in progress state not permitted");
-    gApi.changes().id(changeId).setReadyForReview();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).setReadyForReview());
+    assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
   }
 
   @Test
@@ -348,7 +394,11 @@
     gApi.changes().id(changeId).setWorkInProgress();
 
     com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
-    grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user2.id());
     gApi.changes().id(changeId).setReadyForReview();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
@@ -367,7 +417,7 @@
   }
 
   @Test
-  public void pendingReviewersInNoteDb() throws Exception {
+  public void pendingReviewers() throws Exception {
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
     gApi.projects().name(project.get()).config(conf);
@@ -415,14 +465,13 @@
             .reviewer("byemail2@example.com")
             .reviewer("byemail3@example.com", CC, false)
             .reviewer("byemail4@example.com", CC, false);
-    ReviewResult result = gApi.changes().id(changeId).revision("current").review(in);
+    ReviewResult result = gApi.changes().id(changeId).current().review(in);
     assertThat(result.reviewers).isNotEmpty();
     ChangeInfo info = gApi.changes().id(changeId).get();
     Function<Collection<AccountInfo>, Collection<String>> toEmails =
         ais -> ais.stream().map(ai -> ai.email).collect(toSet());
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
-        .containsExactly(
-            admin.email(), email1, email2, "byemail1@example.com", "byemail2@example.com");
+        .containsExactly(email1, email2, "byemail1@example.com", "byemail2@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
         .containsExactly(email3, email4, "byemail3@example.com", "byemail4@example.com");
     assertThat(info.pendingReviewers.get(REMOVED)).isNull();
@@ -434,7 +483,7 @@
     gApi.changes().id(changeId).reviewer("byemail3@example.com").remove();
     info = gApi.changes().id(changeId).get();
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
-        .containsExactly(admin.email(), email2, "byemail2@example.com");
+        .containsExactly(email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
         .containsExactly(email4, "byemail4@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED)))
@@ -442,10 +491,10 @@
 
     // "Undo" a removal.
     in = ReviewInput.noScore().reviewer(email1);
-    gApi.changes().id(changeId).revision("current").review(in);
+    gApi.changes().id(changeId).current().review(in);
     info = gApi.changes().id(changeId).get();
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
-        .containsExactly(admin.email(), email1, email2, "byemail2@example.com");
+        .containsExactly(email1, email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
         .containsExactly(email4, "byemail4@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED)))
@@ -456,7 +505,7 @@
     info = gApi.changes().id(changeId).get();
     assertThat(info.pendingReviewers).isEmpty();
     assertThat(toEmails.apply(info.reviewers.get(REVIEWER)))
-        .containsExactly(admin.email(), email1, email2, "byemail2@example.com");
+        .containsExactly(email1, email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.reviewers.get(CC)))
         .containsExactly(email4, "byemail4@example.com");
     assertThat(info.reviewers.get(REMOVED)).isNull();
@@ -507,12 +556,14 @@
     String refactor = "Needs some refactoring";
     String ptal = "PTAL";
 
-    grant(
-        project,
-        "refs/heads/master",
-        Permission.TOGGLE_WORK_IN_PROGRESS_STATE,
-        false,
-        REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
+                .ref("refs/heads/master")
+                .group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).setWorkInProgress(refactor);
@@ -538,7 +589,7 @@
     assertThat(r.getChange().change().isWorkInProgress()).isTrue();
 
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(false);
-    ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
+    ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
     assertThat(result.ready).isTrue();
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -551,7 +602,7 @@
     assertThat(r.getChange().change().isWorkInProgress()).isFalse();
 
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
+    ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
     assertThat(result.ready).isNull();
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -568,7 +619,7 @@
             .reviewer(user.email())
             .label("Code-Review", 1)
             .setWorkInProgress(true);
-    gApi.changes().id(r.getChangeId()).revision("current").review(in);
+    gApi.changes().id(r.getChangeId()).current().review(in);
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
     assertThat(info.workInProgress).isTrue();
@@ -583,7 +634,7 @@
     ReviewInput in = ReviewInput.noScore();
     in.ready = true;
     in.workInProgress = true;
-    ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
+    ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
     assertThat(result.error).isEqualTo(PostReview.ERROR_WIP_READY_MUTUALLY_EXCLUSIVE);
   }
 
@@ -622,21 +673,24 @@
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("toggle work in progress state not permitted");
-    gApi.changes().id(r.getChangeId()).current().review(in);
+    AuthException thrown =
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(r.getChangeId()).current().review(in));
+    assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
   }
 
   @Test
   public void reviewWithWorkInProgressByNonOwnerWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    grant(
-        project,
-        "refs/heads/master",
-        Permission.TOGGLE_WORK_IN_PROGRESS_STATE,
-        false,
-        REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
+                .ref("refs/heads/master")
+                .group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).current().review(in);
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -648,9 +702,10 @@
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setReady(true);
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("toggle work in progress state not permitted");
-    gApi.changes().id(r.getChangeId()).current().review(in);
+    AuthException thrown =
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(r.getChangeId()).current().review(in));
+    assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
   }
 
   @Test
@@ -674,105 +729,9 @@
     PushOneCommit.Result r2 = push2.to("refs/for/other");
     assertThat(r2.getChangeId()).isEqualTo(changeId);
 
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("Multiple changes found for " + changeId);
-    gApi.changes().id(changeId).get();
-  }
-
-  @Test
-  public void revert() throws Exception {
-    PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
-    ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert().get();
-
-    // expected messages on source change:
-    // 1. Uploaded patch set 1.
-    // 2. Patch Set 1: Code-Review+2
-    // 3. Change has been successfully merged by Administrator
-    // 4. Patch Set 1: Reverted
-    List<ChangeMessageInfo> sourceMessages =
-        new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
-    assertThat(sourceMessages).hasSize(4);
-    String expectedMessage =
-        String.format("Created a revert of this change as %s", revertChange.changeId);
-    assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
-
-    assertThat(revertChange.messages).hasSize(1);
-    assertThat(revertChange.messages.iterator().next().message).isEqualTo("Uploaded patch set 1.");
-    assertThat(revertChange.revertOf).isEqualTo(gApi.changes().id(r.getChangeId()).get()._number);
-  }
-
-  @Test
-  public void revertNotifications() throws Exception {
-    PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
-
-    sender.clear();
-    ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert().get();
-
-    List<Message> messages = sender.getMessages();
-    assertThat(messages).hasSize(2);
-    assertThat(sender.getMessages(revertChange.changeId, "newchange")).hasSize(1);
-    assertThat(sender.getMessages(r.getChangeId(), "revert")).hasSize(1);
-  }
-
-  @Test
-  public void suppressRevertNotifications() throws Exception {
-    PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
-
-    RevertInput revertInput = new RevertInput();
-    revertInput.notify = NotifyHandling.NONE;
-
-    sender.clear();
-    gApi.changes().id(r.getChangeId()).revert(revertInput).get();
-    assertThat(sender.getMessages()).isEmpty();
-  }
-
-  @Test
-  public void revertPreservesReviewersAndCcs() throws Exception {
-    PushOneCommit.Result r = createChange();
-
-    ReviewInput in = ReviewInput.approve();
-    in.reviewer(user.email());
-    in.reviewer(accountCreator.user2().email(), ReviewerState.CC, true);
-    // Add user as reviewer that will create the revert
-    in.reviewer(accountCreator.admin2().email());
-
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
-
-    // expect both the original reviewers and CCs to be preserved
-    // original owner should be added as reviewer, user requesting the revert (new owner) removed
-    requestScopeOperations.setApiUser(accountCreator.admin2().id());
-    Map<ReviewerState, Collection<AccountInfo>> result =
-        gApi.changes().id(r.getChangeId()).revert().get().reviewers;
-    assertThat(result).containsKey(ReviewerState.REVIEWER);
-
-    List<Integer> reviewers =
-        result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList());
-    assertThat(result).containsKey(ReviewerState.CC);
-    List<Integer> ccs =
-        result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList());
-    assertThat(ccs).containsExactly(accountCreator.user2().id().get());
-    assertThat(reviewers).containsExactly(user.id().get(), admin.id().get());
-  }
-
-  @Test
-  @TestProjectInput(createEmptyCommit = false)
-  public void revertInitialCommit() throws Exception {
-    PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
-
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Cannot revert initial commit");
-    gApi.changes().id(r.getChangeId()).revert();
+    ResourceNotFoundException thrown =
+        assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(changeId).get());
+    assertThat(thrown).hasMessageThat().contains("Multiple changes found for " + changeId);
   }
 
   @FunctionalInterface
@@ -827,19 +786,10 @@
     assertThat(cr.all.get(0).value).isEqualTo(1);
 
     // Rebasing the second change again should fail
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Change is already up to date");
-    gApi.changes().id(changeId).current().rebase();
-  }
-
-  @Test
-  public void rebaseOnNonExistingChange() throws Exception {
-    String changeId = createChange().getChangeId();
-    RebaseInput in = new RebaseInput();
-    in.base = "999999";
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("Base change not found: " + in.base);
-    gApi.changes().id(changeId).rebase(in);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(changeId).current().rebase());
+    assertThat(thrown).hasMessageThat().contains("Change is already up to date");
   }
 
   @Test
@@ -902,6 +852,17 @@
   }
 
   @Test
+  public void rebaseOnNonExistingChange() throws Exception {
+    String changeId = createChange().getChangeId();
+    RebaseInput in = new RebaseInput();
+    in.base = "999999";
+    UnprocessableEntityException exception =
+        assertThrows(
+            UnprocessableEntityException.class, () -> gApi.changes().id(changeId).rebase(in));
+    assertThat(exception).hasMessageThat().isEqualTo("Base change not found: " + in.base);
+  }
+
+  @Test
   public void rebaseFromRelationChainToClosedChange() throws Exception {
     PushOneCommit.Result r1 = createChange();
     testRepo.reset("HEAD~1");
@@ -942,9 +903,9 @@
     // Rebase the second
     String changeId = r2.getChangeId();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("rebase not permitted");
-    gApi.changes().id(changeId).rebase();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
+    assertThat(thrown).hasMessageThat().contains("rebase not permitted");
   }
 
   @Test
@@ -959,7 +920,11 @@
     revision.review(ReviewInput.approve());
     revision.submit();
 
-    grant(project, "refs/heads/master", Permission.REBASE, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
 
     // Rebase the second
     String changeId = r2.getChangeId();
@@ -979,15 +944,19 @@
     revision.review(ReviewInput.approve());
     revision.submit();
 
-    grant(project, "refs/heads/master", Permission.REBASE, false, REGISTERED_USERS);
-    block("refs/for/*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
 
     // Rebase the second
     String changeId = r2.getChangeId();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("rebase not permitted");
-    gApi.changes().id(changeId).rebase();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
+    assertThat(thrown).hasMessageThat().contains("rebase not permitted");
   }
 
   @Test
@@ -1002,13 +971,17 @@
     revision.review(ReviewInput.approve());
     revision.submit();
 
-    block("refs/for/*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    exception.expect(AuthException.class);
-    exception.expectMessage("rebase not permitted");
-    gApi.changes().id(changeId).rebase();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
+    assertThat(thrown).hasMessageThat().contains("rebase not permitted");
   }
 
   @Test
@@ -1024,14 +997,18 @@
     String changeId = changeResult.getChangeId();
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("delete not permitted");
-    gApi.changes().id(changeId).delete();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete());
+    assertThat(thrown).hasMessageThat().contains("delete not permitted");
   }
 
   @Test
   public void deleteNewChangeAsUserWithDeleteChangesPermissionForGroup() throws Exception {
-    allow("refs/*", Permission.DELETE_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     deleteChangeAsUser(admin, user);
   }
 
@@ -1040,32 +1017,40 @@
     GroupApi groupApi = gApi.groups().create(name("delete-change"));
     groupApi.addMembers("user");
 
+    Project.NameKey nameKey = Project.nameKey(name("delete-change"));
     ProjectInput in = new ProjectInput();
-    in.name = name("delete-change");
+    in.name = nameKey.get();
     in.owners = Lists.newArrayListWithCapacity(1);
     in.owners.add(groupApi.name());
     in.createEmptyCommit = true;
-    ProjectApi api = gApi.projects().create(in);
+    gApi.projects().create(in);
 
-    Project.NameKey nameKey = new Project.NameKey(api.get().name);
-
-    try (ProjectConfigUpdate u = updateProject(nameKey)) {
-      Util.allow(u.getConfig(), Permission.DELETE_CHANGES, PROJECT_OWNERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(nameKey)
+        .forUpdate()
+        .add(allow(Permission.DELETE_CHANGES).ref("refs/*").group(PROJECT_OWNERS))
+        .update();
 
     deleteChangeAsUser(nameKey, admin, user);
   }
 
   @Test
   public void deleteChangeAsUserWithDeleteOwnChangesPermissionForGroup() throws Exception {
-    allow("refs/*", Permission.DELETE_OWN_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     deleteChangeAsUser(user, user);
   }
 
   @Test
   public void deleteChangeAsUserWithDeleteOwnChangesPermissionForOwners() throws Exception {
-    allow("refs/*", Permission.DELETE_OWN_CHANGES, CHANGE_OWNER);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(CHANGE_OWNER))
+        .update();
     deleteChangeAsUser(user, user);
   }
 
@@ -1082,7 +1067,11 @@
       com.google.gerrit.acceptance.TestAccount deleteAs)
       throws Exception {
     try {
-      allow(projectName, "refs/*", Permission.VIEW_PRIVATE_CHANGES, ANONYMOUS_USERS);
+      projectOperations
+          .project(projectName)
+          .forUpdate()
+          .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(ANONYMOUS_USERS))
+          .update();
       requestScopeOperations.setApiUser(owner.id());
       ChangeInput in = new ChangeInput();
       in.project = projectName.get();
@@ -1100,12 +1089,16 @@
 
       assertThat(query(changeId)).isEmpty();
 
-      String ref = new Change.Id(id).toRefPrefix() + "1";
+      String ref = Change.id(id).toRefPrefix() + "1";
       eventRecorder.assertRefUpdatedEvents(projectName.get(), ref, null, commit, commit, null);
       eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email());
     } finally {
-      removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES);
-      removePermission(project, "refs/*", Permission.DELETE_CHANGES);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*"))
+          .remove(permissionKey(Permission.DELETE_CHANGES).ref("refs/*"))
+          .update();
     }
   }
 
@@ -1116,18 +1109,26 @@
 
   @Test
   public void deleteNewChangeOfAnotherUserWithDeleteOwnChangesPermission() throws Exception {
-    allow("refs/*", Permission.DELETE_OWN_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     try {
       PushOneCommit.Result changeResult = createChange();
       String changeId = changeResult.getChangeId();
 
       requestScopeOperations.setApiUser(user.id());
-      exception.expect(AuthException.class);
-      exception.expectMessage("delete not permitted");
-      gApi.changes().id(changeId).delete();
+      AuthException thrown =
+          assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete());
+      assertThat(thrown).hasMessageThat().contains("delete not permitted");
     } finally {
-      removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*"))
+          .update();
     }
   }
 
@@ -1152,9 +1153,9 @@
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).abandon();
 
-    exception.expect(AuthException.class);
-    exception.expectMessage("delete not permitted");
-    gApi.changes().id(changeId).delete();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete());
+    assertThat(thrown).hasMessageThat().contains("delete not permitted");
   }
 
   @Test
@@ -1178,15 +1179,19 @@
 
     merge(changeResult);
 
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("delete not permitted");
-    gApi.changes().id(changeId).delete();
+    MethodNotAllowedException thrown =
+        assertThrows(MethodNotAllowedException.class, () -> gApi.changes().id(changeId).delete());
+    assertThat(thrown).hasMessageThat().contains("delete not permitted");
   }
 
   @Test
   @TestProjectInput(cloneAs = "user")
   public void deleteMergedChangeWithDeleteOwnChangesPermission() throws Exception {
-    allow("refs/*", Permission.DELETE_OWN_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     try {
       PushOneCommit.Result changeResult =
@@ -1196,11 +1201,15 @@
       merge(changeResult);
 
       requestScopeOperations.setApiUser(user.id());
-      exception.expect(MethodNotAllowedException.class);
-      exception.expectMessage("delete not permitted");
-      gApi.changes().id(changeId).delete();
+      MethodNotAllowedException thrown =
+          assertThrows(MethodNotAllowedException.class, () -> gApi.changes().id(changeId).delete());
+      assertThat(thrown).hasMessageThat().contains("delete not permitted");
     } finally {
-      removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*"))
+          .update();
     }
   }
 
@@ -1213,10 +1222,11 @@
     merge(changeResult);
     setChangeStatus(id, Change.Status.NEW);
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        String.format("Cannot delete change %s: patch set 1 is already merged", id));
-    gApi.changes().id(changeId).delete();
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.changes().id(changeId).delete());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(String.format("Cannot delete change %s: patch set 1 is already merged", id));
   }
 
   @Test
@@ -1230,10 +1240,10 @@
     Optional<ChangeData> result =
         idx.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of()));
 
-    assertThat(result.isPresent()).isTrue();
+    assertThat(result).isPresent();
     gApi.changes().id(changeId).delete();
     result = idx.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of()));
-    assertThat(result.isPresent()).isFalse();
+    assertThat(result).isEmpty();
   }
 
   @Test
@@ -1265,18 +1275,64 @@
   }
 
   @Test
+  public void deleteChangeRemovesItsChangeEdit() throws Exception {
+    PushOneCommit.Result result = createChange();
+
+    requestScopeOperations.setApiUser(user.id());
+    String changeId = result.getChangeId();
+    gApi.changes().id(changeId).edit().create();
+    gApi.changes()
+        .id(changeId)
+        .edit()
+        .modifyFile(FILE_NAME, RawInputUtil.create("foo".getBytes(UTF_8)));
+
+    requestScopeOperations.setApiUser(admin.id());
+    try (Repository repo = repoManager.openRepository(project)) {
+      String expected =
+          RefNames.refsUsers(user.id()) + "/edit-" + result.getChange().getId() + "/1";
+      assertThat(repo.getRefDatabase().getRefsByPrefix(expected)).isNotEmpty();
+      gApi.changes().id(changeId).delete();
+      assertThat(repo.getRefDatabase().getRefsByPrefix(expected)).isEmpty();
+    }
+  }
+
+  @Test
+  public void deleteChangeDoesntRemoveOtherChangeEdits() throws Exception {
+    PushOneCommit.Result result = createChange();
+    PushOneCommit.Result irrelevantChangeResult = createChange();
+    requestScopeOperations.setApiUser(admin.id());
+    String changeId = result.getChangeId();
+    String irrelevantChangeId = irrelevantChangeResult.getChangeId();
+
+    gApi.changes().id(irrelevantChangeId).edit().create();
+    gApi.changes()
+        .id(irrelevantChangeId)
+        .edit()
+        .modifyFile(FILE_NAME, RawInputUtil.create("foo".getBytes(UTF_8)));
+
+    gApi.changes().id(changeId).delete();
+
+    assertThat(gApi.changes().id(irrelevantChangeId).edit().get()).isPresent();
+  }
+
+  @Test
   public void rebaseUpToDateChange() throws Exception {
     PushOneCommit.Result r = createChange();
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Change is already up to date");
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).rebase();
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).rebase());
+    assertThat(thrown).hasMessageThat().contains("Change is already up to date");
   }
 
   @Test
   public void rebaseConflict() throws Exception {
-    PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+    PushOneCommit.Result r1 = createChange();
+    gApi.changes()
+        .id(r1.getChangeId())
+        .revision(r1.getCommit().name())
+        .review(ReviewInput.approve());
+    gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
 
     PushOneCommit push =
         pushFactory.create(
@@ -1286,11 +1342,11 @@
             PushOneCommit.FILE_NAME,
             "other content",
             "If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5");
-    r = push.to("refs/for/master");
-    r.assertOkStatus();
-
-    exception.expect(ResourceConflictException.class);
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).rebase();
+    PushOneCommit.Result r2 = push.to("refs/for/master");
+    r2.assertOkStatus();
+    assertThrows(
+        ResourceConflictException.class,
+        () -> gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase());
   }
 
   @Test
@@ -1304,23 +1360,23 @@
     ri.base = "";
     gApi.changes().id(r3.getChangeId()).revision(r3.getCommit().name()).rebase(ri);
     PatchSet ps3 = r3.getPatchSet();
-    assertThat(ps3.getId().get()).isEqualTo(2);
+    assertThat(ps3.id().get()).isEqualTo(2);
 
     // rebase r2 onto r3 (referenced by ref)
-    ri.base = ps3.getId().toRefName();
+    ri.base = ps3.id().toRefName();
     gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri);
     PatchSet ps2 = r2.getPatchSet();
-    assertThat(ps2.getId().get()).isEqualTo(2);
+    assertThat(ps2.id().get()).isEqualTo(2);
 
     // rebase r1 onto r2 (referenced by commit)
-    ri.base = ps2.getRevision().get();
+    ri.base = ps2.commitId().name();
     gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri);
     PatchSet ps1 = r1.getPatchSet();
-    assertThat(ps1.getId().get()).isEqualTo(2);
+    assertThat(ps1.id().get()).isEqualTo(2);
 
     // rebase r1 onto r3 (referenced by change number)
     ri.base = String.valueOf(r3.getChange().getId().get());
-    gApi.changes().id(r1.getChangeId()).revision(ps1.getRevision().get()).rebase(ri);
+    gApi.changes().id(r1.getChangeId()).revision(ps1.commitId().name()).rebase(ri);
     assertThat(r1.getPatchSetId().get()).isEqualTo(3);
   }
 
@@ -1335,9 +1391,11 @@
         "base change "
             + r2.getChangeId()
             + " is a descendant of the current change - recursion not allowed";
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(expectedMessage);
-    gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri));
+    assertThat(thrown).hasMessageThat().contains(expectedMessage);
   }
 
   @Test
@@ -1349,9 +1407,11 @@
     ChangeInfo info = info(changeId);
     assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("change is abandoned");
-    gApi.changes().id(changeId).revision(r.getCommit().name()).rebase();
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).revision(r.getCommit().name()).rebase());
+    assertThat(thrown).hasMessageThat().contains("change is abandoned");
   }
 
   @Test
@@ -1371,9 +1431,11 @@
     RebaseInput ri = new RebaseInput();
     ri.base = r.getCommit().name();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("base change is abandoned: " + changeId);
-    gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri));
+    assertThat(thrown).hasMessageThat().contains("base change is abandoned: " + changeId);
   }
 
   @Test
@@ -1383,9 +1445,11 @@
     String commit = r.getCommit().name();
     RebaseInput ri = new RebaseInput();
     ri.base = commit;
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("cannot rebase change onto itself");
-    gApi.changes().id(changeId).revision(commit).rebase(ri);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).revision(commit).rebase(ri));
+    assertThat(thrown).hasMessageThat().contains("cannot rebase change onto itself");
   }
 
   @Test
@@ -1467,11 +1531,12 @@
   public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
-      Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // admin pushes commit of user
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
@@ -1487,12 +1552,8 @@
 
     // check the user cannot see the change
     requestScopeOperations.setApiUser(user.id());
-    try {
-      gApi.changes().id(result.getChangeId()).get();
-      fail("Expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      // Expected.
-    }
+    assertThrows(
+        ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
 
     // check that the author/committer was NOT added as reviewer (he can't see
     // the change)
@@ -1540,11 +1601,12 @@
   public void pushCommitWithFooterOfOtherUserThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
-      Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // admin pushes commit that references 'user' in a footer
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
@@ -1564,12 +1626,8 @@
 
     // check that 'user' cannot see the change
     requestScopeOperations.setApiUser(user.id());
-    try {
-      gApi.changes().id(result.getChangeId()).get();
-      fail("Expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      // Expected.
-    }
+    assertThrows(
+        ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
 
     // check that 'user' was NOT added as cc ('user' can't see the change)
     requestScopeOperations.setApiUser(admin.id());
@@ -1583,11 +1641,12 @@
   public void addReviewerThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
-      Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // create change
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
@@ -1597,12 +1656,8 @@
 
     // check the user cannot see the change
     requestScopeOperations.setApiUser(user.id());
-    try {
-      gApi.changes().id(result.getChangeId()).get();
-      fail("Expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      // Expected.
-    }
+    assertThrows(
+        ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
 
     // try to add user as reviewer
     requestScopeOperations.setApiUser(admin.id());
@@ -1685,16 +1740,36 @@
   }
 
   @Test
+  @UseClockStep
   public void addReviewer() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
+    testAddReviewerViaPostReview(
+        (changeId, reviewer) -> {
+          AddReviewerInput in = new AddReviewerInput();
+          in.reviewer = reviewer;
+          gApi.changes().id(changeId).addReviewer(in);
+        });
+  }
+
+  @Test
+  @UseClockStep
+  public void addReviewerViaPostReview() throws Exception {
+    testAddReviewerViaPostReview(
+        (changeId, reviewer) -> {
+          AddReviewerInput addReviewerInput = new AddReviewerInput();
+          addReviewerInput.reviewer = reviewer;
+          ReviewInput reviewInput = new ReviewInput();
+          reviewInput.reviewers = ImmutableList.of(addReviewerInput);
+          gApi.changes().id(changeId).current().review(reviewInput);
+        });
+  }
+
+  private void testAddReviewerViaPostReview(AddReviewerCaller addReviewer) throws Exception {
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
     Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
 
-    AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email();
-    gApi.changes().id(r.getChangeId()).addReviewer(in);
+    addReviewer.call(r.getChangeId(), user.email());
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
@@ -1706,15 +1781,15 @@
     assertMailReplyTo(m, admin.email());
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
-    // When NoteDb is enabled adding a reviewer records that user as reviewer
-    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
-    // approval on the change which is treated as CC when the ChangeInfo is
-    // created.
+    // Adding a reviewer records that user as reviewer.
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
     assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
 
+    // Nobody was added as CC.
+    assertThat(c.reviewers.get(CC)).isNull();
+
     // Ensure ETag and lastUpdatedOn are updated.
     rsrc = parseResource(r);
     assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
@@ -1728,6 +1803,19 @@
   }
 
   @Test
+  public void postingMessageOnOwnChangeDoesntAddCallerAsReviewer() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.message = "Foo Bar";
+    gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+
+    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+    assertThat(c.reviewers.get(REVIEWER)).isNull();
+    assertThat(c.reviewers.get(CC)).isNull();
+  }
+
+  @Test
   public void listReviewers() throws Exception {
     PushOneCommit.Result r = createChange();
     AddReviewerInput in = new AddReviewerInput();
@@ -1773,20 +1861,20 @@
     // In this case, the child ReviewerInput has a notify=OWNER_REVIEWERS
     // that should be ignored.
     r = createWorkInProgressChange();
-    gApi.changes().id(r.getChangeId()).revision("current").review(batchIn);
+    gApi.changes().id(r.getChangeId()).current().review(batchIn);
     assertThat(sender.getMessages()).isEmpty();
 
     // Top-level notify property can force notifications when adding reviewer
     // via PostReview.
     r = createWorkInProgressChange();
     batchIn.notify = NotifyHandling.OWNER_REVIEWERS;
-    gApi.changes().id(r.getChangeId()).revision("current").review(batchIn);
+    gApi.changes().id(r.getChangeId()).current().review(batchIn);
     assertThat(sender.getMessages()).hasSize(1);
   }
 
   @Test
+  @UseClockStep
   public void addReviewerThatIsNotPerfectMatch() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
@@ -1821,10 +1909,7 @@
     assertMailReplyTo(m, email);
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
-    // When NoteDb is enabled adding a reviewer records that user as reviewer
-    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
-    // approval on the change which is treated as CC when the ChangeInfo is
-    // created.
+    // Adding a reviewer records that user as reviewer.
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
@@ -1837,8 +1922,8 @@
   }
 
   @Test
+  @UseClockStep
   public void addGroupAsReviewersWhenANotPerfectMatchedUserExists() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
@@ -1885,10 +1970,7 @@
     assertMailReplyTo(m, myGroupUserEmail);
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
-    // When NoteDb is enabled adding a reviewer records that user as reviewer
-    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
-    // approval on the change which is treated as CC when the ChangeInfo is
-    // created.
+    // Adding a reviewer records that user as reviewer.
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
@@ -1901,8 +1983,8 @@
   }
 
   @Test
+  @UseClockStep
   public void addSelfAsReviewer() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
@@ -1916,10 +1998,7 @@
     // There should be no email notification when adding self
     assertThat(sender.getMessages()).isEmpty();
 
-    // When NoteDb is enabled adding a reviewer records that user as reviewer
-    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
-    // approval on the change which is treated as CC when the ChangeInfo is
-    // created.
+    // Adding a reviewer records that user as reviewer.
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
@@ -1975,7 +2054,7 @@
         .containsExactly(user.id().get());
 
     // Further test: remove the vote, then comment again. The user should be
-    // implicitly re-added to the ReviewerSet, as a CC if we're using NoteDb.
+    // implicitly re-added to the ReviewerSet, as a CC.
     requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).remove();
     c = gApi.changes().id(r.getChangeId()).get();
@@ -2028,6 +2107,45 @@
   }
 
   @Test
+  public void pluginCanContributeToETagComputation() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String oldETag = parseResource(r).getETag();
+
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(TestChangeETagComputation.withETag("foo"))) {
+      assertThat(parseResource(r).getETag()).isNotEqualTo(oldETag);
+    }
+
+    assertThat(parseResource(r).getETag()).isEqualTo(oldETag);
+  }
+
+  @Test
+  public void returningNullFromETagComputationDoesNotBreakGerrit() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String oldETag = parseResource(r).getETag();
+
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(TestChangeETagComputation.withETag(null))) {
+      assertThat(parseResource(r).getETag()).isEqualTo(oldETag);
+    }
+  }
+
+  @Test
+  public void throwingExceptionFromETagComputationDoesNotBreakGerrit() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String oldETag = parseResource(r).getETag();
+
+    try (Registration registration =
+        extensionRegistry
+            .newRegistration()
+            .add(
+                TestChangeETagComputation.withException(
+                    new StorageException("exception during test")))) {
+      assertThat(parseResource(r).getETag()).isEqualTo(oldETag);
+    }
+  }
+
+  @Test
   public void emailNotificationForFileLevelComment() throws Exception {
     String changeId = createChange().getChangeId();
 
@@ -2068,8 +2186,8 @@
     comment.message = "comment 1";
     review.comments = ImmutableMap.of(comment.path, Lists.newArrayList(comment));
 
-    exception.expect(BadRequestException.class);
-    gApi.changes().id(changeId).current().review(review);
+    assertThrows(
+        BadRequestException.class, () -> gApi.changes().id(changeId).current().review(review));
   }
 
   @Test
@@ -2094,21 +2212,21 @@
 
   @Test
   public void removeReviewerNoVotes() throws Exception {
+    LabelType verified =
+        label("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType verified =
-          category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
       u.getConfig().getLabelSections().put(verified.getName(), verified);
-      AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      String heads = RefNames.REFS_HEADS + "*";
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(Util.verified().getName()),
-          -1,
-          1,
-          registeredUsers,
-          heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(verified.getName())
+                .ref(RefNames.REFS_HEADS + "*")
+                .group(REGISTERED_USERS)
+                .range(-1, 1))
+        .update();
 
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
@@ -2136,8 +2254,9 @@
     // Remove again, and then try to remove once more to verify 404 is
     // returned.
     gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
-    exception.expect(ResourceNotFoundException.class);
-    gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.changes().id(changeId).reviewer(user.id().toString()).remove());
   }
 
   @Test
@@ -2198,9 +2317,11 @@
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove();
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove());
+    assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted");
   }
 
   @Test
@@ -2216,9 +2337,11 @@
     gApi.changes().id(changeId).revision(r.getCommit().name()).submit();
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer("self").remove();
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(r.getChangeId()).reviewer("self").remove());
+    assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted");
   }
 
   @Test
@@ -2250,9 +2373,11 @@
     gApi.changes().id(changeId).abandon();
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove();
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove());
+    assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted");
   }
 
   @Test
@@ -2360,24 +2485,32 @@
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("delete vote not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).deleteVote("Code-Review");
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () ->
+                gApi.changes()
+                    .id(r.getChangeId())
+                    .reviewer(admin.id().toString())
+                    .deleteVote("Code-Review"));
+    assertThat(thrown).hasMessageThat().contains("delete vote not permitted");
   }
 
   @Test
   public void nonVotingReviewerStaysAfterSubmit() throws Exception {
     LabelType verified =
-        category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+        label("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+    String heads = "refs/heads/*";
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().put(verified.getName(), verified);
-      String heads = "refs/heads/*";
-      AccountGroup.UUID owners = systemGroupBackend.getGroup(CHANGE_OWNER).getUUID();
-      AccountGroup.UUID registered = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      Util.allow(u.getConfig(), Permission.forLabel(verified.getName()), -1, 1, owners, heads);
-      Util.allow(u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, registered, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref(heads).group(CHANGE_OWNER).range(-1, 1))
+        .add(allowLabel("Code-Review").ref(heads).group(REGISTERED_USERS).range(-2, +2))
+        .update();
 
     // Set Code-Review+2 and Verified+1 as admin (change owner)
     PushOneCommit.Result r = createChange();
@@ -2473,8 +2606,13 @@
 
   @Test
   public void queryChangesNoLimit() throws Exception {
-    allowGlobalCapabilities(
-        SystemGroupBackend.REGISTERED_USERS, 0, 2, GlobalCapability.QUERY_LIMIT);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(
+            allowCapability(GlobalCapability.QUERY_LIMIT)
+                .group(SystemGroupBackend.REGISTERED_USERS)
+                .range(0, 2))
+        .update();
     for (int i = 0; i < 3; i++) {
       createChange();
     }
@@ -2612,16 +2750,21 @@
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("edit topic name not permitted");
-    gApi.changes().id(r.getChangeId()).topic("mytopic");
+    AuthException thrown =
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(r.getChangeId()).topic("mytopic"));
+    assertThat(thrown).hasMessageThat().contains("edit topic name not permitted");
   }
 
   @Test
   public void editTopicWithPermissionAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
-    grant(project, "refs/heads/master", Permission.EDIT_TOPIC_NAME, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.EDIT_TOPIC_NAME).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).topic("mytopic");
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic");
@@ -2667,16 +2810,22 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("submit not permitted");
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit());
+    assertThat(thrown).hasMessageThat().contains("submit not permitted");
   }
 
   @Test
   public void submitAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    grant(project, "refs/heads/master", Permission.SUBMIT, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
     assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(ChangeStatus.MERGED);
@@ -2692,22 +2841,24 @@
   @Test
   public void commitFooters() throws Exception {
     LabelType verified =
-        category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+        label("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
     LabelType custom1 =
-        category("Custom1", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+        label("Custom1", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
     LabelType custom2 =
-        category("Custom2", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+        label("Custom2", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().put(verified.getName(), verified);
       u.getConfig().getLabelSections().put(custom1.getName(), custom1);
       u.getConfig().getLabelSections().put(custom2.getName(), custom2);
-      String heads = "refs/heads/*";
-      AccountGroup.UUID anon = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-      Util.allow(u.getConfig(), Permission.forLabel("Verified"), -1, 1, anon, heads);
-      Util.allow(u.getConfig(), Permission.forLabel("Custom1"), -1, 1, anon, heads);
-      Util.allow(u.getConfig(), Permission.forLabel("Custom2"), -1, 1, anon, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .add(allowLabel(custom1.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .add(allowLabel(custom2.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .update();
 
     PushOneCommit.Result r1 = createChange();
     r1.assertOkStatus();
@@ -2757,18 +2908,16 @@
   @Test
   public void customCommitFooters() throws Exception {
     PushOneCommit.Result change = createChange();
-    RegistrationHandle handle =
-        changeMessageModifiers.add(
-            "gerrit",
-            (newCommitMessage, original, mergeTip, destination) -> {
-              assertThat(original.getName()).isNotEqualTo(mergeTip.getName());
-              return newCommitMessage + "Custom: " + destination.get();
-            });
     ChangeInfo actual;
-    try {
+    try (Registration registration =
+        extensionRegistry
+            .newRegistration()
+            .add(
+                (newCommitMessage, original, mergeTip, destination) -> {
+                  assertThat(original.getName()).isNotEqualTo(mergeTip.getName());
+                  return newCommitMessage + "Custom: " + destination.branch();
+                })) {
       actual = gApi.changes().id(change.getChangeId()).get(ALL_REVISIONS, COMMIT_FOOTERS);
-    } finally {
-      handle.remove();
     }
     List<String> footers =
         new ArrayList<>(
@@ -2830,10 +2979,11 @@
     assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.value).isEqualTo(0);
 
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.blockLabel(u.getConfig(), "Code-Review", REGISTERED_USERS, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
+        .update();
 
     c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     codeReview = c.labels.get("Code-Review");
@@ -2879,13 +3029,13 @@
 
     info = gApi.changes().id(info._number).get();
     assertThat(info.changeId).isEqualTo(r.getChangeId());
-
-    exception.expect(AuthException.class);
-    gApi.changes().id(triplet).current().review(ReviewInput.approve());
+    assertThrows(
+        AuthException.class,
+        () -> gApi.changes().id(triplet).current().review(ReviewInput.approve()));
   }
 
   @Test
-  public void noteDbCommitsOnPatchSetCreation() throws Exception {
+  public void commitsOnPatchSetCreation() throws Exception {
     PushOneCommit.Result r = createChange();
     pushFactory
         .create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId())
@@ -2895,7 +3045,7 @@
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       RevCommit commitPatchSetCreation =
-          rw.parseCommit(repo.exactRef(changeMetaRef(new Change.Id(c._number))).getObjectId());
+          rw.parseCommit(repo.exactRef(changeMetaRef(Change.id(c._number))).getObjectId());
 
       assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2");
       PersonIdent expectedAuthor =
@@ -2938,8 +3088,7 @@
     in.project = project.get();
     in.newBranch = true;
 
-    exception.expect(ResourceConflictException.class);
-    gApi.changes().create(in).get();
+    assertThrows(ResourceConflictException.class, () -> gApi.changes().create(in).get());
   }
 
   @Test
@@ -2952,7 +3101,11 @@
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
 
     // Block default permission
-    block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
 
     // Create change as admin
     PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
@@ -2960,12 +3113,12 @@
     r1.assertOkStatus();
 
     // Fetch change
-    GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps");
+    GitUtil.fetch(userTestRepo, r1.getPatchSet().refName() + ":ps");
     userTestRepo.reset("ps");
 
     // Amend change as user
     PushOneCommit.Result r2 = amendChange(r1.getChangeId(), "refs/for/master", user, userTestRepo);
-    r2.assertErrorStatus("cannot add patch set to " + r1.getChange().getId().id + ".");
+    r2.assertErrorStatus("cannot add patch set to " + r1.getChange().getId().get() + ".");
   }
 
   @Test
@@ -2980,7 +3133,7 @@
     r1.assertOkStatus();
 
     // Fetch change
-    GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps");
+    GitUtil.fetch(userTestRepo, r1.getPatchSet().refName() + ":ps");
     userTestRepo.reset("ps");
 
     // Amend change as user
@@ -2996,7 +3149,11 @@
     TestRepository<?> adminTestRepo = cloneProject(project, admin);
 
     // Block default permission
-    block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
 
     // Create change as admin
     PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
@@ -3004,7 +3161,7 @@
     r1.assertOkStatus();
 
     // Fetch change
-    GitUtil.fetch(adminTestRepo, r1.getPatchSet().getRefName() + ":ps");
+    GitUtil.fetch(adminTestRepo, r1.getPatchSet().refName() + ":ps");
     adminTestRepo.reset("ps");
 
     // Amend change as admin
@@ -3015,20 +3172,19 @@
 
   @Test
   public void createMergePatchSet() throws Exception {
-    PushOneCommit.Result start = pushTo("refs/heads/master");
-    start.assertOkStatus();
-    // create a change for master
-    PushOneCommit.Result r = createChange();
-    r.assertOkStatus();
-    String changeId = r.getChangeId();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
+    createBranch("dev");
 
-    testRepo.reset(start.getCommit());
+    // create a change for master
+    String changeId = createChange().getChangeId();
+
+    testRepo.reset(initialHead);
     PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
     currentMaster.assertOkStatus();
     String parent = currentMaster.getCommit().getName();
 
     // push a commit into dev branch
-    createBranch("dev");
+    testRepo.reset(initialHead);
     PushOneCommit.Result changeA =
         pushFactory
             .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
@@ -3049,22 +3205,57 @@
   }
 
   @Test
+  public void createMergePatchSet_Conflict() throws Exception {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
+    createBranch("dev");
+
+    // create a change for master
+    String changeId = createChange().getChangeId();
+
+    String fileName = "shared.txt";
+    testRepo.reset(initialHead);
+    PushOneCommit.Result currentMaster =
+        pushFactory
+            .create(admin.newIdent(), testRepo, "change 1", fileName, "content 1")
+            .to("refs/heads/master");
+    currentMaster.assertOkStatus();
+
+    // push a commit into dev branch
+    testRepo.reset(initialHead);
+    PushOneCommit.Result changeA =
+        pushFactory
+            .create(user.newIdent(), testRepo, "change 2", fileName, "content 2")
+            .to("refs/heads/dev");
+    changeA.assertOkStatus();
+    MergeInput mergeInput = new MergeInput();
+    mergeInput.source = "dev";
+    MergePatchSetInput in = new MergePatchSetInput();
+    in.merge = mergeInput;
+    in.subject = "update change by merge ps2";
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).createMergePatchSet(in));
+    assertThat(thrown).hasMessageThat().isEqualTo("merge conflict(s):\n" + fileName);
+  }
+
+  @Test
   public void createMergePatchSetInheritParent() throws Exception {
-    PushOneCommit.Result start = pushTo("refs/heads/master");
-    start.assertOkStatus();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
+    createBranch("dev");
+
     // create a change for master
     PushOneCommit.Result r = createChange();
-    r.assertOkStatus();
     String changeId = r.getChangeId();
     String parent = r.getCommit().getParent(0).getName();
 
     // advance master branch
-    testRepo.reset(start.getCommit());
+    testRepo.reset(initialHead);
     PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
     currentMaster.assertOkStatus();
 
     // push a commit into dev branch
-    createBranch("dev");
+    testRepo.reset(initialHead);
     PushOneCommit.Result changeA =
         pushFactory
             .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
@@ -3090,7 +3281,7 @@
 
   @Test
   public void createMergePatchSetCannotBaseOnInvisibleChange() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     createBranch("foo");
     createBranch("bar");
 
@@ -3107,14 +3298,19 @@
     testRepo.reset(initialHead);
     String changeId = createChange().getChangeId();
 
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("Read not permitted for " + baseChange);
-    gApi.changes().id(changeId).createMergePatchSet(createMergePatchSetInput(baseChange));
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () ->
+                gApi.changes()
+                    .id(changeId)
+                    .createMergePatchSet(createMergePatchSetInput(baseChange)));
+    assertThat(thrown).hasMessageThat().contains("Read not permitted for " + baseChange);
   }
 
   @Test
   public void createMergePatchSetBaseOnChange() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     createBranch("foo");
     createBranch("bar");
 
@@ -3141,6 +3337,46 @@
         .isEqualTo(expectedParent);
   }
 
+  @Test
+  public void createMergePatchSetWithUnupportedMergeStrategy() throws Exception {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
+    createBranch("dev");
+
+    // create a change for master
+    String changeId = createChange().getChangeId();
+
+    String fileName = "shared.txt";
+    String sourceSubject = "source change";
+    String sourceContent = "source content";
+    String targetSubject = "target change";
+    String targetContent = "target content";
+    testRepo.reset(initialHead);
+    PushOneCommit.Result currentMaster =
+        pushFactory
+            .create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent)
+            .to("refs/heads/master");
+    currentMaster.assertOkStatus();
+
+    // push a commit into dev branch
+    testRepo.reset(initialHead);
+    PushOneCommit.Result changeA =
+        pushFactory
+            .create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent)
+            .to("refs/heads/dev");
+    changeA.assertOkStatus();
+    MergeInput mergeInput = new MergeInput();
+    mergeInput.source = "dev";
+    mergeInput.strategy = "unsupported-strategy";
+    MergePatchSetInput in = new MergePatchSetInput();
+    in.merge = mergeInput;
+    in.subject = "update change by merge ps2";
+
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in));
+    assertThat(ex).hasMessageThat().isEqualTo("invalid merge strategy: " + mergeInput.strategy);
+  }
+
   private MergePatchSetInput createMergePatchSetInput(String baseChange) {
     MergeInput mergeInput = new MergeInput();
     mergeInput.source = "foo";
@@ -3162,15 +3398,18 @@
 
     // add new label and assert that it's returned for existing changes
     AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-    LabelType verified = Util.verified();
+    LabelType verified = TestLabels.verified();
     String heads = RefNames.REFS_HEADS + "*";
 
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().put(verified.getName(), verified);
-      Util.allow(
-          u.getConfig(), Permission.forLabel(verified.getName()), -1, 1, registeredUsers, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1))
+        .update();
 
     change = gApi.changes().id(r.getChangeId()).get();
     assertThat(change.labels.keySet()).containsExactly("Code-Review", "Verified");
@@ -3188,9 +3427,16 @@
       // remove label and assert that it's no longer returned for existing
       // changes, even if there is an approval for it
       u.getConfig().getLabelSections().remove(verified.getName());
-      Util.remove(u.getConfig(), Permission.forLabel(verified.getName()), registeredUsers, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(
+            permissionKey(Permission.forLabel(verified.getName()))
+                .ref(heads)
+                .group(registeredUsers))
+        .update();
 
     change = gApi.changes().id(r.getChangeId()).get();
     assertThat(change.labels.keySet()).containsExactly("Code-Review");
@@ -3217,17 +3463,20 @@
     assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review");
     assertPermitted(change, "Code-Review", 2);
 
-    LabelType verified = Util.verified();
+    LabelType verified = TestLabels.verified();
     AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
     String heads = RefNames.REFS_HEADS + "*";
 
     // add new label and assert that it's returned for existing changes
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().put(verified.getName(), verified);
-      Util.allow(
-          u.getConfig(), Permission.forLabel(verified.getName()), -1, 1, registeredUsers, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1))
+        .update();
 
     change = gApi.changes().id(r.getChangeId()).get();
     assertThat(change.labels.keySet()).containsExactly("Code-Review", "Verified");
@@ -3269,9 +3518,13 @@
     // changes, even if there is an approval for it
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().remove(verified.getName());
-      Util.remove(u.getConfig(), Permission.forLabel(verified.getName()), registeredUsers, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(permissionKey(verified.getName()).ref(heads).group(registeredUsers))
+        .update();
 
     change = gApi.changes().id(r.getChangeId()).get();
     assertThat(change.labels.keySet()).containsExactly("Code-Review");
@@ -3280,9 +3533,45 @@
   }
 
   @Test
+  public void notifyConfigForDirectoryTriggersEmail() throws Exception {
+    // Configure notifications on project level.
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
+    GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
+    testRepo.reset("config");
+    PushOneCommit push =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "Configure Notifications",
+            "project.config",
+            "[notify \"my=notify-config\"]\n"
+                + "  email = foo@test.com\n"
+                + "  filter = dir:\\\"foo/bar/baz\\\"");
+    push.to(RefNames.REFS_CONFIG);
+    testRepo.reset(oldHead);
+
+    // Push a change that matches the filter.
+    sender.clear();
+    push =
+        pushFactory.create(
+            admin.newIdent(), testRepo, "Test change", "foo/bar/baz/test.txt", "some content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(Address.parse("foo@test.com"));
+
+    // Comment on the change.
+    sender.clear();
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.message = "some message";
+    gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(Address.parse("foo@test.com"));
+  }
+
+  @Test
   public void checkLabelsForMergedChangeWithNonAuthorCodeReview() throws Exception {
     // Configure Non-Author-Code-Review
-    RevCommit oldHead = getRemoteHead();
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
     testRepo.reset("config");
     PushOneCommit push2 =
@@ -3307,20 +3596,18 @@
     push2.to(RefNames.REFS_CONFIG);
     testRepo.reset(oldHead);
 
-    AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
     String heads = RefNames.REFS_HEADS + "*";
 
     // Allow user to approve
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(Util.codeReview().getName()),
-          -2,
-          2,
-          registeredUsers,
-          heads);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .ref(heads)
+                .group(REGISTERED_USERS)
+                .range(-2, 2))
+        .update();
 
     PushOneCommit.Result r = createChange();
 
@@ -3372,16 +3659,15 @@
     assertThat(approval.permittedVotingRange.min).isEqualTo(-1);
     assertThat(approval.permittedVotingRange.max).isEqualTo(1);
 
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel("Code-Review"),
-          minPermittedValue,
-          maxPermittedValue,
-          REGISTERED_USERS,
-          heads);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel("Code-Review")
+                .ref(heads)
+                .group(REGISTERED_USERS)
+                .range(minPermittedValue, maxPermittedValue))
+        .update();
 
     c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     codeReview = c.labels.get("Code-Review");
@@ -3395,10 +3681,11 @@
 
   @Test
   public void maxPermittedValueBlocked() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.blockLabel(u.getConfig(), "Code-Review", REGISTERED_USERS, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
+        .update();
 
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
@@ -3435,9 +3722,9 @@
     ReviewInput input = new ReviewInput().label("Code-Review", 3);
     gApi.changes().id(changeId).current().review(input);
 
-    Map<String, Short> votes =
-        gApi.changes().id(changeId).current().reviewer(admin.email()).votes();
-    assertThat(votes).isEmpty();
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.changes().id(changeId).current().reviewer(admin.email()));
   }
 
   @Test
@@ -3446,9 +3733,10 @@
     String changeId = createChange().getChangeId();
     ReviewInput in = new ReviewInput().label("Code-Style", 1);
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("label \"Code-Style\" is not a configured label");
-    gApi.changes().id(changeId).current().review(in);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> gApi.changes().id(changeId).current().review(in));
+    assertThat(thrown).hasMessageThat().contains("label \"Code-Style\" is not a configured label");
   }
 
   @Test
@@ -3457,9 +3745,10 @@
     String changeId = createChange().getChangeId();
     ReviewInput in = new ReviewInput().label("Code-Review", 3);
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("label \"Code-Review\": 3 is not a valid value");
-    gApi.changes().id(changeId).current().review(in);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> gApi.changes().id(changeId).current().review(in));
+    assertThat(thrown).hasMessageThat().contains("label \"Code-Review\": 3 is not a valid value");
   }
 
   @Test
@@ -3475,7 +3764,7 @@
             + "U > 0,"
             + "R = label('All-Comments-Resolved', need(_)). \n\n");
 
-    String oldHead = getRemoteHead().name();
+    String oldHead = projectOperations.project(project).getHead("master").name();
     PushOneCommit.Result result1 =
         pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
@@ -3487,56 +3776,14 @@
 
     gApi.changes().id(result1.getChangeId()).current().submit();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Failed to submit 1 change due to the following problems");
-    exception.expectMessage("needs All-Comments-Resolved");
-    gApi.changes().id(result2.getChangeId()).current().submit();
-  }
-
-  @Test
-  public void pureRevertFactBlocksSubmissionOfNonReverts() throws Exception {
-    addPureRevertSubmitRule();
-
-    // Create a change that is not a revert of another change
-    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
-    approve(r1.getChangeId());
-
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Failed to submit 1 change due to the following problems");
-    exception.expectMessage("needs Is-Pure-Revert");
-    gApi.changes().id(r1.getChangeId()).current().submit();
-  }
-
-  @Test
-  public void pureRevertFactBlocksSubmissionOfNonPureReverts() throws Exception {
-    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
-    merge(r1);
-
-    addPureRevertSubmitRule();
-
-    // Create a revert and push a content change
-    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
-    amendChange(revertId);
-    approve(revertId);
-
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Failed to submit 1 change due to the following problems");
-    exception.expectMessage("needs Is-Pure-Revert");
-    gApi.changes().id(revertId).current().submit();
-  }
-
-  @Test
-  public void pureRevertFactAllowsSubmissionOfPureReverts() throws Exception {
-    // Create a change that we can later revert
-    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
-    merge(r1);
-
-    addPureRevertSubmitRule();
-
-    // Create a revert and submit it
-    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
-    approve(revertId);
-    gApi.changes().id(revertId).current().submit();
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(result2.getChangeId()).current().submit());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Failed to submit 1 change due to the following problems");
+    assertThat(thrown).hasMessageThat().contains("needs All-Comments-Resolved");
   }
 
   @Test
@@ -3596,25 +3843,18 @@
   }
 
   @Test
-  public void changeCommitMessageWithNoChangeIdFails() throws Exception {
-    PushOneCommit.Result r = createChange();
-    assertThat(getCommitMessage(r.getChangeId()))
-        .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("missing Change-Id footer");
-    gApi.changes().id(r.getChangeId()).setMessage("modified commit\n");
-  }
-
-  @Test
   public void changeCommitMessageNullNotAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(getCommitMessage(r.getChangeId()))
         .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("NUL character");
-    gApi.changes()
-        .id(r.getChangeId())
-        .setMessage("test\0commit\n\nChange-Id: " + r.getChangeId() + "\n");
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.changes()
+                    .id(r.getChangeId())
+                    .setMessage("test\0commit\n\nChange-Id: " + r.getChangeId() + "\n"));
+    assertThat(thrown).hasMessageThat().contains("NUL character");
   }
 
   @Test
@@ -3623,11 +3863,15 @@
     PushOneCommit.Result r = createChange();
     assertThat(getCommitMessage(r.getChangeId()))
         .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("wrong Change-Id footer");
-    gApi.changes()
-        .id(r.getChangeId())
-        .setMessage("modified commit\n\nChange-Id: " + otherChange.getChangeId() + "\n");
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () ->
+                gApi.changes()
+                    .id(r.getChangeId())
+                    .setMessage(
+                        "modified commit\n\nChange-Id: " + otherChange.getChangeId() + "\n"));
+    assertThat(thrown).hasMessageThat().contains("wrong Change-Id footer");
   }
 
   @Test
@@ -3636,15 +3880,20 @@
     Project.NameKey p = projectOperations.newProject().create();
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
     // Block default permission
-    block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
     // Create change as user
     PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     // Try to change the commit message
-    exception.expect(AuthException.class);
-    exception.expectMessage("modifying commit message not permitted");
-    gApi.changes().id(r.getChangeId()).setMessage("foo");
+    AuthException thrown =
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(r.getChangeId()).setMessage("foo"));
+    assertThat(thrown).hasMessageThat().contains("modifying commit message not permitted");
   }
 
   @Test
@@ -3652,9 +3901,11 @@
     PushOneCommit.Result r = createChange();
     assertThat(getCommitMessage(r.getChangeId()))
         .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("new and existing commit message are the same");
-    gApi.changes().id(r.getChangeId()).setMessage(getCommitMessage(r.getChangeId()));
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(r.getChangeId()).setMessage(getCommitMessage(r.getChangeId())));
+    assertThat(thrown).hasMessageThat().contains("new and existing commit message are the same");
   }
 
   @Test
@@ -3694,114 +3945,13 @@
   }
 
   @Test
-  public void pureRevertReturnsTrueForPureRevert() throws Exception {
-    PushOneCommit.Result r = createChange();
-    merge(r);
-    String revertId = gApi.changes().id(r.getChangeId()).revert().get().id;
-    // Without query parameter
-    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
-    // With query parameter
-    assertThat(
-            gApi.changes()
-                .id(revertId)
-                .pureRevert(getRemoteHead().toObjectId().name())
-                .isPureRevert)
-        .isTrue();
-  }
-
-  @Test
-  public void pureRevertReturnsFalseOnContentChange() throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    merge(r1);
-    // Create a revert and expect pureRevert to be true
-    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
-    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
-
-    // Create a new PS and expect pureRevert to be false
-    PushOneCommit.Result result = amendChange(revertId);
-    result.assertOkStatus();
-    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isFalse();
-  }
-
-  @Test
-  public void pureRevertParameterTakesPrecedence() throws Exception {
-    PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
-    merge(r1);
-    String oldHead = getRemoteHead().toObjectId().name();
-
-    PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content2");
-    merge(r2);
-
-    String revertId = gApi.changes().id(r2.getChangeId()).revert().get().changeId;
-    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
-    assertThat(gApi.changes().id(revertId).pureRevert(oldHead).isPureRevert).isFalse();
-  }
-
-  @Test
-  public void pureRevertReturnsFalseOnInvalidInput() throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    merge(r1);
-
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("invalid object ID");
-    gApi.changes().id(createChange().getChangeId()).pureRevert("invalid id");
-  }
-
-  @Test
-  public void pureRevertReturnsTrueWithCleanRebase() throws Exception {
-    PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
-    merge(r1);
-
-    PushOneCommit.Result r2 = createChange("commit message", "b.txt", "content2");
-    merge(r2);
-
-    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
-    // Rebase revert onto HEAD
-    gApi.changes().id(revertId).rebase();
-    // Check that pureRevert is true which implies that the commit can be rebased onto the original
-    // commit.
-    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
-  }
-
-  @Test
-  public void pureRevertReturnsFalseWithRebaseConflict() throws Exception {
-    // Create an initial commit to serve as claimed original
-    PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
-    merge(r1);
-    String claimedOriginal = getRemoteHead().toObjectId().name();
-
-    // Change contents of the file to provoke a conflict
-    merge(createChange("commit message", "a.txt", "content2"));
-
-    // Create a commit that we can revert
-    PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content3");
-    merge(r2);
-
-    // Create a revert of r2
-    String revertR3Id = gApi.changes().id(r2.getChangeId()).revert().id();
-    // Assert that the change is a pure revert of it's 'revertOf'
-    assertThat(gApi.changes().id(revertR3Id).pureRevert().isPureRevert).isTrue();
-    // Assert that the change is not a pure revert of claimedOriginal because pureRevert is trying
-    // to rebase this on claimed original, which fails.
-    PureRevertInfo pureRevert = gApi.changes().id(revertR3Id).pureRevert(claimedOriginal);
-    assertThat(pureRevert.isPureRevert).isFalse();
-  }
-
-  @Test
-  public void pureRevertThrowsExceptionWhenChangeIsNotARevertAndNoIdProvided() throws Exception {
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("revertOf not set");
-    gApi.changes().id(createChange().getChangeId()).pureRevert();
-  }
-
-  @Test
   public void putTopicExceedLimitFails() throws Exception {
     String changeId = createChange().getChangeId();
     String topic = Stream.generate(() -> "t").limit(2049).collect(joining());
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("topic length exceeds the limit");
-    gApi.changes().id(changeId).topic(topic);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> gApi.changes().id(changeId).topic(topic));
+    assertThat(thrown).hasMessageThat().contains("topic length exceeds the limit");
   }
 
   @Test
@@ -3818,13 +3968,13 @@
 
   private void submittableAfterLosingPermissions(String label) throws Exception {
     String codeReviewLabel = "Code-Review";
-    AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.forLabel(label), -1, +1, registered, "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel(codeReviewLabel), -2, +2, registered, "refs/heads/*");
-      u.save();
-    }
+    AccountGroup.UUID registered = REGISTERED_USERS;
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(label).ref("refs/heads/*").group(registered).range(-1, +1))
+        .add(allowLabel(codeReviewLabel).ref("refs/heads/*").group(registered).range(-2, +2))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     PushOneCommit.Result r = createChange();
@@ -3847,15 +3997,13 @@
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
     requestScopeOperations.setApiUser(admin.id());
-    // Remove user's permission for 'Label'.
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.remove(u.getConfig(), Permission.forLabel(label), registered, "refs/heads/*");
-      // Update user's permitted range for 'Code-Review' to be -1...+1.
-      Util.remove(u.getConfig(), Permission.forLabel(codeReviewLabel), registered, "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel(codeReviewLabel), -1, +1, registered, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(labelPermissionKey(label).ref("refs/heads/*").group(registered))
+        .remove(labelPermissionKey(codeReviewLabel).ref("refs/heads/*").group(registered))
+        .add(allowLabel(codeReviewLabel).ref("refs/heads/*").group(registered).range(-1, +1))
+        .update();
 
     // Verify user's new permitted range.
     requestScopeOperations.setApiUser(user.id());
@@ -3939,7 +4087,7 @@
     if (r == null) {
       return ImmutableList.of();
     }
-    return Iterables.transform(r, a -> new Account.Id(a._accountId));
+    return Iterables.transform(r, a -> Account.id(a._accountId));
   }
 
   private ChangeResource parseResource(PushOneCommit.Result r) throws Exception {
@@ -3954,7 +4102,7 @@
             .filter(e -> e.getValue().stream().anyMatch(a -> a._accountId == accountId.get()))
             .map(Map.Entry::getKey)
             .collect(toSet());
-    assertThat(states.size()).named(states.toString()).isAtMost(1);
+    assertWithMessage(states.toString()).that(states.size()).isAtMost(1);
     return states.stream().findFirst();
   }
 
@@ -3980,10 +4128,7 @@
     public boolean updateChange(ChangeContext ctx) throws Exception {
       Change change = ctx.getChange();
 
-      // Change status in database.
-      change.setStatus(newStatus);
-
-      // Change status in NoteDb.
+      // Change status.
       PatchSet.Id currentPatchSetId = change.currentPatchSetId();
       ctx.getUpdate(currentPatchSetId).setStatus(newStatus);
 
@@ -3991,19 +4136,6 @@
     }
   }
 
-  private void addPureRevertSubmitRule() throws Exception {
-    modifySubmitRules(
-        "submit_rule(submit(R)) :- \n"
-            + "gerrit:pure_revert(1), \n"
-            + "!,"
-            + "gerrit:uploader(U), \n"
-            + "R = label('Is-Pure-Revert', ok(U)).\n"
-            + "submit_rule(submit(R)) :- \n"
-            + "gerrit:pure_revert(U), \n"
-            + "U \\= 1,"
-            + "R = label('Is-Pure-Revert', need(_)). \n\n");
-  }
-
   private void modifySubmitRules(String newContent) throws Exception {
     try (Repository repo = repoManager.openRepository(project);
         TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
@@ -4043,21 +4175,25 @@
 
   @Test
   public void starUnstar() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String triplet = project.get() + "~master~" + r.getChangeId();
-    changeIndexedCounter.clear();
+    ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedCounter)) {
+      PushOneCommit.Result r = createChange();
+      String triplet = project.get() + "~master~" + r.getChangeId();
+      changeIndexedCounter.clear();
 
-    gApi.accounts().self().starChange(triplet);
-    ChangeInfo change = info(triplet);
-    assertThat(change.starred).isTrue();
-    assertThat(change.stars).contains(DEFAULT_LABEL);
-    changeIndexedCounter.assertReindexOf(change);
+      gApi.accounts().self().starChange(triplet);
+      ChangeInfo change = info(triplet);
+      assertThat(change.starred).isTrue();
+      assertThat(change.stars).contains(DEFAULT_LABEL);
+      changeIndexedCounter.assertReindexOf(change);
 
-    gApi.accounts().self().unstarChange(triplet);
-    change = info(triplet);
-    assertThat(change.starred).isNull();
-    assertThat(change.stars).isNull();
-    changeIndexedCounter.assertReindexOf(change);
+      gApi.accounts().self().unstarChange(triplet);
+      change = info(triplet);
+      assertThat(change.starred).isNull();
+      assertThat(change.stars).isNull();
+      changeIndexedCounter.assertReindexOf(change);
+    }
   }
 
   @Test
@@ -4117,9 +4253,9 @@
   public void cannotIgnoreOwnChange() throws Exception {
     String changeId = createChange().getChangeId();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("cannot ignore own change");
-    gApi.changes().id(changeId).ignore(true);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> gApi.changes().id(changeId).ignore(true));
+    assertThat(thrown).hasMessageThat().contains("cannot ignore own change");
   }
 
   @Test
@@ -4130,14 +4266,17 @@
     gApi.accounts().self().starChange(changeId);
     assertThat(gApi.changes().id(changeId).get().starred).isTrue();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        "The labels "
-            + StarredChangesUtil.DEFAULT_LABEL
-            + " and "
-            + StarredChangesUtil.IGNORE_LABEL
-            + " are mutually exclusive. Only one of them can be set.");
-    gApi.changes().id(changeId).ignore(true);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(changeId).ignore(true));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "The labels "
+                + StarredChangesUtil.DEFAULT_LABEL
+                + " and "
+                + StarredChangesUtil.IGNORE_LABEL
+                + " are mutually exclusive. Only one of them can be set.");
   }
 
   @Test
@@ -4148,14 +4287,17 @@
     gApi.changes().id(changeId).ignore(true);
     assertThat(gApi.changes().id(changeId).ignored()).isTrue();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        "The labels "
-            + StarredChangesUtil.DEFAULT_LABEL
-            + " and "
-            + StarredChangesUtil.IGNORE_LABEL
-            + " are mutually exclusive. Only one of them can be set.");
-    gApi.accounts().self().starChange(changeId);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.accounts().self().starChange(changeId));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "The labels "
+                + StarredChangesUtil.DEFAULT_LABEL
+                + " and "
+                + StarredChangesUtil.IGNORE_LABEL
+                + " are mutually exclusive. Only one of them can be set.");
   }
 
   @Test
@@ -4193,21 +4335,28 @@
     gApi.changes().id(changeId).markAsReviewed(true);
     assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        "The labels "
-            + StarredChangesUtil.REVIEWED_LABEL
-            + "/"
-            + 1
-            + " and "
-            + StarredChangesUtil.UNREVIEWED_LABEL
-            + "/"
-            + 1
-            + " are mutually exclusive. Only one of them can be set.");
-    gApi.accounts()
-        .self()
-        .setStars(
-            changeId, new StarsInput(ImmutableSet.of(StarredChangesUtil.UNREVIEWED_LABEL + "/1")));
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.accounts()
+                    .self()
+                    .setStars(
+                        changeId,
+                        new StarsInput(
+                            ImmutableSet.of(StarredChangesUtil.UNREVIEWED_LABEL + "/1"))));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "The labels "
+                + StarredChangesUtil.REVIEWED_LABEL
+                + "/"
+                + 1
+                + " and "
+                + StarredChangesUtil.UNREVIEWED_LABEL
+                + "/"
+                + 1
+                + " are mutually exclusive. Only one of them can be set.");
   }
 
   @Test
@@ -4218,21 +4367,27 @@
     gApi.changes().id(changeId).markAsReviewed(false);
     assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        "The labels "
-            + StarredChangesUtil.REVIEWED_LABEL
-            + "/"
-            + 1
-            + " and "
-            + StarredChangesUtil.UNREVIEWED_LABEL
-            + "/"
-            + 1
-            + " are mutually exclusive. Only one of them can be set.");
-    gApi.accounts()
-        .self()
-        .setStars(
-            changeId, new StarsInput(ImmutableSet.of(StarredChangesUtil.REVIEWED_LABEL + "/1")));
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.accounts()
+                    .self()
+                    .setStars(
+                        changeId,
+                        new StarsInput(ImmutableSet.of(StarredChangesUtil.REVIEWED_LABEL + "/1"))));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "The labels "
+                + StarredChangesUtil.REVIEWED_LABEL
+                + "/"
+                + 1
+                + " and "
+                + StarredChangesUtil.UNREVIEWED_LABEL
+                + "/"
+                + 1
+                + " are mutually exclusive. Only one of them can be set.");
   }
 
   @Test
@@ -4261,9 +4416,14 @@
 
     // label cannot contain whitespace
     String invalidLabel = "invalid label";
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("invalid labels: " + invalidLabel);
-    gApi.accounts().self().setStars(changeId, new StarsInput(ImmutableSet.of(invalidLabel)));
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.accounts()
+                    .self()
+                    .setStars(changeId, new StarsInput(ImmutableSet.of(invalidLabel))));
+    assertThat(thrown).hasMessageThat().contains("invalid labels: " + invalidLabel);
   }
 
   @Test
@@ -4280,7 +4440,8 @@
             ListChangesOption.MESSAGES,
             ListChangesOption.SUBMITTABLE,
             ListChangesOption.WEB_LINKS,
-            ListChangesOption.SKIP_MERGEABLE);
+            ListChangesOption.SKIP_MERGEABLE,
+            ListChangesOption.SKIP_DIFFSTAT);
 
     PushOneCommit.Result change = createChange();
     int number = gApi.changes().id(change.getChangeId()).get()._number;
@@ -4296,7 +4457,7 @@
   }
 
   private BranchApi createBranch(String branch) throws Exception {
-    return createBranch(new Branch.NameKey(project, branch));
+    return createBranch(BranchNameKey.create(project, branch));
   }
 
   private ThrowableSubject assertThatQueryException(String query) throws Exception {
@@ -4307,4 +4468,9 @@
     }
     throw new AssertionError("expected BadRequestException");
   }
+
+  @FunctionalInterface
+  private interface AddReviewerCaller {
+    void call(String changeId, String reviewer) throws RestApiException;
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
index 4551f90..de73c00 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
@@ -15,15 +15,16 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
@@ -54,16 +55,22 @@
 
   @Test
   public void wrongProjectInProjectChangeNumberReturnsNotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("Not found: unknown~" + changeInfo._number);
-    gApi.changes().id("unknown", changeInfo._number);
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id("unknown", changeInfo._number));
+    assertThat(thrown).hasMessageThat().contains("Not found: unknown~" + changeInfo._number);
   }
 
   @Test
   public void wrongIdInProjectChangeNumberReturnsNotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("Not found: " + project.get() + "~" + Integer.MAX_VALUE);
-    gApi.changes().id(project.get(), Integer.MAX_VALUE);
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id(project.get(), Integer.MAX_VALUE));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Not found: " + project.get() + "~" + Integer.MAX_VALUE);
   }
 
   @Test
@@ -74,8 +81,7 @@
 
   @Test
   public void wrongChangeNumberReturnsNotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.changes().id(Integer.MAX_VALUE);
+    assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(Integer.MAX_VALUE));
   }
 
   @Test
@@ -86,25 +92,36 @@
 
   @Test
   public void wrongProjectInTripletChangeIdReturnsNotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("Not found: unknown~" + changeInfo.branch + "~" + changeInfo.changeId);
-    gApi.changes().id("unknown", changeInfo.branch, changeInfo.changeId);
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id("unknown", changeInfo.branch, changeInfo.changeId));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Not found: unknown~" + changeInfo.branch + "~" + changeInfo.changeId);
   }
 
   @Test
   public void wrongBranchInTripletChangeIdReturnsNotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("Not found: " + project.get() + "~unknown~" + changeInfo.changeId);
-    gApi.changes().id(project.get(), "unknown", changeInfo.changeId);
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id(project.get(), "unknown", changeInfo.changeId));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Not found: " + project.get() + "~unknown~" + changeInfo.changeId);
   }
 
   @Test
   public void wrongIdInTripletChangeIdReturnsNotFound() throws Exception {
     String unknownId = "I1234567890";
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage(
-        "Not found: " + project.get() + "~" + changeInfo.branch + "~" + unknownId);
-    gApi.changes().id(project.get(), changeInfo.branch, unknownId);
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id(project.get(), changeInfo.branch, unknownId));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Not found: " + project.get() + "~" + changeInfo.branch + "~" + unknownId);
   }
 
   @Test
@@ -119,7 +136,6 @@
 
   @Test
   public void wrongChangeIdReturnsNotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.changes().id("I1234567890");
+    assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id("I1234567890"));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
index 48c46d2..fcc33c9 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
@@ -23,19 +23,19 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementInfo;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.SubmitRule;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.Optional;
 import org.junit.Test;
 
 public class ChangeSubmitRequirementIT extends AbstractDaemonTest {
@@ -100,24 +100,117 @@
     assertThat(result.get(0).requirements).containsExactly(reqInfo);
   }
 
+  @Test
+  public void submittableQueryRuleNotReady() throws Exception {
+    ChangeApi change = newChangeApi();
+
+    // Satisfy the default rule.
+    approveChange(change);
+
+    // The custom rule is NOT_READY.
+    rule.block(true);
+    change.index();
+
+    assertThat(queryIsSubmittable()).isEmpty();
+  }
+
+  @Test
+  public void submittableQueryRuleError() throws Exception {
+    ChangeApi change = newChangeApi();
+
+    // Satisfy the default rule.
+    approveChange(change);
+
+    rule.status(Optional.of(SubmitRecord.Status.RULE_ERROR));
+    change.index();
+
+    assertThat(queryIsSubmittable()).isEmpty();
+  }
+
+  @Test
+  public void submittableQueryDefaultRejected() throws Exception {
+    ChangeApi change = newChangeApi();
+
+    // CodeReview:-2 the change, causing the default rule to fail.
+    rejectChange(change);
+
+    rule.status(Optional.of(SubmitRecord.Status.OK));
+    change.index();
+
+    assertThat(queryIsSubmittable()).isEmpty();
+  }
+
+  @Test
+  public void submittableQueryRuleOk() throws Exception {
+    ChangeApi change = newChangeApi();
+
+    // Satisfy the default rule.
+    approveChange(change);
+
+    rule.status(Optional.of(SubmitRecord.Status.OK));
+    change.index();
+
+    List<ChangeInfo> result = queryIsSubmittable();
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0).changeId).isEqualTo(change.info().changeId);
+  }
+
+  @Test
+  public void submittableQueryRuleNoRecord() throws Exception {
+    ChangeApi change = newChangeApi();
+
+    // Satisfy the default rule.
+    approveChange(change);
+
+    // Our custom rule isn't providing any submit records.
+    rule.status(Optional.empty());
+    change.index();
+
+    // is:submittable should return the change, since it was approved and the custom rule is not
+    // blocking it.
+    List<ChangeInfo> result = queryIsSubmittable();
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0).changeId).isEqualTo(change.info().changeId);
+  }
+
+  private List<ChangeInfo> queryIsSubmittable() throws Exception {
+    return gApi.changes().query("is:submittable project:" + project.get()).get();
+  }
+
+  private ChangeApi newChangeApi() throws Exception {
+    return gApi.changes().id(createChange().getChangeId());
+  }
+
+  private void approveChange(ChangeApi changeApi) throws Exception {
+    changeApi.current().review(ReviewInput.approve());
+  }
+
+  private void rejectChange(ChangeApi changeApi) throws Exception {
+    changeApi.current().review(ReviewInput.reject());
+  }
+
   @Singleton
   private static class CustomSubmitRule implements SubmitRule {
-    private final AtomicBoolean block = new AtomicBoolean(true);
+    private Optional<SubmitRecord.Status> recordStatus = Optional.empty();
 
     public void block(boolean block) {
-      this.block.set(block);
+      this.status(block ? Optional.of(SubmitRecord.Status.NOT_READY) : Optional.empty());
+    }
+
+    public void status(Optional<SubmitRecord.Status> status) {
+      this.recordStatus = status;
     }
 
     @Override
-    public Collection<SubmitRecord> evaluate(ChangeData changeData, SubmitRuleOptions options) {
-      if (block.get()) {
+    public Optional<SubmitRecord> evaluate(ChangeData changeData) {
+      if (this.recordStatus.isPresent()) {
         SubmitRecord record = new SubmitRecord();
         record.labels = new ArrayList<>();
-        record.status = SubmitRecord.Status.NOT_READY;
+        record.status = this.recordStatus.get();
         record.requirements = ImmutableList.of(req);
-        return ImmutableList.of(record);
+        return Optional.of(record);
       }
-      return ImmutableList.of();
+      return Optional.empty();
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
index ae88afd..42d62bd 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
@@ -15,25 +15,24 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 public class DisablePrivateChangesIT extends AbstractDaemonTest {
-
   @Test
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
   public void createPrivateChangeWithDisablePrivateChangesTrue() throws Exception {
     ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
     input.isPrivate = true;
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("private changes are disabled");
-    gApi.changes().create(input);
+    MethodNotAllowedException thrown =
+        assertThrows(MethodNotAllowedException.class, () -> gApi.changes().create(input));
+    assertThat(thrown).hasMessageThat().contains("private changes are disabled");
   }
 
   @Test
@@ -59,20 +58,6 @@
   }
 
   @Test
-  @GerritConfig(name = "change.allowDrafts", value = "true")
-  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
-  public void pushDraftsWithDisablePrivateChangesTrue() throws Exception {
-    RevCommit initialHead = getRemoteHead();
-    PushOneCommit.Result result =
-        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
-    result.assertErrorStatus();
-
-    testRepo.reset(initialHead);
-    result = pushFactory.create(admin.newIdent(), testRepo).to("refs/drafts/master");
-    result.assertErrorStatus();
-  }
-
-  @Test
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
   public void pushWithDisablePrivateChangesTrue() throws Exception {
     PushOneCommit.Result result =
@@ -82,34 +67,15 @@
   }
 
   @Test
-  @GerritConfig(name = "change.allowDrafts", value = "true")
-  public void pushPrivatesWithDisablePrivateChangesFalse() throws Exception {
-    PushOneCommit.Result result =
-        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%private");
-    assertThat(result.getChange().change().isPrivate()).isTrue();
-  }
-
-  @Test
-  @GerritConfig(name = "change.allowDrafts", value = "true")
-  public void pushDraftsWithDisablePrivateChangesFalse() throws Exception {
-    RevCommit initialHead = getRemoteHead();
-    PushOneCommit.Result result =
-        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
-    assertThat(result.getChange().change().isPrivate()).isTrue();
-
-    testRepo.reset(initialHead);
-    result = pushFactory.create(admin.newIdent(), testRepo).to("refs/drafts/master");
-    assertThat(result.getChange().change().isPrivate()).isTrue();
-  }
-
-  @Test
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
   public void setPrivateWithDisablePrivateChangesTrue() throws Exception {
     PushOneCommit.Result result = createChange();
 
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("private changes are disabled");
-    gApi.changes().id(result.getChangeId()).setPrivate(true, "set private");
+    MethodNotAllowedException thrown =
+        assertThrows(
+            MethodNotAllowedException.class,
+            () -> gApi.changes().id(result.getChangeId()).setPrivate(true, "set private"));
+    assertThat(thrown).hasMessageThat().contains("private changes are disabled");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java b/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
index a08d417..7354ca4 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
+import static com.google.gerrit.entities.Patch.MERGE_LIST;
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
@@ -152,18 +154,26 @@
   public void editMergeList() throws Exception {
     gApi.changes().id(changeId).edit().create();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Invalid path: " + MERGE_LIST);
-    gApi.changes().id(changeId).edit().modifyFile(MERGE_LIST, RawInputUtil.create("new content"));
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () ->
+                gApi.changes()
+                    .id(changeId)
+                    .edit()
+                    .modifyFile(MERGE_LIST, RawInputUtil.create("new content")));
+    assertThat(thrown).hasMessageThat().contains("Invalid path: " + MERGE_LIST);
   }
 
   @Test
   public void deleteMergeList() throws Exception {
     gApi.changes().id(changeId).edit().create();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("no changes were made");
-    gApi.changes().id(changeId).edit().deleteFile(MERGE_LIST);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).edit().deleteFile(MERGE_LIST));
+    assertThat(thrown).hasMessageThat().contains("no changes were made");
   }
 
   private String getMergeListContent(RevCommit... commits) {
@@ -171,7 +181,7 @@
     for (RevCommit c : commits) {
       mergeList
           .append("* ")
-          .append(c.abbreviate(8).name())
+          .append(abbreviateName(c, 8))
           .append(" ")
           .append(c.getShortMessage())
           .append("\n");
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
new file mode 100644
index 0000000..7156c8d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -0,0 +1,290 @@
+// 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.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentForValidation.CommentType;
+import com.google.gerrit.extensions.validators.CommentValidator;
+import com.google.gerrit.server.restapi.change.PostReview;
+import com.google.gerrit.server.update.CommentsRejectedException;
+import com.google.gerrit.testing.TestCommentHelper;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import java.sql.Timestamp;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+
+/** Tests for comment validation in {@link PostReview}. */
+public class PostReviewIT extends AbstractDaemonTest {
+  @Inject private CommentValidator mockCommentValidator;
+  @Inject private TestCommentHelper testCommentHelper;
+
+  private static final String COMMENT_TEXT = "The comment text";
+
+  @Captor private ArgumentCaptor<ImmutableList<CommentForValidation>> capture;
+
+  @Override
+  public Module createModule() {
+    return new FactoryModule() {
+      @Override
+      public void configure() {
+        CommentValidator mockCommentValidator = mock(CommentValidator.class);
+        bind(CommentValidator.class)
+            .annotatedWith(Exports.named(mockCommentValidator.getClass()))
+            .toInstance(mockCommentValidator);
+        bind(CommentValidator.class).toInstance(mockCommentValidator);
+      }
+    };
+  }
+
+  @Before
+  public void resetMock() {
+    initMocks(this);
+    clearInvocations(mockCommentValidator);
+  }
+
+  @Test
+  public void validateCommentsInInput_commentOK() throws Exception {
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(
+                    CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of());
+
+    PushOneCommit.Result r = createChange();
+
+    ReviewInput input = new ReviewInput();
+    CommentInput comment = newComment(r.getChange().currentFilePaths().get(0));
+    comment.updated = new Timestamp(0);
+    input.comments = ImmutableMap.of(comment.path, ImmutableList.of(comment));
+
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
+    gApi.changes().id(r.getChangeId()).current().review(input);
+
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).hasSize(1);
+  }
+
+  @Test
+  public void validateCommentsInInput_commentRejected() throws Exception {
+    CommentForValidation commentForValidation =
+        CommentForValidation.create(CommentType.FILE_COMMENT, COMMENT_TEXT);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(CommentForValidation.create(CommentType.FILE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
+
+    PushOneCommit.Result r = createChange();
+
+    ReviewInput input = new ReviewInput();
+    CommentInput comment = newComment(r.getChange().currentFilePaths().get(0));
+    comment.updated = new Timestamp(0);
+    input.comments = ImmutableMap.of(comment.path, ImmutableList.of(comment));
+
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
+    BadRequestException badRequestException =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(r.getChangeId()).current().review(input));
+    assertThat(badRequestException.getCause()).isInstanceOf(CommentsRejectedException.class);
+    assertThat(
+            Iterables.getOnlyElement(
+                    ((CommentsRejectedException) badRequestException.getCause())
+                        .getCommentValidationFailures())
+                .getComment()
+                .getText())
+        .isEqualTo(COMMENT_TEXT);
+    assertThat(badRequestException.getCause()).hasMessageThat().contains("Oh no!");
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
+  }
+
+  @Test
+  public void validateCommentsInInput_commentCleanedUp() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
+
+    // posting a comment which is empty after trim is a no-op, as the empty comment is dropped
+    // during comment cleanup
+    ReviewInput input = new ReviewInput();
+    CommentInput comment =
+        TestCommentHelper.populate(
+            new CommentInput(), r.getChange().currentFilePaths().get(0), " ");
+    comment.updated = new Timestamp(0);
+    input.comments = ImmutableMap.of(comment.path, ImmutableList.of(comment));
+    gApi.changes().id(r.getChangeId()).current().review(input);
+
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
+  }
+
+  @Test
+  public void validateDrafts_draftOK() throws Exception {
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(
+                    CommentForValidation.CommentType.INLINE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of());
+
+    PushOneCommit.Result r = createChange();
+
+    DraftInput draft =
+        testCommentHelper.newDraft(
+            r.getChange().currentFilePaths().get(0), Side.REVISION, 1, COMMENT_TEXT);
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().getName()).createDraft(draft).get();
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
+
+    ReviewInput input = new ReviewInput();
+    input.drafts = DraftHandling.PUBLISH;
+
+    gApi.changes().id(r.getChangeId()).current().review(input);
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).hasSize(1);
+  }
+
+  @Test
+  public void validateDrafts_draftRejected() throws Exception {
+    CommentForValidation commentForValidation =
+        CommentForValidation.create(CommentType.INLINE_COMMENT, COMMENT_TEXT);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(
+                    CommentForValidation.CommentType.INLINE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
+    PushOneCommit.Result r = createChange();
+
+    DraftInput draft =
+        testCommentHelper.newDraft(
+            r.getChange().currentFilePaths().get(0), Side.REVISION, 1, COMMENT_TEXT);
+    testCommentHelper.addDraft(r.getChangeId(), r.getCommit().getName(), draft);
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
+
+    ReviewInput input = new ReviewInput();
+    input.drafts = DraftHandling.PUBLISH;
+    BadRequestException badRequestException =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(r.getChangeId()).current().review(input));
+    assertThat(badRequestException.getCause()).isInstanceOf(CommentsRejectedException.class);
+    assertThat(
+            Iterables.getOnlyElement(
+                    ((CommentsRejectedException) badRequestException.getCause())
+                        .getCommentValidationFailures())
+                .getComment()
+                .getText())
+        .isEqualTo(draft.message);
+    assertThat(badRequestException.getCause()).hasMessageThat().contains("Oh no!");
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
+  }
+
+  @Test
+  public void validateDrafts_inlineVsFileComments_allOK() throws Exception {
+    PushOneCommit.Result r = createChange();
+    DraftInput draftInline =
+        testCommentHelper.newDraft(
+            r.getChange().currentFilePaths().get(0), Side.REVISION, 1, COMMENT_TEXT);
+    testCommentHelper.addDraft(r.getChangeId(), r.getCommit().getName(), draftInline);
+    DraftInput draftFile = testCommentHelper.newDraft(COMMENT_TEXT);
+    testCommentHelper.addDraft(r.getChangeId(), r.getCommit().getName(), draftFile);
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
+
+    when(mockCommentValidator.validateComments(capture.capture())).thenReturn(ImmutableList.of());
+
+    ReviewInput input = new ReviewInput();
+    input.drafts = DraftHandling.PUBLISH;
+    gApi.changes().id(r.getChangeId()).current().review(input);
+    assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).hasSize(2);
+
+    assertThat(capture.getAllValues()).hasSize(1);
+    assertThat(capture.getValue())
+        .containsExactly(
+            CommentForValidation.create(
+                CommentForValidation.CommentType.INLINE_COMMENT, draftInline.message),
+            CommentForValidation.create(
+                CommentForValidation.CommentType.FILE_COMMENT, draftFile.message));
+  }
+
+  @Test
+  public void validateCommentsInChangeMessage_messageOK() throws Exception {
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of());
+    PushOneCommit.Result r = createChange();
+
+    ReviewInput input = new ReviewInput().message(COMMENT_TEXT);
+    int numMessages = gApi.changes().id(r.getChangeId()).get().messages.size();
+    gApi.changes().id(r.getChangeId()).current().review(input);
+    assertThat(gApi.changes().id(r.getChangeId()).get().messages).hasSize(numMessages + 1);
+    ChangeMessageInfo message =
+        Iterables.getLast(gApi.changes().id(r.getChangeId()).get().messages);
+    assertThat(message.message).contains(COMMENT_TEXT);
+  }
+
+  @Test
+  public void validateCommentsInChangeMessage_messageRejected() throws Exception {
+    CommentForValidation commentForValidation =
+        CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
+    PushOneCommit.Result r = createChange();
+
+    ReviewInput input = new ReviewInput().message(COMMENT_TEXT);
+    assertThat(gApi.changes().id(r.getChangeId()).get().messages)
+        .hasSize(1); // From the initial commit.
+    BadRequestException badRequestException =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(r.getChangeId()).current().review(input));
+    assertThat(badRequestException.getCause()).isInstanceOf(CommentsRejectedException.class);
+    assertThat(
+            Iterables.getOnlyElement(
+                    ((CommentsRejectedException) badRequestException.getCause())
+                        .getCommentValidationFailures())
+                .getComment()
+                .getText())
+        .isEqualTo(COMMENT_TEXT);
+    assertThat(badRequestException.getCause()).hasMessageThat().contains("Oh no!");
+    assertThat(gApi.changes().id(r.getChangeId()).get().messages)
+        .hasSize(1); // Unchanged from before.
+    ChangeMessageInfo message =
+        Iterables.getLast(gApi.changes().id(r.getChangeId()).get().messages);
+    assertThat(message.message).doesNotContain(COMMENT_TEXT);
+  }
+
+  private static CommentInput newComment(String path) {
+    return TestCommentHelper.populate(new CommentInput(), path, PostReviewIT.COMMENT_TEXT);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
index bfcb1a8..f043c9b 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
@@ -15,19 +15,22 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -41,6 +44,7 @@
 
 public class PrivateChangeIT extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -89,9 +93,9 @@
     String changeId = result.getChangeId();
     assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("cannot set merged change to private");
-    gApi.changes().id(changeId).setPrivate(true);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> gApi.changes().id(changeId).setPrivate(true));
+    assertThat(thrown).hasMessageThat().contains("cannot set merged change to private");
   }
 
   @Test
@@ -102,9 +106,9 @@
     gApi.changes().id(changeId).abandon();
     assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("cannot set abandoned change to private");
-    gApi.changes().id(changeId).setPrivate(true);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> gApi.changes().id(changeId).setPrivate(true));
+    assertThat(thrown).hasMessageThat().contains("cannot set abandoned change to private");
   }
 
   @Test
@@ -126,9 +130,11 @@
   public void cannotSetOtherUsersChangePrivate() throws Exception {
     PushOneCommit.Result result = createChange();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to mark private");
-    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(result.getChangeId()).setPrivate(true, null));
+    assertThat(thrown).hasMessageThat().contains("not allowed to mark private");
   }
 
   @Test
@@ -153,9 +159,10 @@
     gApi.changes().id(result.getChangeId()).reviewer(admin.id().toString()).remove();
 
     // This change should not be visible for admin anymore.
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("Not found: " + result.getChangeId());
-    gApi.changes().id(result.getChangeId());
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()));
+    assertThat(thrown).hasMessageThat().contains("Not found: " + result.getChangeId());
   }
 
   @Test
@@ -163,7 +170,11 @@
     PushOneCommit.Result result = createChange();
     gApi.changes().id(result.getChangeId()).setPrivate(true, null);
 
-    allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
   }
@@ -173,7 +184,7 @@
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
     merge(result);
-    markMergedChangePrivate(new Change.Id(gApi.changes().id(changeId).get()._number));
+    markMergedChangePrivate(Change.id(gApi.changes().id(changeId).get()._number));
 
     gApi.changes().id(changeId).setPrivate(false, null);
     assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
@@ -191,9 +202,9 @@
     merge(result);
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to mark private");
-    gApi.changes().id(changeId).setPrivate(true, null);
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).setPrivate(true, null));
+    assertThat(thrown).hasMessageThat().contains("not allowed to mark private");
   }
 
   @Test
@@ -218,7 +229,7 @@
     String changeId = result.getChangeId();
     gApi.changes().id(changeId).addReviewer(admin.id().toString());
     merge(result);
-    markMergedChangePrivate(new Change.Id(gApi.changes().id(changeId).get()._number));
+    markMergedChangePrivate(Change.id(gApi.changes().id(changeId).get()._number));
 
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).setPrivate(false, null);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
index 843527a..6839b96 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
@@ -15,22 +15,32 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.restapi.change.QueryChanges;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.util.Arrays;
 import java.util.List;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Test;
 
 @NoHttpd
 public class QueryChangesIT extends AbstractDaemonTest {
 
   @Inject private Provider<QueryChanges> queryChangesProvider;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   @SuppressWarnings("unchecked")
@@ -48,7 +58,7 @@
     queryChanges.addQuery("is:wip repo:" + project.get());
 
     List<List<ChangeInfo>> result =
-        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE);
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
     assertThat(result).hasSize(2);
     assertThat(result.get(0)).hasSize(2);
     assertThat(result.get(1)).hasSize(1);
@@ -75,7 +85,7 @@
     queryChanges.addQuery(queryWithMoreChanges);
     queryChanges.addQuery(queryWithNoMoreChanges);
     List<List<ChangeInfo>> result =
-        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE);
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
     assertThat(result).hasSize(2);
     assertThat(result.get(0)).hasSize(1);
     assertThat(result.get(1)).hasSize(3);
@@ -88,7 +98,7 @@
     queryChanges2.addQuery(queryWithNoMoreChanges);
     queryChanges2.addQuery(queryWithMoreChanges);
     List<List<ChangeInfo>> result2 =
-        (List<List<ChangeInfo>>) queryChanges2.apply(TopLevelResource.INSTANCE);
+        (List<List<ChangeInfo>>) queryChanges2.apply(TopLevelResource.INSTANCE).value();
     assertThat(result2).hasSize(2);
     assertThat(result2.get(0)).hasSize(3);
     assertThat(result2.get(1)).hasSize(1);
@@ -97,9 +107,87 @@
     assertThat(result2.get(1).get(0)._moreChanges).isTrue();
   }
 
+  @Test
+  public void skipVisibility_rejectedForNonAdmin() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    final QueryChanges queryChanges = queryChangesProvider.get();
+    String query = "is:open repo:" + project.get();
+    queryChanges.addQuery(query);
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> queryChanges.skipVisibility(true));
+    assertThat(thrown).hasMessageThat().isEqualTo("administrate server not permitted");
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void skipVisibility_noReadPermission() throws Exception {
+    createChange().getChangeId();
+    requestScopeOperations.setApiUser(admin.id());
+    QueryChanges queryChanges = queryChangesProvider.get();
+
+    queryChanges.addQuery("is:open repo:" + project.get());
+    List<List<ChangeInfo>> result =
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
+    assertThat(result).hasSize(1);
+
+    try (ProjectConfigUpdate u = updateProject(allProjects)) {
+      ProjectConfig cfg = u.getConfig();
+      removeAllBranchPermissions(cfg, Permission.READ);
+      u.save();
+    }
+
+    queryChanges = queryChangesProvider.get();
+    queryChanges.addQuery("is:open repo:" + project.get());
+    List<List<ChangeInfo>> result2 =
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
+    assertThat(result2).hasSize(0);
+
+    queryChanges = queryChangesProvider.get();
+    queryChanges.addQuery("is:open repo:" + project.get());
+    queryChanges.skipVisibility(true);
+    List<List<ChangeInfo>> result3 =
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
+    assertThat(result3).hasSize(1);
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void skipVisibility_privateChange() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(result.getChangeId()).setPrivate(true);
+
+    requestScopeOperations.setApiUser(admin.id());
+    QueryChanges queryChanges = queryChangesProvider.get();
+
+    queryChanges.addQuery("is:open repo:" + project.get());
+    List<List<ChangeInfo>> result2 =
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
+    assertThat(result2).hasSize(0);
+
+    queryChanges = queryChangesProvider.get();
+    queryChanges.addQuery("is:open repo:" + project.get());
+    queryChanges.skipVisibility(true);
+    List<List<ChangeInfo>> result3 =
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
+    assertThat(result3).hasSize(1);
+  }
+
   private static void assertNoChangeHasMoreChangesSet(List<ChangeInfo> results) {
     for (ChangeInfo info : results) {
       assertThat(info._moreChanges).isNull();
     }
   }
+
+  private static void removeAllBranchPermissions(ProjectConfig cfg, String... permissions) {
+    cfg.getAccessSections().stream()
+        .filter(
+            s ->
+                s.getName().startsWith("refs/heads/")
+                    || s.getName().startsWith("refs/for/")
+                    || s.getName().equals("refs/*"))
+        .forEach(s -> Arrays.stream(permissions).forEach(s::removePermission));
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
new file mode 100644
index 0000000..0607a3c
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -0,0 +1,454 @@
+// 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.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestProjectInput;
+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.RefNames;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.RevertInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ProjectState;
+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.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.permissions.PermissionDeniedException;
+import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+public class RevertIT extends AbstractDaemonTest {
+
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  @Test
+  public void pureRevertFactBlocksSubmissionOfNonReverts() throws Exception {
+    addPureRevertSubmitRule();
+
+    // Create a change that is not a revert of another change
+    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
+    approve(r1.getChangeId());
+
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(r1.getChangeId()).current().submit());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Failed to submit 1 change due to the following problems");
+    assertThat(thrown).hasMessageThat().contains("needs Is-Pure-Revert");
+  }
+
+  @Test
+  public void pureRevertFactBlocksSubmissionOfNonPureReverts() throws Exception {
+    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
+    merge(r1);
+
+    addPureRevertSubmitRule();
+
+    // Create a revert and push a content change
+    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
+    amendChange(revertId);
+    approve(revertId);
+
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(revertId).current().submit());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Failed to submit 1 change due to the following problems");
+    assertThat(thrown).hasMessageThat().contains("needs Is-Pure-Revert");
+  }
+
+  @Test
+  public void pureRevertFactAllowsSubmissionOfPureReverts() throws Exception {
+    // Create a change that we can later revert
+    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
+    merge(r1);
+
+    addPureRevertSubmitRule();
+
+    // Create a revert and submit it
+    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
+    approve(revertId);
+    gApi.changes().id(revertId).current().submit();
+  }
+
+  @Test
+  public void pureRevertReturnsTrueForPureRevert() throws Exception {
+    PushOneCommit.Result r = createChange();
+    merge(r);
+    String revertId = gApi.changes().id(r.getChangeId()).revert().get().id;
+    // Without query parameter
+    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+    // With query parameter
+    assertThat(
+            gApi.changes()
+                .id(revertId)
+                .pureRevert(
+                    projectOperations.project(project).getHead("master").toObjectId().name())
+                .isPureRevert)
+        .isTrue();
+  }
+
+  @Test
+  public void pureRevertReturnsFalseOnContentChange() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    merge(r1);
+    // Create a revert and expect pureRevert to be true
+    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
+    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+
+    // Create a new PS and expect pureRevert to be false
+    PushOneCommit.Result result = amendChange(revertId);
+    result.assertOkStatus();
+    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isFalse();
+  }
+
+  @Test
+  public void pureRevertParameterTakesPrecedence() throws Exception {
+    PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
+    merge(r1);
+    String oldHead = projectOperations.project(project).getHead("master").toObjectId().name();
+
+    PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content2");
+    merge(r2);
+
+    String revertId = gApi.changes().id(r2.getChangeId()).revert().get().changeId;
+    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+    assertThat(gApi.changes().id(revertId).pureRevert(oldHead).isPureRevert).isFalse();
+  }
+
+  @Test
+  public void pureRevertReturnsFalseOnInvalidInput() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    merge(r1);
+
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(createChange().getChangeId()).pureRevert("invalid id"));
+    assertThat(thrown).hasMessageThat().contains("invalid object ID");
+  }
+
+  @Test
+  public void pureRevertReturnsTrueWithCleanRebase() throws Exception {
+    PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
+    merge(r1);
+
+    PushOneCommit.Result r2 = createChange("commit message", "b.txt", "content2");
+    merge(r2);
+
+    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
+    // Rebase revert onto HEAD
+    gApi.changes().id(revertId).rebase();
+    // Check that pureRevert is true which implies that the commit can be rebased onto the original
+    // commit.
+    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+  }
+
+  @Test
+  public void pureRevertReturnsFalseWithRebaseConflict() throws Exception {
+    // Create an initial commit to serve as claimed original
+    PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
+    merge(r1);
+    String claimedOriginal =
+        projectOperations.project(project).getHead("master").toObjectId().name();
+
+    // Change contents of the file to provoke a conflict
+    merge(createChange("commit message", "a.txt", "content2"));
+
+    // Create a commit that we can revert
+    PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content3");
+    merge(r2);
+
+    // Create a revert of r2
+    String revertR3Id = gApi.changes().id(r2.getChangeId()).revert().id();
+    // Assert that the change is a pure revert of it's 'revertOf'
+    assertThat(gApi.changes().id(revertR3Id).pureRevert().isPureRevert).isTrue();
+    // Assert that the change is not a pure revert of claimedOriginal because pureRevert is trying
+    // to rebase this on claimed original, which fails.
+    PureRevertInfo pureRevert = gApi.changes().id(revertR3Id).pureRevert(claimedOriginal);
+    assertThat(pureRevert.isPureRevert).isFalse();
+  }
+
+  @Test
+  public void pureRevertThrowsExceptionWhenChangeIsNotARevertAndNoIdProvided() throws Exception {
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(createChange().getChangeId()).pureRevert());
+    assertThat(thrown).hasMessageThat().contains("revertOf not set");
+  }
+
+  @Test
+  public void revert() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+    ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert().get();
+
+    // expected messages on source change:
+    // 1. Uploaded patch set 1.
+    // 2. Patch Set 1: Code-Review+2
+    // 3. Change has been successfully merged by Administrator
+    // 4. Patch Set 1: Reverted
+    List<ChangeMessageInfo> sourceMessages =
+        new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
+    assertThat(sourceMessages).hasSize(4);
+    String expectedMessage =
+        String.format("Created a revert of this change as %s", revertChange.changeId);
+    assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
+
+    assertThat(revertChange.messages).hasSize(1);
+    assertThat(revertChange.messages.iterator().next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(revertChange.revertOf).isEqualTo(gApi.changes().id(r.getChangeId()).get()._number);
+  }
+
+  @Test
+  public void revertWithDefaultTopic() throws Exception {
+    PushOneCommit.Result result = createChange();
+    gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(result.getChangeId()).topic("topic");
+    gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
+    RevertInput revertInput = new RevertInput();
+    assertThat(gApi.changes().id(result.getChangeId()).revert(revertInput).topic())
+        .isEqualTo("topic");
+  }
+
+  @Test
+  public void revertWithSetTopic() throws Exception {
+    PushOneCommit.Result result = createChange();
+    gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(result.getChangeId()).topic("topic");
+    gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
+    RevertInput revertInput = new RevertInput();
+    revertInput.topic = "reverted-not-default";
+    assertThat(gApi.changes().id(result.getChangeId()).revert(revertInput).topic())
+        .isEqualTo(revertInput.topic);
+  }
+
+  @Test
+  public void revertWithSetMessage() throws Exception {
+    PushOneCommit.Result result = createChange();
+    gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
+    RevertInput revertInput = new RevertInput();
+    revertInput.message = "Message from input";
+    assertThat(gApi.changes().id(result.getChangeId()).revert(revertInput).get().subject)
+        .isEqualTo(revertInput.message);
+  }
+
+  @Test
+  public void revertNotifications() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    sender.clear();
+    ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert().get();
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(2);
+    assertThat(sender.getMessages(revertChange.changeId, "newchange")).hasSize(1);
+    assertThat(sender.getMessages(r.getChangeId(), "revert")).hasSize(1);
+  }
+
+  @Test
+  public void suppressRevertNotifications() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    RevertInput revertInput = new RevertInput();
+    revertInput.notify = NotifyHandling.NONE;
+
+    sender.clear();
+    gApi.changes().id(r.getChangeId()).revert(revertInput).get();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  public void revertPreservesReviewersAndCcs() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    ReviewInput in = ReviewInput.approve();
+    in.reviewer(user.email());
+    in.reviewer(accountCreator.user2().email(), ReviewerState.CC, true);
+    // Add user as reviewer that will create the revert
+    in.reviewer(accountCreator.admin2().email());
+
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    // expect both the original reviewers and CCs to be preserved
+    // original owner should be added as reviewer, user requesting the revert (new owner) removed
+    requestScopeOperations.setApiUser(accountCreator.admin2().id());
+    Map<ReviewerState, Collection<AccountInfo>> result =
+        gApi.changes().id(r.getChangeId()).revert().get().reviewers;
+    assertThat(result).containsKey(ReviewerState.REVIEWER);
+
+    List<Integer> reviewers =
+        result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList());
+    assertThat(result).containsKey(ReviewerState.CC);
+    List<Integer> ccs =
+        result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList());
+    assertThat(ccs).containsExactly(accountCreator.user2().id().get());
+    assertThat(reviewers).containsExactly(user.id().get(), admin.id().get());
+  }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void revertInitialCommit() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(r.getChangeId()).revert());
+    assertThat(thrown).hasMessageThat().contains("Cannot revert initial commit");
+  }
+
+  @Test
+  public void cantRevertNonMergedCommit() throws Exception {
+    PushOneCommit.Result result = createChange();
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(result.getChangeId()).revert());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("change is " + ChangeUtil.status(result.getChange().change()));
+  }
+
+  @Test
+  public void cantCreateRevertWithoutProjectWritePermission() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+    projectCache.checkedGet(project).getProject().setState(ProjectState.READ_ONLY);
+
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(r.getChangeId()).revert());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("project state " + ProjectState.READ_ONLY + " does not permit write");
+  }
+
+  @Test
+  public void cantCreateRevertWithoutCreateChangePermission() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
+
+    PermissionDeniedException thrown =
+        assertThrows(
+            PermissionDeniedException.class, () -> gApi.changes().id(r.getChangeId()).revert());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("not permitted: create change on refs/heads/master");
+  }
+
+  @Test
+  public void cantCreateRevertWithoutReadPermission() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
+
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class, () -> gApi.changes().id(r.getChangeId()).revert());
+    assertThat(thrown).hasMessageThat().contains("Not found: " + r.getChangeId());
+  }
+
+  @Override
+  protected PushOneCommit.Result createChange() throws Exception {
+    return createChange("refs/for/master");
+  }
+
+  @Override
+  protected PushOneCommit.Result createChange(String ref) throws Exception {
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    PushOneCommit.Result result = push.to(ref);
+    result.assertOkStatus();
+    return result;
+  }
+
+  private void addPureRevertSubmitRule() throws Exception {
+    modifySubmitRules(
+        "submit_rule(submit(R)) :- \n"
+            + "gerrit:pure_revert(1), \n"
+            + "!,"
+            + "gerrit:uploader(U), \n"
+            + "R = label('Is-Pure-Revert', ok(U)).\n"
+            + "submit_rule(submit(R)) :- \n"
+            + "gerrit:pure_revert(U), \n"
+            + "U \\= 1,"
+            + "R = label('Is-Pure-Revert', need(_)). \n\n");
+  }
+
+  private void modifySubmitRules(String newContent) throws Exception {
+    try (Repository repo = repoManager.openRepository(project);
+        TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
+      testRepo
+          .branch(RefNames.REFS_CONFIG)
+          .commit()
+          .author(admin.newIdent())
+          .committer(admin.newIdent())
+          .add("rules.pl", newContent)
+          .message("Modify rules.pl")
+          .create();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index d53b305..7e69251 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.extensions.client.ChangeKind.MERGE_FIRST_PARENT_UPDATE;
 import static com.google.gerrit.extensions.client.ChangeKind.NO_CHANGE;
 import static com.google.gerrit.extensions.client.ChangeKind.NO_CODE_CHANGE;
@@ -24,19 +26,21 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
+import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
@@ -44,12 +48,14 @@
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.change.ChangeKindCacheImpl;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.inject.Inject;
+import com.google.inject.name.Named;
 import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
@@ -60,15 +66,20 @@
 
 @NoHttpd
 public class StickyApprovalsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
+  @Inject
+  @Named("change_kind")
+  private Cache<ChangeKindCacheImpl.Key, ChangeKind> changeKindCache;
+
   @Before
   public void setup() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
       // Overwrite "Code-Review" label that is inherited from All-Projects.
       // This way changes to the "Code Review" label don't affect other tests.
       LabelType codeReview =
-          category(
+          label(
               "Code-Review",
               value(2, "Looks good to me, approved"),
               value(1, "Looks good to me, but someone else must approve"),
@@ -79,28 +90,26 @@
       u.getConfig().getLabelSections().put(codeReview.getName(), codeReview);
 
       LabelType verified =
-          category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+          label("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
       verified.setCopyAllScoresIfNoChange(false);
       u.getConfig().getLabelSections().put(verified.getName(), verified);
 
-      AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      String heads = RefNames.REFS_HEADS + "*";
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(Util.codeReview().getName()),
-          -2,
-          2,
-          registeredUsers,
-          heads);
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(Util.verified().getName()),
-          -1,
-          1,
-          registeredUsers,
-          heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .ref(RefNames.REFS_HEADS + "*")
+                .group(REGISTERED_USERS)
+                .range(-2, 2))
+        .add(
+            allowLabel(TestLabels.verified().getName())
+                .ref(RefNames.REFS_HEADS + "*")
+                .group(REGISTERED_USERS)
+                .range(-1, 1))
+        .update();
   }
 
   @Test
@@ -110,6 +119,28 @@
   }
 
   @Test
+  public void stickyOnAnyScore() throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().getLabelSections().get("Code-Review").setCopyAnyScore(true);
+      u.save();
+    }
+
+    for (ChangeKind changeKind :
+        EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
+      testRepo.reset(projectOperations.project(project).getHead("master"));
+
+      String changeId = createChange(changeKind);
+      vote(admin, changeId, 2, 1);
+      vote(user, changeId, 1, -1);
+
+      updateChange(changeId, changeKind);
+      ChangeInfo c = detailedChange(changeId);
+      assertVotes(c, admin, 2, 0, changeKind);
+      assertVotes(c, user, 1, 0, changeKind);
+    }
+  }
+
+  @Test
   public void stickyOnMinScore() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().get("Code-Review").setCopyMinScore(true);
@@ -118,7 +149,7 @@
 
     for (ChangeKind changeKind :
         EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
-      testRepo.reset(getRemoteHead());
+      testRepo.reset(projectOperations.project(project).getHead("master"));
 
       String changeId = createChange(changeKind);
       vote(admin, changeId, -1, 1);
@@ -140,7 +171,7 @@
 
     for (ChangeKind changeKind :
         EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
-      testRepo.reset(getRemoteHead());
+      testRepo.reset(projectOperations.project(project).getHead("master"));
 
       String changeId = createChange(changeKind);
       vote(admin, changeId, 2, 1);
@@ -177,7 +208,7 @@
     assertNotSticky(EnumSet.of(REWORK, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE));
 
     // check that votes are sticky when trivial rebase is done by cherry-pick
-    testRepo.reset(getRemoteHead());
+    testRepo.reset(projectOperations.project(project).getHead("master"));
     changeId = createChange().getChangeId();
     vote(admin, changeId, 2, 1);
     vote(user, changeId, -2, -1);
@@ -188,7 +219,7 @@
     assertVotes(c, user, -2, 0);
 
     // check that votes are not sticky when rework is done by cherry-pick
-    testRepo.reset(getRemoteHead());
+    testRepo.reset(projectOperations.project(project).getHead("master"));
     changeId = createChange().getChangeId();
     vote(admin, changeId, 2, 1);
     vote(user, changeId, -2, -1);
@@ -277,7 +308,7 @@
 
     for (ChangeKind changeKind :
         EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
-      testRepo.reset(getRemoteHead());
+      testRepo.reset(projectOperations.project(project).getHead("master"));
 
       String changeId = createChange(changeKind);
       vote(admin, changeId, 2, 1);
@@ -320,6 +351,42 @@
   }
 
   @Test
+  public void stickyAcrossMultiplePatchSetsDoNotRegressPerformance() throws Exception {
+    // The purpose of this test is to make sure that we compute change kind only against the parent
+    // patch set. Change kind is a heavy operation. In prior version of Gerrit, we computed the
+    // change kind against all prior patch sets. This is a regression that made Gerrit do expensive
+    // work in O(num-patch-sets). This test ensures that we aren't regressing.
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().getLabelSections().get("Code-Review").setCopyMaxScore(true);
+      u.getConfig().getLabelSections().get("Verified").setCopyAllScoresIfNoCodeChange(true);
+      u.save();
+    }
+
+    String changeId = createChange(REWORK);
+    vote(admin, changeId, 2, 1);
+    updateChange(changeId, NO_CODE_CHANGE);
+    updateChange(changeId, NO_CODE_CHANGE);
+    updateChange(changeId, NO_CODE_CHANGE);
+
+    Map<Integer, ObjectId> revisions = new HashMap<>();
+    gApi.changes()
+        .id(changeId)
+        .get()
+        .revisions
+        .forEach(
+            (revId, revisionInfo) ->
+                revisions.put(revisionInfo._number, ObjectId.fromString(revId)));
+    assertThat(revisions.size()).isEqualTo(4);
+    assertChangeKindCacheContains(revisions.get(3), revisions.get(4));
+    assertChangeKindCacheContains(revisions.get(2), revisions.get(3));
+    assertChangeKindCacheContains(revisions.get(1), revisions.get(2));
+
+    assertChangeKindCacheDoesNotContain(revisions.get(1), revisions.get(4));
+    assertChangeKindCacheDoesNotContain(revisions.get(2), revisions.get(4));
+    assertChangeKindCacheDoesNotContain(revisions.get(1), revisions.get(3));
+  }
+
+  @Test
   public void copyMinMaxAcrossMultiplePatchSets() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().get("Code-Review").setCopyMaxScore(true);
@@ -379,13 +446,25 @@
     assertVotes(detailedChange(changeId), admin, label, 0, REWORK);
   }
 
+  private void assertChangeKindCacheContains(ObjectId prior, ObjectId next) {
+    ChangeKind kind =
+        changeKindCache.getIfPresent(ChangeKindCacheImpl.Key.create(prior, next, "recursive"));
+    assertThat(kind).isNotNull();
+  }
+
+  private void assertChangeKindCacheDoesNotContain(ObjectId prior, ObjectId next) {
+    ChangeKind kind =
+        changeKindCache.getIfPresent(ChangeKindCacheImpl.Key.create(prior, next, "recursive"));
+    assertThat(kind).isNull();
+  }
+
   private ChangeInfo detailedChange(String changeId) throws Exception {
     return gApi.changes().id(changeId).get(DETAILED_LABELS, CURRENT_REVISION, CURRENT_COMMIT);
   }
 
   private void assertNotSticky(Set<ChangeKind> changeKinds) throws Exception {
     for (ChangeKind changeKind : changeKinds) {
-      testRepo.reset(getRemoteHead());
+      testRepo.reset(projectOperations.project(project).getHead("master"));
 
       String changeId = createChange(changeKind);
       vote(admin, changeId, +2, 1);
@@ -430,7 +509,7 @@
         noChange(changeId);
         return;
       default:
-        fail("unexpected change kind: " + changeKind);
+        assertWithMessage("unexpected change kind: " + changeKind).fail();
     }
   }
 
@@ -476,7 +555,7 @@
 
   private void trivialRebase(String changeId) throws Exception {
     requestScopeOperations.setApiUser(admin.id());
-    testRepo.reset(getRemoteHead());
+    testRepo.reset(projectOperations.project(project).getHead("master"));
     PushOneCommit push =
         pushFactory.create(
             admin.newIdent(),
@@ -557,10 +636,10 @@
       case NO_CHANGE:
       case MERGE_FIRST_PARENT_UPDATE:
       default:
-        fail("unexpected change kind: " + changeKind);
+        assertWithMessage("unexpected change kind: " + changeKind).fail();
     }
 
-    testRepo.reset(getRemoteHead());
+    testRepo.reset(projectOperations.project(project).getHead("master"));
     PushOneCommit.Result r =
         pushFactory
             .create(
@@ -581,7 +660,7 @@
     CherryPickInput in = new CherryPickInput();
     in.destination = "master";
     in.message = String.format("%s\n\nChange-Id: %s", subject, changeId);
-    ChangeInfo c = gApi.changes().id(changeId).revision("current").cherryPick(in).get();
+    ChangeInfo c = gApi.changes().id(changeId).current().cherryPick(in).get();
     return c.changeId;
   }
 
@@ -634,6 +713,6 @@
     if (changeKind != null) {
       name += "; changeKind = " + changeKind.name();
     }
-    assertThat(vote).named(name).isEqualTo(expectedVote);
+    assertWithMessage(name).that(vote).isEqualTo(expectedVote);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
index 6c0e9ab..dab2d00 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -21,11 +21,14 @@
 import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
 import static com.google.gerrit.extensions.client.SubmitType.REBASE_ALWAYS;
 import static com.google.gerrit.extensions.client.SubmitType.REBASE_IF_NECESSARY;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -33,8 +36,6 @@
 import com.google.gerrit.extensions.common.TestSubmitRuleInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.testing.ConfigSuite;
@@ -237,22 +238,21 @@
     gApi.changes().id(r1.getChangeId()).current().review(ReviewInput.approve());
     gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.approve());
 
-    try {
-      gApi.changes().id(r2.getChangeId()).current().submit();
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(
-              "Failed to submit 2 changes due to the following problems:\n"
-                  + "Change "
-                  + r1.getChange().getId()
-                  + ": Change has submit type "
-                  + "CHERRY_PICK, but previously chose submit type MERGE_IF_NECESSARY "
-                  + "from change "
-                  + r2.getChange().getId()
-                  + " in the same batch");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(r2.getChangeId()).current().submit());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            "Failed to submit 2 changes due to the following problems:\n"
+                + "Change "
+                + r1.getChange().getId()
+                + ": Change has submit type "
+                + "CHERRY_PICK, but previously chose submit type MERGE_IF_NECESSARY "
+                + "from change "
+                + r2.getChange().getId()
+                + " in the same batch");
   }
 
   @Test
@@ -262,8 +262,8 @@
     TestSubmitRuleInput in = new TestSubmitRuleInput();
     in.rule = "invalid prolog rule";
     // We have no rules.pl by default. The fact that the default rules are showing up here is a bug.
-    List<TestSubmitRuleInfo> response = gApi.changes().id(changeId).current().testSubmitRule(in);
-    assertThat(response).containsExactly(invalidPrologRuleInfo());
+    TestSubmitRuleInfo response = gApi.changes().id(changeId).current().testSubmitRule(in);
+    assertThat(response).isEqualTo(invalidPrologRuleInfo());
   }
 
   @Test
@@ -274,8 +274,8 @@
 
     TestSubmitRuleInput in = new TestSubmitRuleInput();
     in.rule = "invalid prolog rule";
-    List<TestSubmitRuleInfo> response = gApi.changes().id(changeId).current().testSubmitRule(in);
-    assertThat(response).containsExactly(invalidPrologRuleInfo());
+    TestSubmitRuleInfo response = gApi.changes().id(changeId).current().testSubmitRule(in);
+    assertThat(response).isEqualTo(invalidPrologRuleInfo());
   }
 
   private static TestSubmitRuleInfo invalidPrologRuleInfo() {
diff --git a/javatests/com/google/gerrit/acceptance/api/config/DiffPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/config/DiffPreferencesIT.java
index fd08838..70aa557 100644
--- a/javatests/com/google/gerrit/acceptance/api/config/DiffPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/config/DiffPreferencesIT.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance.api.config;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.AssertUtil.assertPrefs;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -37,7 +37,7 @@
     DiffPreferencesInfo update = new DiffPreferencesInfo();
     update.lineLength = newLineLength;
     DiffPreferencesInfo result = gApi.config().server().setDefaultDiffPreferences(update);
-    assertThat(result.lineLength).named("lineLength").isEqualTo(newLineLength);
+    assertWithMessage("lineLength").that(result.lineLength).isEqualTo(newLineLength);
 
     result = gApi.config().server().getDefaultDiffPreferences();
     DiffPreferencesInfo expected = DiffPreferencesInfo.defaults();
diff --git a/javatests/com/google/gerrit/acceptance/api/config/EditPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/config/EditPreferencesIT.java
index e89aa3d..02f1ec3 100644
--- a/javatests/com/google/gerrit/acceptance/api/config/EditPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/config/EditPreferencesIT.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance.api.config;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.AssertUtil.assertPrefs;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -37,7 +37,7 @@
     EditPreferencesInfo update = new EditPreferencesInfo();
     update.lineLength = newLineLength;
     EditPreferencesInfo result = gApi.config().server().setDefaultEditPreferences(update);
-    assertThat(result.lineLength).named("lineLength").isEqualTo(newLineLength);
+    assertWithMessage("lineLength").that(result.lineLength).isEqualTo(newLineLength);
 
     result = gApi.config().server().getDefaultEditPreferences();
     EditPreferencesInfo expected = EditPreferencesInfo.defaults();
diff --git a/javatests/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java
index c606982..221e171 100644
--- a/javatests/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance.api.config;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.AssertUtil.assertPrefs;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -36,7 +36,7 @@
     GeneralPreferencesInfo update = new GeneralPreferencesInfo();
     update.signedOffBy = newSignedOffBy;
     GeneralPreferencesInfo result = gApi.config().server().setDefaultPreferences(update);
-    assertThat(result.signedOffBy).named("signedOffBy").isEqualTo(newSignedOffBy);
+    assertWithMessage("signedOffBy").that(result.signedOffBy).isEqualTo(newSignedOffBy);
 
     result = gApi.config().server().getDefaultPreferences();
     GeneralPreferencesInfo expected = GeneralPreferencesInfo.defaults();
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
index d7d311b..41ae370 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
@@ -20,11 +20,11 @@
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
@@ -57,7 +57,7 @@
   @Test
   public void indexingUpdatesTheIndex() throws Exception {
     AccountGroup.UUID groupUuid = createGroup("users");
-    AccountGroup.UUID subgroupUuid = new AccountGroup.UUID("contributors");
+    AccountGroup.UUID subgroupUuid = AccountGroup.uuid("contributors");
     updateGroupWithoutCacheOrIndex(
         groupUuid,
         newGroupUpdate()
@@ -74,7 +74,7 @@
   public void indexCannotBeCorruptedByStaleCache() throws Exception {
     AccountGroup.UUID groupUuid = createGroup("verifiers");
     loadGroupToCache(groupUuid);
-    AccountGroup.UUID subgroupUuid = new AccountGroup.UUID("contributors");
+    AccountGroup.UUID subgroupUuid = AccountGroup.uuid("contributors");
     updateGroupWithoutCacheOrIndex(
         groupUuid,
         newGroupUpdate()
@@ -102,7 +102,7 @@
   @Test
   public void reindexingStaleGroupUpdatesTheIndex() throws Exception {
     AccountGroup.UUID groupUuid = createGroup("users");
-    AccountGroup.UUID subgroupUuid = new AccountGroup.UUID("contributors");
+    AccountGroup.UUID subgroupUuid = AccountGroup.uuid("contributors");
     updateGroupWithoutCacheOrIndex(
         groupUuid,
         newGroupUpdate()
@@ -139,7 +139,7 @@
 
   private AccountGroup.UUID createGroup(String name) throws RestApiException {
     GroupInfo group = gApi.groups().create(name).get();
-    return new AccountGroup.UUID(group.id);
+    return AccountGroup.uuid(group.id);
   }
 
   private void reloadGroupToCache(AccountGroup.UUID groupUuid) {
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
index e269e68..e6c3919 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
@@ -15,19 +15,22 @@
 package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
 import com.google.gerrit.extensions.common.GroupInfo;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.group.db.GroupConfig;
 import com.google.gerrit.server.group.db.GroupNameNotes;
 import com.google.gerrit.server.group.db.testing.GroupTestUtil;
@@ -51,6 +54,7 @@
 public class GroupsConsistencyIT extends AbstractDaemonTest {
 
   @Inject protected GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
   private GroupInfo gAdmin;
   private GroupInfo g1;
   private GroupInfo g2;
@@ -59,7 +63,10 @@
 
   @Before
   public void basicSetup() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     String name1 = groupOperations.newGroup().name("g1").create().get();
     String name2 = groupOperations.newGroup().name("g2").create().get();
@@ -94,7 +101,7 @@
   public void missingGroupRef() throws Exception {
 
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      RefUpdate ru = repo.updateRef(RefNames.refsGroups(new AccountGroup.UUID(g1.id)));
+      RefUpdate ru = repo.updateRef(RefNames.refsGroups(AccountGroup.uuid(g1.id)));
       ru.setForceUpdate(true);
       RefUpdate.Result result = ru.delete();
       assertThat(result).isEqualTo(Result.FORCED);
@@ -109,7 +116,7 @@
     try (Repository repo = repoManager.openRepository(allUsers)) {
       RefRename ru =
           repo.renameRef(
-              RefNames.refsGroups(new AccountGroup.UUID(g1.id)), RefNames.REFS_GROUPS + BOGUS_UUID);
+              RefNames.refsGroups(AccountGroup.uuid(g1.id)), RefNames.REFS_GROUPS + BOGUS_UUID);
       RefUpdate.Result result = ru.rename();
       assertThat(result).isEqualTo(Result.RENAMED);
     }
@@ -123,8 +130,8 @@
     try (Repository repo = repoManager.openRepository(allUsers)) {
       RefRename ru =
           repo.renameRef(
-              RefNames.refsGroups(new AccountGroup.UUID(g1.id)),
-              RefNames.refsGroups(new AccountGroup.UUID(BOGUS_UUID)));
+              RefNames.refsGroups(AccountGroup.uuid(g1.id)),
+              RefNames.refsGroups(AccountGroup.uuid(BOGUS_UUID)));
       RefUpdate.Result result = ru.rename();
       assertThat(result).isEqualTo(Result.RENAMED);
     }
@@ -135,7 +142,7 @@
   @Test
   public void groupRefDoesNotParse() throws Exception {
     updateGroupFile(
-        RefNames.refsGroups(new AccountGroup.UUID(g1.id)),
+        RefNames.refsGroups(AccountGroup.uuid(g1.id)),
         GroupConfig.GROUP_CONFIG_FILE,
         "[this is not valid\n");
     assertError("does not parse");
@@ -145,7 +152,7 @@
   public void nameRefDoesNotParse() throws Exception {
     updateGroupFile(
         RefNames.REFS_GROUPNAMES,
-        GroupNameNotes.getNoteKey(new AccountGroup.NameKey(g1.name)).getName(),
+        GroupNameNotes.getNoteKey(AccountGroup.nameKey(g1.name)).getName(),
         "[this is not valid\n");
     assertError("does not parse");
   }
@@ -158,9 +165,7 @@
     cfg.setString("group", null, "ownerGroupUuid", gAdmin.id);
 
     updateGroupFile(
-        RefNames.refsGroups(new AccountGroup.UUID(g1.id)),
-        GroupConfig.GROUP_CONFIG_FILE,
-        cfg.toText());
+        RefNames.refsGroups(AccountGroup.uuid(g1.id)), GroupConfig.GROUP_CONFIG_FILE, cfg.toText());
     assertError("inconsistent name");
   }
 
@@ -172,9 +177,7 @@
     cfg.setString("group", null, "ownerGroupUuid", gAdmin.id);
 
     updateGroupFile(
-        RefNames.refsGroups(new AccountGroup.UUID(g1.id)),
-        GroupConfig.GROUP_CONFIG_FILE,
-        cfg.toText());
+        RefNames.refsGroups(AccountGroup.uuid(g1.id)), GroupConfig.GROUP_CONFIG_FILE, cfg.toText());
     assertError("shared group id");
   }
 
@@ -186,9 +189,7 @@
     cfg.setString("group", null, "ownerGroupUuid", BOGUS_UUID);
 
     updateGroupFile(
-        RefNames.refsGroups(new AccountGroup.UUID(g1.id)),
-        GroupConfig.GROUP_CONFIG_FILE,
-        cfg.toText());
+        RefNames.refsGroups(AccountGroup.uuid(g1.id)), GroupConfig.GROUP_CONFIG_FILE, cfg.toText());
     assertError("nonexistent owner group");
   }
 
@@ -201,27 +202,26 @@
 
     updateGroupFile(
         RefNames.REFS_GROUPNAMES,
-        GroupNameNotes.getNoteKey(new AccountGroup.NameKey(bogusName)).getName(),
+        GroupNameNotes.getNoteKey(AccountGroup.nameKey(bogusName)).getName(),
         config.toText());
     assertError("entry missing as group ref");
   }
 
   @Test
   public void nonexistentMember() throws Exception {
-    updateGroupFile(RefNames.refsGroups(new AccountGroup.UUID(g1.id)), "members", "314159265\n");
+    updateGroupFile(RefNames.refsGroups(AccountGroup.uuid(g1.id)), "members", "314159265\n");
     assertError("nonexistent member 314159265");
   }
 
   @Test
   public void nonexistentSubgroup() throws Exception {
-    updateGroupFile(
-        RefNames.refsGroups(new AccountGroup.UUID(g1.id)), "subgroups", BOGUS_UUID + "\n");
+    updateGroupFile(RefNames.refsGroups(AccountGroup.uuid(g1.id)), "subgroups", BOGUS_UUID + "\n");
     assertError("has nonexistent subgroup");
   }
 
   @Test
   public void cyclicSubgroup() throws Exception {
-    updateGroupFile(RefNames.refsGroups(new AccountGroup.UUID(g1.id)), "subgroups", g1.id + "\n");
+    updateGroupFile(RefNames.refsGroups(AccountGroup.uuid(g1.id)), "subgroups", g1.id + "\n");
     assertWarning("cycle");
   }
 
@@ -252,7 +252,8 @@
       }
     }
 
-    fail(String.format("could not find %s substring '%s' in %s", want, msg, problems));
+    assertWithMessage(String.format("could not find %s substring '%s' in %s", want, msg, problems))
+        .fail();
   }
 
   private void updateGroupFile(String refName, String fileName, String content) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index b86fb29..b53681b 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -21,19 +21,25 @@
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.acceptance.api.group.GroupAssert.assertGroupInfo;
 import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.truth.Correspondence;
-import com.google.common.truth.Correspondence.BinaryPredicate;
 import com.google.common.util.concurrent.AtomicLongMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -41,13 +47,19 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 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.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -60,18 +72,12 @@
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.events.GroupIndexedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.InternalGroup;
@@ -85,11 +91,9 @@
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.index.group.StalenessChecker;
 import com.google.gerrit.server.notedb.Sequences;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.TestTimeUtil;
+import com.google.gerrit.testing.GerritJUnit.ThrowingRunnable;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.lang.annotation.Retention;
@@ -100,7 +104,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -117,33 +120,24 @@
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
+@UseClockStep
 public class GroupsIT extends AbstractDaemonTest {
   @Inject @ServerInitiated private GroupsUpdate groupsUpdate;
   @Inject private AccountOperations accountOperations;
-  @Inject private DynamicSet<GroupIndexedListener> groupIndexedListeners;
   @Inject private GroupIncludeCache groupIncludeCache;
   @Inject private GroupIndexer groupIndexer;
   @Inject private GroupOperations groupOperations;
   @Inject private Groups groups;
   @Inject private GroupsConsistencyChecker consistencyChecker;
   @Inject private PeriodicGroupIndexer slaveGroupIndexer;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private Sequences seq;
   @Inject private StalenessChecker stalenessChecker;
-
-  @Before
-  public void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-  }
+  @Inject private ExtensionRegistry extensionRegistry;
 
   @After
   public void consistencyCheck() throws Exception {
@@ -168,14 +162,16 @@
 
   @Test
   public void addToNonExistingGroup_NotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.groups().id("non-existing").addMembers("admin");
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.groups().id("non-existing").addMembers("admin"));
   }
 
   @Test
   public void removeFromNonExistingGroup_NotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.groups().id("non-existing").removeMembers("admin");
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.groups().id("non-existing").removeMembers("admin"));
   }
 
   @Test
@@ -215,7 +211,7 @@
   @Test
   public void cachedGroupByNameIsUpdatedOnCreation() throws Exception {
     String newGroupName = name("newGroup");
-    AccountGroup.NameKey nameKey = new AccountGroup.NameKey(newGroupName);
+    AccountGroup.NameKey nameKey = AccountGroup.nameKey(newGroupName);
     assertThat(groupCache.get(nameKey)).isEmpty();
     gApi.groups().create(newGroupName);
     assertThat(groupCache.get(nameKey)).isPresent();
@@ -231,8 +227,9 @@
 
   @Test
   public void addNonExistingMember_UnprocessableEntity() throws Exception {
-    exception.expect(UnprocessableEntityException.class);
-    gApi.groups().id("Administrators").addMembers("non-existing");
+    assertThrows(
+        UnprocessableEntityException.class,
+        () -> gApi.groups().id("Administrators").addMembers("non-existing"));
   }
 
   @Test
@@ -351,9 +348,9 @@
   public void createDuplicateInternalGroupCaseSensitiveName_Conflict() throws Exception {
     String dupGroupName = name("dupGroup");
     gApi.groups().create(dupGroupName);
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("group '" + dupGroupName + "' already exists");
-    gApi.groups().create(dupGroupName);
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.groups().create(dupGroupName));
+    assertThat(thrown).hasMessageThat().contains("group '" + dupGroupName + "' already exists");
   }
 
   @Test
@@ -369,33 +366,71 @@
   @Test
   public void createDuplicateSystemGroupCaseSensitiveName_Conflict() throws Exception {
     String newGroupName = "Registered Users";
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("group 'Registered Users' already exists");
-    gApi.groups().create(newGroupName);
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.groups().create(newGroupName));
+    assertThat(thrown).hasMessageThat().contains("group 'Registered Users' already exists");
   }
 
   @Test
   public void createDuplicateSystemGroupCaseInsensitiveName_Conflict() throws Exception {
     String newGroupName = "registered users";
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("group 'Registered Users' already exists");
-    gApi.groups().create(newGroupName);
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.groups().create(newGroupName));
+    assertThat(thrown).hasMessageThat().contains("group 'Registered Users' already exists");
   }
 
   @Test
   @GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
   public void createGroupWithConfiguredNameOfSystemGroup_Conflict() throws Exception {
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("group 'All Users' already exists");
-    gApi.groups().create("all users");
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.groups().create("all users"));
+    assertThat(thrown).hasMessageThat().contains("group 'All Users' already exists");
   }
 
   @Test
   @GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
   public void createGroupWithDefaultNameOfSystemGroup_Conflict() throws Exception {
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("group name 'Anonymous Users' is reserved");
-    gApi.groups().create("anonymous users");
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.groups().create("anonymous users"));
+    assertThat(thrown).hasMessageThat().contains("group name 'Anonymous Users' is reserved");
+  }
+
+  @Test
+  public void createGroupWithUuid() throws Exception {
+    AccountGroup.UUID uuid = AccountGroup.UUID.parse("4eb25d1cca562f53b9356117f33840706a36a349");
+    GroupInput input = new GroupInput();
+    input.uuid = uuid.get();
+    input.name = name("new-group");
+    GroupInfo info = gApi.groups().create(input).get();
+    assertThat(info.name).isEqualTo(input.name);
+    assertThat(info.id).isEqualTo(input.uuid);
+  }
+
+  @Test
+  public void createGroupWithExistingUuid_Conflict() throws Exception {
+    GroupInfo existingGroup = gApi.groups().create(name("new-group")).get();
+    GroupInput input = new GroupInput();
+    input.uuid = existingGroup.id;
+    input.name = name("another-new-group");
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.groups().create(input).get());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(String.format("group with UUID '%s' already exists", input.uuid));
+  }
+
+  @Test
+  public void createGroupWithInvalidUuid_BadRequest() throws Exception {
+    AccountGroup.UUID uuid = AccountGroup.UUID.parse("foo:bar");
+    GroupInput input = new GroupInput();
+    input.uuid = uuid.get();
+    input.name = name("new-group");
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> gApi.groups().create(input).get());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(String.format("invalid group UUID '%s'", input.uuid));
   }
 
   @Test
@@ -414,8 +449,7 @@
   @Test
   public void createGroupWithoutCapability_Forbidden() throws Exception {
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.groups().create(name("newGroup"));
+    assertThrows(AuthException.class, () -> gApi.groups().create(name("newGroup")));
   }
 
   @Test
@@ -441,7 +475,7 @@
     GroupInfo group = gApi.groups().create(groupInput).get();
 
     Collection<AccountGroup.UUID> groups = groupIncludeCache.getGroupsWithMember(accountId);
-    assertThat(groups).containsExactly(new AccountGroup.UUID(group.id));
+    assertThat(groups).containsExactly(AccountGroup.uuid(group.id));
   }
 
   @Test
@@ -481,8 +515,7 @@
   @Test
   @GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
   public void getSystemGroupByDefaultName_NotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.groups().id("Anonymous-Users").get();
+    assertThrows(ResourceNotFoundException.class, () -> gApi.groups().id("Anonymous-Users").get());
   }
 
   @Test
@@ -498,8 +531,7 @@
     String name = name("Users");
     gApi.groups().create(name).get();
 
-    exception.expect(ResourceConflictException.class);
-    gApi.groups().create(name);
+    assertThrows(ResourceConflictException.class, () -> gApi.groups().create(name));
   }
 
   @Test
@@ -528,9 +560,7 @@
 
     String name2 = name("Name2");
     gApi.groups().create(name2);
-
-    exception.expect(ResourceConflictException.class);
-    gApi.groups().id(group1.id).name(name2);
+    assertThrows(ResourceConflictException.class, () -> gApi.groups().id(group1.id).name(name2));
   }
 
   @Test
@@ -554,8 +584,7 @@
     gApi.groups().id(group.id).name(newName);
 
     assertGroupDoesNotExist(name);
-    exception.expect(ResourceNotFoundException.class);
-    gApi.groups().id(name).get();
+    assertThrows(ResourceNotFoundException.class, () -> gApi.groups().id(name).get());
   }
 
   @Test
@@ -626,14 +655,15 @@
     assertThat(Url.decode(gApi.groups().id(name).owner().id)).isEqualTo(adminUUID);
 
     // set non existing owner
-    exception.expect(UnprocessableEntityException.class);
-    gApi.groups().id(name).owner("Non-Existing Group");
+    assertThrows(
+        UnprocessableEntityException.class,
+        () -> gApi.groups().id(name).owner("Non-Existing Group"));
   }
 
   @Test
   public void listNonExistingGroupIncludes_NotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.groups().id("non-existing").includedGroups();
+    assertThrows(
+        ResourceNotFoundException.class, () -> gApi.groups().id("non-existing").includedGroups());
   }
 
   @Test
@@ -645,8 +675,9 @@
   @Test
   public void includeNonExistingGroup() throws Exception {
     AccountGroup.UUID gx = groupOperations.newGroup().create();
-    exception.expect(UnprocessableEntityException.class);
-    gApi.groups().id(gx.get()).addGroups("non-existing");
+    assertThrows(
+        UnprocessableEntityException.class,
+        () -> gApi.groups().id(gx.get()).addGroups("non-existing"));
   }
 
   @Test
@@ -675,8 +706,7 @@
 
   @Test
   public void listNonExistingGroupMembers_NotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.groups().id("non-existing").members();
+    assertThrows(ResourceNotFoundException.class, () -> gApi.groups().id("non-existing").members());
   }
 
   @Test
@@ -806,13 +836,13 @@
 
     // By UUID
     List<GroupInfo> owned = gApi.groups().list().withOwnedBy(parent.get()).get();
-    assertThat(owned.stream().map(g -> new AccountGroup.UUID(g.id)).collect(toList()))
+    assertThat(owned.stream().map(g -> AccountGroup.uuid(g.id)).collect(toList()))
         .containsExactlyElementsIn(children);
 
     // By name
     String parentName = groupOperations.group(parent).get().name();
     owned = gApi.groups().list().withOwnedBy(parentName).get();
-    assertThat(owned.stream().map(g -> new AccountGroup.UUID(g.id)).collect(toList()))
+    assertThat(owned.stream().map(g -> AccountGroup.uuid(g.id)).collect(toList()))
         .containsExactlyElementsIn(children);
 
     // By group that does not own any others
@@ -820,9 +850,11 @@
     assertThat(owned).isEmpty();
 
     // By non-existing group
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("Group Not Found: does-not-exist");
-    gApi.groups().list().withOwnedBy("does-not-exist").get();
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.groups().list().withOwnedBy("does-not-exist").get());
+    assertThat(thrown).hasMessageThat().contains("Group Not Found: does-not-exist");
   }
 
   @Test
@@ -984,7 +1016,7 @@
   }
 
   private void deleteGroupRef(String groupId) throws Exception {
-    AccountGroup.UUID uuid = new AccountGroup.UUID(groupId);
+    AccountGroup.UUID uuid = AccountGroup.uuid(groupId);
     try (Repository repo = repoManager.openRepository(allUsers)) {
       RefUpdate ru = repo.updateRef(RefNames.refsGroups(uuid));
       ru.setForceUpdate(true);
@@ -996,11 +1028,7 @@
     gApi.groups().id(uuid.get()).index();
 
     // Verify "sub-group" has been deleted.
-    try {
-      gApi.groups().id(uuid.get()).get();
-      fail("expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-    }
+    assertThrows(ResourceNotFoundException.class, () -> gApi.groups().id(uuid.get()).get());
   }
 
   // reindex is tested by {@link AbstractQueryGroupsTest#reindex}
@@ -1023,9 +1051,9 @@
 
     // user cannot reindex any group
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to index group");
-    gApi.groups().id(group.id).index();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.groups().id(group.id).index());
+    assertThat(thrown).hasMessageThat().contains("not allowed to index group");
   }
 
   @Test
@@ -1037,10 +1065,12 @@
   @Test
   public void pushToDeletedGroupBranchIsRejectedForAllUsersRepo() throws Exception {
     // refs/deleted-groups is only visible with ACCESS_DATABASE
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     String groupRef =
-        RefNames.refsDeletedGroups(
-            new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
+        RefNames.refsDeletedGroups(AccountGroup.uuid(gApi.groups().create(name("foo")).get().id));
     createBranch(allUsers, groupRef);
     assertPushToGroupBranch(allUsers, groupRef, "group update not allowed");
   }
@@ -1048,7 +1078,10 @@
   @Test
   public void pushToGroupNamesBranchIsRejectedForAllUsersRepo() throws Exception {
     // refs/meta/group-names isn't usually available for fetch, so grant ACCESS_DATABASE
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     assertPushToGroupBranch(allUsers, RefNames.REFS_GROUPNAMES, "group update not allowed");
   }
 
@@ -1056,7 +1089,7 @@
   public void pushToGroupsBranchForNonAllUsersRepo() throws Exception {
     assertCreateGroupBranch(project);
     String groupRef =
-        RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
+        RefNames.refsGroups(AccountGroup.uuid(gApi.groups().create(name("foo")).get().id));
     createBranch(project, groupRef);
     assertPushToGroupBranch(project, groupRef, null);
   }
@@ -1065,8 +1098,7 @@
   public void pushToDeletedGroupsBranchForNonAllUsersRepo() throws Exception {
     assertCreateGroupBranch(project);
     String groupRef =
-        RefNames.refsDeletedGroups(
-            new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
+        RefNames.refsDeletedGroups(AccountGroup.uuid(gApi.groups().create(name("foo")).get().id));
     createBranch(project, groupRef);
     assertPushToGroupBranch(project, groupRef, null);
   }
@@ -1079,15 +1111,18 @@
 
   private void assertPushToGroupBranch(
       Project.NameKey project, String groupRefName, String expectedErrorOnUpdate) throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.allow(cfg, Permission.CREATE, REGISTERED_USERS, RefNames.REFS_GROUPS + "*");
-      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_GROUPS + "*");
-      Util.allow(cfg, Permission.CREATE, REGISTERED_USERS, RefNames.REFS_DELETED_GROUPS + "*");
-      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_DELETED_GROUPS + "*");
-      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_GROUPNAMES);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .add(
+            allow(Permission.CREATE)
+                .ref(RefNames.REFS_DELETED_GROUPS + "*")
+                .group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_DELETED_GROUPS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPNAMES).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> repo = cloneProject(project);
 
@@ -1106,12 +1141,12 @@
   }
 
   private void assertCreateGroupBranch(Project.NameKey project) throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.allow(cfg, Permission.CREATE, REGISTERED_USERS, RefNames.REFS_GROUPS + "*");
-      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_GROUPS + "*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
     TestRepository<InMemoryRepository> repo = cloneProject(project);
     PushOneCommit.Result r =
         pushFactory
@@ -1122,13 +1157,13 @@
   }
 
   @Test
-  public void pushToGroupBranchForReviewForAllUsersRepoIsRejectedOnSubmit() throws Exception {
+  public void pushToGroupBranchForReviewForAllUsersRepoIsRejectedOnSubmit() throws Throwable {
     pushToGroupBranchForReviewAndSubmit(
         allUsers, RefNames.refsGroups(adminGroupUuid()), "group update not allowed");
   }
 
   @Test
-  public void pushToGroupBranchForReviewForNonAllUsersRepoAndSubmit() throws Exception {
+  public void pushToGroupBranchForReviewForNonAllUsersRepoAndSubmit() throws Throwable {
     String groupRef = RefNames.refsGroups(adminGroupUuid());
     createBranch(project, groupRef);
     pushToGroupBranchForReviewAndSubmit(project, groupRef, null);
@@ -1162,14 +1197,14 @@
   @Test
   public void cannotCreateGroupBranch() throws Exception {
     testCannotCreateGroupBranch(
-        RefNames.REFS_GROUPS + "*", RefNames.refsGroups(new AccountGroup.UUID(name("foo"))));
+        RefNames.REFS_GROUPS + "*", RefNames.refsGroups(AccountGroup.uuid(name("foo"))));
   }
 
   @Test
   public void cannotCreateDeletedGroupBranch() throws Exception {
     testCannotCreateGroupBranch(
         RefNames.REFS_DELETED_GROUPS + "*",
-        RefNames.refsDeletedGroups(new AccountGroup.UUID(name("foo"))));
+        RefNames.refsDeletedGroups(AccountGroup.uuid(name("foo"))));
   }
 
   @Test
@@ -1192,15 +1227,22 @@
       }
 
       // refs/meta/group-names is only visible with ACCESS_DATABASE
-      allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+      projectOperations
+          .allProjectsForUpdate()
+          .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+          .update();
 
       testCannotCreateGroupBranch(RefNames.REFS_GROUPNAMES, RefNames.REFS_GROUPNAMES);
     }
   }
 
   private void testCannotCreateGroupBranch(String refPattern, String groupRef) throws Exception {
-    grant(allUsers, refPattern, Permission.CREATE);
-    grant(allUsers, refPattern, Permission.PUSH);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(refPattern).group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(refPattern).group(adminGroupUuid()))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(groupRef);
@@ -1220,7 +1262,10 @@
   @Test
   public void cannotDeleteDeletedGroupBranch() throws Exception {
     // refs/deleted-groups is only visible with ACCESS_DATABASE
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     String groupRef = RefNames.refsDeletedGroups(AccountGroup.uuid(name("foo")));
     createBranch(allUsers, groupRef);
     testCannotDeleteGroupBranch(RefNames.REFS_DELETED_GROUPS + "*", groupRef);
@@ -1229,13 +1274,20 @@
   @Test
   public void cannotDeleteGroupNamesBranch() throws Exception {
     // refs/meta/group-names is only visible with ACCESS_DATABASE
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     testCannotDeleteGroupBranch(RefNames.REFS_GROUPNAMES, RefNames.REFS_GROUPNAMES);
   }
 
   private void testCannotDeleteGroupBranch(String refPattern, String groupRef) throws Exception {
-    grant(allUsers, refPattern, Permission.DELETE, true, REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.DELETE).ref(refPattern).group(REGISTERED_USERS).force(true))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     PushResult r = deleteRef(allUsersRepo, groupRef);
@@ -1259,7 +1311,7 @@
   public void stalenessChecker() throws Exception {
     // Newly created group is not stale
     GroupInfo groupInfo = gApi.groups().create(name("foo")).get();
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID(groupInfo.id);
+    AccountGroup.UUID groupUuid = AccountGroup.uuid(groupInfo.id);
     assertThat(stalenessChecker.isStale(groupUuid)).isFalse();
 
     // Manual update makes index document stale
@@ -1329,9 +1381,7 @@
     restartAsSlave();
 
     GroupIndexedCounter groupIndexedCounter = new GroupIndexedCounter();
-    RegistrationHandle groupIndexEventCounterHandle =
-        groupIndexedListeners.add("gerrit", groupIndexedCounter);
-    try {
+    try (Registration registration = extensionRegistry.newRegistration().add(groupIndexedCounter)) {
       // Running the reindexer right after startup should not need to reindex any group since
       // reindexing was already done on startup.
       slaveGroupIndexer.run();
@@ -1340,12 +1390,12 @@
       // Create a group without updating the cache or index,
       // then run the reindexer -> only the new group is reindexed.
       String groupName = "foo";
-      AccountGroup.UUID groupUuid = new AccountGroup.UUID(groupName + "-UUID");
+      AccountGroup.UUID groupUuid = AccountGroup.uuid(groupName + "-UUID");
       groupsUpdate.createGroupInNoteDb(
           InternalGroupCreation.builder()
               .setGroupUUID(groupUuid)
-              .setNameKey(new AccountGroup.NameKey(groupName))
-              .setId(new AccountGroup.Id(seq.nextGroupId()))
+              .setNameKey(AccountGroup.nameKey(groupName))
+              .setId(AccountGroup.id(seq.nextGroupId()))
               .build(),
           InternalGroupUpdate.builder().build());
       slaveGroupIndexer.run();
@@ -1367,8 +1417,6 @@
       }
       slaveGroupIndexer.run();
       groupIndexedCounter.assertReindexOf(groupUuid);
-    } finally {
-      groupIndexEventCounterHandle.remove();
     }
   }
 
@@ -1386,25 +1434,18 @@
     restartAsSlave();
 
     GroupIndexedCounter groupIndexedCounter = new GroupIndexedCounter();
-    RegistrationHandle groupIndexEventCounterHandle =
-        groupIndexedListeners.add("gerrit", groupIndexedCounter);
-    try {
+    try (Registration registration = extensionRegistry.newRegistration().add(groupIndexedCounter)) {
       // No group indexing happened on startup. All groups should be reindexed now.
       slaveGroupIndexer.run();
       groupIndexedCounter.assertReindexOf(expectedGroups);
-    } finally {
-      groupIndexEventCounterHandle.remove();
     }
   }
 
   private static Correspondence<AccountInfo, String> getAccountToUsernameCorrespondence() {
     return Correspondence.from(
-        new BinaryPredicate<AccountInfo, String>() {
-          @Override
-          public boolean apply(AccountInfo actualAccount, String expectedName) {
-            String username = actualAccount == null ? null : actualAccount.username;
-            return Objects.equals(username, expectedName);
-          }
+        (actualAccount, expectedName) -> {
+          String username = actualAccount == null ? null : actualAccount.username;
+          return Objects.equals(username, expectedName);
         },
         "has username");
   }
@@ -1420,10 +1461,17 @@
   }
 
   private void pushToGroupBranchForReviewAndSubmit(
-      Project.NameKey project, String groupRef, String expectedError) throws Exception {
-    grantLabel(
-        "Code-Review", -2, 2, project, RefNames.REFS_GROUPS + "*", false, REGISTERED_USERS, false);
-    grant(project, RefNames.REFS_GROUPS + "*", Permission.SUBMIT, false, REGISTERED_USERS);
+      Project.NameKey project, String groupRef, String expectedError) throws Throwable {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel("Code-Review")
+                .ref(RefNames.REFS_GROUPS + "*")
+                .group(REGISTERED_USERS)
+                .range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> repo = cloneProject(project);
     fetch(repo, groupRef + ":groupRef");
@@ -1434,14 +1482,16 @@
             .create(admin.newIdent(), repo, "Update group config", "group.config", "some content")
             .to(MagicBranch.NEW_CHANGE + groupRef);
     r.assertOkStatus();
-    assertThat(r.getChange().change().getDest().get()).isEqualTo(groupRef);
+    assertThat(r.getChange().change().getDest().branch()).isEqualTo(groupRef);
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
 
+    ThrowingRunnable submit = () -> gApi.changes().id(r.getChangeId()).current().submit();
     if (expectedError != null) {
-      exception.expect(ResourceConflictException.class);
-      exception.expectMessage("group update not allowed");
+      Throwable thrown = assertThrows(ResourceConflictException.class, submit);
+      assertThat(thrown).hasMessageThat().contains("group update not allowed");
+    } else {
+      submit.run();
     }
-    gApi.changes().id(r.getChangeId()).current().submit();
   }
 
   private void createBranch(Project.NameKey project, String ref) throws IOException {
@@ -1518,16 +1568,11 @@
   private static void assertIncludes(List<GroupInfo> includes, String... expectedNames) {
     List<String> names = includes.stream().map(i -> i.name).collect(toImmutableList());
     assertThat(names).containsExactlyElementsIn(Arrays.asList(expectedNames));
-    assertThat(names).isOrdered();
+    assertThat(names).isInOrder();
   }
 
   private void assertBadRequest(ListRequest req) throws Exception {
-    try {
-      req.get();
-      fail("Expected BadRequestException");
-    } catch (BadRequestException e) {
-      // Expected
-    }
+    assertThrows(BadRequestException.class, () -> req.get());
   }
 
   @Target({METHOD})
@@ -1547,24 +1592,18 @@
       countsByGroup.clear();
     }
 
-    long getCount(AccountGroup.UUID groupUuid) {
-      return countsByGroup.get(groupUuid.get());
-    }
-
     void assertReindexOf(AccountGroup.UUID groupUuid) {
       assertReindexOf(ImmutableList.of(groupUuid));
     }
 
     void assertReindexOf(List<AccountGroup.UUID> groupUuids) {
-      for (AccountGroup.UUID groupUuid : groupUuids) {
-        assertThat(getCount(groupUuid)).named(groupUuid.get()).isEqualTo(1);
-      }
-      assertThat(countsByGroup).hasSize(groupUuids.size());
+      Map<String, Long> expected = groupUuids.stream().collect(toMap(u -> u.get(), u -> 1L));
+      assertThat(countsByGroup.asMap()).containsExactlyEntriesIn(expected);
       clear();
     }
 
     void assertNoReindex() {
-      assertThat(countsByGroup).isEmpty();
+      assertThat(countsByGroup.asMap()).isEmpty();
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
index 5e143c0..6fcca8c 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
@@ -15,13 +15,14 @@
 package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth8.assertThat;
+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.exceptions.NoSuchGroupException;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.group.db.GroupsUpdate;
@@ -36,12 +37,9 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
 public class GroupsUpdateIT {
   @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
   @Inject @ServerInitiated private Provider<GroupsUpdate> groupsUpdateProvider;
   @Inject private Groups groups;
 
@@ -56,7 +54,7 @@
     createGroup(groupCreation, groupUpdate);
 
     Stream<String> allGroupNames = getAllGroupNames();
-    assertThat(allGroupNames).containsAllOf("users", "verifiers");
+    assertThat(allGroupNames).containsAtLeast("users", "verifiers");
   }
 
   @Test
@@ -65,23 +63,23 @@
 
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
-            .setName(new AccountGroup.NameKey("contributors"))
+            .setName(AccountGroup.nameKey("contributors"))
             .setMemberModification(
                 new CreateAnotherGroupOnceAsSideEffectOfMemberModification("verifiers"))
             .build();
-    updateGroup(new AccountGroup.UUID("users-UUID"), groupUpdate);
+    updateGroup(AccountGroup.uuid("users-UUID"), groupUpdate);
 
     Stream<String> allGroupNames = getAllGroupNames();
-    assertThat(allGroupNames).containsAllOf("contributors", "verifiers");
+    assertThat(allGroupNames).containsAtLeast("contributors", "verifiers");
   }
 
   @Test
   public void groupUpdateFailsWithExceptionForNotExistingGroup() throws Exception {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder().setDescription("A description for the group").build();
-
-    expectedException.expect(NoSuchGroupException.class);
-    updateGroup(new AccountGroup.UUID("nonexistent-group-UUID"), groupUpdate);
+    assertThrows(
+        NoSuchGroupException.class,
+        () -> updateGroup(AccountGroup.uuid("nonexistent-group-UUID"), groupUpdate));
   }
 
   private void createGroup(String groupName, String groupUuid) throws Exception {
@@ -107,9 +105,9 @@
 
   private static InternalGroupCreation getGroupCreation(String groupName, String groupUuid) {
     return InternalGroupCreation.builder()
-        .setGroupUUID(new AccountGroup.UUID(groupUuid))
-        .setNameKey(new AccountGroup.NameKey(groupName))
-        .setId(new AccountGroup.Id(Math.abs(groupName.hashCode())))
+        .setGroupUUID(AccountGroup.uuid(groupUuid))
+        .setNameKey(AccountGroup.nameKey(groupName))
+        .setId(AccountGroup.id(Math.abs(groupName.hashCode())))
         .build();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 2e455528..67da084 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.api.plugin;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
 
@@ -34,6 +35,7 @@
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 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;
@@ -64,6 +66,7 @@
           "plugin_e.js");
 
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private MandatoryPluginsCollection mandatoryPluginsCollection;
 
   @Test
   @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
@@ -111,7 +114,13 @@
     assertBadRequest(list().regex(".*in-b").prefix("a"));
     assertBadRequest(list().substring(".*in-b").prefix("a"));
 
-    // Disable
+    // Disable mandatory
+    mandatoryPluginsCollection.add("plugin_e");
+    assertThrows(MethodNotAllowedException.class, () -> gApi.plugins().name("plugin_e").disable());
+    api = gApi.plugins().name("plugin_e");
+    assertThat(api.get().disabled).isNull();
+
+    // Disable non-mandatory
     api = gApi.plugins().name("plugin-a");
     api.disable();
     api = gApi.plugins().name("plugin-a");
@@ -130,12 +139,7 @@
 
     // Non-admin cannot disable
     requestScopeOperations.setApiUser(user.id());
-    try {
-      gApi.plugins().name("plugin-a").disable();
-      fail("Expected AuthException");
-    } catch (AuthException expected) {
-      // Expected
-    }
+    assertThrows(AuthException.class, () -> gApi.plugins().name("plugin-a").disable());
   }
 
   @SuppressWarnings("deprecation")
@@ -149,15 +153,16 @@
 
   @Test
   public void installNotAllowed() throws Exception {
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("remote plugin administration is disabled");
-    gApi.plugins().install("test.js", new InstallPluginInput());
+    MethodNotAllowedException thrown =
+        assertThrows(
+            MethodNotAllowedException.class,
+            () -> gApi.plugins().install("test.js", new InstallPluginInput()));
+    assertThat(thrown).hasMessageThat().contains("remote plugin administration is disabled");
   }
 
   @Test
   public void getNonExistingThrowsNotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.plugins().name("does-not-exist");
+    assertThrows(ResourceNotFoundException.class, () -> gApi.plugins().name("does-not-exist"));
   }
 
   private ListRequest list() throws RestApiException {
@@ -223,11 +228,6 @@
   }
 
   private void assertBadRequest(ListRequest req) throws Exception {
-    try {
-      req.get();
-      fail("Expected BadRequestException");
-    } catch (BadRequestException e) {
-      // Expected
-    }
+    assertThrows(BadRequestException.class, () -> req.get());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginLoaderIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginLoaderIT.java
new file mode 100644
index 0000000..7eb3680
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginLoaderIT.java
@@ -0,0 +1,42 @@
+// 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.api.plugin;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.server.plugins.MissingMandatoryPluginsException;
+import org.junit.Test;
+import org.junit.runner.Description;
+
+@NoHttpd
+public class PluginLoaderIT extends AbstractDaemonTest {
+
+  Description testDescription;
+
+  @Override
+  protected void beforeTest(Description description) throws Exception {
+    this.testDescription = description;
+  }
+
+  @Override
+  protected void afterTest() throws Exception {}
+
+  @Test(expected = MissingMandatoryPluginsException.class)
+  @GerritConfig(name = "plugins.mandatory", value = "my-mandatory-plugin")
+  public void shouldFailToStartGerritWhenMandatoryPluginsAreMissing() throws Exception {
+    super.beforeTest(testDescription);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 8cdd2f66..b8c1818 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -15,6 +15,11 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -23,17 +28,15 @@
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
 import com.google.gerrit.extensions.api.config.AccessCheckInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -62,36 +65,41 @@
     privilegedUser = accountCreator.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden");
     groupOperations.group(privilegedGroupUuid).forUpdate().addMember(privilegedUser.id()).update();
 
-    try (ProjectConfigUpdate u = updateProject(secretProject)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.allow(cfg, Permission.READ, privilegedGroupUuid, "refs/*");
-      Util.block(cfg, Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(secretProject)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(privilegedGroupUuid))
+        .add(block(Permission.READ).ref("refs/*").group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
 
-    try (ProjectConfigUpdate u = updateProject(secretRefProject)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.deny(cfg, Permission.READ, SystemGroupBackend.ANONYMOUS_USERS, "refs/*");
-      Util.allow(cfg, Permission.READ, privilegedGroupUuid, "refs/heads/secret/*");
-      Util.block(cfg, Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/heads/secret/*");
-      Util.allow(cfg, Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(secretRefProject)
+        .forUpdate()
+        .add(deny(Permission.READ).ref("refs/*").group(SystemGroupBackend.ANONYMOUS_USERS))
+        .add(allow(Permission.READ).ref("refs/heads/secret/*").group(privilegedGroupUuid))
+        .add(
+            block(Permission.READ)
+                .ref("refs/heads/secret/*")
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .add(allow(Permission.READ).ref("refs/heads/*").group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
 
     // Ref permission
-    try (ProjectConfigUpdate u = updateProject(normalProject)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.allow(cfg, Permission.VIEW_PRIVATE_CHANGES, privilegedGroupUuid, "refs/*");
-      Util.allow(cfg, Permission.FORGE_SERVER, privilegedGroupUuid, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(normalProject)
+        .forUpdate()
+        .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(privilegedGroupUuid))
+        .add(allow(Permission.FORGE_SERVER).ref("refs/*").group(privilegedGroupUuid))
+        .update();
   }
 
   @Test
   public void emptyInput() throws Exception {
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("input requires 'account'");
-    gApi.projects().name(normalProject.get()).checkAccess(new AccessCheckInput());
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(normalProject.get()).checkAccess(new AccessCheckInput()));
+    assertThat(thrown).hasMessageThat().contains("input requires 'account'");
   }
 
   @Test
@@ -101,9 +109,11 @@
     in.permission = "notapermission";
     in.ref = "refs/heads/master";
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("not recognized");
-    gApi.projects().name(normalProject.get()).checkAccess(in);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(normalProject.get()).checkAccess(in));
+    assertThat(thrown).hasMessageThat().contains("not recognized");
   }
 
   @Test
@@ -112,9 +122,11 @@
     in.account = user.email();
     in.permission = "forge_author";
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("must set 'ref'");
-    gApi.projects().name(normalProject.get()).checkAccess(in);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(normalProject.get()).checkAccess(in));
+    assertThat(thrown).hasMessageThat().contains("must set 'ref'");
   }
 
   @Test
@@ -124,9 +136,11 @@
     in.permission = "rebase";
     in.ref = "refs/heads/master";
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("recognized as ref permission");
-    gApi.projects().name(normalProject.get()).checkAccess(in);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(normalProject.get()).checkAccess(in));
+    assertThat(thrown).hasMessageThat().contains("recognized as ref permission");
   }
 
   @Test
@@ -136,9 +150,11 @@
     in.permission = "rebase";
     in.ref = "refs/heads/master";
 
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("Account 'doesnotexist@invalid.com' not found");
-    gApi.projects().name(normalProject.get()).checkAccess(in);
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.projects().name(normalProject.get()).checkAccess(in));
+    assertThat(thrown).hasMessageThat().contains("Account 'doesnotexist@invalid.com' not found");
   }
 
   private static class TestCase {
@@ -231,13 +247,16 @@
       try {
         info = gApi.projects().name(tc.project).checkAccess(tc.input);
       } catch (RestApiException e) {
-        fail(String.format("check.access(%s, %s): exception %s", tc.project, in, e));
+        assertWithMessage(String.format("check.access(%s, %s): exception %s", tc.project, in, e))
+            .fail();
       }
 
       int want = tc.want;
       if (want != info.status) {
-        fail(
-            String.format("check.access(%s, %s) = %d, want %d", tc.project, in, info.status, want));
+        assertWithMessage(
+                String.format(
+                    "check.access(%s, %s) = %d, want %d", tc.project, in, info.status, want))
+            .fail();
       }
 
       switch (want) {
@@ -253,7 +272,7 @@
           assertThat(info.message).isNull();
           break;
         default:
-          fail(String.format("unknown code %d", want));
+          assertWithMessage(String.format("unknown code %d", want)).fail();
       }
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
index 388ea30..27dd16a 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
@@ -50,7 +51,7 @@
   @Test
   public void noProblem() throws Exception {
     PushOneCommit.Result r = createChange("refs/for/master");
-    String branch = r.getChange().change().getDest().get();
+    String branch = r.getChange().change().getDest().branch();
 
     ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
     assertThat(info.status).isEqualTo(ChangeStatus.NEW);
@@ -115,7 +116,7 @@
   @Test
   public void detectAutoCloseableChangeByChangeId() throws Exception {
     PushOneCommit.Result r = createChange("refs/for/master");
-    String branch = r.getChange().change().getDest().get();
+    String branch = r.getChange().change().getDest().branch();
 
     RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
     serverSideTestRepo.branch(branch).update(amendedCommit);
@@ -138,7 +139,7 @@
   @Test
   public void fixAutoCloseableChangeByChangeId() throws Exception {
     PushOneCommit.Result r = createChange("refs/for/master");
-    String branch = r.getChange().change().getDest().get();
+    String branch = r.getChange().change().getDest().branch();
 
     RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
     serverSideTestRepo.branch(branch).update(amendedCommit);
@@ -162,7 +163,7 @@
   @Test
   public void maxCommits() throws Exception {
     PushOneCommit.Result r = createChange("refs/for/master");
-    String branch = r.getChange().change().getDest().get();
+    String branch = r.getChange().change().getDest().branch();
 
     RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
     serverSideTestRepo.branch(branch).update(amendedCommit);
@@ -196,7 +197,7 @@
   @Test
   public void skipCommits() throws Exception {
     PushOneCommit.Result r = createChange("refs/for/master");
-    String branch = r.getChange().change().getDest().get();
+    String branch = r.getChange().change().getDest().branch();
 
     RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
     serverSideTestRepo.branch(branch).update(amendedCommit);
@@ -232,18 +233,21 @@
     CheckProjectInput input = new CheckProjectInput();
     input.autoCloseableChangesCheck = new AutoCloseableChangesCheckInput();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("branch is required");
-    gApi.projects().name(project.get()).check(input);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> gApi.projects().name(project.get()).check(input));
+    assertThat(thrown).hasMessageThat().contains("branch is required");
   }
 
   @Test
   public void nonExistingBranch() throws Exception {
     CheckProjectInput input = checkProjectInputForAutoCloseableCheck("non-existing");
 
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("branch 'non-existing' not found");
-    gApi.projects().name(project.get()).check(input);
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.projects().name(project.get()).check(input));
+    assertThat(thrown).hasMessageThat().contains("branch 'non-existing' not found");
   }
 
   @Test
@@ -266,11 +270,14 @@
     input.autoCloseableChangesCheck.maxCommits =
         ProjectsConsistencyChecker.AUTO_CLOSE_MAX_COMMITS_LIMIT + 1;
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        "max commits can at most be set to "
-            + ProjectsConsistencyChecker.AUTO_CLOSE_MAX_COMMITS_LIMIT);
-    gApi.projects().name(project.get()).check(input);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> gApi.projects().name(project.get()).check(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "max commits can at most be set to "
+                + ProjectsConsistencyChecker.AUTO_CLOSE_MAX_COMMITS_LIMIT);
   }
 
   private RevCommit pushCommitWithoutChangeIdForReview() throws Exception {
@@ -280,10 +287,12 @@
             .branch("HEAD")
             .commit()
             .message("A change")
+            .insertChangeId()
             .author(admin.newIdent())
             .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()))
             .create();
     pushHead(testRepo, "refs/for/master");
+
     return commit;
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
index a341b3c..c7955a56 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
@@ -15,6 +15,10 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.stream.Collectors.toList;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
@@ -22,7 +26,10 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.IncludedInInfo;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -33,7 +40,7 @@
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.RevisionInfo;
-import com.google.gerrit.reviewdb.client.Branch;
+import com.google.inject.Inject;
 import java.util.Iterator;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
@@ -42,6 +49,8 @@
 
 @NoHttpd
 public class CommitIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void getCommitInfo() throws Exception {
     Result result = createChange();
@@ -75,22 +84,93 @@
     assertThat(getIncludedIn(result.getCommit().getId()).branches).containsExactly("master");
     assertThat(getIncludedIn(result.getCommit().getId()).tags).isEmpty();
 
-    grant(project, R_TAGS + "*", Permission.CREATE_TAG);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE_TAG).ref(R_TAGS + "*").group(adminGroupUuid()))
+        .update();
     gApi.projects().name(result.getChange().project().get()).tag("test-tag").create(new TagInput());
 
     assertThat(getIncludedIn(result.getCommit().getId()).tags).containsExactly("test-tag");
 
-    createBranch(new Branch.NameKey(project.get(), "test-branch"));
+    createBranch(BranchNameKey.create(project, "test-branch"));
 
     assertThat(getIncludedIn(result.getCommit().getId()).branches)
         .containsExactly("master", "test-branch");
   }
 
   @Test
+  public void includedInMergedChange_filtersOutNonVisibleBranches() throws Exception {
+    Result baseChange = createAndSubmitChange("refs/for/master");
+
+    createBranch(BranchNameKey.create(project, "test-branch-1"));
+    createBranch(BranchNameKey.create(project, "test-branch-2"));
+    createAndSubmitChange("refs/for/test-branch-1");
+    createAndSubmitChange("refs/for/test-branch-2");
+
+    assertThat(getIncludedIn(baseChange.getCommit().getId()).branches)
+        .containsExactly("master", "test-branch-1", "test-branch-2");
+
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/test-branch-1").group(REGISTERED_USERS))
+        .update();
+
+    assertThat(getIncludedIn(baseChange.getCommit().getId()).branches)
+        .containsExactly("master", "test-branch-2");
+  }
+
+  @Test
+  public void includedInMergedChange_filtersOutNonVisibleTags() throws Exception {
+    String tagBase = "tag_base";
+    String tagBranch1 = "tag_1";
+
+    Result baseChange = createAndSubmitChange("refs/for/master");
+    createLightWeightTag(tagBase);
+    assertThat(getIncludedIn(baseChange.getCommit().getId()).tags).containsExactly(tagBase);
+
+    createBranch(BranchNameKey.create(project, "test-branch-1"));
+    createAndSubmitChange("refs/for/test-branch-1");
+    createLightWeightTag(tagBranch1);
+    assertThat(getIncludedIn(baseChange.getCommit().getId()).tags)
+        .containsExactly(tagBase, tagBranch1);
+
+    projectOperations
+        .project(project)
+        .forUpdate()
+        // Tag permissions are controlled by read permissions on branches. Blocking read permission
+        // on test-branch-1 so that tagBranch1 becomes non-visible
+        .add(block(Permission.READ).ref("refs/heads/test-branch-1").group(REGISTERED_USERS))
+        .update();
+    assertThat(getIncludedIn(baseChange.getCommit().getId()).tags).containsExactly(tagBase);
+  }
+
+  @Test
+  public void cherryPickWithoutMessage() throws Exception {
+    String branch = "foo";
+
+    // Create change to cherry-pick
+    RevCommit revCommit = createChange().getCommit();
+
+    // Create target branch to cherry-pick to.
+    gApi.projects().name(project.get()).branch(branch).create(new BranchInput());
+
+    // Cherry-pick without message.
+    CherryPickInput input = new CherryPickInput();
+    input.destination = branch;
+    String changeId =
+        gApi.projects().name(project.get()).commit(revCommit.name()).cherryPick(input).get().id;
+
+    // Expect that the message of the cherry-picked commit was used for the cherry-pick change.
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
+    assertThat(revInfo).isNotNull();
+    assertThat(revInfo.commit.message).isEqualTo(revCommit.getFullMessage());
+  }
+
+  @Test
   public void cherryPickCommitWithoutChangeId() throws Exception {
-    // This test is a little superfluous, since the current cherry-pick code ignores
-    // the commit message of the to-be-cherry-picked change, using the one in
-    // CherryPickInput instead.
     CherryPickInput input = new CherryPickInput();
     input.destination = "foo";
     input.message = "it goes to foo branch";
@@ -148,4 +228,15 @@
     assertThat(actual.email).isEqualTo(expected.email());
     assertThat(actual.name).isEqualTo(expected.fullName());
   }
+
+  private Result createAndSubmitChange(String branch) throws Exception {
+    Result r = createChange(branch);
+    approve(r.getChangeId());
+    gApi.changes().id(r.getChangeId()).current().submit();
+    return r;
+  }
+
+  private void createLightWeightTag(String tagName) throws Exception {
+    pushHead(testRepo, RefNames.REFS_TAGS + tagName, false, false);
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java b/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
index f597392..6442645 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
@@ -15,12 +15,15 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
@@ -31,6 +34,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.restapi.project.DashboardsCollection;
+import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
@@ -40,21 +44,25 @@
 
 @NoHttpd
 public class DashboardIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   @Before
   public void setup() throws Exception {
-    allow("refs/meta/dashboards/*", Permission.CREATE, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/meta/dashboards/*").group(REGISTERED_USERS))
+        .update();
   }
 
   @Test
   public void defaultDashboardDoesNotExist() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    project().defaultDashboard().get();
+    assertThrows(ResourceNotFoundException.class, () -> project().defaultDashboard().get());
   }
 
   @Test
   public void dashboardDoesNotExist() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    project().dashboard("my:dashboard").get();
+    assertThrows(ResourceNotFoundException.class, () -> project().dashboard("my:dashboard").get());
   }
 
   @Test
@@ -110,8 +118,7 @@
     project().removeDefaultDashboard();
     assertThat(project().dashboard(info.id).get().isDefault).isNull();
 
-    exception.expect(ResourceNotFoundException.class);
-    project().defaultDashboard().get();
+    assertThrows(ResourceNotFoundException.class, () -> project().defaultDashboard().get());
   }
 
   @Test
@@ -133,9 +140,9 @@
   @Test
   public void cannotGetDashboardWithInheritedForNonDefault() throws Exception {
     DashboardInfo info = createTestDashboard();
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("inherited flag can only be used with default");
-    project().dashboard(info.id).get(true);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> project().dashboard(info.id).get(true));
+    assertThat(thrown).hasMessageThat().contains("inherited flag can only be used with default");
   }
 
   private void assertDashboardInfo(DashboardInfo actual, DashboardInfo expected) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 766f853..7f00930b 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -15,17 +15,24 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.project.ProjectState.INHERITED_FROM_GLOBAL;
 import static com.google.gerrit.server.project.ProjectState.INHERITED_FROM_PARENT;
 import static com.google.gerrit.server.project.ProjectState.OVERRIDDEN_BY_GLOBAL;
 import static com.google.gerrit.server.project.ProjectState.OVERRIDDEN_BY_PARENT;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toSet;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.AtomicLongMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -33,6 +40,9 @@
 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.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
@@ -45,15 +55,12 @@
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.events.ProjectIndexedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.CommentLinkInfoImpl;
@@ -66,16 +73,10 @@
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
@@ -87,12 +88,9 @@
   private static final String JIRA_LINK = "http://jira.example.com/?id=$2";
   private static final String JIRA_MATCH = "(jira\\\\s+#?)(\\\\d+)";
 
-  @Inject private DynamicSet<ProjectIndexedListener> projectIndexedListeners;
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
-
-  private ProjectIndexedCounter projectIndexedCounter;
-  private RegistrationHandle projectIndexedCounterHandle;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   @Override
   public Module createModule() {
@@ -106,53 +104,49 @@
     };
   }
 
-  @Before
-  public void addProjectIndexedCounter() {
-    projectIndexedCounter = new ProjectIndexedCounter();
-    projectIndexedCounterHandle = projectIndexedListeners.add("gerrit", projectIndexedCounter);
-  }
+  @Test
+  public void createProject() throws Exception {
+    ProjectIndexedCounter projectIndexedCounter = new ProjectIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectIndexedCounter)) {
+      String name = name("foo");
+      assertThat(gApi.projects().create(name).get().name).isEqualTo(name);
 
-  @After
-  public void removeProjectIndexedCounter() {
-    if (projectIndexedCounterHandle != null) {
-      projectIndexedCounterHandle.remove();
+      RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
+      eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG, null, head);
+
+      eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master", new String[] {});
+      projectIndexedCounter.assertReindexOf(name);
     }
   }
 
   @Test
-  public void createProject() throws Exception {
-    String name = name("foo");
-    assertThat(gApi.projects().create(name).get().name).isEqualTo(name);
-
-    RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
-    eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG, null, head);
-
-    eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master", new String[] {});
-    projectIndexedCounter.assertReindexOf(name);
-  }
-
-  @Test
   public void createProjectWithInitialBranches() throws Exception {
-    String name = name("foo");
-    ProjectInput input = new ProjectInput();
-    input.name = name;
-    input.createEmptyCommit = true;
-    input.branches = ImmutableList.of("master", "foo");
-    assertThat(gApi.projects().create(input).get().name).isEqualTo(name);
-    assertThat(
-            gApi.projects().name(name).branches().get().stream().map(b -> b.ref).collect(toSet()))
-        .containsExactly("refs/heads/foo", "refs/heads/master", "HEAD", RefNames.REFS_CONFIG);
+    ProjectIndexedCounter projectIndexedCounter = new ProjectIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectIndexedCounter)) {
 
-    RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
-    eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG, null, head);
+      String name = name("foo");
+      ProjectInput input = new ProjectInput();
+      input.name = name;
+      input.createEmptyCommit = true;
+      input.branches = ImmutableList.of("master", "foo");
+      assertThat(gApi.projects().create(input).get().name).isEqualTo(name);
+      assertThat(
+              gApi.projects().name(name).branches().get().stream().map(b -> b.ref).collect(toSet()))
+          .containsExactly("refs/heads/foo", "refs/heads/master", "HEAD", RefNames.REFS_CONFIG);
 
-    head = getRemoteHead(name, "refs/heads/foo");
-    eventRecorder.assertRefUpdatedEvents(name, "refs/heads/foo", null, head);
+      RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
+      eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG, null, head);
 
-    head = getRemoteHead(name, "refs/heads/master");
-    eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master", null, head);
+      head = getRemoteHead(name, "refs/heads/foo");
+      eventRecorder.assertRefUpdatedEvents(name, "refs/heads/foo", null, head);
 
-    projectIndexedCounter.assertReindexOf(name);
+      head = getRemoteHead(name, "refs/heads/master");
+      eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master", null, head);
+
+      projectIndexedCounter.assertReindexOf(name);
+    }
   }
 
   @Test
@@ -196,17 +190,17 @@
   public void createProjectWithMismatchedInput() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = name("foo");
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("name must match input.name");
-    gApi.projects().name("bar").create(in);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> gApi.projects().name("bar").create(in));
+    assertThat(thrown).hasMessageThat().contains("name must match input.name");
   }
 
   @Test
   public void createProjectNoNameInInput() throws Exception {
     ProjectInput in = new ProjectInput();
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("input.name is required");
-    gApi.projects().create(in);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> gApi.projects().create(in));
+    assertThat(thrown).hasMessageThat().contains("input.name is required");
   }
 
   @Test
@@ -214,9 +208,9 @@
     ProjectInput in = new ProjectInput();
     in.name = name("baz");
     gApi.projects().create(in);
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Project already exists");
-    gApi.projects().create(in);
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.projects().create(in));
+    assertThat(thrown).hasMessageThat().contains("Project already exists");
   }
 
   @Test
@@ -225,9 +219,9 @@
     in.name = name("baz");
     in.parent = "non-existing";
 
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("Project Not Found: " + in.parent);
-    gApi.projects().create(in);
+    UnprocessableEntityException thrown =
+        assertThrows(UnprocessableEntityException.class, () -> gApi.projects().create(in));
+    assertThat(thrown).hasMessageThat().contains("Project Not Found: " + in.parent);
   }
 
   @Test
@@ -236,9 +230,9 @@
     in.name = name("baz");
     in.parent = in.name;
 
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("Project Not Found: " + in.parent);
-    gApi.projects().create(in);
+    UnprocessableEntityException thrown =
+        assertThrows(UnprocessableEntityException.class, () -> gApi.projects().create(in));
+    assertThat(thrown).hasMessageThat().contains("Project Not Found: " + in.parent);
   }
 
   @Test
@@ -246,52 +240,67 @@
     ProjectInput in = new ProjectInput();
     in.name = name("foo");
     in.parent = allUsers.get();
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(String.format("Cannot inherit from '%s' project", allUsers.get()));
-    gApi.projects().create(in);
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.projects().create(in));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(String.format("Cannot inherit from '%s' project", allUsers.get()));
   }
 
   @Test
   public void createAndDeleteBranch() throws Exception {
-    assertThat(hasHead(project, "foo")).isFalse();
+    ProjectIndexedCounter projectIndexedCounter = new ProjectIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectIndexedCounter)) {
 
-    gApi.projects().name(project.get()).branch("foo").create(new BranchInput());
-    assertThat(getRemoteHead(project.get(), "foo")).isNotNull();
-    projectIndexedCounter.assertNoReindex();
+      assertThat(hasHead(project, "foo")).isFalse();
 
-    gApi.projects().name(project.get()).branch("foo").delete();
-    assertThat(hasHead(project, "foo")).isFalse();
-    projectIndexedCounter.assertNoReindex();
+      gApi.projects().name(project.get()).branch("foo").create(new BranchInput());
+      assertThat(getRemoteHead(project.get(), "foo")).isNotNull();
+      projectIndexedCounter.assertNoReindex();
+
+      gApi.projects().name(project.get()).branch("foo").delete();
+      assertThat(hasHead(project, "foo")).isFalse();
+      projectIndexedCounter.assertNoReindex();
+    }
   }
 
   @Test
   public void createAndDeleteBranchByPush() throws Exception {
-    grant(project, "refs/*", Permission.PUSH, true);
-    projectIndexedCounter.clear();
+    ProjectIndexedCounter projectIndexedCounter = new ProjectIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectIndexedCounter)) {
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()).force(true))
+          .update();
+      projectIndexedCounter.clear();
 
-    assertThat(hasHead(project, "foo")).isFalse();
+      assertThat(hasHead(project, "foo")).isFalse();
 
-    PushOneCommit.Result r = pushTo("refs/heads/foo");
-    r.assertOkStatus();
-    assertThat(getRemoteHead(project.get(), "foo")).isEqualTo(r.getCommit());
-    projectIndexedCounter.assertNoReindex();
+      PushOneCommit.Result r = pushTo("refs/heads/foo");
+      r.assertOkStatus();
+      assertThat(getRemoteHead(project.get(), "foo")).isEqualTo(r.getCommit());
+      projectIndexedCounter.assertNoReindex();
 
-    PushResult r2 = GitUtil.pushOne(testRepo, null, "refs/heads/foo", false, true, null);
-    assertThat(r2.getRemoteUpdate("refs/heads/foo").getStatus()).isEqualTo(Status.OK);
-    assertThat(hasHead(project, "foo")).isFalse();
-    projectIndexedCounter.assertNoReindex();
+      PushResult r2 = GitUtil.pushOne(testRepo, null, "refs/heads/foo", false, true, null);
+      assertThat(r2.getRemoteUpdate("refs/heads/foo").getStatus()).isEqualTo(Status.OK);
+      assertThat(hasHead(project, "foo")).isFalse();
+      projectIndexedCounter.assertNoReindex();
+    }
   }
 
   @Test
   public void descriptionChangeCausesRefUpdate() throws Exception {
-    RevCommit initialHead = getRemoteHead(project, RefNames.REFS_CONFIG);
+    RevCommit initialHead = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
     assertThat(gApi.projects().name(project.get()).description()).isEmpty();
     DescriptionInput in = new DescriptionInput();
     in.description = "new project description";
     gApi.projects().name(project.get()).description(in);
     assertThat(gApi.projects().name(project.get()).description()).isEqualTo(in.description);
 
-    RevCommit updatedHead = getRemoteHead(project, RefNames.REFS_CONFIG);
+    RevCommit updatedHead = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
     eventRecorder.assertRefUpdatedEvents(
         project.get(), RefNames.REFS_CONFIG, initialHead, updatedHead);
   }
@@ -310,7 +319,7 @@
 
   @Test
   public void configChangeCausesRefUpdate() throws Exception {
-    RevCommit initialHead = getRemoteHead(project, RefNames.REFS_CONFIG);
+    RevCommit initialHead = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
 
     ConfigInfo info = gApi.projects().name(project.get()).config();
     assertThat(info.defaultSubmitType.value).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
@@ -321,7 +330,7 @@
     info = gApi.projects().name(project.get()).config();
     assertThat(info.defaultSubmitType.value).isEqualTo(SubmitType.CHERRY_PICK);
 
-    RevCommit updatedHead = getRemoteHead(project, RefNames.REFS_CONFIG);
+    RevCommit updatedHead = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
     eventRecorder.assertRefUpdatedEvents(
         project.get(), RefNames.REFS_CONFIG, initialHead, updatedHead);
   }
@@ -385,9 +394,9 @@
   public void nonOwnerCannotSetConfig() throws Exception {
     ConfigInput input = createTestConfigInput();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("write refs/meta/config not permitted");
-    gApi.projects().name(project.get()).config(input);
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.projects().name(project.get()).config(input));
+    assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted");
   }
 
   @Test
@@ -403,8 +412,9 @@
 
   @Test
   public void setHeadToNonexistentBranch() throws Exception {
-    exception.expect(UnprocessableEntityException.class);
-    gApi.projects().name(project.get()).head("does-not-exist");
+    assertThrows(
+        UnprocessableEntityException.class,
+        () -> gApi.projects().name(project.get()).head("does-not-exist"));
   }
 
   @Test
@@ -420,9 +430,9 @@
   public void setHeadNotAllowed() throws Exception {
     gApi.projects().name(project.get()).branch("test").create(new BranchInput());
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("not permitted: set HEAD on refs/heads/test");
-    gApi.projects().name(project.get()).head("test");
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.projects().name(project.get()).head("test"));
+    assertThat(thrown).hasMessageThat().contains("not permitted: set HEAD on refs/heads/test");
   }
 
   @Test
@@ -452,8 +462,18 @@
     assertThat(gApi.projects().name(project.get()).config().state).isEqualTo(ProjectState.HIDDEN);
 
     // Revoke OWNER permission for admin and block them from reading the project's refs
-    block(project, RefNames.REFS + "*", Permission.OWNER, SystemGroupBackend.REGISTERED_USERS);
-    block(project, RefNames.REFS + "*", Permission.READ, SystemGroupBackend.REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            block(Permission.OWNER)
+                .ref(RefNames.REFS + "*")
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .add(
+            block(Permission.READ)
+                .ref(RefNames.REFS + "*")
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
 
     // HIDDEN => ACTIVE
     ConfigInput ci2 = new ConfigInput();
@@ -465,22 +485,49 @@
 
   @Test
   public void reindexProject() throws Exception {
-    projectOperations.newProject().parent(project).create();
-    projectIndexedCounter.clear();
+    ProjectIndexedCounter projectIndexedCounter = new ProjectIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectIndexedCounter)) {
 
-    gApi.projects().name(allProjects.get()).index(false);
-    projectIndexedCounter.assertReindexOf(allProjects.get());
+      projectOperations.newProject().parent(project).create();
+      projectIndexedCounter.clear();
+
+      gApi.projects().name(allProjects.get()).index(false);
+      projectIndexedCounter.assertReindexOf(allProjects.get());
+    }
   }
 
   @Test
   public void reindexProjectWithChildren() throws Exception {
-    Project.NameKey middle = projectOperations.newProject().parent(project).create();
-    Project.NameKey leave = projectOperations.newProject().parent(middle).create();
-    projectIndexedCounter.clear();
+    ProjectIndexedCounter projectIndexedCounter = new ProjectIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectIndexedCounter)) {
 
-    gApi.projects().name(project.get()).index(true);
-    projectIndexedCounter.assertReindexExactly(
-        ImmutableMap.of(project.get(), 1L, middle.get(), 1L, leave.get(), 1L));
+      Project.NameKey middle = projectOperations.newProject().parent(project).create();
+      Project.NameKey leave = projectOperations.newProject().parent(middle).create();
+      projectIndexedCounter.clear();
+
+      gApi.projects().name(project.get()).index(true);
+      projectIndexedCounter.assertReindexExactly(
+          ImmutableMap.of(project.get(), 1L, middle.get(), 1L, leave.get(), 1L));
+    }
+  }
+
+  @Test
+  public void reindexChangesOfProject() throws Exception {
+    Change.Id changeId1 = createChange().getChange().getId();
+    Change.Id changeId2 = createChange().getChange().getId();
+
+    ChangeIndexedListener changeIndexedListener = mock(ChangeIndexedListener.class);
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedListener)) {
+      gApi.projects().name(project.get()).indexChanges();
+
+      verify(changeIndexedListener, times(1))
+          .onChangeScheduledForIndexing(project.get(), changeId1.get());
+      verify(changeIndexedListener, times(1))
+          .onChangeScheduledForIndexing(project.get(), changeId2.get());
+    }
   }
 
   @Test
@@ -645,9 +692,9 @@
 
   @Test
   public void invalidMaxObjectSizeIsRejected() throws Exception {
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("100 foo");
-    setMaxObjectSize("100 foo");
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> setMaxObjectSize("100 foo"));
+    assertThat(thrown).hasMessageThat().contains("100 foo");
   }
 
   @Test
@@ -669,7 +716,8 @@
 
   @Test
   public void cannotPushLabelDefinitionWithDuplicateValues() throws Exception {
-    Config cfg = readAllProjectsConfig();
+    Config cfg = new Config();
+    cfg.fromText(projectOperations.project(allProjects).getConfig().toText());
     cfg.setStringList(
         "label",
         "Code-Review",
@@ -692,7 +740,8 @@
     // Update the definition of the Code-Review label so that it has the value "+1 LGTM" twice.
     // This update bypasses all validation checks so that the duplicate label value doesn't get
     // rejected.
-    Config cfg = readAllProjectsConfig();
+    Config cfg = new Config();
+    cfg.fromText(projectOperations.project(allProjects).getConfig().toText());
     cfg.setStringList(
         "label",
         "Code-Review",
@@ -717,20 +766,6 @@
         .containsExactly("+1", "LGTM", " 0", "No Value", "-1", "Looks Bad");
   }
 
-  private Config readAllProjectsConfig() throws Exception {
-    try (TestRepository<Repository> repo =
-        new TestRepository<>(repoManager.openRepository(allProjects))) {
-      RevWalk rw = repo.getRevWalk();
-      RevTree tree = rw.parseTree(repo.getRepository().resolve("HEAD"));
-      RevObject obj = rw.parseAny(repo.get(tree, "project.config"));
-      ObjectLoader loader = rw.getObjectReader().open(obj);
-      String text = new String(loader.getCachedBytes(), UTF_8);
-      Config cfg = new Config();
-      cfg.fromText(text);
-      return cfg;
-    }
-  }
-
   private CommentLinkInfo commentLinkInfo(String name, String match, String link) {
     return new CommentLinkInfoImpl(name, match, link, null /*html*/, null /*enabled*/);
   }
@@ -800,22 +835,17 @@
       countsByProject.clear();
     }
 
-    long getCount(String projectName) {
-      return countsByProject.get(projectName);
-    }
-
     void assertReindexOf(String projectName) {
       assertReindexOf(projectName, 1);
     }
 
-    void assertReindexOf(String projectName, int expectedCount) {
-      assertThat(getCount(projectName)).isEqualTo(expectedCount);
-      assertThat(countsByProject).hasSize(1);
+    void assertReindexOf(String projectName, long expectedCount) {
+      assertThat(countsByProject.asMap()).containsExactly(projectName, expectedCount);
       clear();
     }
 
     void assertNoReindex() {
-      assertThat(countsByProject).isEmpty();
+      assertThat(countsByProject.asMap()).isEmpty();
     }
 
     void assertReindexExactly(ImmutableMap<String, Long> expected) {
@@ -825,7 +855,7 @@
   }
 
   protected RevCommit getRemoteHead(String project, String branch) throws Exception {
-    return getRemoteHead(new Project.NameKey(project), branch);
+    return projectOperations.project(Project.nameKey(project)).getHead(branch);
   }
 
   boolean hasHead(Project.NameKey k, String b) {
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
index 6b511f6..019df0e 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -15,11 +15,14 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.RefState;
@@ -28,7 +31,6 @@
 import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.index.project.ProjectIndexer;
 import com.google.gerrit.index.query.FieldBundle;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.index.project.StalenessChecker;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.inject.Inject;
@@ -60,7 +62,7 @@
     Optional<FieldBundle> result =
         i.getRaw(project, QueryOptions.create(indexConfig, 0, 1, FIELDS));
 
-    assertThat(result.isPresent()).isTrue();
+    assertThat(result).isPresent();
     Iterable<byte[]> refState = result.get().getValue(ProjectField.REF_STATE);
     assertThat(refState).isNotEmpty();
 
@@ -117,15 +119,15 @@
 
   private void updateProjectConfigWithoutIndexUpdate(
       Project.NameKey project, Consumer<ProjectConfig> update) throws Exception {
-    try (AutoCloseable ignored = disableProjectIndex()) {
-      try (ProjectConfigUpdate u = updateProject(project)) {
-        update.accept(u.getConfig());
-        u.save();
-      }
-    } catch (UnsupportedOperationException e) {
-      // Drop, as we just wanted to drop the index update
-      return;
-    }
-    fail("should have a UnsupportedOperationException");
+    assertThrows(
+        UnsupportedOperationException.class,
+        () -> {
+          try (AutoCloseable ignored = disableProjectIndex()) {
+            try (ProjectConfigUpdate u = updateProject(project)) {
+              update.accept(u.getConfig());
+              u.save();
+            }
+          }
+        });
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
index 3c1428d..cf7aab4 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
@@ -22,11 +24,11 @@
 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.Project;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.inject.Inject;
@@ -34,7 +36,6 @@
 
 @NoHttpd
 public class SetParentIT extends AbstractDaemonTest {
-
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
@@ -42,8 +43,7 @@
   public void setParentNotAllowed() throws Exception {
     String parent = projectOperations.newProject().create().get();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.projects().name(project.get()).parent(parent);
+    assertThrows(AuthException.class, () -> gApi.projects().name(project.get()).parent(parent));
   }
 
   @Test
@@ -51,8 +51,7 @@
   public void setParentNotAllowedForNonOwners() throws Exception {
     String parent = projectOperations.newProject().create().get();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.projects().name(project.get()).parent(parent);
+    assertThrows(AuthException.class, () -> gApi.projects().name(project.get()).parent(parent));
   }
 
   @Test
@@ -75,7 +74,11 @@
   public void setParentAllowedForOwners() throws Exception {
     String parent = projectOperations.newProject().create().get();
     requestScopeOperations.setApiUser(user.id());
-    grant(project, "refs/*", Permission.OWNER, false, SystemGroupBackend.REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
     gApi.projects().name(project.get()).parent(parent);
     assertThat(gApi.projects().name(project.get()).parent()).isEqualTo(parent);
   }
@@ -96,47 +99,63 @@
 
   @Test
   public void setParentForAllProjectsNotAllowed() throws Exception {
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("cannot set parent of " + AllProjectsNameProvider.DEFAULT);
-    gApi.projects().name(allProjects.get()).parent(project.get());
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.projects().name(allProjects.get()).parent(project.get()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("cannot set parent of " + AllProjectsNameProvider.DEFAULT);
   }
 
   @Test
   public void setParentToSelfNotAllowed() throws Exception {
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("cannot set parent to self");
-    gApi.projects().name(project.get()).parent(project.get());
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.projects().name(project.get()).parent(project.get()));
+    assertThat(thrown).hasMessageThat().contains("cannot set parent to self");
   }
 
   @Test
   public void setParentToOwnChildNotAllowed() throws Exception {
     String child = projectOperations.newProject().parent(project).create().get();
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("cycle exists between");
-    gApi.projects().name(project.get()).parent(child);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.projects().name(project.get()).parent(child));
+    assertThat(thrown).hasMessageThat().contains("cycle exists between");
   }
 
   @Test
   public void setParentToGrandchildNotAllowed() throws Exception {
     Project.NameKey child = projectOperations.newProject().parent(project).create();
     String grandchild = projectOperations.newProject().parent(child).create().get();
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("cycle exists between");
-    gApi.projects().name(project.get()).parent(grandchild);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.projects().name(project.get()).parent(grandchild));
+    assertThat(thrown).hasMessageThat().contains("cycle exists between");
   }
 
   @Test
   public void setParentToNonexistentProject() throws Exception {
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("not found");
-    gApi.projects().name(project.get()).parent("non-existing");
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.projects().name(project.get()).parent("non-existing"));
+    assertThat(thrown).hasMessageThat().contains("not found");
   }
 
   @Test
   public void setParentToAllUsersNotAllowed() throws Exception {
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(String.format("Cannot inherit from '%s' project", allUsers.get()));
-    gApi.projects().name(project.get()).parent(allUsers.get());
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.projects().name(project.get()).parent(allUsers.get()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(String.format("Cannot inherit from '%s' project", allUsers.get()));
   }
 
   @Test
@@ -145,8 +164,9 @@
 
     String parent = projectOperations.newProject().create().get();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("All-Users must inherit from All-Projects");
-    gApi.projects().name(allUsers.get()).parent(parent);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> gApi.projects().name(allUsers.get()).parent(parent));
+    assertThat(thrown).hasMessageThat().contains("All-Users must inherit from All-Projects");
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 38195a9..c1162ff 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -16,10 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.entities.Patch.COMMIT_MSG;
+import static com.google.gerrit.entities.Patch.MERGE_LIST;
 import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.FileInfoSubject.assertThat;
-import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
-import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toMap;
 
@@ -2756,7 +2757,7 @@
 
       RevCommit parentCommit = c.getParents()[0];
       String parentCommitId =
-          testRepo.getRevWalk().getObjectReader().abbreviate(parentCommit.getId(), 8).name();
+          abbreviateName(parentCommit, 8, testRepo.getRevWalk().getObjectReader());
       headers.add("Parent:     " + parentCommitId + " (" + parentCommit.getShortMessage() + ")");
 
       SimpleDateFormat dtfmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 2375607..32941ff 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -21,10 +21,12 @@
 import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
 import static com.google.gerrit.acceptance.PushOneCommit.PATCH_FILE_ONLY;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.entities.Patch.COMMIT_MSG;
+import static com.google.gerrit.entities.Patch.MERGE_LIST;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
-import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
-import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -38,12 +40,20 @@
 import com.google.common.collect.Iterators;
 import com.google.common.collect.ListMultimap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+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.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.DraftApi;
@@ -72,8 +82,6 @@
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -83,11 +91,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.restapi.change.GetRevisionActions;
@@ -115,11 +118,10 @@
 import org.junit.Test;
 
 public class RevisionIT extends AbstractDaemonTest {
-
-  @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
-  @Inject private DynamicSet<PatchSetWebLink> patchSetLinks;
   @Inject private GetRevisionActions getRevisionActions;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   @Test
   public void reviewTriplet() throws Exception {
@@ -182,14 +184,13 @@
     assertThat(approval.postSubmit).isNull();
 
     // Reducing vote is not allowed.
-    try {
-      gApi.changes().id(changeId).current().review(ReviewInput.dislike());
-      fail("expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).current().review(ReviewInput.dislike()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
     approval = getApproval(changeId, label);
     assertThat(approval.value).isEqualTo(1);
     assertThat(approval.postSubmit).isNull();
@@ -202,14 +203,13 @@
     assertPermitted(gApi.changes().id(changeId).get(DETAILED_LABELS), "Code-Review", 2);
 
     // Decreasing to previous post-submit vote is still not allowed.
-    try {
-      gApi.changes().id(changeId).current().review(ReviewInput.dislike());
-      fail("expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
-    }
+    thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).current().review(ReviewInput.dislike()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
     approval = getApproval(changeId, label);
     assertThat(approval.value).isEqualTo(2);
     assertThat(approval.postSubmit).isTrue();
@@ -261,9 +261,11 @@
     ReviewInput in = new ReviewInput();
     in.label("Code-Review", 0);
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Cannot reduce vote on labels for closed change: Code-Review");
-    revision(r).review(in);
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> revision(r).review(in));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Cannot reduce vote on labels for closed change: Code-Review");
   }
 
   @TestProjectInput(submitType = SubmitType.CHERRY_PICK)
@@ -279,28 +281,36 @@
     PatchSetApproval psa =
         Iterators.getOnlyElement(
             cd.currentApprovals().stream().filter(a -> !a.isLegacySubmit()).iterator());
-    assertThat(psa.getPatchSetId().get()).isEqualTo(2);
-    assertThat(psa.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa.getValue()).isEqualTo(2);
-    assertThat(psa.isPostSubmit()).isFalse();
+    assertThat(psa.patchSetId().get()).isEqualTo(2);
+    assertThat(psa.label()).isEqualTo("Code-Review");
+    assertThat(psa.value()).isEqualTo(2);
+    assertThat(psa.postSubmit()).isFalse();
   }
 
   @Test
   public void voteOnAbandonedChange() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).abandon();
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("change is closed");
-    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.reject());
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(r.getChangeId()).current().review(ReviewInput.reject()));
+    assertThat(thrown).hasMessageThat().contains("change is closed");
   }
 
   @Test
   public void voteNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("is restricted");
-    gApi.changes().id(r.getChange().getId().get()).current().review(ReviewInput.approve());
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () ->
+                gApi.changes()
+                    .id(r.getChange().getId().get())
+                    .current()
+                    .review(ReviewInput.approve()));
+    assertThat(thrown).hasMessageThat().contains("is restricted");
   }
 
   @Test
@@ -331,6 +341,34 @@
   }
 
   @Test
+  public void cherryPickWithoutMessage() throws Exception {
+    String branch = "foo";
+
+    // Create change to cherry-pick
+    PushOneCommit.Result change = createChange();
+    RevCommit revCommit = change.getCommit();
+
+    // Create target branch to cherry-pick to.
+    gApi.projects().name(project.get()).branch(branch).create(new BranchInput());
+
+    // Cherry-pick without message.
+    CherryPickInput input = new CherryPickInput();
+    input.destination = branch;
+    String changeId =
+        gApi.changes()
+            .id(change.getChangeId())
+            .revision(revCommit.name())
+            .cherryPickAsInfo(input)
+            .id;
+
+    // Expect that the message of the cherry-picked commit was used for the cherry-pick change.
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
+    assertThat(revInfo).isNotNull();
+    assertThat(revInfo.commit.message).isEqualTo(revCommit.getFullMessage());
+  }
+
+  @Test
   public void cherryPickSetChangeId() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     CherryPickInput in = new CherryPickInput();
@@ -456,9 +494,11 @@
     cherry.current().review(ReviewInput.approve());
     cherry.current().submit();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Cherry pick failed: identical tree");
-    orig.revision(r.getCommit().name()).cherryPick(in);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> orig.revision(r.getCommit().name()).cherryPick(in));
+    assertThat(thrown).hasMessageThat().contains("Cherry pick failed: identical tree");
   }
 
   @Test
@@ -482,9 +522,11 @@
     ChangeApi orig = gApi.changes().id(triplet);
     assertThat(orig.get().messages).hasSize(1);
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Cherry pick failed: merge conflict");
-    orig.revision(r.getCommit().name()).cherryPick(in);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> orig.revision(r.getCommit().name()).cherryPick(in));
+    assertThat(thrown).hasMessageThat().contains("Cherry pick failed: merge conflict");
   }
 
   @Test
@@ -522,12 +564,11 @@
     CherryPickInput in = new CherryPickInput();
     in.destination = destBranch;
     in.message = "Cherry-Pick";
-    try {
-      changeApi.revision(r.getCommit().name()).cherryPickAsInfo(in);
-      fail("expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e.getMessage()).isEqualTo("Cherry pick failed: merge conflict");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> changeApi.revision(r.getCommit().name()).cherryPickAsInfo(in));
+    assertThat(thrown).hasMessageThat().isEqualTo("Cherry pick failed: merge conflict");
 
     // Cherry-pick with auto merge should succeed.
     in.allowConflicts = true;
@@ -551,8 +592,8 @@
     ByteArrayOutputStream os = new ByteArrayOutputStream();
     bin.writeTo(os);
     String fileContent = new String(os.toByteArray(), UTF_8);
-    String destSha1 = getRemoteHead(project, destBranch).abbreviate(6).name();
-    String changeSha1 = r.getCommit().abbreviate(6).name();
+    String destSha1 = abbreviateName(projectOperations.project(project).getHead(destBranch), 6);
+    String changeSha1 = abbreviateName(r.getCommit(), 6);
     assertThat(fileContent)
         .isEqualTo(
             "<<<<<<< HEAD   ("
@@ -604,16 +645,15 @@
     CherryPickInput in = new CherryPickInput();
     in.destination = "foo";
     in.message = r1.getCommit().getFullMessage();
-    try {
-      gApi.changes().id(t1).current().cherryPick(in);
-      fail("expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e.getMessage())
-          .isEqualTo(
-              "Cannot create new patch set of change "
-                  + info(t2)._number
-                  + " because it is abandoned");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(t1).current().cherryPick(in));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            "Cannot create new patch set of change "
+                + info(t2)._number
+                + " because it is abandoned");
 
     gApi.changes().id(t2).restore();
     gApi.changes().id(t1).current().cherryPick(in);
@@ -629,7 +669,7 @@
         createCherryPickableMerge(parent1FileName, parent2FileName);
 
     String cherryPickBranchName = "branch_for_cherry_pick";
-    createBranch(new Branch.NameKey(project, cherryPickBranchName));
+    createBranch(BranchNameKey.create(project, cherryPickBranchName));
 
     CherryPickInput cherryPickInput = new CherryPickInput();
     cherryPickInput.destination = cherryPickBranchName;
@@ -656,7 +696,7 @@
         createCherryPickableMerge(parent1FileName, parent2FileName);
 
     String cherryPickBranchName = "branch_for_cherry_pick";
-    createBranch(new Branch.NameKey(project, cherryPickBranchName));
+    createBranch(BranchNameKey.create(project, cherryPickBranchName));
 
     CherryPickInput cherryPickInput = new CherryPickInput();
     cherryPickInput.destination = cherryPickBranchName;
@@ -684,17 +724,24 @@
         createCherryPickableMerge(parent1FileName, parent2FileName);
 
     String cherryPickBranchName = "branch_for_cherry_pick";
-    createBranch(new Branch.NameKey(project, cherryPickBranchName));
+    createBranch(BranchNameKey.create(project, cherryPickBranchName));
 
     CherryPickInput cherryPickInput = new CherryPickInput();
     cherryPickInput.destination = cherryPickBranchName;
     cherryPickInput.message = "Cherry-pick a merge commit to another branch";
     cherryPickInput.parent = 0;
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        "Cherry Pick: Parent 0 does not exist. Please specify a parent in range [1, 2].");
-    gApi.changes().id(mergeChangeResult.getChangeId()).current().cherryPick(cherryPickInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.changes()
+                    .id(mergeChangeResult.getChangeId())
+                    .current()
+                    .cherryPick(cherryPickInput));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Cherry Pick: Parent 0 does not exist. Please specify a parent in range [1, 2].");
   }
 
   @Test
@@ -705,24 +752,31 @@
         createCherryPickableMerge(parent1FileName, parent2FileName);
 
     String cherryPickBranchName = "branch_for_cherry_pick";
-    createBranch(new Branch.NameKey(project, cherryPickBranchName));
+    createBranch(BranchNameKey.create(project, cherryPickBranchName));
 
     CherryPickInput cherryPickInput = new CherryPickInput();
     cherryPickInput.destination = cherryPickBranchName;
     cherryPickInput.message = "Cherry-pick a merge commit to another branch";
     cherryPickInput.parent = 3;
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        "Cherry Pick: Parent 3 does not exist. Please specify a parent in range [1, 2].");
-    gApi.changes().id(mergeChangeResult.getChangeId()).current().cherryPick(cherryPickInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.changes()
+                    .id(mergeChangeResult.getChangeId())
+                    .current()
+                    .cherryPick(cherryPickInput));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Cherry Pick: Parent 3 does not exist. Please specify a parent in range [1, 2].");
   }
 
   @Test
   public void cherryPickNotify() throws Exception {
-    createBranch(new Branch.NameKey(project, "branch-1"));
-    createBranch(new Branch.NameKey(project, "branch-2"));
-    createBranch(new Branch.NameKey(project, "branch-3"));
+    createBranch(BranchNameKey.create(project, "branch-1"));
+    createBranch(BranchNameKey.create(project, "branch-2"));
+    createBranch(BranchNameKey.create(project, "branch-3"));
 
     // Creates a change for 'admin'.
     PushOneCommit.Result result = createChange();
@@ -761,7 +815,7 @@
 
   @Test
   public void cherryPickKeepReviewers() throws Exception {
-    createBranch(new Branch.NameKey(project, "stable"));
+    createBranch(BranchNameKey.create(project, "stable"));
 
     // Change is created by 'admin'.
     PushOneCommit.Result r = createChange();
@@ -795,7 +849,7 @@
 
   @Test
   public void cherryPickToMergedChangeRevision() throws Exception {
-    createBranch(new Branch.NameKey(project, "foo"));
+    createBranch(BranchNameKey.create(project, "foo"));
 
     PushOneCommit.Result dstChange = createChange(testRepo, "foo", SUBJECT, "b.txt", "b", "t");
     dstChange.assertOkStatus();
@@ -819,7 +873,7 @@
 
   @Test
   public void cherryPickToOpenChangeRevision() throws Exception {
-    createBranch(new Branch.NameKey(project, "foo"));
+    createBranch(BranchNameKey.create(project, "foo"));
 
     PushOneCommit.Result dstChange = createChange(testRepo, "foo", SUBJECT, "b.txt", "b", "t");
     dstChange.assertOkStatus();
@@ -837,7 +891,7 @@
 
   @Test
   public void cherryPickToNonVisibleChangeFails() throws Exception {
-    createBranch(new Branch.NameKey(project, "foo"));
+    createBranch(BranchNameKey.create(project, "foo"));
 
     PushOneCommit.Result dstChange = createChange(testRepo, "foo", SUBJECT, "b.txt", "b", "t");
     dstChange.assertOkStatus();
@@ -852,10 +906,13 @@
     input.message = srcChange.getCommit().getFullMessage();
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage(
-        String.format("Commit %s does not exist on branch refs/heads/foo", input.base));
-    gApi.changes().id(srcChange.getChangeId()).current().cherryPick(input).get();
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.changes().id(srcChange.getChangeId()).current().cherryPick(input).get());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(String.format("Commit %s does not exist on branch refs/heads/foo", input.base));
   }
 
   @Test
@@ -869,12 +926,16 @@
     input.base = change2.getCommit().name();
     input.message = change1.getCommit().getFullMessage();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        String.format(
-            "Change %s with commit %s is abandoned",
-            change2.getChange().getId().get(), input.base));
-    gApi.changes().id(change1.getChangeId()).current().cherryPick(input);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(change1.getChangeId()).current().cherryPick(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            String.format(
+                "Change %s with commit %s is abandoned",
+                change2.getChange().getId().get(), input.base));
   }
 
   @Test
@@ -886,9 +947,13 @@
     input.base = "invalid-sha1";
     input.message = change1.getCommit().getFullMessage();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(String.format("Base %s doesn't represent a valid SHA-1", input.base));
-    gApi.changes().id(change1.getChangeId()).current().cherryPick(input);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(change1.getChangeId()).current().cherryPick(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(String.format("Base %s doesn't represent a valid SHA-1", input.base));
   }
 
   @Test
@@ -929,7 +994,7 @@
 
   @Test
   public void cherryPickToNonExistingBaseCommit() throws Exception {
-    createBranch(new Branch.NameKey(project, "foo"));
+    createBranch(BranchNameKey.create(project, "foo"));
     PushOneCommit.Result result = createChange();
 
     CherryPickInput input = new CherryPickInput();
@@ -1050,25 +1115,22 @@
 
     // Using the API returns the correct value, and reindexes as well.
     CountDownLatch reindexed = new CountDownLatch(1);
-    RegistrationHandle handle =
-        changeIndexedListeners.add(
-            "gerrit",
-            new ChangeIndexedListener() {
-              @Override
-              public void onChangeIndexed(String projectName, int id) {
-                if (id == id2.get()) {
-                  reindexed.countDown();
-                }
-              }
+    ChangeIndexedListener listener =
+        new ChangeIndexedListener() {
+          @Override
+          public void onChangeIndexed(String projectName, int id) {
+            if (id == id2.get()) {
+              reindexed.countDown();
+            }
+          }
 
-              @Override
-              public void onChangeDeleted(int id) {}
-            });
-    try {
+          @Override
+          public void onChangeDeleted(int id) {}
+        };
+
+    try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
       assertMergeable(r2.getChangeId(), true);
       reindexed.await();
-    } finally {
-      handle.remove();
     }
 
     List<ChangeInfo> changes = search.call();
@@ -1228,16 +1290,26 @@
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("edit description not permitted");
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () ->
+                gApi.changes()
+                    .id(r.getChangeId())
+                    .revision(r.getCommit().name())
+                    .description("test"));
+    assertThat(thrown).hasMessageThat().contains("edit description not permitted");
   }
 
   @Test
   public void setDescriptionAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
-    grant(project, "refs/heads/master", Permission.OWNER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
     assertDescription(r, "test");
@@ -1276,10 +1348,14 @@
   @Test
   public void commit() throws Exception {
     WebLinkInfo expectedWebLinkInfo = new WebLinkInfo("foo", "imageUrl", "url");
-    RegistrationHandle handle =
-        patchSetLinks.add("gerrit", (projectName, commit) -> expectedWebLinkInfo);
-
-    try {
+    PatchSetWebLink link =
+        new PatchSetWebLink() {
+          @Override
+          public WebLinkInfo getPatchSetWebLink(String projectName, String commit) {
+            return expectedWebLinkInfo;
+          }
+        };
+    try (Registration registration = extensionRegistry.newRegistration().add(link)) {
       PushOneCommit.Result r = createChange();
       RevCommit c = r.getCommit();
 
@@ -1301,8 +1377,6 @@
       assertThat(webLinkInfo.imageUrl).isEqualTo(expectedWebLinkInfo.imageUrl);
       assertThat(webLinkInfo.url).isEqualTo(expectedWebLinkInfo.url);
       assertThat(webLinkInfo.target).isEqualTo(expectedWebLinkInfo.target);
-    } finally {
-      handle.remove();
     }
   }
 
@@ -1413,8 +1487,8 @@
 
   @Test
   public void commentOnNonExistingFile() throws Exception {
-    PushOneCommit.Result r = createChange();
-    r = updateChange(r, "new content");
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = updateChange(r1, "new content");
     CommentInput in = new CommentInput();
     in.line = 1;
     in.message = "nit: trailing whitespace";
@@ -1425,10 +1499,14 @@
     reviewInput.comments = comments;
     reviewInput.message = "comment test";
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        String.format("not found in revision %d,1", r.getChange().change().getId().id));
-    gApi.changes().id(r.getChangeId()).revision(1).review(reviewInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(r2.getChangeId()).revision(1).review(reviewInput));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            String.format("not found in revision %d,1", r2.getChange().change().getId().get()));
   }
 
   @Test
@@ -1456,9 +1534,11 @@
     String res = new String(os.toByteArray(), UTF_8);
     assertThat(res).isEqualTo(PATCH_FILE_ONLY);
 
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("File not found: nonexistent-file.");
-    changeApi.revision(r.getCommit().name()).patch("nonexistent-file");
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> changeApi.revision(r.getCommit().name()).patch("nonexistent-file"));
+    assertThat(thrown).hasMessageThat().contains("File not found: nonexistent-file.");
   }
 
   @Test
@@ -1506,13 +1586,16 @@
 
     // check if it's blocked to delete a vote on a non-current patch set.
     requestScopeOperations.setApiUser(admin.id());
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("Cannot access on non-current patch set");
-    gApi.changes()
-        .id(r.getChangeId())
-        .revision(r.getCommit().getName())
-        .reviewer(user.id().toString())
-        .deleteVote("Code-Review");
+    MethodNotAllowedException thrown =
+        assertThrows(
+            MethodNotAllowedException.class,
+            () ->
+                gApi.changes()
+                    .id(r.getChangeId())
+                    .revision(r.getCommit().getName())
+                    .reviewer(user.id().toString())
+                    .deleteVote("Code-Review"));
+    assertThat(thrown).hasMessageThat().contains("Cannot access on non-current patch set");
   }
 
   @Test
@@ -1622,9 +1705,9 @@
     RevCommit initialCommit = getHead(repo(), "HEAD");
 
     String branchAName = "branchA";
-    createBranch(new Branch.NameKey(project, branchAName));
+    createBranch(BranchNameKey.create(project, branchAName));
     String branchBName = "branchB";
-    createBranch(new Branch.NameKey(project, branchBName));
+    createBranch(BranchNameKey.create(project, branchBName));
 
     PushOneCommit.Result changeAResult =
         pushFactory
@@ -1659,6 +1742,6 @@
   }
 
   private static Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
-    return Iterables.transform(r, a -> new Account.Id(a._accountId));
+    return Iterables.transform(r, a -> Account.id(a._accountId));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index d58210a..486b157 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.RobotCommentInfoSubject.assertThatList;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
@@ -164,9 +165,10 @@
     int sizeOfRest = 451;
     fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - sizeOfRest + 1);
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("limit");
-    addRobotComment(changeId, withFixRobotCommentInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> addRobotComment(changeId, withFixRobotCommentInput));
+    assertThat(thrown).hasMessageThat().contains("limit");
   }
 
   @Test
@@ -187,9 +189,10 @@
     int sizeLimit = 10 * 1024;
     fixReplacementInfo.replacement = getStringFor(sizeLimit);
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("limit");
-    addRobotComment(changeId, withFixRobotCommentInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> addRobotComment(changeId, withFixRobotCommentInput));
+    assertThat(thrown).hasMessageThat().contains("limit");
   }
 
   @Test
@@ -254,12 +257,15 @@
   public void descriptionOfFixSuggestionIsMandatory() throws Exception {
     fixSuggestionInfo.description = null;
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        String.format(
-            "A description is required for the suggested fix of the robot comment on %s",
-            withFixRobotCommentInput.path));
-    addRobotComment(changeId, withFixRobotCommentInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> addRobotComment(changeId, withFixRobotCommentInput));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            String.format(
+                "A description is required for the suggested fix of the robot comment on %s",
+                withFixRobotCommentInput.path));
   }
 
   @Test
@@ -278,13 +284,16 @@
   public void fixReplacementsAreMandatory() throws Exception {
     fixSuggestionInfo.replacements = Collections.emptyList();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        String.format(
-            "At least one replacement is required"
-                + " for the suggested fix of the robot comment on %s",
-            withFixRobotCommentInput.path));
-    addRobotComment(changeId, withFixRobotCommentInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> addRobotComment(changeId, withFixRobotCommentInput));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            String.format(
+                "At least one replacement is required"
+                    + " for the suggested fix of the robot comment on %s",
+                withFixRobotCommentInput.path));
   }
 
   @Test
@@ -305,12 +314,15 @@
   public void pathOfFixReplacementIsMandatory() throws Exception {
     fixReplacementInfo.path = null;
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        String.format(
-            "A file path must be given for the replacement of the robot comment on %s",
-            withFixRobotCommentInput.path));
-    addRobotComment(changeId, withFixRobotCommentInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> addRobotComment(changeId, withFixRobotCommentInput));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            String.format(
+                "A file path must be given for the replacement of the robot comment on %s",
+                withFixRobotCommentInput.path));
   }
 
   @Test
@@ -331,20 +343,24 @@
   public void rangeOfFixReplacementIsMandatory() throws Exception {
     fixReplacementInfo.range = null;
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        String.format(
-            "A range must be given for the replacement of the robot comment on %s",
-            withFixRobotCommentInput.path));
-    addRobotComment(changeId, withFixRobotCommentInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> addRobotComment(changeId, withFixRobotCommentInput));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            String.format(
+                "A range must be given for the replacement of the robot comment on %s",
+                withFixRobotCommentInput.path));
   }
 
   @Test
   public void rangeOfFixReplacementNeedsToBeValid() throws Exception {
     fixReplacementInfo.range = createRange(13, 9, 5, 10);
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("Range (13:9 - 5:10)");
-    addRobotComment(changeId, withFixRobotCommentInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> addRobotComment(changeId, withFixRobotCommentInput));
+    assertThat(thrown).hasMessageThat().contains("Range (13:9 - 5:10)");
   }
 
   @Test
@@ -364,9 +380,10 @@
         createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
     withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("overlap");
-    addRobotComment(changeId, withFixRobotCommentInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> addRobotComment(changeId, withFixRobotCommentInput));
+    assertThat(thrown).hasMessageThat().contains("overlap");
   }
 
   @Test
@@ -461,13 +478,16 @@
   public void replacementStringOfFixReplacementIsMandatory() throws Exception {
     fixReplacementInfo.replacement = null;
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(
-        String.format(
-            "A content for replacement must be "
-                + "indicated for the replacement of the robot comment on %s",
-            withFixRobotCommentInput.path));
-    addRobotComment(changeId, withFixRobotCommentInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> addRobotComment(changeId, withFixRobotCommentInput));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            String.format(
+                "A content for replacement must be "
+                    + "indicated for the replacement of the robot comment on %s",
+                withFixRobotCommentInput.path));
   }
 
   @Test
@@ -602,9 +622,11 @@
 
     List<String> fixIds = getFixIds(robotCommentInfos);
     gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("merge");
-    gApi.changes().id(changeId).current().applyFix(fixIds.get(1));
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).current().applyFix(fixIds.get(1)));
+    assertThat(thrown).hasMessageThat().contains("merge");
   }
 
   @Test
@@ -708,8 +730,9 @@
     List<String> fixIds = getFixIds(robotCommentInfos);
     String fixId = Iterables.getOnlyElement(fixIds);
 
-    exception.expect(ResourceNotFoundException.class);
-    gApi.changes().id(changeId).current().applyFix(fixId);
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.changes().id(changeId).current().applyFix(fixId));
   }
 
   @Test
@@ -728,9 +751,11 @@
     List<String> fixIds = getFixIds(robotCommentInfos);
     String fixId = Iterables.getOnlyElement(fixIds);
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("current");
-    gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId));
+    assertThat(thrown).hasMessageThat().contains("current");
   }
 
   @Test
@@ -783,9 +808,11 @@
     List<String> fixIds = getFixIds(robotCommentInfos);
     String fixId = Iterables.getOnlyElement(fixIds);
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("based");
-    gApi.changes().id(changeId).current().applyFix(fixId);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).current().applyFix(fixId));
+    assertThat(thrown).hasMessageThat().contains("based");
   }
 
   @Test
@@ -846,8 +873,9 @@
     String fixId = Iterables.getOnlyElement(fixIds);
     String nonExistentFixId = fixId + "_non-existent";
 
-    exception.expect(ResourceNotFoundException.class);
-    gApi.changes().id(changeId).current().applyFix(nonExistentFixId);
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.changes().id(changeId).current().applyFix(nonExistentFixId));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 0f2018d..73542ca 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -16,17 +16,17 @@
 
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.entities.Patch.COMMIT_MSG;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
 import static com.google.gerrit.extensions.restapi.testing.BinaryResultSubject.assertThat;
-import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
@@ -36,19 +36,25 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeEditDetailOption;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.FileInfo;
@@ -56,15 +62,11 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.restapi.change.ChangeEdits.EditMessage;
 import com.google.gerrit.server.restapi.change.ChangeEdits.Post;
 import com.google.gerrit.server.restapi.change.ChangeEdits.Put;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonReader;
 import com.google.inject.Inject;
@@ -80,11 +82,10 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.AfterClass;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
+@UseClockStep
 public class ChangeEditIT extends AbstractDaemonTest {
 
   private static final String FILE_NAME = "foo";
@@ -102,16 +103,6 @@
   private String changeId2;
   private PatchSet ps;
 
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Before
   public void setUp() throws Exception {
     changeId = newChange(admin.newIdent());
@@ -196,7 +187,7 @@
     adminRestSession.post(urlPublish(changeId)).assertNoContent();
     assertThat(getEdit(changeId)).isAbsent();
     PatchSet newCurrentPatchSet = getCurrentPatchSet(changeId);
-    assertThat(newCurrentPatchSet.getId()).isNotEqualTo(oldCurrentPatchSet.getId());
+    assertThat(newCurrentPatchSet.id()).isNotEqualTo(oldCurrentPatchSet.id());
     assertChangeMessages(
         changeId,
         ImmutableList.of(
@@ -249,13 +240,13 @@
     PatchSet currentPatchSet = getCurrentPatchSet(changeId2);
 
     Optional<EditInfo> originalEdit = getEdit(changeId2);
-    assertThat(originalEdit).value().baseRevision().isEqualTo(previousPatchSet.getRevision().get());
+    assertThat(originalEdit).value().baseRevision().isEqualTo(previousPatchSet.commitId().name());
     Timestamp beforeRebase = originalEdit.get().commit.committer.date;
     gApi.changes().id(changeId2).edit().rebase();
     ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_NEW);
     ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME2), CONTENT_NEW2);
     Optional<EditInfo> rebasedEdit = getEdit(changeId2);
-    assertThat(rebasedEdit).value().baseRevision().isEqualTo(currentPatchSet.getRevision().get());
+    assertThat(rebasedEdit).value().baseRevision().isEqualTo(currentPatchSet.commitId().name());
     assertThat(rebasedEdit).value().commit().committer().date().isNotEqualTo(beforeRebase);
   }
 
@@ -268,13 +259,13 @@
     PatchSet currentPatchSet = getCurrentPatchSet(changeId2);
 
     Optional<EditInfo> originalEdit = getEdit(changeId2);
-    assertThat(originalEdit).value().baseRevision().isEqualTo(previousPatchSet.getRevision().get());
+    assertThat(originalEdit).value().baseRevision().isEqualTo(previousPatchSet.commitId().name());
     Timestamp beforeRebase = originalEdit.get().commit.committer.date;
     adminRestSession.post(urlRebase(changeId2)).assertNoContent();
     ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_NEW);
     ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME2), CONTENT_NEW2);
     Optional<EditInfo> rebasedEdit = getEdit(changeId2);
-    assertThat(rebasedEdit).value().baseRevision().isEqualTo(currentPatchSet.getRevision().get());
+    assertThat(rebasedEdit).value().baseRevision().isEqualTo(currentPatchSet.commitId().name());
     assertThat(rebasedEdit).value().commit().committer().date().isNotEqualTo(beforeRebase);
   }
 
@@ -284,7 +275,7 @@
     createEmptyEditFor(changeId2);
     gApi.changes().id(changeId2).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
     Optional<EditInfo> edit = getEdit(changeId2);
-    assertThat(edit).value().baseRevision().isEqualTo(currentPatchSet.getRevision().get());
+    assertThat(edit).value().baseRevision().isEqualTo(currentPatchSet.commitId().name());
     PushOneCommit push =
         pushFactory.create(
             admin.newIdent(),
@@ -341,12 +332,15 @@
     createEmptyEditFor(changeId);
     String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("wrong Change-Id footer");
-    gApi.changes()
-        .id(changeId)
-        .edit()
-        .modifyCommitMessage(commitMessage.replaceAll(changeId, changeId2));
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () ->
+                gApi.changes()
+                    .id(changeId)
+                    .edit()
+                    .modifyCommitMessage(commitMessage.replaceAll(changeId, changeId2)));
+    assertThat(thrown).hasMessageThat().isEqualTo("wrong Change-Id footer");
   }
 
   @Test
@@ -355,12 +349,15 @@
     createEmptyEditFor(changeId);
     String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("missing Change-Id footer");
-    gApi.changes()
-        .id(changeId)
-        .edit()
-        .modifyCommitMessage(commitMessage.replaceAll("(Change-Id:).*", ""));
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () ->
+                gApi.changes()
+                    .id(changeId)
+                    .edit()
+                    .modifyCommitMessage(commitMessage.replaceAll("(Change-Id:).*", "")));
+    assertThat(thrown).hasMessageThat().isEqualTo("missing Change-Id footer");
   }
 
   @Test
@@ -368,9 +365,13 @@
     createEmptyEditFor(changeId);
     String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("New commit message cannot be same as existing commit message");
-    gApi.changes().id(changeId).edit().modifyCommitMessage(commitMessage);
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).edit().modifyCommitMessage(commitMessage));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("New commit message cannot be same as existing commit message");
   }
 
   @Test
@@ -378,9 +379,13 @@
     createEmptyEditFor(changeId);
     String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("New commit message cannot be same as existing commit message");
-    gApi.changes().id(changeId).edit().modifyCommitMessage(commitMessage + "\n\n");
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).edit().modifyCommitMessage(commitMessage + "\n\n"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("New commit message cannot be same as existing commit message");
   }
 
   @Test
@@ -431,7 +436,7 @@
     r = adminRestSession.getJsonAccept(urlEditMessage(changeId, true));
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
-      RevCommit commit = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+      RevCommit commit = rw.parseCommit(ObjectId.fromString(ps.commitId().name()));
       assertThat(readContentFromJson(r)).isEqualTo(commit.getFullMessage());
     }
 
@@ -635,16 +640,22 @@
   @Test
   public void writeNoChanges() throws Exception {
     createEmptyEditFor(changeId);
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("no changes were made");
-    gApi.changes().id(changeId).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_OLD));
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () ->
+                gApi.changes()
+                    .id(changeId)
+                    .edit()
+                    .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_OLD)));
+    assertThat(thrown).hasMessageThat().contains("no changes were made");
   }
 
   @Test
   public void editCommitMessageCopiesLabelScores() throws Exception {
     String cr = "Code-Review";
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType codeReview = Util.codeReview();
+      LabelType codeReview = TestLabels.codeReview();
       codeReview.setCopyAllScoresIfNoCodeChange(true);
       u.getConfig().getLabelSections().put(cr, codeReview);
       u.save();
@@ -737,7 +748,11 @@
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
 
     // Block default permission
-    block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
 
     // Create change as user
     PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
@@ -745,8 +760,7 @@
     r1.assertOkStatus();
 
     // Try to create edit as admin
-    exception.expect(AuthException.class);
-    createEmptyEditFor(r1.getChangeId());
+    assertThrows(AuthException.class, () -> createEmptyEditFor(r1.getChangeId()));
   }
 
   @Test
@@ -755,9 +769,11 @@
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     gApi.changes().id(changeId).current().submit();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(String.format("change %s is merged", change._number));
-    createArbitraryEditFor(changeId);
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> createArbitraryEditFor(changeId));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(String.format("change %s is merged", change._number));
   }
 
   @Test
@@ -765,9 +781,32 @@
     ChangeInfo change = gApi.changes().id(changeId).get();
     gApi.changes().id(changeId).abandon();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(String.format("change %s is abandoned", change._number));
-    createArbitraryEditFor(changeId);
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> createArbitraryEditFor(changeId));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(String.format("change %s is abandoned", change._number));
+  }
+
+  @Test
+  public void sha1sOfTwoChangesWithSameContentAfterEditDiffer() throws Exception {
+    ChangeInput changeInput = new ChangeInput();
+    changeInput.project = project.get();
+    changeInput.branch = "master";
+    changeInput.subject = "Empty change";
+    changeInput.status = ChangeStatus.NEW;
+
+    ChangeInfo info1 = gApi.changes().create(changeInput).get();
+    gApi.changes().id(info1._number).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
+    gApi.changes().id(info1._number).edit().publish(new PublishChangeEditInput());
+    info1 = gApi.changes().id(info1._number).get();
+
+    ChangeInfo info2 = gApi.changes().create(changeInput).get();
+    gApi.changes().id(info2._number).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
+    gApi.changes().id(info2._number).edit().publish(new PublishChangeEditInput());
+    info2 = gApi.changes().id(info2._number).get();
+
+    assertThat(info1.currentRevision).isNotEqualTo(info2.currentRevision);
   }
 
   private void createArbitraryEditFor(String changeId) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractForcePush.java b/javatests/com/google/gerrit/acceptance/git/AbstractForcePush.java
index 8ebb2bd..3b80312 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractForcePush.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractForcePush.java
@@ -16,14 +16,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.deleteRef;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.OK;
 import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.REJECTED_OTHER_REASON;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.transport.PushResult;
@@ -31,6 +34,7 @@
 import org.junit.Test;
 
 public abstract class AbstractForcePush extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void forcePushNotAllowed() throws Exception {
@@ -55,7 +59,11 @@
   @Test
   public void forcePushAllowed() throws Exception {
     ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
-    grant(project, "refs/*", Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()).force(true))
+        .update();
     PushOneCommit push1 =
         pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r1 = push1.to("refs/heads/master");
@@ -80,19 +88,31 @@
 
   @Test
   public void deleteNotAllowedWithOnlyPushPermission() throws Exception {
-    grant(project, "refs/*", Permission.PUSH, false);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()))
+        .update();
     assertDeleteRef(REJECTED_OTHER_REASON);
   }
 
   @Test
   public void deleteAllowedWithForcePushPermission() throws Exception {
-    grant(project, "refs/*", Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()).force(true))
+        .update();
     assertDeleteRef(OK);
   }
 
   @Test
   public void deleteAllowedWithDeletePermission() throws Exception {
-    grant(project, "refs/*", Permission.DELETE, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE).ref("refs/*").group(adminGroupUuid()).force(true))
+        .update();
     assertDeleteRef(OK);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java b/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java
index cdd0470..35f8270 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java
@@ -19,8 +19,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.FakeGroupAuditService;
 import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.pgm.http.jetty.JettyServer;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.audit.HttpAuditEvent;
 import com.google.inject.Inject;
 import java.util.Optional;
@@ -73,16 +73,15 @@
     assertThat(receivePack.what).endsWith("/git-receive-pack");
     assertThat(receivePack.params).isEmpty();
     assertThat(receivePack.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
+    assertThat(jettyServer.numActiveSessions()).isEqualTo(0);
   }
 
   @Test
-  @Sandboxed
   public void anonymousUploadPackAuditEventLog() throws Exception {
     uploadPackAuditEventLog(Constants.DEFAULT_REMOTE_NAME, Optional.empty());
   }
 
   @Test
-  @Sandboxed
   public void authenticatedUploadPackAuditEventLog() throws Exception {
     String remote = "authenticated";
     Config cfg = testRepo.git().getRepository().getConfig();
@@ -108,7 +107,6 @@
     assertThat(auditEvents).hasSize(2);
 
     HttpAuditEvent lsRemote = auditEvents.get(0);
-    // Repo URL doesn't include /a, so fetching doesn't cause authentication.
     assertThat(lsRemote.who.toString())
         .isEqualTo(
             accountId.map(id -> "IdentifiedUser[account " + id.get() + "]").orElse("ANONYMOUS"));
@@ -117,7 +115,7 @@
     assertThat(lsRemote.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
 
     HttpAuditEvent uploadPack = auditEvents.get(1);
-    assertThat(lsRemote.who.toString())
+    assertThat(uploadPack.who.toString())
         .isEqualTo(
             accountId.map(id -> "IdentifiedUser[account " + id.get() + "]").orElse("ANONYMOUS"));
     assertThat(uploadPack.what).endsWith("/git-upload-pack");
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 216677d..ea82013 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -17,12 +17,17 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.assertPushRejected;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.GitUtil.pushOne;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
@@ -33,10 +38,9 @@
 import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static java.util.Comparator.comparing;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 
@@ -46,17 +50,27 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.SkipProjectClone;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 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.BooleanProjectConfig;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -77,30 +91,18 @@
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
-import com.google.gerrit.extensions.common.testing.EditInfoSubject;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.receive.NoteDbPushOption;
 import com.google.gerrit.server.git.receive.ReceiveConstants;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
-import com.google.gerrit.server.git.validators.CommitValidators.ChangeIdValidator;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -117,6 +119,7 @@
 import java.util.stream.Stream;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -128,52 +131,51 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.After;
-import org.junit.AfterClass;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
 @SkipProjectClone
+@UseClockStep
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
   protected enum Protocol {
-    // TODO(dborowitz): TEST.
+    // Only test protocols which are actually served by the Gerrit server, since each separate test
+    // class is large and slow.
+    //
+    // This list excludes the test InProcessProtocol, which is used by large numbers of other
+    // acceptance tests. Small tests of InProcessProtocol are still possible, without incurring a
+    // new large slow test.
     SSH,
     HTTP
   }
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   private static String NEW_CHANGE_INDICATOR = " [NEW]";
   private LabelType patchSetLock;
 
-  @Inject private DynamicSet<CommitValidationListener> commitValidators;
-
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Before
   public void setUpPatchSetLock() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
-      patchSetLock = Util.patchSetLock();
+      patchSetLock = TestLabels.patchSetLock();
       u.getConfig().getLabelSections().put(patchSetLock.getName(), patchSetLock);
-      AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(patchSetLock.getName()),
-          0,
-          1,
-          anonymousUsers,
-          "refs/heads/*");
       u.save();
     }
-    grant(project, "refs/heads/*", Permission.LABEL + "Patch-Set-Lock");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(patchSetLock.getName())
+                .ref("refs/heads/*")
+                .group(ANONYMOUS_USERS)
+                .range(0, 1))
+        .add(
+            allowLabel(patchSetLock.getName())
+                .ref("refs/heads/*")
+                .group(adminGroupUuid())
+                .range(0, 1))
+        .update();
   }
 
   @After
@@ -859,7 +861,7 @@
     assertThat(r.getChange().change().isWorkInProgress()).isTrue();
 
     // Admin user trying to move from WIP to ready should succeed.
-    GitUtil.fetch(testRepo, r.getPatchSet().getRefName() + ":ps");
+    GitUtil.fetch(testRepo, r.getPatchSet().refName() + ":ps");
     testRepo.reset("ps");
     r = amendChange(r.getChangeId(), "refs/for/master%ready", user, testRepo);
     r.assertOkStatus();
@@ -875,7 +877,7 @@
     assertThat(r.getChange().change().isWorkInProgress()).isFalse();
 
     // Admin user trying to move from ready to WIP should succeed.
-    GitUtil.fetch(testRepo, r.getPatchSet().getRefName() + ":ps");
+    GitUtil.fetch(testRepo, r.getPatchSet().refName() + ":ps");
     testRepo.reset("ps");
     r = amendChange(r.getChangeId(), "refs/for/master%wip", admin, testRepo);
     r.assertOkStatus();
@@ -886,16 +888,38 @@
 
     // Non owner, non admin and non project owner cannot flip wip bit:
     TestAccount user2 = accountCreator.user2();
-    grant(
-        project, "refs/*", Permission.FORGE_COMMITTER, false, SystemGroupBackend.REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allow(Permission.FORGE_COMMITTER)
+                .ref("refs/*")
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
     TestRepository<?> user2Repo = cloneProject(project, user2);
-    GitUtil.fetch(user2Repo, r.getPatchSet().getRefName() + ":ps");
+    GitUtil.fetch(user2Repo, r.getPatchSet().refName() + ":ps");
     user2Repo.reset("ps");
     r = amendChange(r.getChangeId(), "refs/for/master%ready", user2, user2Repo);
-    r.assertErrorStatus(ReceiveConstants.ONLY_CHANGE_OWNER_OR_PROJECT_OWNER_CAN_MODIFY_WIP);
+    r.assertErrorStatus(ReceiveConstants.ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP);
+
+    // Non owner, non admin and non project owner with toggleWipState should succeed.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
+                .ref(RefNames.REFS_HEADS + "*")
+                .group(REGISTERED_USERS))
+        .update();
+    r = amendChange(r.getChangeId(), "refs/for/master%ready", user2, user2Repo);
+    r.assertOkStatus();
 
     // Project owner trying to move from WIP to ready should succeed.
-    allow("refs/*", Permission.OWNER, SystemGroupBackend.REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
     r = amendChange(r.getChangeId(), "refs/for/master%ready", user2, user2Repo);
     r.assertOkStatus();
   }
@@ -1196,14 +1220,17 @@
   @Test
   public void pushWithMultipleApprovals() throws Exception {
     LabelType Q =
-        category("Custom-Label", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
-    AccountGroup.UUID anon = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+        label("Custom-Label", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
     String heads = "refs/heads/*";
     try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.forLabel("Custom-Label"), -1, 1, anon, heads);
       u.getConfig().getLabelSections().put(Q.getName(), Q);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Custom-Label").ref(heads).group(ANONYMOUS_USERS).range(-1, 1))
+        .update();
 
     RevCommit c =
         commitBuilder()
@@ -1225,40 +1252,6 @@
   }
 
   @Test
-  @GerritConfig(name = "receive.allowPushToRefsChanges", value = "true")
-  public void pushToRefsChangesAllowed() throws Exception {
-    PushOneCommit.Result r = pushOneCommitToRefsChanges();
-    r.assertOkStatus();
-  }
-
-  @Test
-  public void pushNewPatchsetToRefsChanges() throws Exception {
-    PushOneCommit.Result r = pushOneCommitToRefsChanges();
-    r.assertErrorStatus("upload to refs/changes not allowed");
-  }
-
-  @Test
-  @GerritConfig(name = "receive.allowPushToRefsChanges", value = "false")
-  public void pushToRefsChangesNotAllowed() throws Exception {
-    PushOneCommit.Result r = pushOneCommitToRefsChanges();
-    r.assertErrorStatus("upload to refs/changes not allowed");
-  }
-
-  private PushOneCommit.Result pushOneCommitToRefsChanges() throws Exception {
-    PushOneCommit.Result r = pushTo("refs/for/master");
-    r.assertOkStatus();
-    PushOneCommit push =
-        pushFactory.create(
-            admin.newIdent(),
-            testRepo,
-            PushOneCommit.SUBJECT,
-            "b.txt",
-            "anotherContent",
-            r.getChangeId());
-    return push.to("refs/changes/" + r.getChange().change().getId().get());
-  }
-
-  @Test
   public void pushNewPatchsetToPatchSetLockedChange() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
@@ -1364,7 +1357,11 @@
     r.assertOkStatus();
 
     setUseSignedOffBy(InheritableBoolean.TRUE);
-    block(project, "refs/heads/master", Permission.FORGE_COMMITTER, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.FORGE_COMMITTER).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
 
     push =
         pushFactory.create(
@@ -1420,7 +1417,7 @@
 
     // create a second change as user (depends on the change from admin)
     TestRepository<?> userRepo = cloneProject(project, user);
-    GitUtil.fetch(userRepo, r.getPatchSet().getRefName() + ":change");
+    GitUtil.fetch(userRepo, r.getPatchSet().refName() + ":change");
     userRepo.reset("change");
     push =
         pushFactory.create(
@@ -1434,7 +1431,11 @@
 
   @Test
   public void pushSameCommitTwiceUsingMagicBranchBaseOption() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result rBase = pushTo("refs/heads/master");
     rBase.assertOkStatus();
 
@@ -1530,8 +1531,8 @@
 
     // Check that a change was created for each.
     for (RevCommit c : commits) {
-      assertThat(byCommit(c).change().getSubject())
-          .named("change for " + c.name())
+      assertWithMessage("change for " + c.name())
+          .that(byCommit(c).change().getSubject())
           .isEqualTo(c.getShortMessage());
     }
 
@@ -1543,9 +1544,9 @@
       RevCommit c2 = commits2.get(i);
       String name = "change for " + c2.name();
       ChangeData cd = byCommit(c);
-      assertThat(cd.change().getSubject()).named(name).isEqualTo(c2.getShortMessage());
-      assertThat(getPatchSetRevisions(cd))
-          .named(name)
+      assertWithMessage(name).that(cd.change().getSubject()).isEqualTo(c2.getShortMessage());
+      assertWithMessage(name)
+          .that(getPatchSetRevisions(cd))
           .containsExactlyEntriesIn(ImmutableMap.of(1, c.name(), 2, c2.name()));
     }
 
@@ -1608,37 +1609,13 @@
     RemoteRefUpdate refUpdate = r.getRemoteUpdate(ref);
     assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
     String reason =
-        String.format(
-            "commit %s: missing Change-Id in message footer", c.toObjectId().abbreviate(7).name());
+        String.format("commit %s: missing Change-Id in message footer", abbreviateName(c));
     assertThat(refUpdate.getMessage()).isEqualTo(reason);
 
     assertThat(r.getMessages()).contains("\nERROR: " + reason);
   }
 
   @Test
-  @GerritConfig(name = "receive.allowPushToRefsChanges", value = "true")
-  public void testPushWithChangedChangeId() throws Exception {
-    PushOneCommit.Result r = pushTo("refs/for/master");
-    r.assertOkStatus();
-    PushOneCommit push =
-        pushFactory.create(
-            admin.newIdent(),
-            testRepo,
-            PushOneCommit.SUBJECT
-                + "\n\n"
-                + "Change-Id: I55eab7c7a76e95005fa9cc469aa8f9fc16da9eba\n",
-            "b.txt",
-            "anotherContent",
-            r.getChangeId());
-    r = push.to("refs/changes/" + r.getChange().change().getId().get());
-    r.assertErrorStatus(
-        String.format(
-            "commit %s: %s",
-            r.getCommit().abbreviate(RevId.ABBREV_LEN).name(),
-            ChangeIdValidator.CHANGE_ID_MISMATCH_MSG));
-  }
-
-  @Test
   public void pushWithMultipleChangeIds() throws Exception {
     testPushWithMultipleChangeIds();
   }
@@ -1814,30 +1791,11 @@
   }
 
   @Test
-  @GerritConfig(name = "receive.allowPushToRefsChanges", value = "true")
-  public void accidentallyPushNewPatchSetDirectlyToBranchAndRecoverByPushingToRefsChanges()
-      throws Exception {
-    Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch();
-    ChangeData cd = byChangeId(id);
-    String ps1Rev = Iterables.getOnlyElement(cd.patchSets()).getRevision().get();
-
-    String r = "refs/changes/" + id;
-    assertPushOk(pushHead(testRepo, r, false), r);
-
-    // Added a new patch set and auto-closed the change.
-    cd = byChangeId(id);
-    assertThat(cd.change().isMerged()).isTrue();
-    assertThat(getPatchSetRevisions(cd))
-        .containsExactlyEntriesIn(
-            ImmutableMap.of(1, ps1Rev, 2, testRepo.getRepository().resolve("HEAD").name()));
-  }
-
-  @Test
   public void accidentallyPushNewPatchSetDirectlyToBranchAndCantRecoverByPushingToRefsFor()
       throws Exception {
     Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch();
     ChangeData cd = byChangeId(id);
-    String ps1Rev = Iterables.getOnlyElement(cd.patchSets()).getRevision().get();
+    String ps1Rev = Iterables.getOnlyElement(cd.patchSets()).commitId().name();
 
     String r = "refs/for/master";
     assertPushRejected(pushHead(testRepo, r, false), r, "no new changes");
@@ -1850,7 +1808,11 @@
 
   @Test
   public void forcePushAbandonedChange() throws Exception {
-    grant(project, "refs/*", Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()).force(true))
+        .update();
     PushOneCommit push1 =
         pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r = push1.to("refs/for/master");
@@ -1923,7 +1885,7 @@
   @Test
   public void pushNewPatchsetOverridingStickyLabel() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType codeReview = Util.codeReview();
+      LabelType codeReview = TestLabels.codeReview();
       codeReview.setCopyMaxScore(true);
       u.getConfig().getLabelSections().put(codeReview.getName(), codeReview);
       u.save();
@@ -1946,7 +1908,11 @@
   @Test
   public void createChangeForMergedCommit() throws Exception {
     String master = "refs/heads/master";
-    grant(project, master, Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(master).group(adminGroupUuid()).force(true))
+        .update();
 
     // Update master with a direct push.
     RevCommit c1 = testRepo.commit().message("Non-change 1").create();
@@ -2045,7 +2011,11 @@
   @Test
   public void mergedOptionWithExistingChangeInsertsPatchSet() throws Exception {
     String master = "refs/heads/master";
-    grant(project, master, Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(master).group(adminGroupUuid()).force(true))
+        .update();
 
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
@@ -2238,53 +2208,6 @@
     amendChanges(unChanged.toObjectId(), commits, "refs/for/master%publish-comments");
   }
 
-  @Test
-  public void pushWithDraftOptionIsDisabledPerDefault() throws Exception {
-    for (String ref : ImmutableSet.of("refs/drafts/master", "refs/for/master%draft")) {
-      PushOneCommit.Result r = pushTo(ref);
-      r.assertErrorStatus();
-      r.assertMessage("draft workflow is disabled");
-    }
-  }
-
-  @GerritConfig(name = "change.allowDrafts", value = "true")
-  @Test
-  public void pushDraftGetsPrivateChange() throws Exception {
-    String changeId1 = createChange("refs/drafts/master").getChangeId();
-    String changeId2 = createChange("refs/for/master%draft").getChangeId();
-
-    ChangeInfo info1 = gApi.changes().id(changeId1).get();
-    ChangeInfo info2 = gApi.changes().id(changeId2).get();
-
-    assertThat(info1.status).isEqualTo(ChangeStatus.NEW);
-    assertThat(info2.status).isEqualTo(ChangeStatus.NEW);
-    assertThat(info1.isPrivate).isTrue();
-    assertThat(info2.isPrivate).isTrue();
-    assertThat(info1.revisions).hasSize(1);
-    assertThat(info2.revisions).hasSize(1);
-  }
-
-  @GerritConfig(name = "change.allowDrafts", value = "true")
-  @Sandboxed
-  @Test
-  public void pushWithDraftOptionToExistingNewChangeGetsChangeEdit() throws Exception {
-    String changeId = createChange().getChangeId();
-    EditInfoSubject.assertThat(getEdit(changeId)).isAbsent();
-
-    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
-    ChangeStatus originalChangeStatus = changeInfo.status;
-
-    PushOneCommit.Result result = amendChange(changeId, "refs/drafts/master");
-    result.assertOkStatus();
-
-    changeInfo = gApi.changes().id(changeId).get();
-    assertThat(changeInfo.status).isEqualTo(originalChangeStatus);
-    assertThat(changeInfo.isPrivate).isNull();
-    assertThat(changeInfo.revisions).hasSize(1);
-
-    EditInfoSubject.assertThat(getEdit(changeId)).isPresent();
-  }
-
   @GerritConfig(name = "receive.maxBatchCommits", value = "2")
   @Test
   public void maxBatchCommits() throws Exception {
@@ -2294,24 +2217,16 @@
   @GerritConfig(name = "receive.maxBatchCommits", value = "2")
   @Test
   public void maxBatchCommitsWithDefaultValidator() throws Exception {
-    TestValidator validator = new TestValidator();
-    RegistrationHandle handle = commitValidators.add("test-validator", validator);
-    try {
+    try (Registration registration = extensionRegistry.newRegistration().add(new TestValidator())) {
       testMaxBatchCommits();
-    } finally {
-      handle.remove();
     }
   }
 
   @GerritConfig(name = "receive.maxBatchCommits", value = "2")
   @Test
   public void maxBatchCommitsWithValidateAllCommitsValidator() throws Exception {
-    TestValidator validator = new TestValidator(true);
-    RegistrationHandle handle = commitValidators.add("test-validator", validator);
-    try {
+    try (Registration registration = extensionRegistry.newRegistration().add(new TestValidator())) {
       testMaxBatchCommits();
-    } finally {
-      handle.remove();
     }
   }
 
@@ -2369,10 +2284,7 @@
   public void skipValidation() throws Exception {
     String master = "refs/heads/master";
     TestValidator validator = new TestValidator();
-    RegistrationHandle handle = commitValidators.add("test-validator", validator);
-    RegistrationHandle handle2 = null;
-
-    try {
+    try (Registration registration = extensionRegistry.newRegistration().add(validator)) {
       // Validation listener is called on normal push
       PushOneCommit push =
           pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
@@ -2401,20 +2313,16 @@
       // Validation listener that needs to validate all commits gets called even
       // when the skip option is used.
       TestValidator validator2 = new TestValidator(true);
-      handle2 = commitValidators.add("test-validator-2", validator2);
-      PushOneCommit push4 =
-          pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
-      push4.setPushOptions(ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
-      r = push4.to(master);
-      r.assertOkStatus();
-      // First listener was not called; its count remains the same.
-      assertThat(validator.count()).isEqualTo(1);
-      // Second listener was called.
-      assertThat(validator2.count()).isEqualTo(1);
-    } finally {
-      handle.remove();
-      if (handle2 != null) {
-        handle2.remove();
+      try (Registration registration2 = extensionRegistry.newRegistration().add(validator2)) {
+        PushOneCommit push4 =
+            pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
+        push4.setPushOptions(ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
+        r = push4.to(master);
+        r.assertOkStatus();
+        // First listener was not called; its count remains the same.
+        assertThat(validator.count()).isEqualTo(1);
+        // Second listener was called.
+        assertThat(validator2.count()).isEqualTo(1);
       }
     }
   }
@@ -2435,12 +2343,19 @@
     pr = pushOne(testRepo, c.name(), ref, false, false, opts);
     assertPushRejected(pr, ref, "NoteDb update requires access database permission");
 
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     pr = pushOne(testRepo, c.name(), ref, false, false, opts);
     assertPushRejected(pr, ref, "prohibited by Gerrit: not permitted: create");
 
-    grant(project, "refs/changes/*", Permission.CREATE);
-    grant(project, "refs/changes/*", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/changes/*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref("refs/changes/*").group(adminGroupUuid()))
+        .update();
     grantSkipValidation(project, "refs/changes/*", REGISTERED_USERS);
     pr = pushOne(testRepo, c.name(), ref, false, false, opts);
     assertPushOk(pr, ref);
@@ -2489,9 +2404,9 @@
     assertThat(pr.getMessages())
         .contains(
             "warning: no changes between prior commit "
-                + c.abbreviate(7).name()
+                + abbreviateName(c)
                 + " and new commit "
-                + amended.abbreviate(7).name());
+                + abbreviateName(amended));
   }
 
   @Test
@@ -2517,8 +2432,7 @@
     pr = pushHead(testRepo, r, false);
     assertPushOk(pr, r);
     assertThat(pr.getMessages())
-        .contains(
-            "warning: " + amended.abbreviate(7).name() + ": no files changed, message updated");
+        .contains("warning: " + abbreviateName(amended) + ": no files changed, message updated");
   }
 
   @Test
@@ -2542,8 +2456,7 @@
     pr = pushHead(testRepo, r, false);
     assertPushOk(pr, r);
     assertThat(pr.getMessages())
-        .contains(
-            "warning: " + amended.abbreviate(7).name() + ": no files changed, author changed");
+        .contains("warning: " + abbreviateName(amended) + ": no files changed, author changed");
   }
 
   @Test
@@ -2570,7 +2483,7 @@
     pr = pushHead(testRepo, r, false);
     assertPushOk(pr, r);
     assertThat(pr.getMessages())
-        .contains("warning: " + amended.abbreviate(7).name() + ": no files changed, was rebased");
+        .contains("warning: " + abbreviateName(amended) + ": no files changed, was rebased");
   }
 
   @Test
@@ -2641,6 +2554,76 @@
             + "\n");
   }
 
+  @Test
+  public void cannotPushTheSameCommitTwiceForReviewToTheSameBranch() throws Exception {
+    testCannotPushTheSameCommitTwiceForReviewToTheSameBranch();
+  }
+
+  @Test
+  public void cannotPushTheSameCommitTwiceForReviewToTheSameBranchCreateNewChangeForAllNotInTarget()
+      throws Exception {
+    enableCreateNewChangeForAllNotInTarget();
+    testCannotPushTheSameCommitTwiceForReviewToTheSameBranch();
+  }
+
+  private void testCannotPushTheSameCommitTwiceForReviewToTheSameBranch() throws Exception {
+    setRequireChangeId(InheritableBoolean.FALSE);
+
+    // create a commit without Change-Id
+    testRepo
+        .branch("HEAD")
+        .commit()
+        .author(user.newIdent())
+        .committer(user.newIdent())
+        .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
+        .message(PushOneCommit.SUBJECT)
+        .create();
+
+    // push the commit for review to create a change
+    PushResult r = pushHead(testRepo, "refs/for/master");
+    assertPushOk(r, "refs/for/master");
+
+    // try to push the same commit for review again to create another change on the same branch,
+    // it's expected that this is rejected with "no new changes"
+    r = pushHead(testRepo, "refs/for/master");
+    assertPushRejected(r, "refs/for/master", "no new changes");
+  }
+
+  @Test
+  public void pushTheSameCommitTwiceForReviewToDifferentBranches() throws Exception {
+    setRequireChangeId(InheritableBoolean.FALSE);
+
+    // create a commit without Change-Id
+    testRepo
+        .branch("HEAD")
+        .commit()
+        .author(user.newIdent())
+        .committer(user.newIdent())
+        .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
+        .message(PushOneCommit.SUBJECT)
+        .create();
+
+    // push the commit for review to create a change
+    PushResult r = pushHead(testRepo, "refs/for/master");
+    assertPushOk(r, "refs/for/master");
+
+    // create another branch
+    gApi.projects().name(project.get()).branch("otherBranch").create(new BranchInput());
+
+    // try to push the same commit for review again to create a change on another branch,
+    // it's expected that this is rejected with "no new changes" since
+    // CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET is false
+    r = pushHead(testRepo, "refs/for/otherBranch");
+    assertPushRejected(r, "refs/for/otherBranch", "no new changes");
+
+    enableCreateNewChangeForAllNotInTarget();
+
+    // try to push the same commit for review again to create a change on another branch,
+    // now it should succeed since CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET is true
+    r = pushHead(testRepo, "refs/for/otherBranch");
+    assertPushOk(r, "refs/for/otherBranch");
+  }
+
   private DraftInput newDraft(String path, int line, String message) {
     DraftInput d = new DraftInput();
     d.path = path;
@@ -2685,11 +2668,11 @@
       ChangeData cd = byCommit(c);
       String name = "reviewers for " + (i + 1);
       if (expectedReviewer != null) {
-        assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.id());
+        assertWithMessage(name).that(cd.reviewers().all()).containsExactly(expectedReviewer.id());
         // Remove reviewer from PS1 so we can test adding this same reviewer on PS2 below.
         gApi.changes().id(cd.getId().get()).reviewer(expectedReviewer.id().toString()).remove();
       }
-      assertThat(byCommit(c).reviewers().all()).named(name).isEmpty();
+      assertWithMessage(name).that(byCommit(c).reviewers().all()).isEmpty();
     }
 
     List<RevCommit> commits2 = amendChanges(initialHead, commits, r);
@@ -2698,9 +2681,9 @@
       ChangeData cd = byCommit(c);
       String name = "reviewers for " + (i + 1);
       if (expectedReviewer != null) {
-        assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.id());
+        assertWithMessage(name).that(cd.reviewers().all()).containsExactly(expectedReviewer.id());
       } else {
-        assertThat(byCommit(c).reviewers().all()).named(name).isEmpty();
+        assertWithMessage(name).that(byCommit(c).reviewers().all()).isEmpty();
       }
     }
   }
@@ -2767,20 +2750,20 @@
   private static Map<Integer, String> getPatchSetRevisions(ChangeData cd) throws Exception {
     Map<Integer, String> revisions = new HashMap<>();
     for (PatchSet ps : cd.patchSets()) {
-      revisions.put(ps.getPatchSetId(), ps.getRevision().get());
+      revisions.put(ps.number(), ps.commitId().name());
     }
     return revisions;
   }
 
   private ChangeData byCommit(ObjectId id) throws Exception {
     List<ChangeData> cds = queryProvider.get().byCommit(id);
-    assertThat(cds).named("change for " + id.name()).hasSize(1);
+    assertWithMessage("change for " + id.name()).that(cds).hasSize(1);
     return cds.get(0);
   }
 
   private ChangeData byChangeId(Change.Id id) throws Exception {
     List<ChangeData> cds = queryProvider.get().byLegacyChangeId(id);
-    assertThat(cds).named("change " + id).hasSize(1);
+    assertWithMessage("change " + id).that(cds).hasSize(1);
     return cds.get(0);
   }
 
@@ -2808,13 +2791,14 @@
   private void grantSkipValidation(Project.NameKey project, String ref, AccountGroup.UUID groupUuid)
       throws Exception {
     // See SKIP_VALIDATION implementation in default permission backend.
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.FORGE_AUTHOR, groupUuid, ref);
-      Util.allow(u.getConfig(), Permission.FORGE_COMMITTER, groupUuid, ref);
-      Util.allow(u.getConfig(), Permission.FORGE_SERVER, groupUuid, ref);
-      Util.allow(u.getConfig(), Permission.PUSH_MERGE, groupUuid, "refs/for/" + ref);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.FORGE_AUTHOR).ref(ref).group(groupUuid))
+        .add(allow(Permission.FORGE_COMMITTER).ref(ref).group(groupUuid))
+        .add(allow(Permission.FORGE_SERVER).ref(ref).group(groupUuid))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/for/" + ref).group(groupUuid))
+        .update();
   }
 
   private PushOneCommit.Result amendChange(String changeId, String ref) throws Exception {
@@ -2833,4 +2817,8 @@
         ? infos.stream().map(a -> a.email).collect(toImmutableList())
         : ImmutableList.of();
   }
+
+  private String abbreviateName(AnyObjectId id) throws Exception {
+    return ObjectIds.abbreviateName(id, testRepo.getRevWalk().getObjectReader());
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
index c02a5ed..7cfb0f2 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
@@ -17,21 +17,30 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+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.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Change;
+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;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -48,6 +57,7 @@
 
 public abstract class AbstractSubmitOnPush extends AbstractDaemonTest {
   @Inject private ApprovalsUtil approvalsUtil;
+  @Inject private ProjectOperations projectOperations;
 
   @Before
   public void blockAnonymous() throws Exception {
@@ -56,7 +66,11 @@
 
   @Test
   public void submitOnPush() throws Exception {
-    grant(project, "refs/for/refs/heads/master", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r = pushTo("refs/for/master%submit");
     r.assertOkStatus();
     r.assertChange(Change.Status.MERGED, null, admin);
@@ -66,7 +80,11 @@
 
   @Test
   public void submitOnPushToRefsMetaConfig() throws Exception {
-    grant(project, "refs/for/refs/meta/config", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/meta/config").group(adminGroupUuid()))
+        .update();
 
     git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
     testRepo.reset(RefNames.REFS_CONFIG);
@@ -84,7 +102,11 @@
     push("refs/heads/master", "one change", "a.txt", "some content");
     testRepo.reset(objectId);
 
-    grant(project, "refs/for/refs/heads/master", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r =
         push("refs/for/master%submit", "other change", "a.txt", "other content");
     r.assertErrorStatus();
@@ -100,7 +122,11 @@
     push(master, "one change", "a.txt", "some content");
     testRepo.reset(objectId);
 
-    grant(project, "refs/for/refs/heads/master", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r =
         push("refs/for/master%submit", "other change", "b.txt", "other content");
     r.assertOkStatus();
@@ -113,7 +139,11 @@
     PushOneCommit.Result r =
         push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
 
-    grant(project, "refs/for/refs/heads/master", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
     r =
         push(
             "refs/for/master%submit",
@@ -153,7 +183,11 @@
 
   @Test
   public void mergeOnPushToBranch() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r =
         push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
     r.assertOkStatus();
@@ -162,28 +196,32 @@
     assertCommit(project, "refs/heads/master");
 
     ChangeData cd =
-        Iterables.getOnlyElement(queryProvider.get().byKey(new Change.Key(r.getChangeId())));
+        Iterables.getOnlyElement(queryProvider.get().byKey(Change.key(r.getChangeId())));
     RevCommit c = r.getCommit();
-    PatchSet.Id psId = cd.currentPatchSet().getId();
+    PatchSet.Id psId = cd.currentPatchSet().id();
     assertThat(psId.get()).isEqualTo(1);
     assertThat(cd.change().isMerged()).isTrue();
     assertSubmitApproval(psId);
 
     assertThat(cd.patchSets()).hasSize(1);
-    assertThat(cd.patchSet(psId).getRevision().get()).isEqualTo(c.name());
+    assertThat(cd.patchSet(psId).commitId()).isEqualTo(c);
   }
 
   @Test
   public void correctNewRevOnMergeByPushToBranch() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
     push("refs/for/master", PushOneCommit.SUBJECT, "one.txt", "One");
     PushOneCommit.Result r = push("refs/for/master", PushOneCommit.SUBJECT, "two.txt", "Two");
     startEventRecorder();
     git().push().setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master")).call();
     List<ChangeMergedEvent> changeMergedEvents =
         eventRecorder.getChangeMergedEvents(project.get(), "refs/heads/master", 2);
-    assertThat(changeMergedEvents.get(0).newRev).isEqualTo(r.getPatchSet().getRevision().get());
-    assertThat(changeMergedEvents.get(1).newRev).isEqualTo(r.getPatchSet().getRevision().get());
+    assertThat(changeMergedEvents.get(0).newRev).isEqualTo(r.getPatchSet().commitId().name());
+    assertThat(changeMergedEvents.get(1).newRev).isEqualTo(r.getPatchSet().commitId().name());
   }
 
   @Test
@@ -191,10 +229,14 @@
     enableCreateNewChangeForAllNotInTarget();
     String master = "refs/heads/master";
     String other = "refs/heads/other";
-    grant(project, master, Permission.PUSH);
-    grant(project, other, Permission.CREATE);
-    grant(project, other, Permission.PUSH);
-    RevCommit masterRev = getRemoteHead();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(master).group(adminGroupUuid()))
+        .add(allow(Permission.CREATE).ref(other).group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(other).group(adminGroupUuid()))
+        .update();
+    RevCommit masterRev = projectOperations.project(project).getHead("master");
     pushCommitTo(masterRev, other);
     PushOneCommit.Result r = createChange();
     r.assertOkStatus();
@@ -202,7 +244,7 @@
     pushCommitTo(commit, master);
     assertCommit(project, master);
     ChangeData cd =
-        Iterables.getOnlyElement(queryProvider.get().byKey(new Change.Key(r.getChangeId())));
+        Iterables.getOnlyElement(queryProvider.get().byKey(Change.key(r.getChangeId())));
     assertThat(cd.change().isMerged()).isTrue();
 
     RemoteRefUpdate.Status status = pushCommitTo(commit, "refs/for/other");
@@ -211,8 +253,8 @@
     pushCommitTo(commit, other);
     assertCommit(project, other);
 
-    for (ChangeData c : queryProvider.get().byKey(new Change.Key(r.getChangeId()))) {
-      if (c.change().getDest().get().equals(other)) {
+    for (ChangeData c : queryProvider.get().byKey(Change.key(r.getChangeId()))) {
+      if (c.change().getDest().branch().equals(other)) {
         assertThat(c.change().isMerged()).isTrue();
       }
     }
@@ -228,7 +270,11 @@
 
   @Test
   public void mergeOnPushToBranchWithNewPatchset() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     RevCommit c1 = r.getCommit();
@@ -256,13 +302,17 @@
     assertSubmitApproval(psId2);
 
     assertThat(cd.patchSets()).hasSize(2);
-    assertThat(cd.patchSet(psId1).getRevision().get()).isEqualTo(c1.name());
-    assertThat(cd.patchSet(psId2).getRevision().get()).isEqualTo(c2.name());
+    assertThat(cd.patchSet(psId1).commitId()).isEqualTo(c1);
+    assertThat(cd.patchSet(psId2).commitId()).isEqualTo(c2);
   }
 
   @Test
   public void mergeOnPushToBranchWithOldPatchset() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     RevCommit c1 = r.getCommit();
@@ -273,26 +323,30 @@
     r = amendChange(changeId);
     ChangeData cd = r.getChange();
     PatchSet.Id psId2 = cd.change().currentPatchSetId();
-    assertThat(psId2.getParentKey()).isEqualTo(psId1.getParentKey());
+    assertThat(psId2.changeId()).isEqualTo(psId1.changeId());
     assertThat(psId2.get()).isEqualTo(2);
 
     testRepo.reset(c1);
     assertPushOk(pushHead(testRepo, "refs/heads/master", false), "refs/heads/master");
 
-    cd = changeDataFactory.create(project, psId1.getParentKey());
+    cd = changeDataFactory.create(project, psId1.changeId());
     Change c = cd.change();
     assertThat(c.isMerged()).isTrue();
     assertThat(c.currentPatchSetId()).isEqualTo(psId1);
-    assertThat(cd.patchSets().stream().map(PatchSet::getId).collect(toList()))
+    assertThat(cd.patchSets().stream().map(PatchSet::id).collect(toList()))
         .containsExactly(psId1, psId2);
   }
 
   @Test
   public void mergeMultipleOnPushToBranchWithNewPatchset() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
 
     // Create 2 changes.
-    ObjectId initialHead = getRemoteHead();
+    ObjectId initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result r1 = createChange("Change 1", "a", "a");
     r1.assertOkStatus();
     PushOneCommit.Result r2 = createChange("Change 2", "b", "b");
@@ -323,27 +377,96 @@
     assertThat(cd2.change().isMerged()).isTrue();
     PatchSet.Id psId2_2 = cd2.change().currentPatchSetId();
     assertThat(psId2_2.get()).isEqualTo(2);
-    assertThat(cd2.patchSet(psId2_1).getRevision().get()).isEqualTo(c2_1.name());
-    assertThat(cd2.patchSet(psId2_2).getRevision().get()).isEqualTo(c2_2.name());
+    assertThat(cd2.patchSet(psId2_1).commitId()).isEqualTo(c2_1);
+    assertThat(cd2.patchSet(psId2_2).commitId()).isEqualTo(c2_2);
 
     ChangeData cd1 = r1.getChange();
     assertThat(cd1.change().isMerged()).isTrue();
     PatchSet.Id psId1_2 = cd1.change().currentPatchSetId();
     assertThat(psId1_2.get()).isEqualTo(2);
-    assertThat(cd1.patchSet(psId1_1).getRevision().get()).isEqualTo(c1_1.name());
-    assertThat(cd1.patchSet(psId1_2).getRevision().get()).isEqualTo(c1_2.name());
+    assertThat(cd1.patchSet(psId1_1).commitId()).isEqualTo(c1_1);
+    assertThat(cd1.patchSet(psId1_2).commitId()).isEqualTo(c1_2);
+  }
+
+  @Test
+  public void pushForSubmitWithNotifyOption() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
+
+    TestAccount user = accountCreator.user();
+    String pushSpec = "refs/for/master%reviewer=" + user.email();
+    sender.clear();
+
+    PushOneCommit.Result result = pushTo(pushSpec + ",submit,notify=" + NotifyHandling.NONE);
+    result.assertOkStatus();
+    assertThat(sender.getMessages()).isEmpty();
+
+    sender.clear();
+    result = pushTo(pushSpec + ",submit,notify=" + NotifyHandling.OWNER);
+    result.assertOkStatus();
+    assertThat(sender.getMessages()).isEmpty();
+
+    sender.clear();
+    result = pushTo(pushSpec + ",submit,notify=" + NotifyHandling.OWNER_REVIEWERS);
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user, null);
+
+    sender.clear();
+    result = pushTo(pushSpec + ",submit,notify=" + NotifyHandling.ALL);
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user, null);
+
+    sender.clear();
+    result = pushTo(pushSpec + ",submit"); // default is notify = ALL
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user, null);
+  }
+
+  @Test
+  public void pushForSubmitWithNotifyingUsersExplicitly() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
+
+    TestAccount user = accountCreator.user();
+    String pushSpec = "refs/for/master%reviewer=" + user.email() + ",cc=" + user.email();
+
+    TestAccount user2 = accountCreator.user2();
+
+    sender.clear();
+    PushOneCommit.Result result =
+        pushTo(pushSpec + ",submit,notify=" + NotifyHandling.NONE + ",notify-to=" + user2.email());
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user2, RecipientType.TO);
+
+    sender.clear();
+    result =
+        pushTo(pushSpec + ",submit,notify=" + NotifyHandling.NONE + ",notify-cc=" + user2.email());
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user2, RecipientType.CC);
+
+    sender.clear();
+    result =
+        pushTo(pushSpec + ",submit,notify=" + NotifyHandling.NONE + ",notify-bcc=" + user2.email());
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user2, RecipientType.BCC);
   }
 
   private PatchSetApproval getSubmitter(PatchSet.Id patchSetId) throws Exception {
-    ChangeNotes notes = notesFactory.createChecked(project, patchSetId.getParentKey()).load();
+    ChangeNotes notes = notesFactory.createChecked(project, patchSetId.changeId()).load();
     return approvalsUtil.getSubmitter(notes, patchSetId);
   }
 
   private void assertSubmitApproval(PatchSet.Id patchSetId) throws Exception {
     PatchSetApproval a = getSubmitter(patchSetId);
     assertThat(a.isLegacySubmit()).isTrue();
-    assertThat(a.getValue()).isEqualTo((short) 1);
-    assertThat(a.getAccountId()).isEqualTo(admin.id());
+    assertThat(a.value()).isEqualTo((short) 1);
+    assertThat(a.accountId()).isEqualTo(admin.id());
   }
 
   private void assertCommit(Project.NameKey project, String branch) throws Exception {
@@ -381,4 +504,45 @@
         pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content, changeId);
     return push.to(ref);
   }
+
+  /**
+   * Makes sure that two emails are sent: one for the change creation, and one for the submit.
+   *
+   * @param expected The account expected to receive message.
+   * @param expectedRecipientType The notification's type: To/Cc/Bcc. if {@code null} then it is not
+   *     needed to check the recipientType. It is meant for -notify without other flags like
+   *     notify-cc, notify-to, and notify-bcc. With the -notify flag, the message can sometimes be
+   *     sent as "To" and sometimes can be sent as "Cc".
+   */
+  private void assertThatEmailsForChangeCreationAndSubmitWereSent(
+      TestAccount expected, @Nullable RecipientType expectedRecipientType) {
+    String expectedEmail = expected.email();
+    String expectedFullName = expected.fullName();
+    Address expectedAddress = new Address(expectedFullName, expectedEmail);
+    assertThat(sender.getMessages()).hasSize(2);
+    Message message = sender.getMessages().get(0);
+    assertThat(message.body().contains("review")).isTrue();
+    assertAddress(message, expectedAddress, expectedRecipientType);
+    message = sender.getMessages().get(1);
+    assertThat(message.rcpt()).containsExactly(expectedAddress);
+    assertAddress(message, expectedAddress, expectedRecipientType);
+    assertThat(message.body().contains("submitted")).isTrue();
+  }
+
+  private void assertAddress(
+      Message message, Address expectedAddress, @Nullable RecipientType expectedRecipientType) {
+    assertThat(message.rcpt()).containsExactly(expectedAddress);
+    if (expectedRecipientType != null
+        && expectedRecipientType
+            != RecipientType.BCC) { // When Bcc, it does not appear in the header.
+      String expectedRecipientTypeString = "To";
+      if (expectedRecipientType == RecipientType.CC) {
+        expectedRecipientTypeString = "Cc";
+      }
+      assertThat(
+              ((EmailHeader.AddressList) message.headers().get(expectedRecipientTypeString))
+                  .getAddressList())
+          .containsExactly(expectedAddress);
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index d1349d0..01323a0 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
@@ -22,8 +24,8 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.inject.Inject;
@@ -58,11 +60,12 @@
 
 public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
+
   protected TestRepository<?> superRepo;
   protected Project.NameKey superKey;
   protected TestRepository<?> subRepo;
   protected Project.NameKey subKey;
-  @Inject protected ProjectOperations projectOperations;
 
   protected SubmitType getSubmitType() {
     return cfg.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
@@ -104,8 +107,12 @@
   }
 
   protected void grantPush(Project.NameKey project) throws Exception {
-    grant(project, "refs/heads/*", Permission.PUSH);
-    grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid()))
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid()))
+        .update();
   }
 
   protected Project.NameKey createProjectForPush(SubmitType submitType) throws Exception {
@@ -472,7 +479,7 @@
         ObjectInserter ins = serverRepo.newObjectInserter();
         RevWalk rw = new RevWalk(serverRepo)) {
       Ref ref = serverRepo.exactRef(refName);
-      assertThat(ref).named(refName).isNotNull();
+      assertWithMessage(refName).that(ref).isNotNull();
       ObjectId oldCommitId = ref.getObjectId();
 
       DirCache dc = DirCache.newInCore();
diff --git a/javatests/com/google/gerrit/acceptance/git/BUILD b/javatests/com/google/gerrit/acceptance/git/BUILD
index 24a83e0..ef54c92 100644
--- a/javatests/com/google/gerrit/acceptance/git/BUILD
+++ b/javatests/com/google/gerrit/acceptance/git/BUILD
@@ -8,6 +8,8 @@
     deps = [
         ":push_for_review",
         ":submodule_util",
+        "//java/com/google/gerrit/git",
+        "//java/com/google/gerrit/server/git/receive/testing",
         "//lib/commons:lang",
     ],
 ) for f in glob(["*IT.java"])]
@@ -18,6 +20,7 @@
     srcs = glob(["Abstract*.java"]),
     deps = [
         "//java/com/google/gerrit/acceptance:lib",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/mail",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java b/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java
index ac0cbd8..e2aa666 100644
--- a/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.acceptance.git;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import org.eclipse.jgit.api.errors.TransportException;
 import org.eclipse.jgit.junit.TestRepository;
@@ -50,8 +53,15 @@
         .add(".gitmodules", config.toText())
         .create();
 
-    exception.expectMessage(expectedErrorMessage);
-    exception.expect(TransportException.class);
-    repo.git().push().setRemote("origin").setRefSpecs(new RefSpec("HEAD:refs/for/master")).call();
+    TransportException thrown =
+        assertThrows(
+            TransportException.class,
+            () ->
+                repo.git()
+                    .push()
+                    .setRemote("origin")
+                    .setRefSpecs(new RefSpec("HEAD:refs/for/master"))
+                    .call());
+    assertThat(thrown).hasMessageThat().contains(expectedErrorMessage);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
index 6516b32..b51263e 100644
--- a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
@@ -19,8 +19,9 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
+import com.google.gerrit.git.ObjectIds;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
@@ -76,8 +77,9 @@
     assertThat(c.getMessage().toLowerCase()).doesNotContain(implicitMergeOf(m.getCommit()));
   }
 
-  private static String implicitMergeOf(ObjectId commit) {
-    return "implicit merge of " + commit.abbreviate(7).name();
+  private String implicitMergeOf(ObjectId commit) throws Exception {
+    return "implicit merge of "
+        + ObjectIds.abbreviateName(commit, testRepo.getRevWalk().getObjectReader());
   }
 
   private void setRejectImplicitMerges() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/git/PushAccountIT.java b/javatests/com/google/gerrit/acceptance/git/PushAccountIT.java
new file mode 100644
index 0000000..2f677e2
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/PushAccountIT.java
@@ -0,0 +1,778 @@
+// 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.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountIndexedCounter;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+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.RefNames;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.server.ServerInitiated;
+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;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.EnumSet;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Test;
+
+/** Tests account behavior when users push to accounts refs. */
+public class PushAccountIT extends AbstractDaemonTest {
+
+  @ConfigSuite.Default
+  public static Config enableSignedPushConfig() {
+    return defaultConfig();
+  }
+
+  @ConfigSuite.Config
+  public static Config disableInMemoryRefCache() {
+    // Run these tests for both enabled and disabled in-memory ref caches. This is an implementation
+    // detail of ReceiveCommits that makes the logic either base it's computation on previously
+    // advertised refs or a make it query a ref database.
+    Config cfg = defaultConfig();
+    cfg.setBoolean("receive", null, "enableInMemoryRefCache", false);
+    return cfg;
+  }
+
+  private static Config defaultConfig() {
+    Config cfg = new Config();
+    cfg.setBoolean("receive", null, "enableSignedPush", true);
+
+    // Disable the staleness checker so that tests that verify the number of expected index events
+    // are stable.
+    cfg.setBoolean("index", null, "autoReindexIfStale", false);
+
+    return cfg;
+  }
+
+  @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private Sequences seq;
+
+  @Test
+  public void pushToUserBranch() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+      PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
+      push.to(RefNames.refsUsers(admin.id())).assertOkStatus();
+      accountIndexedCounter.assertReindexOf(admin);
+
+      push = pushFactory.create(admin.newIdent(), allUsersRepo);
+      push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
+      accountIndexedCounter.assertReindexOf(admin);
+    }
+  }
+
+  @Test
+  public void pushToUserBranchForReview() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String userRefName = RefNames.refsUsers(admin.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRefName + ":userRef");
+      allUsersRepo.reset("userRef");
+      PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
+      PushOneCommit.Result r = push.to(MagicBranch.NEW_CHANGE + userRefName);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRefName);
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      gApi.changes().id(r.getChangeId()).current().submit();
+      accountIndexedCounter.assertReindexOf(admin);
+
+      push = pushFactory.create(admin.newIdent(), allUsersRepo);
+      r = push.to(MagicBranch.NEW_CHANGE + RefNames.REFS_USERS_SELF);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRefName);
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      gApi.changes().id(r.getChangeId()).current().submit();
+      accountIndexedCounter.assertReindexOf(admin);
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchForReviewAndSubmit() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String userRef = RefNames.refsUsers(admin.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, "out-of-office");
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      gApi.changes().id(r.getChangeId()).current().submit();
+      accountIndexedCounter.assertReindexOf(admin);
+
+      AccountInfo info = gApi.accounts().self().get();
+      assertThat(info.email).isEqualTo(admin.email());
+      assertThat(info.name).isEqualTo(admin.fullName());
+      assertThat(info.status).isEqualTo("out-of-office");
+    }
+  }
+
+  @Test
+  public void pushAccountConfigWithPrefEmailThatDoesNotExistAsExtIdToUserBranchForReviewAndSubmit()
+      throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
+      String userRef = RefNames.refsUsers(foo.id());
+      accountIndexedCounter.clear();
+
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      String email = "some.email@example.com";
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, email);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  foo.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      requestScopeOperations.setApiUser(foo.id());
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      gApi.changes().id(r.getChangeId()).current().submit();
+
+      accountIndexedCounter.assertReindexOf(foo);
+
+      AccountInfo info = gApi.accounts().self().get();
+      assertThat(info.email).isEqualTo(email);
+      assertThat(info.name).isEqualTo(foo.fullName());
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfConfigIsInvalid()
+      throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String userRef = RefNames.refsUsers(admin.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  "invalid config")
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.changes().id(r.getChangeId()).current().submit());
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains(
+              String.format(
+                  "invalid account configuration: commit '%s' has an invalid '%s' file for account"
+                      + " '%s': Invalid config file %s in commit %s",
+                  r.getCommit().name(),
+                  AccountProperties.ACCOUNT_CONFIG,
+                  admin.id(),
+                  AccountProperties.ACCOUNT_CONFIG,
+                  r.getCommit().name()));
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfPreferredEmailIsInvalid()
+      throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String userRef = RefNames.refsUsers(admin.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      String noEmail = "no.email";
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, noEmail);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.changes().id(r.getChangeId()).current().submit());
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains(
+              String.format(
+                  "invalid account configuration: invalid preferred email '%s' for account '%s'",
+                  noEmail, admin.id()));
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfOwnAccountIsDeactivated()
+      throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String userRef = RefNames.refsUsers(admin.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.changes().id(r.getChangeId()).current().submit());
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains("invalid account configuration: cannot deactivate own account");
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchForReviewDeactivateOtherAccount() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      projectOperations
+          .allProjectsForUpdate()
+          .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+          .update();
+
+      TestAccount foo = accountCreator.create(name("foo"));
+      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
+      String userRef = RefNames.refsUsers(foo.id());
+      accountIndexedCounter.clear();
+
+      projectOperations
+          .project(allUsers)
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
+          .add(allowLabel("Code-Review").ref(userRef).group(adminGroupUuid()).range(-2, 2))
+          .add(allow(Permission.SUBMIT).ref(userRef).group(adminGroupUuid()))
+          .update();
+
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      gApi.changes().id(r.getChangeId()).current().submit();
+      accountIndexedCounter.assertReindexOf(foo);
+
+      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
+    }
+  }
+
+  @Test
+  public void pushWatchConfigToUserBranch() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config wc = new Config();
+      wc.setString(
+          ProjectWatches.PROJECT,
+          project.get(),
+          ProjectWatches.KEY_NOTIFY,
+          ProjectWatches.NotifyValue.create(null, EnumSet.of(NotifyType.ALL_COMMENTS)).toString());
+      PushOneCommit push =
+          pushFactory.create(
+              admin.newIdent(),
+              allUsersRepo,
+              "Add project watch",
+              ProjectWatches.WATCH_CONFIG,
+              wc.toText());
+      push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
+      accountIndexedCounter.assertReindexOf(admin);
+
+      String invalidNotifyValue = "]invalid[";
+      wc.setString(
+          ProjectWatches.PROJECT, project.get(), ProjectWatches.KEY_NOTIFY, invalidNotifyValue);
+      push =
+          pushFactory.create(
+              admin.newIdent(),
+              allUsersRepo,
+              "Add invalid project watch",
+              ProjectWatches.WATCH_CONFIG,
+              wc.toText());
+      PushOneCommit.Result r = push.to(RefNames.REFS_USERS_SELF);
+      r.assertErrorStatus("invalid account configuration");
+      r.assertMessage(
+          String.format(
+              "%s: Invalid project watch of account %d for project %s: %s",
+              ProjectWatches.WATCH_CONFIG, admin.id().get(), project.get(), invalidNotifyValue));
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranch() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestAccount oooUser = accountCreator.create("away", "away@mail.invalid", "Ambrose Way");
+      requestScopeOperations.setApiUser(oooUser.id());
+
+      // Must clone as oooUser to ensure the push is allowed.
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, oooUser);
+      fetch(allUsersRepo, RefNames.refsUsers(oooUser.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, "out-of-office");
+
+      accountIndexedCounter.clear();
+      pushFactory
+          .create(
+              oooUser.newIdent(),
+              allUsersRepo,
+              "Update account config",
+              AccountProperties.ACCOUNT_CONFIG,
+              ac.toText())
+          .to(RefNames.refsUsers(oooUser.id()))
+          .assertOkStatus();
+
+      accountIndexedCounter.assertReindexOf(oooUser);
+
+      AccountInfo info = gApi.accounts().self().get();
+      assertThat(info.email).isEqualTo(oooUser.email());
+      assertThat(info.name).isEqualTo(oooUser.fullName());
+      assertThat(info.status).isEqualTo("out-of-office");
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchIsRejectedIfConfigIsInvalid() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  "invalid config")
+              .to(RefNames.REFS_USERS_SELF);
+      r.assertErrorStatus("invalid account configuration");
+      r.assertMessage(
+          String.format(
+              "commit '%s' has an invalid '%s' file for account '%s':"
+                  + " Invalid config file %s in commit %s",
+              r.getCommit().name(),
+              AccountProperties.ACCOUNT_CONFIG,
+              admin.id(),
+              AccountProperties.ACCOUNT_CONFIG,
+              r.getCommit().name()));
+      accountIndexedCounter.assertNoReindex();
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchIsRejectedIfPreferredEmailIsInvalid() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      String noEmail = "no.email";
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, noEmail);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(RefNames.REFS_USERS_SELF);
+      r.assertErrorStatus("invalid account configuration");
+      r.assertMessage(
+          String.format("invalid preferred email '%s' for account '%s'", noEmail, admin.id()));
+      accountIndexedCounter.assertNoReindex();
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchInvalidPreferredEmailButNotChanged() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
+      String userRef = RefNames.refsUsers(foo.id());
+
+      String noEmail = "no.email";
+      accountsUpdateProvider
+          .get()
+          .update("Set Preferred Email", foo.id(), u -> u.setPreferredEmail(noEmail));
+      accountIndexedCounter.clear();
+
+      projectOperations
+          .project(allUsers)
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref(userRef).group(REGISTERED_USERS))
+          .update();
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      String status = "in vacation";
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, status);
+
+      pushFactory
+          .create(
+              foo.newIdent(),
+              allUsersRepo,
+              "Update account config",
+              AccountProperties.ACCOUNT_CONFIG,
+              ac.toText())
+          .to(userRef)
+          .assertOkStatus();
+      accountIndexedCounter.assertReindexOf(foo);
+
+      AccountInfo info = gApi.accounts().id(foo.id().get()).get();
+      assertThat(info.email).isEqualTo(noEmail);
+      assertThat(info.name).isEqualTo(foo.fullName());
+      assertThat(info.status).isEqualTo(status);
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchIfPreferredEmailDoesNotExistAsExtId() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
+      String userRef = RefNames.refsUsers(foo.id());
+      accountIndexedCounter.clear();
+
+      projectOperations
+          .project(allUsers)
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
+          .update();
+
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      String email = "some.email@example.com";
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, email);
+
+      pushFactory
+          .create(
+              foo.newIdent(),
+              allUsersRepo,
+              "Update account config",
+              AccountProperties.ACCOUNT_CONFIG,
+              ac.toText())
+          .to(userRef)
+          .assertOkStatus();
+      accountIndexedCounter.assertReindexOf(foo);
+
+      AccountInfo info = gApi.accounts().id(foo.id().get()).get();
+      assertThat(info.email).isEqualTo(email);
+      assertThat(info.name).isEqualTo(foo.fullName());
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchIsRejectedIfOwnAccountIsDeactivated() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(RefNames.REFS_USERS_SELF);
+      r.assertErrorStatus("invalid account configuration");
+      r.assertMessage("cannot deactivate own account");
+      accountIndexedCounter.assertNoReindex();
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchDeactivateOtherAccount() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      projectOperations
+          .allProjectsForUpdate()
+          .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+          .update();
+
+      TestAccount foo = accountCreator.create(name("foo"));
+      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
+      String userRef = RefNames.refsUsers(foo.id());
+      accountIndexedCounter.clear();
+
+      projectOperations
+          .project(allUsers)
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
+          .update();
+
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
+
+      pushFactory
+          .create(
+              admin.newIdent(),
+              allUsersRepo,
+              "Update account config",
+              AccountProperties.ACCOUNT_CONFIG,
+              ac.toText())
+          .to(userRef)
+          .assertOkStatus();
+      accountIndexedCounter.assertReindexOf(foo);
+
+      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
+    }
+  }
+
+  @Test
+  public void cannotCreateNonUserBranchUnderRefsUsersWithAccessDatabaseCapability()
+      throws Exception {
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .update();
+
+    String userRef = RefNames.REFS_USERS + "foo";
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
+    r.assertErrorStatus();
+    assertThat(r.getMessage()).contains("Not allowed to create non-user branch under refs/users/.");
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNull();
+    }
+  }
+
+  @Test
+  public void cannotCreateUserBranch() throws Exception {
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .update();
+
+    String userRef = RefNames.refsUsers(Account.id(seq.nextAccountId()));
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
+    r.assertErrorStatus();
+    assertThat(r.getMessage()).contains("Not allowed to create user branch.");
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNull();
+    }
+  }
+
+  @Test
+  public void createUserBranchWithAccessDatabaseCapability() throws Exception {
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .update();
+
+    String userRef = RefNames.refsUsers(Account.id(seq.nextAccountId()));
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef).assertOkStatus();
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNotNull();
+    }
+  }
+
+  private Config getAccountConfig(TestRepository<?> allUsersRepo) throws Exception {
+    Config ac = new Config();
+    try (TreeWalk tw =
+        TreeWalk.forPath(
+            allUsersRepo.getRepository(),
+            AccountProperties.ACCOUNT_CONFIG,
+            getHead(allUsersRepo.getRepository(), "HEAD").getTree())) {
+      assertThat(tw).isNotNull();
+      ac.fromText(
+          new String(
+              allUsersRepo
+                  .getRevWalk()
+                  .getObjectReader()
+                  .open(tw.getObjectId(0), OBJ_BLOB)
+                  .getBytes(),
+              UTF_8));
+    }
+    return ac;
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index cc19ad2..f9c751f 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.git.testing.PushResultSubject.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.stream.Collectors.toList;
@@ -24,19 +24,17 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 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.Permission;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import java.util.function.Consumer;
@@ -54,14 +52,13 @@
 import org.junit.Test;
 
 public class PushPermissionsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
   public void setUp() throws Exception {
     try (ProjectConfigUpdate u = updateProject(allProjects)) {
       ProjectConfig cfg = u.getConfig();
-      cfg.getProject()
-          .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
 
       // Remove push-related permissions, so they can be added back individually by test methods.
       removeAllBranchPermissions(
@@ -73,13 +70,15 @@
           Permission.PUSH_MERGE,
           Permission.SUBMIT);
       removeAllGlobalCapabilities(cfg, GlobalCapability.ADMINISTRATE_SERVER);
-
-      // Include some auxiliary permissions.
-      Util.allow(cfg, Permission.FORGE_AUTHOR, REGISTERED_USERS, "refs/*");
-      Util.allow(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, "refs/*");
-
       u.save();
     }
+
+    // Include some auxiliary permissions.
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allow(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.FORGE_COMMITTER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
   }
 
   @Test
@@ -94,17 +93,6 @@
   }
 
   @Test
-  public void mixingDirectChangesAndRegularPush() throws Exception {
-    testRepo.branch("HEAD").commit().create();
-    PushResult r = push("HEAD:refs/heads/master", "HEAD:refs/changes/01/101");
-
-    String msg = "cannot combine normal pushes and magic pushes";
-    assertThat(r.getRemoteUpdate("refs/heads/master")).isNotEqualTo(Status.OK);
-    assertThat(r.getRemoteUpdate("refs/changes/01/101")).isNotEqualTo(Status.OK);
-    assertThat(r.getRemoteUpdate("refs/heads/master").getMessage()).isEqualTo(msg);
-  }
-
-  @Test
   public void fastForwardUpdateDenied() throws Exception {
     testRepo.branch("HEAD").commit().create();
     PushResult r = push("HEAD:refs/heads/master");
@@ -202,7 +190,11 @@
 
   @Test
   public void refsMetaConfigUpdateRequiresProjectOwner() throws Exception {
-    grant(project, "refs/meta/config", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/meta/config").group(REGISTERED_USERS))
+        .update();
 
     forceFetch("refs/meta/config");
     ObjectId commit = testRepo.branch("refs/meta/config").commit().create();
@@ -222,7 +214,11 @@
             "Contact an administrator to fix the permissions");
     assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
 
-    grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // Re-fetch refs/meta/config from the server because the grant changed it, and we want a
     // fast-forward.
@@ -249,9 +245,14 @@
 
   @Test
   public void updateBySubmitDenied() throws Exception {
-    grant(project, "refs/for/refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
-    ObjectId commit = testRepo.branch("HEAD").commit().create();
+    ObjectId commit =
+        testRepo.branch("HEAD").commit().message("test commit").insertChangeId().create();
     assertThat(push("HEAD:refs/for/master")).onlyRef("refs/for/master").isOk();
     gApi.changes().id(commit.name()).current().review(ReviewInput.approve());
 
@@ -267,18 +268,23 @@
 
   @Test
   public void addPatchSetDenied() throws Exception {
-    grant(project, "refs/for/refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     ChangeInput ci = new ChangeInput();
     ci.project = project.get();
     ci.branch = "master";
     ci.subject = "A change";
-    Change.Id id = new Change.Id(gApi.changes().create(ci).get()._number);
+    Change.Id id = Change.id(gApi.changes().create(ci).get()._number);
 
     requestScopeOperations.setApiUser(admin.id());
-    ObjectId ps1Id = forceFetch(new PatchSet.Id(id, 1).toRefName());
+    ObjectId ps1Id = forceFetch(PatchSet.id(id, 1).toRefName());
     ObjectId ps2Id = testRepo.amend(ps1Id).add("file", "content").create();
     PushResult r = push(ps2Id.name() + ":refs/for/master");
+    // Admin had ADD_PATCH_SET removed in setup.
     assertThat(r)
         .onlyRef("refs/for/master")
         .isRejected("cannot add patch set to " + id.get() + ".");
@@ -288,7 +294,11 @@
 
   @Test
   public void skipValidationDenied() throws Exception {
-    grant(project, "refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
     testRepo.branch("HEAD").commit().create();
     PushResult r =
@@ -305,7 +315,11 @@
 
   @Test
   public void accessDatabaseForNoteDbDenied() throws Exception {
-    grant(project, "refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
     testRepo.branch("HEAD").commit().create();
     PushResult r =
@@ -322,8 +336,12 @@
 
   @Test
   public void administrateServerForUpdateParentDenied() throws Exception {
-    grant(project, "refs/meta/config", Permission.PUSH, false, REGISTERED_USERS);
-    grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/meta/config").group(REGISTERED_USERS))
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     String project2 = name("project2");
     gApi.projects().create(project2);
@@ -398,7 +416,7 @@
       case REJECTED_OTHER_REASON:
       case RENAMED:
       default:
-        assert_().fail("fetch failed to update local %s: %s", ref, u.getResult());
+        assertWithMessage("fetch failed to update local %s: %s", ref, u.getResult()).fail();
         break;
     }
     return u.getNewObjectId();
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 5bed77c..ea9f48e 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -18,11 +18,14 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toMap;
 
-import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -30,28 +33,29 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 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.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.receive.ReceiveCommitsAdvertiseRefsHook;
+import com.google.gerrit.server.git.receive.ReceiveCommitsAdvertiseRefsHookChain;
+import com.google.gerrit.server.git.receive.testing.TestRefAdvertiser;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
@@ -71,6 +75,9 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.AdvertiseRefsHook;
+import org.eclipse.jgit.transport.ReceivePack;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -79,11 +86,15 @@
   @Inject private AllUsersName allUsersName;
   @Inject private ChangeNoteUtil noteUtil;
   @Inject private PermissionBackend permissionBackend;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   private AccountGroup.UUID admins;
   private AccountGroup.UUID nonInteractiveUsers;
 
+  private RevCommit rcMaster;
+  private RevCommit rcBranch;
+
   private ChangeData cd1;
   private String psRef1;
   private String metaRef1;
@@ -122,9 +133,12 @@
       for (AccessSection sec : u.getConfig().getAccessSections()) {
         sec.removePermission(Permission.READ);
       }
-      Util.allow(u.getConfig(), Permission.READ, admins, "refs/*");
       u.save();
     }
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(admins))
+        .update();
 
     // Remove all read permissions on All-Users.
     try (ProjectConfigUpdate u = updateProject(allUsers)) {
@@ -135,60 +149,96 @@
     }
   }
 
+  // Building the following:
+  //   rcMaster (c1 master master-tag) <-- rcBranch (c2 branch branch-tag)
+  //      \                                    \
+  //    (c3_open)                            (c4_open)
+  //
   private void setUpChanges() throws Exception {
     gApi.projects().name(project.get()).branch("branch").create(new BranchInput());
 
     // First 2 changes are merged, which means the tags pointing to them are
     // visible.
-    allow("refs/for/refs/heads/*", Permission.SUBMIT, admins);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(admins))
+        .update();
+
+    //   rcMaster (c1 master)
     PushOneCommit.Result mr =
         pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%submit");
     mr.assertOkStatus();
     cd1 = mr.getChange();
-    psRef1 = cd1.currentPatchSet().getId().toRefName();
+    rcMaster = mr.getCommit();
+    psRef1 = cd1.currentPatchSet().id().toRefName();
     metaRef1 = RefNames.changeMetaRef(cd1.getId());
+
+    //   rcMaster (c1 master) <-- rcBranch (c2 branch)
     PushOneCommit.Result br =
         pushFactory.create(admin.newIdent(), testRepo).to("refs/for/branch%submit");
     br.assertOkStatus();
     cd2 = br.getChange();
-    psRef2 = cd2.currentPatchSet().getId().toRefName();
+    rcBranch = br.getCommit();
+    psRef2 = cd2.currentPatchSet().id().toRefName();
     metaRef2 = RefNames.changeMetaRef(cd2.getId());
 
     // Second 2 changes are unmerged.
+    //   rcMaster (c1 master) <-- rcBranch (c2 branch)
+    //      \
+    //    (c3_open)
+    //
     mr = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
     mr.assertOkStatus();
     cd3 = mr.getChange();
-    psRef3 = cd3.currentPatchSet().getId().toRefName();
+    psRef3 = cd3.currentPatchSet().id().toRefName();
     metaRef3 = RefNames.changeMetaRef(cd3.getId());
+
+    //   rcMaster (c1 master) <-- rcBranch (c2 branch)
+    //      \                        \
+    //     (c3_open)                (c4_open)
     br = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/branch");
     br.assertOkStatus();
     cd4 = br.getChange();
-    psRef4 = cd4.currentPatchSet().getId().toRefName();
+    psRef4 = cd4.currentPatchSet().id().toRefName();
     metaRef4 = RefNames.changeMetaRef(cd4.getId());
 
     try (Repository repo = repoManager.openRepository(project)) {
-      // master-tag -> master
+      //   rcMaster (c1 master master-tag) <-- rcBranch (c2 branch)
+      //       \                                  \
+      //     (c3_open)                          (c4_open)
       RefUpdate mtu = repo.updateRef("refs/tags/master-tag");
       mtu.setExpectedOldObjectId(ObjectId.zeroId());
       mtu.setNewObjectId(repo.exactRef("refs/heads/master").getObjectId());
       assertThat(mtu.update()).isEqualTo(RefUpdate.Result.NEW);
 
-      // branch-tag -> branch
+      //   rcMaster (c1 master master-tag) <-- rcBranch (c2 branch branch-tag)
+      //       \                                  \
+      //     (c3_open)                          (c4_open)
       RefUpdate btu = repo.updateRef("refs/tags/branch-tag");
       btu.setExpectedOldObjectId(ObjectId.zeroId());
       btu.setNewObjectId(repo.exactRef("refs/heads/branch").getObjectId());
       assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+
+      // Create a tag for the tree of the commit on 'master'
+      // tree-tag -> master.tree
+      RefUpdate ttu = repo.updateRef("refs/tags/tree-tag");
+      ttu.setExpectedOldObjectId(ObjectId.zeroId());
+      ttu.setNewObjectId(rcMaster.getTree().toObjectId());
+      assertThat(ttu.update()).isEqualTo(RefUpdate.Result.NEW);
     }
   }
 
   @Test
+  @GerritConfig(name = "auth.skipFullRefEvaluationIfAllRefsAreVisible", value = "false")
   public void uploadPackAllRefsVisibleNoRefsMetaConfig() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      Util.allow(u.getConfig(), Permission.READ, admins, RefNames.REFS_CONFIG);
-      Util.doNotInherit(u.getConfig(), Permission.READ, RefNames.REFS_CONFIG);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(admins))
+        .setExclusiveGroup(permissionKey(Permission.READ).ref(RefNames.REFS_CONFIG), true)
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
@@ -205,12 +255,46 @@
         "refs/heads/master",
         "refs/tags/branch-tag",
         "refs/tags/master-tag");
+    // tree-tag not visible. See comment in subsetOfBranchesVisibleIncludingHead.
+  }
+
+  @Test
+  @GerritConfig(name = "auth.skipFullRefEvaluationIfAllRefsAreVisible", value = "true")
+  public void uploadPackAllRefsVisibleNoRefsMetaConfigSkipFullRefEval() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(admins))
+        .setExclusiveGroup(permissionKey(Permission.READ).ref(RefNames.REFS_CONFIG), true)
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+    assertUploadPackRefs(
+        "HEAD",
+        psRef1,
+        metaRef1,
+        psRef2,
+        metaRef2,
+        psRef3,
+        metaRef3,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/heads/master",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag",
+        "refs/tags/tree-tag");
   }
 
   @Test
   public void uploadPackAllRefsVisibleWithRefsMetaConfig() throws Exception {
-    allow("refs/*", Permission.READ, REGISTERED_USERS);
-    allow(RefNames.REFS_CONFIG, Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+        .update();
 
     assertUploadPackRefs(
         "HEAD",
@@ -226,23 +310,46 @@
         "refs/heads/master",
         RefNames.REFS_CONFIG,
         "refs/tags/branch-tag",
-        "refs/tags/master-tag");
+        "refs/tags/master-tag",
+        "refs/tags/tree-tag");
+  }
+
+  @Test
+  public void grantReadOnRefsTagsIsNoOp() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+    assertUploadPackRefs(); // We expect no refs returned
   }
 
   @Test
   public void uploadPackSubsetOfBranchesVisibleIncludingHead() throws Exception {
-    allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
-    deny("refs/heads/branch", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(deny(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
         "HEAD", psRef1, metaRef1, psRef3, metaRef3, "refs/heads/master", "refs/tags/master-tag");
+    // tree-tag is not visible because we don't look at trees reachable from
+    // refs
   }
 
   @Test
   public void uploadPackSubsetOfBranchesVisibleNotIncludingHead() throws Exception {
-    deny("refs/heads/master", Permission.READ, REGISTERED_USERS);
-    allow("refs/heads/branch", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(deny(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
@@ -255,11 +362,16 @@
         // master branch is not visible but master-tag is reachable from branch
         // (since PushOneCommit always bases changes on each other).
         "refs/tags/master-tag");
+    // tree-tag not visible. See comment in subsetOfBranchesVisibleIncludingHead.
   }
 
   @Test
   public void uploadPackSubsetOfBranchesVisibleWithEdit() throws Exception {
-    allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
 
     // Admin's edit is not visible.
     requestScopeOperations.setApiUser(admin.id());
@@ -278,12 +390,17 @@
         "refs/heads/master",
         "refs/tags/master-tag",
         "refs/users/01/1000001/edit-" + cd3.getId() + "/1");
+    // tree-tag not visible. See comment in subsetOfBranchesVisibleIncludingHead.
   }
 
   @Test
   public void uploadPackSubsetOfBranchesAndEditsVisibleWithViewPrivateChanges() throws Exception {
-    allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
-    allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // Admin's edit on change3 is visible.
     requestScopeOperations.setApiUser(admin.id());
@@ -306,13 +423,21 @@
         "refs/tags/master-tag",
         "refs/users/00/1000000/edit-" + cd3.getId() + "/1",
         "refs/users/01/1000001/edit-" + cd3.getId() + "/1");
+    // tree-tag not visible. See comment in subsetOfBranchesVisibleIncludingHead.
   }
 
   @Test
   public void uploadPackSubsetOfRefsVisibleWithAccessDatabase() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    deny("refs/heads/master", Permission.READ, REGISTERED_USERS);
-    allow("refs/heads/branch", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(deny(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(cd3.getId().get()).edit().create();
@@ -335,6 +460,7 @@
         "refs/tags/master-tag",
         // All edits are visible due to accessDatabase capability.
         "refs/users/00/1000000/edit-" + cd3.getId() + "/1");
+    // tree-tag not visible. See comment in subsetOfBranchesVisibleIncludingHead.
   }
 
   @Test
@@ -349,7 +475,11 @@
   }
 
   private void uploadPackNoSearchingChangeCacheImpl() throws Exception {
-    allow("refs/heads/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     assertRefs(
@@ -370,21 +500,29 @@
         "refs/heads/master",
         "refs/tags/branch-tag",
         "refs/tags/master-tag");
+    // tree-tag not visible. See comment in subsetOfBranchesVisibleIncludingHead.
   }
 
   @Test
   public void uploadPackSequencesWithAccessDatabase() throws Exception {
     assertRefs(allProjects, user, true);
 
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     assertRefs(allProjects, user, true, "refs/sequences/changes");
   }
 
   @Test
   public void uploadPackAllRefsAreVisibleOrphanedTag() throws Exception {
-    allow("refs/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     // Delete the pending change on 'branch' and 'branch' itself so that the tag gets orphaned
-    gApi.changes().id(cd4.getId().id).delete();
+    gApi.changes().id(cd4.getId().get()).delete();
     gApi.projects().name(project.get()).branch("refs/heads/branch").delete();
 
     requestScopeOperations.setApiUser(user.id());
@@ -399,28 +537,517 @@
         metaRef3,
         "refs/heads/master",
         "refs/tags/branch-tag",
+        "refs/tags/master-tag",
+        "refs/tags/tree-tag");
+  }
+
+  @Test
+  public void uploadPackSubsetRefsVisibleOrphanedTagInvisible() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
+    // Create a tag for the pending change on 'branch' so that the tag is orphaned
+    try (Repository repo = repoManager.openRepository(project)) {
+      // change4-tag -> psRef4
+      RefUpdate ctu = repo.updateRef("refs/tags/change4-tag");
+      ctu.setExpectedOldObjectId(ObjectId.zeroId());
+      ctu.setNewObjectId(repo.exactRef(psRef4).getObjectId());
+      assertThat(ctu.update()).isEqualTo(RefUpdate.Result.NEW);
+    }
+
+    requestScopeOperations.setApiUser(user.id());
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        // See comment in subsetOfBranchesVisibleNotIncludingHead.
+        "refs/tags/master-tag");
+  }
+
+  // first  ls-remote: rcMaster (c1 master)
+  // second ls-remote: rcMaster (c1 master) <- newchange1 (master-newtag)
+  @Test
+  public void uploadPackNewCommitOrphanTagInvisible() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    // rcMaster (c1 master)
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        // See comment in subsetOfBranchesVisibleNotIncludingHead.
+        "refs/tags/master-tag");
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      PushOneCommit.Result r =
+          pushFactory.create(admin.newIdent(), testRepo).setParent(rcMaster).to("refs/for/master");
+      r.assertOkStatus();
+
+      // rcMaster (c1 master) <- newchange1 (master-newtag)
+      RefUpdate btu = repo.updateRef("refs/tags/master-newtag");
+      btu.setExpectedOldObjectId(ObjectId.zeroId());
+      btu.setNewObjectId(r.getCommit());
+      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+    }
+
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        // See comment in subsetOfBranchesVisibleNotIncludingHead.
+        "refs/tags/master-tag");
+  }
+
+  // first  ls-remote: rcBranch (c2) <- newcommit1                 <- newcommit2 (branch)
+  // second ls-remote: rcBranch (c2) <- newcommit1 (branch-newtag) <- newcommit2 (branch)
+  @Test
+  public void uploadPackNewReachableTagVisible() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      // c2 <- newcommit1 (branch)
+      PushOneCommit.Result r =
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(rcBranch)
+              .to("refs/heads/branch");
+      r.assertOkStatus();
+      RevCommit tagRc = r.getCommit();
+
+      // c2 <- newcommit1 <- newcommit2 (branch)
+      r = pushFactory.create(admin.newIdent(), testRepo).setParent(tagRc).to("refs/heads/branch");
+      r.assertOkStatus();
+
+      assertUploadPackRefs(
+          psRef2,
+          metaRef2,
+          psRef4,
+          metaRef4,
+          "refs/heads/branch",
+          "refs/tags/branch-tag",
+          // See comment in subsetOfBranchesVisibleNotIncludingHead.
+          "refs/tags/master-tag");
+
+      // c2 <- newcommit1 (branch-newtag) <- newcommit2 (branch)
+      RefUpdate btu = repo.updateRef("refs/tags/branch-newtag");
+      btu.setExpectedOldObjectId(ObjectId.zeroId());
+      btu.setNewObjectId(tagRc);
+      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+    }
+
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        "refs/tags/branch-newtag",
+        // See comment in subsetOfBranchesVisibleNotIncludingHead.
+        "refs/tags/master-tag");
+  }
+
+  // first  ls-remote: rcBranch (c2) <- newcommit1 (branch)
+  // second ls-remote: rcBranch (c2) <- newcommit1                 <- newcommit2 (branch)
+  // third  ls-remote: rcBranch (c2) <- newcommit1 (branch-newtag) <- newcommit2 (branch)
+  @Test
+  public void uploadPackBranchFFNewTagOldBranchVisible() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      // rcBranch (c2) <- newcommit1 (branch)
+      PushOneCommit.Result r =
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(rcBranch)
+              .to("refs/heads/branch");
+      r.assertOkStatus();
+      RevCommit tagRc = r.getCommit();
+
+      assertUploadPackRefs(
+          psRef2,
+          metaRef2,
+          psRef4,
+          metaRef4,
+          "refs/heads/branch",
+          "refs/tags/branch-tag",
+          // See comment in subsetOfBranchesVisibleNotIncludingHead.
+          "refs/tags/master-tag");
+
+      // rcBranch (c2) <- newcommit1 <- newcommit2 (branch)
+      r = pushFactory.create(admin.newIdent(), testRepo).setParent(tagRc).to("refs/heads/branch");
+      r.assertOkStatus();
+
+      // rcBranch (c2) <- newcommit1 (branch-newtag) <- newcommit2 (branch)
+      RefUpdate btu = repo.updateRef("refs/tags/branch-newtag");
+      btu.setExpectedOldObjectId(ObjectId.zeroId());
+      btu.setNewObjectId(tagRc);
+      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+    }
+
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        "refs/tags/branch-newtag",
+        // See comment in subsetOfBranchesVisibleNotIncludingHead.
+        "refs/tags/master-tag");
+  }
+
+  // first  ls-remote: rcBranch (c2)        <- newcommit1 (branch-oldtag) <- newcommit2 (branch)
+  // second ls-remote: rcBranch (c2 branch) <- newcommit1 (branch-oldtag)
+  @Test
+  public void uploadPackBranchRewindMakeTagUnreachableInVisible() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      // rcBranch (c2) <- newcommit1 (branch)
+      PushOneCommit.Result r =
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(rcBranch)
+              .to("refs/heads/branch");
+      r.assertOkStatus();
+      RevCommit tagRc = r.getCommit();
+
+      // rcBranch (c2) <- newcommit1 <- newcommit2 (branch)
+      r = pushFactory.create(admin.newIdent(), testRepo).setParent(tagRc).to("refs/heads/branch");
+      r.assertOkStatus();
+      RevCommit bRc = r.getCommit();
+
+      // rcBranch (c2) <- newcommit1 (branch-oldtag) <- newcommit2 (branch)
+      RefUpdate btu = repo.updateRef("refs/tags/branch-oldtag");
+      btu.setExpectedOldObjectId(ObjectId.zeroId());
+      btu.setNewObjectId(tagRc);
+      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+
+      assertUploadPackRefs(
+          psRef2,
+          metaRef2,
+          psRef4,
+          metaRef4,
+          "refs/heads/branch",
+          "refs/tags/branch-tag",
+          "refs/tags/branch-oldtag",
+          // See comment in subsetOfBranchesVisibleNotIncludingHead.
+          "refs/tags/master-tag");
+
+      // rcBranch (c2 branch) <- newcommit1 (branch-oldtag) <- newcommit2
+      btu = repo.updateRef("refs/heads/branch");
+      btu.setExpectedOldObjectId(bRc);
+      btu.setNewObjectId(rcBranch);
+      btu.setForceUpdate(true);
+      assertThat(btu.update()).isEqualTo(RefUpdate.Result.FORCED);
+    }
+
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        // See comment in subsetOfBranchesVisibleNotIncludingHead.
+        "refs/tags/master-tag");
+  }
+
+  // first  ls-remote: rcBranch (c2 branch) <- newcommit1 (new-tag)
+  // second ls-remote: rcBranch (c2 branch) <- newcommit1 (new-tag) <- newcommit2 (new-branch)
+  @Test
+  public void uploadPackCreateBranchTagReachableVisible() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/new-branch").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      // rcBranch (c2 branch) <- newcommit1 (branch-newtag)
+      PushOneCommit.Result r =
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(rcBranch)
+              .to("refs/tags/new-tag");
+      r.assertOkStatus();
+      RevCommit tagRc = r.getCommit();
+
+      assertUploadPackRefs();
+
+      // rcBranch (c2) <- newcommit1 (branch-newtag) <- newcommit2 (branch)
+      r =
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(tagRc)
+              .to("refs/heads/new-branch");
+      r.assertOkStatus();
+    }
+
+    assertUploadPackRefs(
+        "refs/heads/new-branch",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag",
+        "refs/tags/new-tag");
+  }
+
+  // first  ls-remote: rcBranch (c2 branch)               <- newcommit1 (updated-tag)
+  // second ls-remote: rcBranch (c2 branch updated-tag)
+  @Test
+  public void uploadPackTagUpdatedReachableVisible() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      // rcBranch (c2 branch) <- newcommit1 (updated-tag)
+      PushOneCommit.Result r =
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(rcBranch)
+              .to("refs/tags/updated-tag");
+      r.assertOkStatus();
+      RevCommit tagRc = r.getCommit();
+
+      assertUploadPackRefs(
+          psRef2,
+          metaRef2,
+          psRef4,
+          metaRef4,
+          "refs/heads/branch",
+          "refs/tags/branch-tag",
+          "refs/tags/master-tag");
+
+      // rcBranch (c2 branch updated-tag)
+      RefUpdate btu = repo.updateRef("refs/tags/updated-tag");
+      btu.setExpectedOldObjectId(tagRc);
+      btu.setNewObjectId(rcBranch);
+      btu.setForceUpdate(true);
+      assertThat(btu.update()).isEqualTo(RefUpdate.Result.FORCED);
+    }
+
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag",
+        "refs/tags/updated-tag");
+  }
+
+  // first  ls-remote: rcBranch (c2 branch updated-tag)
+  // second ls-remote: rcBranch (c2 branch)             <- newcommit1 (updated-tag)
+  @Test
+  public void uploadPackTagUpdatedUnreachableInvisible() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      // rcBranch (c2 branch updated-tag)
+      RefUpdate btu = repo.updateRef("refs/tags/updated-tag");
+      btu.setExpectedOldObjectId(ObjectId.zeroId());
+      btu.setNewObjectId(rcBranch);
+      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+
+      assertUploadPackRefs(
+          psRef2,
+          metaRef2,
+          psRef4,
+          metaRef4,
+          "refs/heads/branch",
+          "refs/tags/branch-tag",
+          "refs/tags/master-tag",
+          "refs/tags/updated-tag");
+
+      // rcBranch (c2 branch) <- newcommit1 (updated-tag)
+      PushOneCommit.Result r =
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(rcBranch)
+              .to("refs/tags/updated-tag");
+      r.assertOkStatus();
+    }
+
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
+  }
+
+  // first  ls-remote: rcBranch (c2 branch branch-tag)
+  // second ls-remote: rcBranch (c2 branch)
+  @Test
+  public void uploadPackTagDeleted() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .add(allow(Permission.DELETE).ref("refs/tags/branch-tag").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/tags/branch-tag").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    // rcBranch (c2 branch branch-tag)
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
+
+    // rcBranch (c2 branch)
+    try (Repository repo = repoManager.openRepository(project)) {
+      RefUpdate btu = repo.updateRef("refs/tags/branch-tag");
+      btu.setExpectedOldObjectId(rcBranch);
+      btu.setNewObjectId(ObjectId.zeroId());
+      btu.setForceUpdate(true);
+      assertThat(btu.delete()).isEqualTo(RefUpdate.Result.FORCED);
+    }
+
+    assertUploadPackRefs(
+        psRef2, metaRef2, psRef4, metaRef4, "refs/heads/branch", "refs/tags/master-tag");
+  }
+
+  // first  ls-remote: rcBranch (c2 branch) <- newcommit1 (new-tag) <- newcommit2 (new-branch)
+  // second ls-remote: rcBranch (c2 branch) <- newcommit1 (new-tag)
+  @Test
+  public void uploadPackBranchDeleteTagUnreachableInvisible() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .add(allow(Permission.READ).ref("refs/heads/new-branch").group(REGISTERED_USERS))
+        .add(allow(Permission.DELETE).ref("refs/heads/new-branch").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      // rcBranch (branch) <- newcommit1 (new-tag)
+      PushOneCommit.Result r =
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(rcBranch)
+              .to("refs/tags/new-tag");
+      r.assertOkStatus();
+      RevCommit tagRc = r.getCommit();
+
+      // rcBranch (c2 branch) <- newcommit1 (new-tag) <- newcommit2 (new-branch)
+      r =
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(tagRc)
+              .to("refs/heads/new-branch");
+      r.assertOkStatus();
+    }
+
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        "refs/heads/new-branch",
+        "refs/tags/new-tag",
+        "refs/tags/master-tag");
+
+    // rcBranch (c2 branch) <- newcommit1 (new-tag)
+    gApi.projects().name(project.get()).branch("refs/heads/new-branch").delete();
+
+    assertUploadPackRefs(
+        psRef2,
+        metaRef2,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
         "refs/tags/master-tag");
   }
 
   @Test
   public void receivePackListsOpenChangesAsAdditionalHaves() throws Exception {
-    ReceiveCommitsAdvertiseRefsHook.Result r = getReceivePackRefs();
+    TestRefAdvertiser.Result r = getReceivePackRefs();
     assertThat(r.allRefs().keySet())
         .containsExactly(
             // meta refs are excluded
-            "HEAD",
             "refs/heads/branch",
             "refs/heads/master",
             "refs/meta/config",
             "refs/tags/branch-tag",
-            "refs/tags/master-tag");
+            "refs/tags/master-tag",
+            "refs/tags/tree-tag");
     assertThat(r.additionalHaves()).containsExactly(obj(cd3, 1), obj(cd4, 1));
   }
 
   @Test
   public void receivePackRespectsVisibilityOfOpenChanges() throws Exception {
-    allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
-    deny("refs/heads/branch", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(deny(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
 
     assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(cd3, 1));
@@ -442,7 +1069,7 @@
         TestRepository<Repository> tr = new TestRepository<>(repo)) {
       String subject = "Subject for missing commit";
       Change c = new Change(cd3.change());
-      PatchSet.Id psId = new PatchSet.Id(cd3.getId(), 2);
+      PatchSet.Id psId = PatchSet.id(cd3.getId(), 2);
       c.setCurrentPatchSet(psId, subject, c.getOriginalSubject());
 
       PersonIdent committer = serverIdent.get();
@@ -483,7 +1110,11 @@
 
   @Test
   public void advertisedReferencesOmitUserBranchesOfOtherUsers() throws Exception {
-    allow(allUsersName, RefNames.REFS_USERS + "*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_USERS + "*").group(REGISTERED_USERS))
+        .update();
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
       assertThat(getUserRefs(git))
@@ -493,7 +1124,10 @@
 
   @Test
   public void advertisedReferencesIncludeAllUserBranchesWithAccessDatabase() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
       assertThat(getUserRefs(git))
@@ -515,7 +1149,11 @@
 
   @Test
   public void advertisedReferencesOmitGroupBranchesOfNonOwnedGroups() throws Exception {
-    allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
     AccountGroup.UUID users = createGroup("Users", admins, user);
     AccountGroup.UUID foos = createGroup("Foos", users);
     AccountGroup.UUID bars = createSelfOwnedGroup("Bars", user);
@@ -528,7 +1166,10 @@
 
   @Test
   public void advertisedReferencesIncludeAllGroupBranchesWithAccessDatabase() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     AccountGroup.UUID users = createGroup("Users", admins);
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
@@ -542,8 +1183,15 @@
 
   @Test
   public void advertisedReferencesIncludeAllGroupBranchesForAdmins() throws Exception {
-    allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+        .update();
     AccountGroup.UUID users = createGroup("Users", admins);
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
@@ -557,7 +1205,11 @@
 
   @Test
   public void advertisedReferencesOmitNoteDbNotesBranches() throws Exception {
-    allow(allUsersName, RefNames.REFS + "*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS + "*").group(REGISTERED_USERS))
+        .update();
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
       assertThat(getRefs(git)).containsNoneOf(RefNames.REFS_EXTERNAL_IDS, RefNames.REFS_GROUPNAMES);
@@ -566,11 +1218,15 @@
 
   @Test
   public void advertisedReferencesOmitPrivateChangesOfOtherUsers() throws Exception {
-    allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<?> userTestRepository = cloneProject(project, user);
     try (Git git = userTestRepository.git()) {
-      String change3RefName = cd3.currentPatchSet().getRefName();
+      String change3RefName = cd3.currentPatchSet().refName();
       assertWithMessage("Precondition violated").that(getRefs(git)).contains(change3RefName);
 
       gApi.changes().id(cd3.getId().get()).setPrivate(true, null);
@@ -583,11 +1239,15 @@
     assume()
         .that(baseConfig.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true))
         .isTrue();
-    allow("refs/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<?> userTestRepository = cloneProject(project, user);
     try (Git git = userTestRepository.git()) {
-      String change3RefName = cd3.currentPatchSet().getRefName();
+      String change3RefName = cd3.currentPatchSet().refName();
       assertWithMessage("Precondition violated").that(getRefs(git)).contains(change3RefName);
 
       gApi.changes().id(cd3.getId().get()).setPrivate(true, null);
@@ -599,11 +1259,15 @@
   @GerritConfig(name = "auth.skipFullRefEvaluationIfAllRefsAreVisible", value = "false")
   public void advertisedReferencesOmitPrivateChangesOfOtherUsersWhenShortcutDisabled()
       throws Exception {
-    allow("refs/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<?> userTestRepository = cloneProject(project, user);
     try (Git git = userTestRepository.git()) {
-      String change3RefName = cd3.currentPatchSet().getRefName();
+      String change3RefName = cd3.currentPatchSet().refName();
       assertWithMessage("Precondition violated").that(getRefs(git)).contains(change3RefName);
 
       gApi.changes().id(cd3.getId().get()).setPrivate(true, null);
@@ -613,8 +1277,16 @@
 
   @Test
   public void advertisedReferencesOmitDraftCommentRefsOfOtherUsers() throws Exception {
-    allow(project, "refs/*", Permission.READ, REGISTERED_USERS);
-    allow(allUsersName, "refs/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     DraftInput draftInput = new DraftInput();
@@ -633,8 +1305,16 @@
 
   @Test
   public void advertisedReferencesOmitStarredChangesRefsOfOtherUsers() throws Exception {
-    allow(project, "refs/*", Permission.READ, REGISTERED_USERS);
-    allow(allUsersName, "refs/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().starChange(cd3.getId().toString());
@@ -649,7 +1329,10 @@
 
   @Test
   public void hideMetadata() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     // create change
     TestRepository<?> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_USERS_SELF + ":userRef");
@@ -662,7 +1345,6 @@
 
     List<String> expectedNonMetaRefs =
         ImmutableList.of(
-            RefNames.REFS_USERS_SELF,
             RefNames.refsUsers(admin.id()),
             RefNames.refsUsers(user.id()),
             RefNames.REFS_EXTERNAL_IDS,
@@ -719,7 +1401,7 @@
   }
 
   private List<String> getRefs(Git git) throws Exception {
-    return getRefs(git, Predicates.alwaysTrue());
+    return getRefs(git, x -> true);
   }
 
   private List<String> getUserRefs(Git git) throws Exception {
@@ -760,11 +1442,16 @@
     }
   }
 
-  private ReceiveCommitsAdvertiseRefsHook.Result getReceivePackRefs() throws Exception {
-    ReceiveCommitsAdvertiseRefsHook hook =
-        new ReceiveCommitsAdvertiseRefsHook(queryProvider, project);
+  private TestRefAdvertiser.Result getReceivePackRefs() throws Exception {
     try (Repository repo = repoManager.openRepository(project)) {
-      return hook.advertiseRefs(getAllRefs(repo));
+      AdvertiseRefsHook adv =
+          ReceiveCommitsAdvertiseRefsHookChain.createForTest(
+              queryProvider, project, identifiedUserFactory.create(admin.id()));
+      ReceivePack rp = new ReceivePack(repo);
+      rp.setAdvertiseRefsHook(adv);
+      TestRefAdvertiser advertiser = new TestRefAdvertiser(repo);
+      rp.sendAdvertisedRefs(advertiser);
+      return advertiser.result();
     }
   }
 
@@ -773,10 +1460,10 @@
   }
 
   private static ObjectId obj(ChangeData cd, int psNum) throws Exception {
-    PatchSet.Id psId = new PatchSet.Id(cd.getId(), psNum);
+    PatchSet.Id psId = PatchSet.id(cd.getId(), psNum);
     PatchSet ps = cd.patchSet(psId);
     assertWithMessage("%s not found in %s", psId, cd.patchSets()).that(ps).isNotNull();
-    return ObjectId.fromString(ps.getRevision().get());
+    return ps.commitId();
   }
 
   private AccountGroup.UUID createSelfOwnedGroup(String name, TestAccount... members)
@@ -792,7 +1479,7 @@
     groupInput.ownerId = ownerGroup != null ? ownerGroup.get() : null;
     groupInput.members =
         Arrays.stream(members).map(m -> String.valueOf(m.id().get())).collect(toList());
-    return new AccountGroup.UUID(gApi.groups().create(groupInput).get().id);
+    return AccountGroup.uuid(gApi.groups().create(groupInput).get().id);
   }
 
   private static Map<String, Ref> getAllRefs(Repository repo) throws IOException {
diff --git a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
index 9b823b7..876e342 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
@@ -15,8 +15,10 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.acceptance.GitUtil.deleteRef;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.eclipse.jgit.transport.ReceiveCommand.Type.CREATE;
 import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
@@ -24,11 +26,12 @@
 import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.events.RefReceivedEvent;
 import com.google.gerrit.server.git.validators.RefOperationValidationListener;
@@ -47,17 +50,16 @@
 public class RefOperationValidationIT extends AbstractDaemonTest {
   private static final String TEST_REF = "refs/heads/protected";
 
-  @Inject DynamicSet<RefOperationValidationListener> validators;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
-  private class TestRefValidator implements RefOperationValidationListener, AutoCloseable {
+  private static class TestRefValidator implements RefOperationValidationListener {
     private final ReceiveCommand.Type rejectType;
     private final String rejectRef;
-    private final RegistrationHandle handle;
 
     public TestRefValidator(ReceiveCommand.Type rejectType) {
       this.rejectType = rejectType;
       this.rejectRef = TEST_REF;
-      this.handle = validators.add("test-" + rejectType.name(), this);
     }
 
     @Override
@@ -69,27 +71,35 @@
       }
       return Collections.emptyList();
     }
+  }
 
-    @Override
-    public void close() throws Exception {
-      handle.remove();
-    }
+  private Registration testValidator(ReceiveCommand.Type rejectType) {
+    return extensionRegistry.newRegistration().add(new TestRefValidator(rejectType));
   }
 
   @Test
   public void rejectRefCreation() throws Exception {
-    try (TestRefValidator validator = new TestRefValidator(CREATE)) {
-      gApi.projects().name(project.get()).branch(TEST_REF).create(new BranchInput());
-      assert_().fail("expected exception");
-    } catch (RestApiException expected) {
+    try (Registration registration = testValidator(CREATE)) {
+      RestApiException expected =
+          assertThrows(
+              RestApiException.class,
+              () -> gApi.projects().name(project.get()).branch(TEST_REF).create(new BranchInput()));
       assertThat(expected).hasMessageThat().contains(CREATE.name());
     }
   }
 
+  private void grant(String permission) {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(permission).ref("refs/*").group(REGISTERED_USERS).force(true))
+        .update();
+  }
+
   @Test
   public void rejectRefCreationByPush() throws Exception {
-    try (TestRefValidator validator = new TestRefValidator(CREATE)) {
-      grant(project, "refs/*", Permission.PUSH, true);
+    try (Registration registration = testValidator(CREATE)) {
+      grant(Permission.PUSH);
       PushOneCommit push1 =
           pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
       PushOneCommit.Result r1 = push1.to("refs/heads/master");
@@ -102,10 +112,11 @@
   @Test
   public void rejectRefDeletion() throws Exception {
     gApi.projects().name(project.get()).branch(TEST_REF).create(new BranchInput());
-    try (TestRefValidator validator = new TestRefValidator(DELETE)) {
-      gApi.projects().name(project.get()).branch(TEST_REF).delete();
-      assert_().fail("expected exception");
-    } catch (RestApiException expected) {
+    try (Registration registration = testValidator(DELETE)) {
+      RestApiException expected =
+          assertThrows(
+              RestApiException.class,
+              () -> gApi.projects().name(project.get()).branch(TEST_REF).delete());
       assertThat(expected).hasMessageThat().contains(DELETE.name());
     }
   }
@@ -113,8 +124,8 @@
   @Test
   public void rejectRefDeletionByPush() throws Exception {
     gApi.projects().name(project.get()).branch(TEST_REF).create(new BranchInput());
-    grant(project, "refs/*", Permission.DELETE, true);
-    try (TestRefValidator validator = new TestRefValidator(DELETE)) {
+    grant(Permission.DELETE);
+    try (Registration registration = testValidator(DELETE)) {
       PushResult result = deleteRef(testRepo, TEST_REF);
       RemoteRefUpdate refUpdate = result.getRemoteUpdate(TEST_REF);
       assertThat(refUpdate.getMessage()).contains(DELETE.name());
@@ -124,8 +135,8 @@
   @Test
   public void rejectRefUpdateFastForward() throws Exception {
     gApi.projects().name(project.get()).branch(TEST_REF).create(new BranchInput());
-    try (TestRefValidator validator = new TestRefValidator(UPDATE)) {
-      grant(project, "refs/*", Permission.PUSH, true);
+    try (Registration registration = testValidator(UPDATE)) {
+      grant(Permission.PUSH);
       PushOneCommit push1 =
           pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
       PushOneCommit.Result r1 = push1.to(TEST_REF);
@@ -136,9 +147,9 @@
   @Test
   public void rejectRefUpdateNonFastForward() throws Exception {
     gApi.projects().name(project.get()).branch(TEST_REF).create(new BranchInput());
-    try (TestRefValidator validator = new TestRefValidator(UPDATE_NONFASTFORWARD)) {
+    try (Registration registration = testValidator(UPDATE_NONFASTFORWARD)) {
       ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
-      grant(project, "refs/*", Permission.PUSH, true);
+      grant(Permission.PUSH);
       PushOneCommit push1 =
           pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
       PushOneCommit.Result r1 = push1.to(TEST_REF);
@@ -161,8 +172,8 @@
   public void rejectRefUpdateNonFastForwardToExistingCommit() throws Exception {
     gApi.projects().name(project.get()).branch(TEST_REF).create(new BranchInput());
 
-    try (TestRefValidator validator = new TestRefValidator(UPDATE_NONFASTFORWARD)) {
-      grant(project, "refs/*", Permission.PUSH, true);
+    try (Registration registration = testValidator(UPDATE_NONFASTFORWARD)) {
+      grant(Permission.PUSH);
       PushOneCommit push1 =
           pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
       PushOneCommit.Result r1 = push1.to("refs/heads/master");
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index e090fae..09da628 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -16,15 +16,16 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -45,6 +46,8 @@
     return submitWholeTopicEnabledConfig();
   }
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   @GerritConfig(name = "submodule.enableSuperProjectSubscriptions", value = "false")
   public void testSubscriptionWithoutGlobalServerSetting() throws Exception {
@@ -108,7 +111,7 @@
   public void subscriptionWildcardACLForMissingProject() throws Exception {
 
     allowMatchingSubmoduleSubscription(
-        subKey, "refs/heads/*", new Project.NameKey("not-existing-super-project"), "refs/heads/*");
+        subKey, "refs/heads/*", Project.nameKey("not-existing-super-project"), "refs/heads/*");
     pushChangeTo(subRepo, "master");
   }
 
@@ -379,10 +382,7 @@
   @Test
   public void subscriptionFailOnWrongProjectACL() throws Exception {
     allowMatchingSubmoduleSubscription(
-        subKey,
-        "refs/heads/master",
-        new Project.NameKey("wrong-super-project"),
-        "refs/heads/master");
+        subKey, "refs/heads/master", Project.nameKey("wrong-super-project"), "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     createSubmoduleSubscription(superRepo, "master", subKey, "master");
@@ -479,127 +479,107 @@
   }
 
   @Test
+  @UseClockStep
   public void superRepoCommitHasSameAuthorAsSubmoduleCommit() throws Exception {
     // Make sure that the commit is created at an earlier timestamp than the submit timestamp.
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    try {
-      allowMatchingSubmoduleSubscription(
-          subKey, "refs/heads/master", superKey, "refs/heads/master");
-      createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
-      PushOneCommit.Result pushResult =
-          createChange(subRepo, "refs/heads/master", "Change", "a.txt", "some content", null);
-      approve(pushResult.getChangeId());
-      gApi.changes().id(pushResult.getChangeId()).current().submit();
+    PushOneCommit.Result pushResult =
+        createChange(subRepo, "refs/heads/master", "Change", "a.txt", "some content", null);
+    approve(pushResult.getChangeId());
+    gApi.changes().id(pushResult.getChangeId()).current().submit();
 
-      // Expect that the author name/email is preserved for the superRepo commit, but a new author
-      // timestamp is used.
-      PersonIdent authorIdent = getAuthor(superRepo, "master");
-      assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
-      assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
-      assertThat(authorIdent.getWhen())
-          .isGreaterThan(pushResult.getCommit().getAuthorIdent().getWhen());
-    } finally {
-      TestTimeUtil.useSystemTime();
-    }
+    // Expect that the author name/email is preserved for the superRepo commit, but a new author
+    // timestamp is used.
+    PersonIdent authorIdent = getAuthor(superRepo, "master");
+    assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
+    assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
+    assertThat(authorIdent.getWhen())
+        .isGreaterThan(pushResult.getCommit().getAuthorIdent().getWhen());
   }
 
   @Test
+  @UseClockStep
   public void superRepoCommitHasSameAuthorAsSubmoduleCommits() throws Exception {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
-    // Make sure that the commits are created at different timestamps and that the submit timestamp
-    // is afterwards.
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    try {
+    Project.NameKey proj2 = createProjectForPush(getSubmitType());
 
-      Project.NameKey proj2 = createProjectForPush(getSubmitType());
+    TestRepository<?> subRepo2 = cloneProject(proj2);
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
 
-      TestRepository<?> subRepo2 = cloneProject(proj2);
-      allowMatchingSubmoduleSubscription(
-          subKey, "refs/heads/master", superKey, "refs/heads/master");
-      allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
+    Config config = new Config();
+    prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
+    prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
+    pushSubmoduleConfig(superRepo, "master", config);
 
-      Config config = new Config();
-      prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
-      prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
-      pushSubmoduleConfig(superRepo, "master", config);
+    String topic = "foo";
 
-      String topic = "foo";
+    PushOneCommit.Result pushResult1 =
+        createChange(subRepo, "refs/heads/master", "Change 1", "a.txt", "some content", topic);
+    approve(pushResult1.getChangeId());
 
-      PushOneCommit.Result pushResult1 =
-          createChange(subRepo, "refs/heads/master", "Change 1", "a.txt", "some content", topic);
-      approve(pushResult1.getChangeId());
+    PushOneCommit.Result pushResult2 =
+        createChange(subRepo2, "refs/heads/master", "Change 2", "b.txt", "other content", topic);
+    approve(pushResult2.getChangeId());
 
-      PushOneCommit.Result pushResult2 =
-          createChange(subRepo2, "refs/heads/master", "Change 2", "b.txt", "other content", topic);
-      approve(pushResult2.getChangeId());
+    // Submit the topic, 2 changes with the same author.
+    gApi.changes().id(pushResult1.getChangeId()).current().submit();
 
-      // Submit the topic, 2 changes with the same author.
-      gApi.changes().id(pushResult1.getChangeId()).current().submit();
-
-      // Expect that the author name/email is preserved for the superRepo commit, but a new author
-      // timestamp is used.
-      PersonIdent authorIdent = getAuthor(superRepo, "master");
-      assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
-      assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
-      assertThat(authorIdent.getWhen())
-          .isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
-      assertThat(authorIdent.getWhen())
-          .isGreaterThan(pushResult2.getCommit().getAuthorIdent().getWhen());
-    } finally {
-      TestTimeUtil.useSystemTime();
-    }
+    // Expect that the author name/email is preserved for the superRepo commit, but a new author
+    // timestamp is used.
+    PersonIdent authorIdent = getAuthor(superRepo, "master");
+    assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
+    assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
+    assertThat(authorIdent.getWhen())
+        .isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
+    assertThat(authorIdent.getWhen())
+        .isGreaterThan(pushResult2.getCommit().getAuthorIdent().getWhen());
   }
 
   @Test
+  @UseClockStep
   public void superRepoCommitHasGerritAsAuthorIfAuthorsOfSubmoduleCommitsDiffer() throws Exception {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
-    // Make sure that the commits are created at different timestamps and that the submit timestamp
-    // is afterwards.
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    try {
-      Project.NameKey proj2 = createProjectForPush(getSubmitType());
-      TestRepository<InMemoryRepository> repo2 = cloneProject(proj2, user);
+    Project.NameKey proj2 = createProjectForPush(getSubmitType());
+    TestRepository<InMemoryRepository> repo2 = cloneProject(proj2, user);
 
-      allowMatchingSubmoduleSubscription(
-          subKey, "refs/heads/master", superKey, "refs/heads/master");
-      allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
 
-      Config config = new Config();
-      prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
-      prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
-      pushSubmoduleConfig(superRepo, "master", config);
+    Config config = new Config();
+    prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
+    prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
+    pushSubmoduleConfig(superRepo, "master", config);
 
-      String topic = "foo";
+    String topic = "foo";
 
-      // Create change as admin.
-      PushOneCommit.Result pushResult1 =
-          createChange(subRepo, "refs/heads/master", "Change 1", "a.txt", "some content", topic);
-      approve(pushResult1.getChangeId());
+    // Create change as admin.
+    PushOneCommit.Result pushResult1 =
+        createChange(subRepo, "refs/heads/master", "Change 1", "a.txt", "some content", topic);
+    approve(pushResult1.getChangeId());
 
-      // Create change as user.
-      PushOneCommit push =
-          pushFactory.create(user.newIdent(), repo2, "Change 2", "b.txt", "other content");
-      PushOneCommit.Result pushResult2 = push.to("refs/for/master/" + name(topic));
-      approve(pushResult2.getChangeId());
+    // Create change as user.
+    PushOneCommit push =
+        pushFactory.create(user.newIdent(), repo2, "Change 2", "b.txt", "other content");
+    PushOneCommit.Result pushResult2 = push.to("refs/for/master/" + name(topic));
+    approve(pushResult2.getChangeId());
 
-      // Submit the topic, 2 changes with the different author.
-      gApi.changes().id(pushResult1.getChangeId()).current().submit();
+    // Submit the topic, 2 changes with the different author.
+    gApi.changes().id(pushResult1.getChangeId()).current().submit();
 
-      // Expect that the Gerrit server identity is chosen as author for the superRepo commit and a
-      // new author timestamp is used.
-      PersonIdent authorIdent = getAuthor(superRepo, "master");
-      assertThat(authorIdent.getName()).isEqualTo(serverIdent.get().getName());
-      assertThat(authorIdent.getEmailAddress()).isEqualTo(serverIdent.get().getEmailAddress());
-      assertThat(authorIdent.getWhen())
-          .isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
-      assertThat(authorIdent.getWhen())
-          .isGreaterThan(pushResult2.getCommit().getAuthorIdent().getWhen());
-    } finally {
-      TestTimeUtil.useSystemTime();
-    }
+    // Expect that the Gerrit server identity is chosen as author for the superRepo commit and a
+    // new author timestamp is used.
+    PersonIdent authorIdent = getAuthor(superRepo, "master");
+    assertThat(authorIdent.getName()).isEqualTo(serverIdent.get().getName());
+    assertThat(authorIdent.getEmailAddress()).isEqualTo(serverIdent.get().getEmailAddress());
+    assertThat(authorIdent.getWhen())
+        .isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
+    assertThat(authorIdent.getWhen())
+        .isGreaterThan(pushResult2.getCommit().getAuthorIdent().getWhen());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index dc84d13..283c95f 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -17,17 +17,23 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
 import java.util.ArrayDeque;
 import java.util.Map;
 import org.apache.commons.lang.RandomStringUtils;
@@ -67,6 +73,8 @@
     return submitByRebaseIfNecessaryConfig();
   }
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void subscriptionUpdateOfManyChanges() throws Exception {
     allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
@@ -136,7 +144,7 @@
     gApi.changes().id(id2).current().review(ReviewInput.approve());
     gApi.changes().id(id3).current().review(ReviewInput.approve());
 
-    Map<Branch.NameKey, ObjectId> preview = fetchFromSubmitPreview(id1);
+    Map<BranchNameKey, ObjectId> preview = fetchFromSubmitPreview(id1);
     gApi.changes().id(id1).current().submit();
     ObjectId subRepoId =
         subRepo
@@ -152,8 +160,8 @@
     // As the submodules have changed commits, the superproject tree will be
     // different, so we cannot directly compare the trees here, so make
     // assumptions only about the changed branches:
-    assertThat(preview).containsKey(new Branch.NameKey(superKey, "refs/heads/master"));
-    assertThat(preview).containsKey(new Branch.NameKey(subKey, "refs/heads/master"));
+    assertThat(preview).containsKey(BranchNameKey.create(superKey, "refs/heads/master"));
+    assertThat(preview).containsKey(BranchNameKey.create(subKey, "refs/heads/master"));
 
     if ((getSubmitType() == SubmitType.CHERRY_PICK)
         || (getSubmitType() == SubmitType.REBASE_ALWAYS)) {
@@ -282,8 +290,12 @@
               .name(prefix + "sub" + i)
               .submitType(getSubmitType())
               .create();
-      grant(subKey[i], "refs/heads/*", Permission.PUSH);
-      grant(subKey[i], "refs/for/refs/heads/*", Permission.SUBMIT);
+      projectOperations
+          .project(subKey[i])
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid()))
+          .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid()))
+          .update();
       sub[i] = cloneProject(subKey[i]);
     }
 
@@ -393,7 +405,7 @@
     gApi.changes().id(subChangeId).current().submit();
 
     expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");
-    RevCommit superHead = getRemoteHead(superKey, "master");
+    RevCommit superHead = projectOperations.project(superKey).getHead("master");
     assertThat(superHead.getShortMessage()).contains("some message");
     assertThat(superHead.getId()).isNotEqualTo(superId);
   }
@@ -427,7 +439,7 @@
 
     gApi.changes().id(subChangeId).current().submit();
 
-    RevCommit superHead = getRemoteHead(superKey, "master");
+    RevCommit superHead = projectOperations.project(superKey).getHead("master");
     assertThat(superHead.getShortMessage()).isEqualTo("some message");
     assertThat(superHead.getId()).isEqualTo(superId);
   }
@@ -614,7 +626,7 @@
     expectToHaveSubmoduleState(topRepo, "master", botKey, bottomRepo, "master");
   }
 
-  private String prepareBranchCircularSubscription() throws Exception {
+  private void testBranchCircularSubscription(ThrowingConsumer<String> apiCall) throws Exception {
     Project.NameKey topKey = createProjectForPush(getSubmitType());
     Project.NameKey midKey = createProjectForPush(getSubmitType());
     Project.NameKey botKey = createProjectForPush(getSubmitType());
@@ -634,23 +646,24 @@
     String changeId = getChangeId(bottomRepo, bottomMasterHead).get();
 
     approve(changeId);
-    exception.expectMessage("Branch level circular subscriptions detected");
-    exception.expectMessage(topKey.get() + ",refs/heads/master");
-    exception.expectMessage(midKey.get() + ",refs/heads/master");
-    exception.expectMessage(botKey.get() + ",refs/heads/master");
-    return changeId;
+
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> apiCall.accept(changeId));
+    assertThat(thrown).hasMessageThat().contains("Branch level circular subscriptions detected");
+    assertThat(thrown).hasMessageThat().contains(topKey.get() + ",refs/heads/master");
+    assertThat(thrown).hasMessageThat().contains(midKey.get() + ",refs/heads/master");
+    assertThat(thrown).hasMessageThat().contains(botKey.get() + ",refs/heads/master");
   }
 
   @Test
   public void branchCircularSubscription() throws Exception {
-    String changeId = prepareBranchCircularSubscription();
-    gApi.changes().id(changeId).current().submit();
+    testBranchCircularSubscription(changeId -> gApi.changes().id(changeId).current().submit());
   }
 
   @Test
   public void branchCircularSubscriptionPreview() throws Exception {
-    String changeId = prepareBranchCircularSubscription();
-    gApi.changes().id(changeId).current().submitPreview();
+    testBranchCircularSubscription(
+        changeId -> gApi.changes().id(changeId).current().submitPreview());
   }
 
   @Test
@@ -672,10 +685,13 @@
     approve(getChangeId(subRepo, subMasterHead).get());
     approve(getChangeId(superRepo, superDevHead).get());
 
-    exception.expectMessage("Project level circular subscriptions detected");
-    exception.expectMessage(subKey.get());
-    exception.expectMessage(superKey.get());
-    gApi.changes().id(getChangeId(subRepo, subMasterHead).get()).current().submit();
+    Throwable thrown =
+        assertThrows(
+            Throwable.class,
+            () -> gApi.changes().id(getChangeId(subRepo, subMasterHead).get()).current().submit());
+    assertThat(thrown).hasMessageThat().contains("Project level circular subscriptions detected");
+    assertThat(thrown).hasMessageThat().contains(subKey.get());
+    assertThat(thrown).hasMessageThat().contains(superKey.get());
   }
 
   @Test
@@ -739,13 +755,13 @@
     approve(getChangeId(repoB, bDevHead).get());
 
     gApi.changes().id(getChangeId(repoA, aDevHead).get()).current().submit();
-    assertThat(getRemoteHead(keyA, "refs/heads/master").getShortMessage())
+    assertThat(projectOperations.project(keyA).getHead("refs/heads/master").getShortMessage())
         .contains("some message in a master.txt");
-    assertThat(getRemoteHead(keyA, "refs/heads/dev").getShortMessage())
+    assertThat(projectOperations.project(keyA).getHead("refs/heads/dev").getShortMessage())
         .contains("some message in a dev.txt");
-    assertThat(getRemoteHead(keyB, "refs/heads/master").getShortMessage())
+    assertThat(projectOperations.project(keyB).getHead("refs/heads/master").getShortMessage())
         .contains("some message in b master.txt");
-    assertThat(getRemoteHead(keyB, "refs/heads/dev").getShortMessage())
+    assertThat(projectOperations.project(keyB).getHead("refs/heads/dev").getShortMessage())
         .contains("some message in b dev.txt");
   }
 
@@ -838,13 +854,13 @@
 
     sub1.git().fetch().call();
     RevWalk rw1 = sub1.getRevWalk();
-    RevCommit master1 = rw1.parseCommit(getRemoteHead(subKey1, "master"));
+    RevCommit master1 = rw1.parseCommit(projectOperations.project(subKey1).getHead("master"));
     RevCommit change1Ps = parseCurrentRevision(rw1, changeId1);
     assertThat(rw1.isMergedInto(change1Ps, master1)).isTrue();
 
     sub2.git().fetch().call();
     RevWalk rw2 = sub2.getRevWalk();
-    RevCommit master2 = rw2.parseCommit(getRemoteHead(subKey2, "master"));
+    RevCommit master2 = rw2.parseCommit(projectOperations.project(subKey2).getHead("master"));
     RevCommit change2Ps = parseCurrentRevision(rw2, changeId2);
     assertThat(rw2.isMergedInto(change2Ps, master2)).isTrue();
 
@@ -898,6 +914,6 @@
   }
 
   private Project.NameKey nameKey(String s) {
-    return new Project.NameKey(name(s));
+    return Project.nameKey(name(s));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
index c07d512..cad0b83 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.pgm;
 
 import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.truth.StreamSubject.streams;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
@@ -26,11 +27,11 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.StandaloneSiteTest;
 import com.google.gerrit.acceptance.pgm.IndexUpgradeController.UpgradeAttempt;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.index.GerritIndexStatus;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
@@ -73,13 +74,13 @@
           .containsExactly(changeId);
       // Query account index
       assertThat(gApi.accounts().query("admin").get().stream().map(a -> a._accountId))
-          .containsExactly(adminId.get());
+          .containsExactly(admin.id().get());
       // Query group index
       assertThat(
               gApi.groups().query("Group").withOption(MEMBERS).get().stream()
                   .flatMap(g -> g.members.stream())
                   .map(a -> a._accountId))
-          .containsExactly(adminId.get());
+          .containsExactly(admin.id().get());
       // Query project index
       assertThat(gApi.projects().query(project.get()).get().stream().map(p -> p.name))
           .containsExactly(project.get());
@@ -195,7 +196,7 @@
       // Updating and searching old schema version works.
       Provider<InternalChangeQuery> queryProvider =
           ctx.getInjector().getProvider(InternalChangeQuery.class);
-      assertThat(queryProvider.get().byKey(new Change.Key(changeId))).hasSize(1);
+      assertThat(queryProvider.get().byKey(Change.key(changeId))).hasSize(1);
       assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty();
 
       GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
@@ -223,7 +224,7 @@
   }
 
   private void setUpChange() throws Exception {
-    project = new Project.NameKey("reindex-project-test");
+    project = Project.nameKey("reindex-project-test");
     try (ServerContext ctx = startServer()) {
       configureIndex(ctx.getInjector());
       GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
@@ -240,7 +241,7 @@
   }
 
   private void enableSlaveMode() throws Exception {
-    updateConfig(config -> config.setBoolean("container", null, "slave", true));
+    updateConfig(config -> config.setBoolean("container", null, "replica", true));
   }
 
   private void updateConfig(Consumer<Config> configConsumer) throws Exception {
@@ -255,30 +256,31 @@
   }
 
   private void assertSearchVersion(ServerContext ctx, int expected) {
-    assertThat(
+    assertWithMessage("search version")
+        .that(
             ctx.getInjector()
                 .getInstance(ChangeIndexCollection.class)
                 .getSearchIndex()
                 .getSchema()
                 .getVersion())
-        .named("search version")
         .isEqualTo(expected);
   }
 
   private void assertWriteVersions(ServerContext ctx, Integer... expected) {
-    assertThat(
+    assertWithMessage("write versions")
+        .about(streams())
+        .that(
             ctx.getInjector().getInstance(ChangeIndexCollection.class).getWriteIndexes().stream()
                 .map(i -> i.getSchema().getVersion()))
-        .named("write versions")
         .containsExactlyElementsIn(ImmutableSet.copyOf(expected));
   }
 
   private void assertReady(int expectedReady) throws Exception {
     Set<Integer> allVersions = ChangeSchemaDefinitions.INSTANCE.getSchemas().keySet();
     GerritIndexStatus status = new GerritIndexStatus(sitePaths);
-    assertThat(
+    assertWithMessage("ready state for index versions")
+        .that(
             allVersions.stream().collect(toImmutableMap(v -> v, v -> status.getReady(CHANGES, v))))
-        .named("ready state for index versions")
         .isEqualTo(allVersions.stream().collect(toImmutableMap(v -> v, v -> v == expectedReady)));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index 7b99a55..f23cc10 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -25,11 +25,6 @@
 public class ElasticReindexIT extends AbstractReindexTests {
 
   @ConfigSuite.Default
-  public static Config elasticsearchV6() {
-    return getConfig(ElasticVersion.V6_8);
-  }
-
-  @ConfigSuite.Config
   public static Config elasticsearchV7() {
     return getConfig(ElasticVersion.V7_8);
   }
diff --git a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
index 7cc47c3..4caee64 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.acceptance.pgm;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectIndexCollection;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import java.io.IOException;
@@ -52,9 +52,9 @@
       QueryOptions opts =
           QueryOptions.create(IndexConfig.createDefault(), 0, 1, ImmutableSet.of("name"));
       Optional<ProjectData> allProjectsData = projectIndex.getSearchIndex().get(allProjects, opts);
-      assertThat(allProjectsData.isPresent()).isTrue();
+      assertThat(allProjectsData).isPresent();
       Optional<ProjectData> allUsersData = projectIndex.getSearchIndex().get(allUsers, opts);
-      assertThat(allUsersData.isPresent()).isTrue();
+      assertThat(allUsersData).isPresent();
     }
   }
 
@@ -65,7 +65,7 @@
     // Simulate a projects indexes files modified in the past by 3 seconds
     Optional<Instant> projectsLastModified =
         getProjectsIndexLastModified(sitePaths.index_dir).map(t -> t.minusSeconds(3));
-    assertThat((projectsLastModified).isPresent()).isTrue();
+    assertThat(projectsLastModified).isPresent();
     setProjectsIndexLastModifiedInThePast(sitePaths.index_dir, projectsLastModified.get());
 
     initSite();
diff --git a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
new file mode 100644
index 0000000..bc45460
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
@@ -0,0 +1,76 @@
+// 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.rest;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.apache.http.HttpStatus.SC_OK;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import java.io.IOException;
+import java.util.regex.Pattern;
+import org.apache.http.message.BasicHeader;
+import org.junit.Test;
+
+public class RestApiServletIT extends AbstractDaemonTest {
+  private static String ANY_REST_API = "/accounts/self/capabilities";
+  private static BasicHeader ACCEPT_STAR_HEADER = new BasicHeader("Accept", "*/*");
+  private static Pattern ANY_SPACE = Pattern.compile("\\s");
+
+  @Test
+  public void restResponseBodyShouldBeCompactWithoutSpaces() throws Exception {
+    RestResponse response = adminRestSession.getWithHeader(ANY_REST_API, ACCEPT_STAR_HEADER);
+    assertThat(response.getStatusCode()).isEqualTo(SC_OK);
+
+    assertThat(contentWithoutMagicJson(response)).doesNotContainMatch(ANY_SPACE);
+  }
+
+  @Test
+  public void restResponseBodyShouldBeCompactWithoutSpacesWhenPPIsZero() throws Exception {
+    assertThat(contentWithoutMagicJson(prettyJsonRestResponse("prettyPrint", 0)))
+        .doesNotContainMatch(ANY_SPACE);
+  }
+
+  @Test
+  public void restResponseBodyShouldBeCompactWithoutSpacesWhenPrerryPrintIsZero() throws Exception {
+    assertThat(contentWithoutMagicJson(prettyJsonRestResponse("pp", 0)))
+        .doesNotContainMatch(ANY_SPACE);
+  }
+
+  @Test
+  public void restResponseBodyShouldBePrettyfiedWhenPPIsOne() throws Exception {
+    assertThat(contentWithoutMagicJson(prettyJsonRestResponse("pp", 1))).containsMatch(ANY_SPACE);
+  }
+
+  @Test
+  public void restResponseBodyShouldBePrettyfiedWhenPrettyPrintIsOne() throws Exception {
+    assertThat(contentWithoutMagicJson(prettyJsonRestResponse("prettyPrint", 1)))
+        .containsMatch(ANY_SPACE);
+  }
+
+  private RestResponse prettyJsonRestResponse(String ppArgument, int ppValue) throws Exception {
+    RestResponse response =
+        adminRestSession.getWithHeader(
+            ANY_REST_API + "?" + ppArgument + "=" + ppValue, ACCEPT_STAR_HEADER);
+    assertThat(response.getStatusCode()).isEqualTo(SC_OK);
+
+    return response;
+  }
+
+  private String contentWithoutMagicJson(RestResponse response) throws IOException {
+    return response.getEntityContent().substring(RestApiServlet.JSON_MAGIC.length);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index b30dc41..52de5ad 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -16,215 +16,320 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.apache.http.HttpStatus.SC_CREATED;
+import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.apache.http.HttpStatus.SC_OK;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.truth.Expect;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.httpd.restapi.ParameterParser;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.SubmitRule;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import org.apache.http.message.BasicHeader;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
+/**
+ * This test tests the tracing of requests.
+ *
+ * <p>To verify that tracing is working we do:
+ *
+ * <ul>
+ *   <li>Register a plugin extension that we know is invoked when the request is done. Within the
+ *       implementation of this plugin extension we access the status of the thread local state in
+ *       the {@link LoggingContext} and store it locally in the plugin extension class.
+ *   <li>Do a request (e.g. REST) that triggers the plugin extension.
+ *   <li>When the plugin extension is invoked it records the current logging context.
+ *   <li>After the request is done the test verifies that logging context that was recorded by the
+ *       plugin extension has the expected state.
+ * </ul>
+ */
 public class TraceIT extends AbstractDaemonTest {
   @Rule public final Expect expect = Expect.create();
 
-  @Inject private DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
-  @Inject private DynamicSet<CommitValidationListener> commitValidationListeners;
+  @Inject private ExtensionRegistry extensionRegistry;
   @Inject private WorkQueue workQueue;
 
-  private TraceValidatingProjectCreationValidationListener projectCreationListener;
-  private RegistrationHandle projectCreationListenerRegistrationHandle;
-  private TraceValidatingCommitValidationListener commitValidationListener;
-  private RegistrationHandle commitValidationRegistrationHandle;
-
-  @Before
-  public void setup() {
-    projectCreationListener = new TraceValidatingProjectCreationValidationListener();
-    projectCreationListenerRegistrationHandle =
-        projectCreationValidationListeners.add("gerrit", projectCreationListener);
-    commitValidationListener = new TraceValidatingCommitValidationListener();
-    commitValidationRegistrationHandle =
-        commitValidationListeners.add("gerrit", commitValidationListener);
-  }
-
-  @After
-  public void cleanup() {
-    projectCreationListenerRegistrationHandle.remove();
-    commitValidationRegistrationHandle.remove();
-  }
-
   @Test
   public void restCallWithoutTrace() throws Exception {
-    RestResponse response = adminRestSession.put("/projects/new1");
-    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
-    assertThat(projectCreationListener.traceId).isNull();
-    assertThat(projectCreationListener.isLoggingForced).isFalse();
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new1");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new1");
+    }
+  }
+
+  @Test
+  public void restCallForChangeSetsProjectTag() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    TraceChangeIndexedListener changeIndexedListener = new TraceChangeIndexedListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedListener)) {
+      RestResponse response =
+          adminRestSession.post(
+              "/changes/" + changeId + "/revisions/current/review", ReviewInput.approve());
+      assertThat(response.getStatusCode()).isEqualTo(SC_OK);
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(changeIndexedListener.tags.get("project")).containsExactly(project.get());
+    }
   }
 
   @Test
   public void restCallWithTraceRequestParam() throws Exception {
-    RestResponse response =
-        adminRestSession.put("/projects/new2?" + ParameterParser.TRACE_PARAMETER);
-    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
-    assertThat(projectCreationListener.traceId).isNotNull();
-    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response =
+          adminRestSession.put("/projects/new2?" + ParameterParser.TRACE_PARAMETER);
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
+      assertThat(projectCreationListener.traceId).isNotNull();
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new2");
+    }
   }
 
   @Test
   public void restCallWithTraceRequestParamAndProvidedTraceId() throws Exception {
-    RestResponse response =
-        adminRestSession.put("/projects/new3?" + ParameterParser.TRACE_PARAMETER + "=issue/123");
-    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
-    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
-    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response =
+          adminRestSession.put("/projects/new3?" + ParameterParser.TRACE_PARAMETER + "=issue/123");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
+      assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new3");
+    }
   }
 
   @Test
   public void restCallWithTraceHeader() throws Exception {
-    RestResponse response =
-        adminRestSession.putWithHeader(
-            "/projects/new4", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, null));
-    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
-    assertThat(projectCreationListener.traceId).isNotNull();
-    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response =
+          adminRestSession.putWithHeader(
+              "/projects/new4", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, null));
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
+      assertThat(projectCreationListener.traceId).isNotNull();
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new4");
+    }
   }
 
   @Test
   public void restCallWithTraceHeaderAndProvidedTraceId() throws Exception {
-    RestResponse response =
-        adminRestSession.putWithHeader(
-            "/projects/new5", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/123"));
-    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
-    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
-    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response =
+          adminRestSession.putWithHeader(
+              "/projects/new5", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/123"));
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
+      assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new5");
+    }
   }
 
   @Test
   public void restCallWithTraceRequestParamAndTraceHeader() throws Exception {
-    // trace ID only specified by trace header
-    RestResponse response =
-        adminRestSession.putWithHeader(
-            "/projects/new6?trace", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/123"));
-    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
-    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
-    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      // trace ID only specified by trace header
+      RestResponse response =
+          adminRestSession.putWithHeader(
+              "/projects/new6?trace", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/123"));
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
+      assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new6");
 
-    // trace ID only specified by trace request parameter
-    response =
-        adminRestSession.putWithHeader(
-            "/projects/new7?trace=issue/123", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, null));
-    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
-    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
-    assertThat(projectCreationListener.isLoggingForced).isTrue();
+      // trace ID only specified by trace request parameter
+      response =
+          adminRestSession.putWithHeader(
+              "/projects/new7?trace=issue/123",
+              new BasicHeader(RestApiServlet.X_GERRIT_TRACE, null));
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
+      assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new7");
 
-    // same trace ID specified by trace header and trace request parameter
-    response =
-        adminRestSession.putWithHeader(
-            "/projects/new8?trace=issue/123",
-            new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/123"));
-    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
-    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
-    assertThat(projectCreationListener.isLoggingForced).isTrue();
+      // same trace ID specified by trace header and trace request parameter
+      response =
+          adminRestSession.putWithHeader(
+              "/projects/new8?trace=issue/123",
+              new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/123"));
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
+      assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new8");
 
-    // different trace IDs specified by trace header and trace request parameter
-    response =
-        adminRestSession.putWithHeader(
-            "/projects/new9?trace=issue/123",
-            new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/456"));
-    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeaders(RestApiServlet.X_GERRIT_TRACE))
-        .containsExactly("issue/123", "issue/456");
-    assertThat(projectCreationListener.traceIds).containsExactly("issue/123", "issue/456");
-    assertThat(projectCreationListener.isLoggingForced).isTrue();
+      // different trace IDs specified by trace header and trace request parameter
+      response =
+          adminRestSession.putWithHeader(
+              "/projects/new9?trace=issue/123",
+              new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/456"));
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeaders(RestApiServlet.X_GERRIT_TRACE))
+          .containsExactly("issue/123", "issue/456");
+      assertThat(projectCreationListener.traceIds).containsExactly("issue/123", "issue/456");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new9");
+    }
   }
 
   @Test
   public void pushWithoutTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
-    PushOneCommit.Result r = push.to("refs/heads/master");
-    r.assertOkStatus();
-    assertThat(commitValidationListener.traceId).isNull();
-    assertThat(commitValidationListener.isLoggingForced).isFalse();
+    TraceValidatingCommitValidationListener commitValidationListener =
+        new TraceValidatingCommitValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(commitValidationListener)) {
+      PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+      PushOneCommit.Result r = push.to("refs/heads/master");
+      r.assertOkStatus();
+      assertThat(commitValidationListener.traceId).isNull();
+      assertThat(commitValidationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
+    }
   }
 
   @Test
   public void pushWithTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
-    push.setPushOptions(ImmutableList.of("trace"));
-    PushOneCommit.Result r = push.to("refs/heads/master");
-    r.assertOkStatus();
-    assertThat(commitValidationListener.traceId).isNotNull();
-    assertThat(commitValidationListener.isLoggingForced).isTrue();
+    TraceValidatingCommitValidationListener commitValidationListener =
+        new TraceValidatingCommitValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(commitValidationListener)) {
+      PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+      push.setPushOptions(ImmutableList.of("trace"));
+      PushOneCommit.Result r = push.to("refs/heads/master");
+      r.assertOkStatus();
+      assertThat(commitValidationListener.traceId).isNotNull();
+      assertThat(commitValidationListener.isLoggingForced).isTrue();
+      assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
+    }
   }
 
   @Test
   public void pushWithTraceAndProvidedTraceId() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
-    push.setPushOptions(ImmutableList.of("trace=issue/123"));
-    PushOneCommit.Result r = push.to("refs/heads/master");
-    r.assertOkStatus();
-    assertThat(commitValidationListener.traceId).isEqualTo("issue/123");
-    assertThat(commitValidationListener.isLoggingForced).isTrue();
+    TraceValidatingCommitValidationListener commitValidationListener =
+        new TraceValidatingCommitValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(commitValidationListener)) {
+      PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+      push.setPushOptions(ImmutableList.of("trace=issue/123"));
+      PushOneCommit.Result r = push.to("refs/heads/master");
+      r.assertOkStatus();
+      assertThat(commitValidationListener.traceId).isEqualTo("issue/123");
+      assertThat(commitValidationListener.isLoggingForced).isTrue();
+      assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
+    }
   }
 
   @Test
   public void pushForReviewWithoutTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
-    PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertOkStatus();
-    assertThat(commitValidationListener.traceId).isNull();
-    assertThat(commitValidationListener.isLoggingForced).isFalse();
+    TraceValidatingCommitValidationListener commitValidationListener =
+        new TraceValidatingCommitValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(commitValidationListener)) {
+      PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+      PushOneCommit.Result r = push.to("refs/for/master");
+      r.assertOkStatus();
+      assertThat(commitValidationListener.traceId).isNull();
+      assertThat(commitValidationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
+    }
   }
 
   @Test
   public void pushForReviewWithTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
-    push.setPushOptions(ImmutableList.of("trace"));
-    PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertOkStatus();
-    assertThat(commitValidationListener.traceId).isNotNull();
-    assertThat(commitValidationListener.isLoggingForced).isTrue();
+    TraceValidatingCommitValidationListener commitValidationListener =
+        new TraceValidatingCommitValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(commitValidationListener)) {
+      PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+      push.setPushOptions(ImmutableList.of("trace"));
+      PushOneCommit.Result r = push.to("refs/for/master");
+      r.assertOkStatus();
+      assertThat(commitValidationListener.traceId).isNotNull();
+      assertThat(commitValidationListener.isLoggingForced).isTrue();
+      assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
+    }
   }
 
   @Test
   public void pushForReviewWithTraceAndProvidedTraceId() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
-    push.setPushOptions(ImmutableList.of("trace=issue/123"));
-    PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertOkStatus();
-    assertThat(commitValidationListener.traceId).isEqualTo("issue/123");
-    assertThat(commitValidationListener.isLoggingForced).isTrue();
+    TraceValidatingCommitValidationListener commitValidationListener =
+        new TraceValidatingCommitValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(commitValidationListener)) {
+      PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+      push.setPushOptions(ImmutableList.of("trace=issue/123"));
+      PushOneCommit.Result r = push.to("refs/for/master");
+      r.assertOkStatus();
+      assertThat(commitValidationListener.traceId).isEqualTo("issue/123");
+      assertThat(commitValidationListener.isLoggingForced).isTrue();
+      assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
+    }
   }
 
   @Test
@@ -263,6 +368,409 @@
     assertForceLogging(false);
   }
 
+  @Test
+  public void performanceLoggingForRestCall() throws Exception {
+    TestPerformanceLogger testPerformanceLogger = new TestPerformanceLogger();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(testPerformanceLogger)) {
+      RestResponse response = adminRestSession.put("/projects/new10");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+
+      // This assertion assumes that the server invokes the PerformanceLogger plugins before it
+      // sends
+      // the response to the client. If this assertion gets flaky it's likely that this got changed
+      // on
+      // server-side.
+      assertThat(testPerformanceLogger.logEntries()).isNotEmpty();
+    }
+  }
+
+  @Test
+  public void performanceLoggingForPush() throws Exception {
+    TestPerformanceLogger testPerformanceLogger = new TestPerformanceLogger();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(testPerformanceLogger)) {
+      PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+      PushOneCommit.Result r = push.to("refs/heads/master");
+      r.assertOkStatus();
+      assertThat(testPerformanceLogger.logEntries()).isNotEmpty();
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.performanceLogging", value = "false")
+  public void noPerformanceLoggingIfDisabled() throws Exception {
+    TestPerformanceLogger testPerformanceLogger = new TestPerformanceLogger();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(testPerformanceLogger)) {
+      RestResponse response = adminRestSession.put("/projects/new11");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+
+      PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+      PushOneCommit.Result r = push.to("refs/heads/master");
+      r.assertOkStatus();
+
+      assertThat(testPerformanceLogger.logEntries()).isEmpty();
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new12")
+  public void traceProject() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new12");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new12");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+  public void traceProjectMatchRegEx() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new13");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new13");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "foo.*")
+  public void traceProjectNoMatch() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new13");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new13");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "][")
+  public void traceProjectInvalidRegEx() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new14");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new14");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+  public void traceAccount() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new15");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new15");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000001")
+  public void traceAccountNoMatch() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new16");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new16");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "999")
+  public void traceAccountNotFound() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new17");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new17");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "invalid")
+  public void traceAccountInvalidId() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new18");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new18");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestType", value = "REST")
+  public void traceRequestType() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new19");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new19");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestType", value = "SSH")
+  public void traceRequestTypeNoMatch() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new20");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new20");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestType", value = "FOO")
+  public void traceProjectInvalidRequestType() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new21");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new21");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+  public void traceProjectForAccount() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new22");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new22");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "foo.*")
+  public void traceProjectForAccountNoProjectMatch() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new23");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000001")
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+  public void traceProjectForAccountNoAccountMatch() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new24");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new24");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*")
+  public void traceRequestUri() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new23");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*/foo")
+  public void traceRequestUriNoMatch() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new23");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "][")
+  public void traceRequestUriInvalidRegEx() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/new24");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("new24");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
+  public void autoRetryWithTrace() throws Exception {
+    String changeId = createChange().getChangeId();
+    approve(changeId);
+
+    TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+    traceSubmitRule.failAlways = true;
+    try (Registration registration = extensionRegistry.newRegistration().add(traceSubmitRule)) {
+      RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+      assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).startsWith("retry-on-failure-");
+      assertThat(traceSubmitRule.traceId).startsWith("retry-on-failure-");
+      assertThat(traceSubmitRule.isLoggingForced).isTrue();
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
+  public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
+    String changeId = createChange().getChangeId();
+    approve(changeId);
+
+    TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+    traceSubmitRule.failAlways = true;
+    try (Registration registration =
+        extensionRegistry
+            .newRegistration()
+            .add(traceSubmitRule)
+            .add(
+                new ExceptionHook() {
+                  @Override
+                  public boolean shouldRetry(Throwable t) {
+                    return true;
+                  }
+                })) {
+      RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+      assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(traceSubmitRule.traceId).isNull();
+      assertThat(traceSubmitRule.isLoggingForced).isFalse();
+    }
+  }
+
+  @Test
+  public void noAutoRetryWithTraceIfDisabled() throws Exception {
+    String changeId = createChange().getChangeId();
+    approve(changeId);
+
+    TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+    traceSubmitRule.failOnce = true;
+    try (Registration registration = extensionRegistry.newRegistration().add(traceSubmitRule)) {
+      RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+      assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(traceSubmitRule.traceId).isNull();
+      assertThat(traceSubmitRule.isLoggingForced).isFalse();
+    }
+  }
+
   private void assertForceLogging(boolean expected) {
     assertThat(LoggingContext.getInstance().shouldForceLogging(null, null, false))
         .isEqualTo(expected);
@@ -273,6 +781,7 @@
     String traceId;
     ImmutableSet<String> traceIds;
     Boolean isLoggingForced;
+    ImmutableSetMultimap<String, String> tags;
 
     @Override
     public void validateNewProject(CreateProjectArgs args) throws ValidationException {
@@ -280,12 +789,14 @@
           Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
       this.traceIds = LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID");
       this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
+      this.tags = LoggingContext.getInstance().getTagsAsMap();
     }
   }
 
   private static class TraceValidatingCommitValidationListener implements CommitValidationListener {
     String traceId;
     Boolean isLoggingForced;
+    ImmutableSetMultimap<String, String> tags;
 
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
@@ -293,7 +804,67 @@
       this.traceId =
           Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
       this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
+      this.tags = LoggingContext.getInstance().getTagsAsMap();
       return ImmutableList.of();
     }
   }
+
+  private static class TraceChangeIndexedListener implements ChangeIndexedListener {
+    ImmutableSetMultimap<String, String> tags;
+
+    @Override
+    public void onChangeIndexed(String projectName, int id) {
+      this.tags = LoggingContext.getInstance().getTagsAsMap();
+    }
+
+    @Override
+    public void onChangeDeleted(int id) {}
+  }
+
+  private static class TraceSubmitRule implements SubmitRule {
+    String traceId;
+    Boolean isLoggingForced;
+    boolean failOnce;
+    boolean failAlways;
+
+    @Override
+    public Optional<SubmitRecord> evaluate(ChangeData changeData) {
+      this.traceId =
+          Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
+      this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
+
+      if (failOnce || failAlways) {
+        failOnce = false;
+        throw new IllegalStateException("forced failure from test");
+      }
+
+      SubmitRecord submitRecord = new SubmitRecord();
+      submitRecord.status = SubmitRecord.Status.OK;
+      return Optional.of(submitRecord);
+    }
+  }
+
+  private static class TestPerformanceLogger implements PerformanceLogger {
+    private List<PerformanceLogEntry> logEntries = new ArrayList<>();
+
+    @Override
+    public void log(String operation, long durationMs, Metadata metadata) {
+      logEntries.add(PerformanceLogEntry.create(operation, metadata));
+    }
+
+    ImmutableList<PerformanceLogEntry> logEntries() {
+      return ImmutableList.copyOf(logEntries);
+    }
+  }
+
+  @AutoValue
+  abstract static class PerformanceLogEntry {
+    static PerformanceLogEntry create(String operation, Metadata metadata) {
+      return new AutoValue_TraceIT_PerformanceLogEntry(operation, metadata);
+    }
+
+    abstract String operation();
+
+    abstract Metadata metadata();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java b/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
index 5e652c0..03202f2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
@@ -18,21 +18,33 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.reviewdb.client.Account;
 import java.util.List;
 
 public class AccountAssert {
-
-  public static void assertAccountInfo(TestAccount a, AccountInfo ai) {
-    assertThat(a.id().get()).isEqualTo(ai._accountId);
-    assertThat(a.fullName()).isEqualTo(ai.name);
-    assertThat(a.email()).isEqualTo(ai.email);
+  /**
+   * Asserts an AccountInfo for an active account.
+   *
+   * @param testAccount the TestAccount which the provided AccountInfo is expected to match
+   * @param accountInfo the AccountInfo that should be asserted
+   */
+  public static void assertAccountInfo(TestAccount testAccount, AccountInfo accountInfo) {
+    assertThat(accountInfo._accountId).isEqualTo(testAccount.id().get());
+    assertThat(accountInfo.name).isEqualTo(testAccount.fullName());
+    assertThat(accountInfo.email).isEqualTo(testAccount.email());
+    assertThat(accountInfo.inactive).isNull();
   }
 
+  /**
+   * Asserts an AccountInfos for active accounts.
+   *
+   * @param expected the TestAccounts which the provided AccountInfos are expected to match
+   * @param actual the AccountInfos that should be asserted
+   */
   public static void assertAccountInfos(List<TestAccount> expected, List<AccountInfo> actual) {
     Iterable<Account.Id> expectedIds = TestAccount.ids(expected);
-    Iterable<Account.Id> actualIds = Iterables.transform(actual, a -> new Account.Id(a._accountId));
+    Iterable<Account.Id> actualIds = Iterables.transform(actual, a -> Account.id(a._accountId));
     assertThat(actualIds).containsExactlyElementsIn(expectedIds).inOrder();
     for (int i = 0; i < expected.size(); i++) {
       AccountAssert.assertAccountInfo(expected.get(i), actual.get(i));
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/BUILD b/javatests/com/google/gerrit/acceptance/rest/account/BUILD
index 66ea6f3..a00a8b2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/account/BUILD
@@ -5,7 +5,10 @@
     srcs = glob(["*IT.java"]),
     group = "rest_account",
     labels = ["rest"],
-    deps = [":util"],
+    deps = [
+        ":util",
+        "//java/com/google/gerrit/server/account/externalids/testing",
+    ],
 )
 
 java_library(
@@ -18,7 +21,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/acceptance:lib",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//lib:junit",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java b/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
index e7ce43f..be4fde0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.common.data.GlobalCapability.ACCESS_DATABASE;
 import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
 import static com.google.gerrit.common.data.GlobalCapability.BATCH_CHANGES_LIMIT;
@@ -26,24 +29,30 @@
 import static com.google.gerrit.common.data.GlobalCapability.RUN_AS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
-import com.google.common.collect.Iterables;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 public class CapabilitiesIT extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void capabilitiesUser() throws Exception {
-    Iterable<String> all =
-        Iterables.filter(
-            GlobalCapability.getAllNames(),
-            c -> !ADMINISTRATE_SERVER.equals(c) && !PRIORITY.equals(c));
-
-    allowGlobalCapabilities(REGISTERED_USERS, all);
+    ImmutableList<String> all =
+        GlobalCapability.getAllNames().stream()
+            .filter(c -> !ADMINISTRATE_SERVER.equals(c) && !PRIORITY.equals(c))
+            .collect(toImmutableList());
+    TestProjectUpdate.Builder allowBuilder = projectOperations.allProjectsForUpdate();
+    all.forEach(c -> allowBuilder.add(allowCapability(c).group(REGISTERED_USERS)));
+    allowBuilder.update();
     try {
       RestResponse r = userRestSession.get("/accounts/self/capabilities");
       r.assertOK();
@@ -67,7 +76,9 @@
         }
       }
     } finally {
-      removeGlobalCapabilities(REGISTERED_USERS, all);
+      TestProjectUpdate.Builder removeBuilder = projectOperations.allProjectsForUpdate();
+      all.forEach(c -> removeBuilder.remove(capabilityKey(c).group(REGISTERED_USERS)));
+      removeBuilder.update();
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java b/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java
index aca6c4c..1b83d92 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth8.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
 import org.junit.Test;
@@ -31,4 +32,44 @@
     r.assertCreated();
     assertThat(accountCache.getByUsername(input.username)).isPresent();
   }
+
+  @Test
+  @GerritConfig(name = "auth.userNameToLowerCase", value = "false")
+  public void createAccountRestApiUserNameToLowerCaseFalse() throws Exception {
+    AccountInput input = new AccountInput();
+    input.username = "JohnDoe";
+    assertThat(accountCache.getByUsername(input.username)).isEmpty();
+    RestResponse r = adminRestSession.put("/accounts/" + input.username, input);
+    r.assertCreated();
+    assertThat(accountCache.getByUsername(input.username)).isPresent();
+  }
+
+  @Test
+  @GerritConfig(name = "auth.userNameToLowerCase", value = "true")
+  public void createAccountRestApiUserNameToLowerCaseTrue() throws Exception {
+    testUserNameToLowerCase("John1", "John1", "john1");
+    assertThat(accountCache.getByUsername("John1")).isEmpty();
+
+    testUserNameToLowerCase("john2", "John2", "john2");
+    assertThat(accountCache.getByUsername("John2")).isEmpty();
+
+    testUserNameToLowerCase("John3", "john3", "john3");
+    assertThat(accountCache.getByUsername("John3")).isEmpty();
+
+    testUserNameToLowerCase("John4", "johN4", "john4");
+    assertThat(accountCache.getByUsername("John4")).isEmpty();
+    assertThat(accountCache.getByUsername("johN4")).isEmpty();
+
+    testUserNameToLowerCase("john5", "john5", "john5");
+  }
+
+  private void testUserNameToLowerCase(String usernameUrl, String usernameInput, String usernameDb)
+      throws Exception {
+    AccountInput input = new AccountInput();
+    input.username = usernameInput;
+    assertThat(accountCache.getByUsername(usernameDb)).isEmpty();
+    RestResponse r = adminRestSession.put("/accounts/" + usernameUrl, input);
+    r.assertCreated();
+    assertThat(accountCache.getByUsername(usernameDb)).isPresent();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
index 84f218d..aaeed02 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.collect.ImmutableMultimap;
@@ -24,13 +25,13 @@
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.accounts.EmailApi;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.common.EmailInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
@@ -149,16 +150,20 @@
   @Test
   public void setPreferredEmailToNonExistingEmail() throws Exception {
     String email = "non-existing@example.com";
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("Not found: " + email);
-    gApi.accounts().self().email(email).setPreferred();
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.accounts().self().email(email).setPreferred());
+    assertThat(thrown).hasMessageThat().contains("Not found: " + email);
   }
 
   @Test
   public void setPreferredEmailToEmailOfOtherAccount() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("Not found: " + user.email());
-    gApi.accounts().self().email(user.email()).setPreferred();
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.accounts().self().email(user.email()).setPreferred());
+    assertThat(thrown).hasMessageThat().contains("Not found: " + user.email());
   }
 
   @Test
@@ -201,9 +206,11 @@
     Context oldCtx =
         createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id(), user.email()));
     try {
-      exception.expect(ResourceConflictException.class);
-      exception.expectMessage("email in use by another account");
-      gApi.accounts().self().email(user.email()).setPreferred();
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.accounts().self().email(user.email()).setPreferred());
+      assertThat(thrown).hasMessageThat().contains("email in use by another account");
     } finally {
       atrScope.set(oldCtx);
     }
@@ -248,9 +255,7 @@
 
     // Now the email is no longer found
     requestScopeOperations.resetCurrentApiUser();
-    emailApi = gApi.accounts().self().email(email);
-    exception.expect(ResourceNotFoundException.class);
-    emailApi.get();
+    assertThrows(ResourceNotFoundException.class, () -> gApi.accounts().self().email(email).get());
   }
 
   private Set<String> getEmails() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index f08fa44..6e16435 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -16,16 +16,21 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
+import static com.google.gerrit.server.account.externalids.testing.ExternalIdTestUtil.insertExternalIdWithEmptyNote;
+import static com.google.gerrit.server.account.externalids.testing.ExternalIdTestUtil.insertExternalIdWithInvalidConfig;
+import static com.google.gerrit.server.account.externalids.testing.ExternalIdTestUtil.insertExternalIdWithKeyThatDoesntMatchNoteId;
+import static com.google.gerrit.server.account.externalids.testing.ExternalIdTestUtil.insertExternalIdWithoutAccountId;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -33,9 +38,12 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 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.RefNames;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
@@ -44,8 +52,6 @@
 import com.google.gerrit.extensions.common.AccountExternalIdInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -54,6 +60,7 @@
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.testing.ConfigSuite;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -70,13 +77,8 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -91,11 +93,26 @@
   @Inject private ExternalIds externalIds;
   @Inject private ExternalIdReader externalIdReader;
   @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
+  @ConfigSuite.Default
+  public static Config partialCacheReloadingEnabled() {
+    Config cfg = new Config();
+    cfg.setBoolean("cache", "external_ids_map", "enablePartialReloads", true);
+    return cfg;
+  }
+
+  @ConfigSuite.Config
+  public static Config partialCacheReloadingDisabled() {
+    Config cfg = new Config();
+    cfg.setBoolean("cache", "external_ids_map", "enablePartialReloads", false);
+    return cfg;
+  }
+
   @Test
   public void getExternalIds() throws Exception {
-    Collection<ExternalId> expectedIds = getAccountState(user.id()).getExternalIds();
+    Collection<ExternalId> expectedIds = getAccountState(user.id()).externalIds();
     List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
 
     RestResponse response = userRestSession.get("/accounts/self/external.ids");
@@ -112,16 +129,20 @@
   @Test
   public void getExternalIdsOfOtherUserNotAllowed() throws Exception {
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("access database not permitted");
-    gApi.accounts().id(admin.id().get()).getExternalIds();
+    AuthException thrown =
+        assertThrows(
+            AuthException.class, () -> gApi.accounts().id(admin.id().get()).getExternalIds());
+    assertThat(thrown).hasMessageThat().contains("access database not permitted");
   }
 
   @Test
   public void getExternalIdsOfOtherUserWithAccessDatabase() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
-    Collection<ExternalId> expectedIds = getAccountState(admin.id()).getExternalIds();
+    Collection<ExternalId> expectedIds = getAccountState(admin.id()).externalIds();
     List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
 
     RestResponse response = userRestSession.get("/accounts/" + admin.id() + "/external.ids");
@@ -165,27 +186,38 @@
   public void deleteExternalIdsOfOtherUserNotAllowed() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("access database not permitted");
-    gApi.accounts()
-        .id(admin.id().get())
-        .deleteExternalIds(extIds.stream().map(e -> e.identity).collect(toList()));
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () ->
+                gApi.accounts()
+                    .id(admin.id().get())
+                    .deleteExternalIds(extIds.stream().map(e -> e.identity).collect(toList())));
+    assertThat(thrown).hasMessageThat().contains("access database not permitted");
   }
 
   @Test
   public void deleteExternalIdOfOtherUserUnderOwnAccount_UnprocessableEntity() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage(String.format("External id %s does not exist", extIds.get(0).identity));
-    gApi.accounts()
-        .self()
-        .deleteExternalIds(extIds.stream().map(e -> e.identity).collect(toList()));
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () ->
+                gApi.accounts()
+                    .self()
+                    .deleteExternalIds(extIds.stream().map(e -> e.identity).collect(toList())));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(String.format("External id %s does not exist", extIds.get(0).identity));
   }
 
   @Test
   public void deleteExternalIdsOfOtherUserWithAccessDatabase() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds();
 
@@ -248,29 +280,33 @@
 
   @Test
   public void fetchExternalIdsBranch() throws Exception {
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
+    final TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
 
     // refs/meta/external-ids is only visible to users with the 'Access Database' capability
-    try {
-      fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
-      fail("expected TransportException");
-    } catch (TransportException e) {
-      assertThat(e.getMessage())
-          .isEqualTo(
-              "Remote does not have " + RefNames.REFS_EXTERNAL_IDS + " available for fetch.");
-    }
+    TransportException thrown =
+        assertThrows(
+            TransportException.class, () -> fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Remote does not have " + RefNames.REFS_EXTERNAL_IDS + " available for fetch.");
 
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     // re-clone to get new request context, otherwise the old global capabilities are still cached
     // in the IdentifiedUser object
-    allUsersRepo = cloneProject(allUsers, user);
-    fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
+    TestRepository<InMemoryRepository> allUsersRepo2 = cloneProject(allUsers, user);
+    fetch(allUsersRepo2, RefNames.REFS_EXTERNAL_IDS);
   }
 
   @Test
   public void pushToExternalIdsBranch() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
@@ -295,14 +331,21 @@
 
   @Test
   public void pushToExternalIdsBranchRejectsExternalIdWithoutAccountId() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
     allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
 
     insertExternalIdWithoutAccountId(
-        allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
+        allUsersRepo.getRepository(),
+        allUsersRepo.getRevWalk(),
+        admin.newIdent(),
+        admin.id(),
+        "foo:bar");
     allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
 
     allowPushOfExternalIds();
@@ -313,14 +356,21 @@
   @Test
   public void pushToExternalIdsBranchRejectsExternalIdWithKeyThatDoesntMatchTheNoteId()
       throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
     allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
 
     insertExternalIdWithKeyThatDoesntMatchNoteId(
-        allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
+        allUsersRepo.getRepository(),
+        allUsersRepo.getRevWalk(),
+        admin.newIdent(),
+        admin.id(),
+        "foo:bar");
     allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
 
     allowPushOfExternalIds();
@@ -330,14 +380,17 @@
 
   @Test
   public void pushToExternalIdsBranchRejectsExternalIdWithInvalidConfig() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
     allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
 
     insertExternalIdWithInvalidConfig(
-        allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
+        allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), admin.newIdent(), "foo:bar");
     allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
 
     allowPushOfExternalIds();
@@ -347,14 +400,17 @@
 
   @Test
   public void pushToExternalIdsBranchRejectsExternalIdWithEmptyNote() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
     allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
 
     insertExternalIdWithEmptyNote(
-        allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
+        allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), admin.newIdent(), "foo:bar");
     allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
 
     allowPushOfExternalIds();
@@ -387,7 +443,10 @@
 
   private void testPushToExternalIdsBranchRejectsInvalidExternalId(ExternalId invalidExtId)
       throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
@@ -403,7 +462,10 @@
 
   @Test
   public void readExternalIdsWhenInvalidExternalIdsExist() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.resetCurrentApiUser();
 
     insertValidExternalIds();
@@ -424,7 +486,10 @@
 
   @Test
   public void checkConsistency() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.resetCurrentApiUser();
 
     insertValidExternalIds();
@@ -446,9 +511,11 @@
 
   @Test
   public void checkConsistencyNotAllowed() throws Exception {
-    exception.expect(AuthException.class);
-    exception.expectMessage("access database not permitted");
-    gApi.config().server().checkConsistency(new ConsistencyCheckInput());
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.config().server().checkConsistency(new ConsistencyCheckInput()));
+    assertThat(thrown).hasMessageThat().contains("access database not permitted");
   }
 
   private ConsistencyProblemInfo consistencyError(String message) {
@@ -525,7 +592,8 @@
     try (Repository repo = repoManager.openRepository(allUsers);
         RevWalk rw = new RevWalk(repo)) {
       String externalId = nextId(scheme, i);
-      String noteId = insertExternalIdWithoutAccountId(repo, rw, externalId);
+      String noteId =
+          insertExternalIdWithoutAccountId(repo, rw, admin.newIdent(), admin.id(), externalId);
       expectedProblems.add(
           consistencyError(
               "Invalid external ID config for note '"
@@ -535,7 +603,9 @@
                   + ".accountId' is missing, expected account ID"));
 
       externalId = nextId(scheme, i);
-      noteId = insertExternalIdWithKeyThatDoesntMatchNoteId(repo, rw, externalId);
+      noteId =
+          insertExternalIdWithKeyThatDoesntMatchNoteId(
+              repo, rw, admin.newIdent(), admin.id(), externalId);
       expectedProblems.add(
           consistencyError(
               "Invalid external ID config for note '"
@@ -546,12 +616,12 @@
                   + noteId
                   + "'"));
 
-      noteId = insertExternalIdWithInvalidConfig(repo, rw, nextId(scheme, i));
+      noteId = insertExternalIdWithInvalidConfig(repo, rw, admin.newIdent(), nextId(scheme, i));
       expectedProblems.add(
           consistencyError(
               "Invalid external ID config for note '" + noteId + "': Invalid line in config file"));
 
-      noteId = insertExternalIdWithEmptyNote(repo, rw, nextId(scheme, i));
+      noteId = insertExternalIdWithEmptyNote(repo, rw, admin.newIdent(), nextId(scheme, i));
       expectedProblems.add(
           consistencyError(
               "Invalid external ID config for note '"
@@ -570,125 +640,8 @@
         "password");
   }
 
-  private String insertExternalIdWithoutAccountId(Repository repo, RevWalk rw, String externalId)
-      throws IOException {
-    return insertExternalId(
-        repo,
-        rw,
-        (ins, noteMap) -> {
-          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id());
-          ObjectId noteId = extId.key().sha1();
-          Config c = new Config();
-          extId.writeToConfig(c);
-          c.unset("externalId", extId.key().get(), "accountId");
-          byte[] raw = c.toText().getBytes(UTF_8);
-          ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
-          noteMap.set(noteId, dataBlob);
-          return noteId;
-        });
-  }
-
-  private String insertExternalIdWithKeyThatDoesntMatchNoteId(
-      Repository repo, RevWalk rw, String externalId) throws IOException {
-    return insertExternalId(
-        repo,
-        rw,
-        (ins, noteMap) -> {
-          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id());
-          ObjectId noteId = ExternalId.Key.parse(externalId + "x").sha1();
-          Config c = new Config();
-          extId.writeToConfig(c);
-          byte[] raw = c.toText().getBytes(UTF_8);
-          ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
-          noteMap.set(noteId, dataBlob);
-          return noteId;
-        });
-  }
-
-  private String insertExternalIdWithInvalidConfig(Repository repo, RevWalk rw, String externalId)
-      throws IOException {
-    return insertExternalId(
-        repo,
-        rw,
-        (ins, noteMap) -> {
-          ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
-          byte[] raw = "bad-config".getBytes(UTF_8);
-          ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
-          noteMap.set(noteId, dataBlob);
-          return noteId;
-        });
-  }
-
-  private String insertExternalIdWithEmptyNote(Repository repo, RevWalk rw, String externalId)
-      throws IOException {
-    return insertExternalId(
-        repo,
-        rw,
-        (ins, noteMap) -> {
-          ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
-          byte[] raw = "".getBytes(UTF_8);
-          ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
-          noteMap.set(noteId, dataBlob);
-          return noteId;
-        });
-  }
-
-  private String insertExternalId(Repository repo, RevWalk rw, ExternalIdInserter extIdInserter)
-      throws IOException {
-    ObjectId rev = ExternalIdReader.readRevision(repo);
-    NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
-
-    try (ObjectInserter ins = repo.newObjectInserter()) {
-      ObjectId noteId = extIdInserter.addNote(ins, noteMap);
-
-      CommitBuilder cb = new CommitBuilder();
-      cb.setMessage("Update external IDs");
-      cb.setTreeId(noteMap.writeTree(ins));
-      cb.setAuthor(admin.newIdent());
-      cb.setCommitter(admin.newIdent());
-      if (!rev.equals(ObjectId.zeroId())) {
-        cb.setParentId(rev);
-      } else {
-        cb.setParentIds(); // Ref is currently nonexistent, commit has no parents.
-      }
-      if (cb.getTreeId() == null) {
-        if (rev.equals(ObjectId.zeroId())) {
-          cb.setTreeId(ins.insert(OBJ_TREE, new byte[] {})); // No parent, assume empty tree.
-        } else {
-          RevCommit p = rw.parseCommit(rev);
-          cb.setTreeId(p.getTree()); // Copy tree from parent.
-        }
-      }
-      ObjectId commitId = ins.insert(cb);
-      ins.flush();
-
-      RefUpdate u = repo.updateRef(RefNames.REFS_EXTERNAL_IDS);
-      u.setExpectedOldObjectId(rev);
-      u.setNewObjectId(commitId);
-      RefUpdate.Result res = u.update();
-      switch (res) {
-        case NEW:
-        case FAST_FORWARD:
-        case NO_CHANGE:
-        case RENAMED:
-        case FORCED:
-          break;
-        case LOCK_FAILURE:
-        case IO_FAILURE:
-        case NOT_ATTEMPTED:
-        case REJECTED:
-        case REJECTED_CURRENT_BRANCH:
-        case REJECTED_MISSING_OBJECT:
-        case REJECTED_OTHER_REASON:
-        default:
-          throw new IOException("Updating external IDs failed with " + res);
-      }
-      return noteId.getName();
-    }
-  }
-
   private ExternalId createExternalIdForNonExistingAccount(String externalId) {
-    return ExternalId.create(ExternalId.Key.parse(externalId), new Account.Id(1));
+    return ExternalId.create(ExternalId.Key.parse(externalId), Account.id(1));
   }
 
   private ExternalId createExternalIdWithInvalidEmail(String externalId) {
@@ -715,7 +668,7 @@
   @Test
   public void readExternalIdWithAccountIdThatCanBeExpressedInKiB() throws Exception {
     ExternalId.Key extIdKey = ExternalId.Key.parse("foo:bar");
-    Account.Id accountId = new Account.Id(1024 * 100);
+    Account.Id accountId = Account.id(1024 * 100);
     accountsUpdateProvider
         .get()
         .insert(
@@ -757,23 +710,25 @@
 
   @Test
   public void byAccountFailIfReadingExternalIdsFails() throws Exception {
+    assume().that(isPartialCacheReloadingEnabled()).isFalse();
+
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // update external ID branch so that external IDs need to be reloaded
       insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id()));
 
-      exception.expect(IOException.class);
-      externalIds.byAccount(admin.id());
+      assertThrows(IOException.class, () -> externalIds.byAccount(admin.id()));
     }
   }
 
   @Test
   public void byEmailFailIfReadingExternalIdsFails() throws Exception {
+    assume().that(isPartialCacheReloadingEnabled()).isFalse();
+
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // update external ID branch so that external IDs need to be reloaded
       insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id()));
 
-      exception.expect(IOException.class);
-      externalIds.byEmail(admin.email());
+      assertThrows(IOException.class, () -> externalIds.byEmail(admin.email()));
     }
   }
 
@@ -910,6 +865,10 @@
     }
   }
 
+  private boolean isPartialCacheReloadingEnabled() {
+    return cfg.getBoolean("cache", "external_ids_map", "enablePartialReloads", true);
+  }
+
   private void insertExtId(ExternalId extId) throws Exception {
     accountsUpdateProvider
         .get()
@@ -976,9 +935,13 @@
     return info;
   }
 
-  private void allowPushOfExternalIds() throws IOException, ConfigInvalidException {
-    grant(allUsers, RefNames.REFS_EXTERNAL_IDS, Permission.READ);
-    grant(allUsers, RefNames.REFS_EXTERNAL_IDS, Permission.PUSH);
+  private void allowPushOfExternalIds() {
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_EXTERNAL_IDS).group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_EXTERNAL_IDS).group(adminGroupUuid()))
+        .update();
   }
 
   private void assertRefUpdateFailure(RemoteRefUpdate update, String msg) {
@@ -990,9 +953,4 @@
     externalIdReader.setFailOnLoad(true);
     return () -> externalIdReader.setFailOnLoad(false);
   }
-
-  @FunctionalInterface
-  private interface ExternalIdInserter {
-    public ObjectId addNote(ObjectInserter ins, NoteMap noteMap) throws IOException;
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
index 27ae8b12..9202f42 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
@@ -19,8 +19,8 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountDetailInfo;
-import com.google.gerrit.reviewdb.client.Account;
 import org.junit.Test;
 
 public class GetAccountDetailIT extends AbstractDaemonTest {
@@ -30,6 +30,6 @@
     AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
     assertAccountInfo(admin, info);
     Account account = getAccount(admin.id());
-    assertThat(info.registeredOn).isEqualTo(account.getRegisteredOn());
+    assertThat(info.registeredOn).isEqualTo(account.registeredOn());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
index 11f7c0f..782638a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
@@ -14,19 +14,26 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfo;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 @NoHttpd
 public class GetAccountIT extends AbstractDaemonTest {
-  @Test(expected = ResourceNotFoundException.class)
+  @Inject private AccountOperations accountOperations;
+
+  @Test
   public void getNonExistingAccount_NotFound() throws Exception {
-    gApi.accounts().id("non-existing").get();
+    assertThrows(ResourceNotFoundException.class, () -> gApi.accounts().id("non-existing").get());
   }
 
   @Test
@@ -50,6 +57,16 @@
     testGetAccount("self", admin);
   }
 
+  @Test
+  public void getInactiveAccount() throws Exception {
+    accountOperations.account(user.id()).forUpdate().inactive().update();
+    AccountInfo accountInfo = gApi.accounts().id(user.id().get()).get();
+    assertThat(accountInfo._accountId).isEqualTo(user.id().get());
+    assertThat(accountInfo.name).isEqualTo(user.fullName());
+    assertThat(accountInfo.email).isEqualTo(user.email());
+    assertThat(accountInfo.inactive).isTrue();
+  }
+
   private void testGetAccount(String id, TestAccount expectedAccount) throws Exception {
     assertAccountInfo(expectedAccount, gApi.accounts().id(id).get());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 4dec505..faaba06 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -15,9 +15,15 @@
 package com.google.gerrit.acceptance.rest.account;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -28,10 +34,17 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 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.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
@@ -49,17 +62,11 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.account.AccountControl;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import org.apache.http.Header;
@@ -73,6 +80,7 @@
   @Inject private ApprovalsUtil approvalsUtil;
   @Inject private ChangeMessagesUtil cmUtil;
   @Inject private CommentsUtil commentsUtil;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   private RestSession anonRestSession;
@@ -106,11 +114,11 @@
     revision.review(in);
 
     PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
-    assertThat(psa.getPatchSetId().get()).isEqualTo(1);
-    assertThat(psa.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa.getAccountId()).isEqualTo(user.id());
-    assertThat(psa.getValue()).isEqualTo(1);
-    assertThat(psa.getRealAccountId()).isEqualTo(admin.id());
+    assertThat(psa.patchSetId().get()).isEqualTo(1);
+    assertThat(psa.label()).isEqualTo("Code-Review");
+    assertThat(psa.accountId()).isEqualTo(user.id());
+    assertThat(psa.value()).isEqualTo(1);
+    assertThat(psa.realAccountId()).isEqualTo(admin.id());
 
     ChangeData cd = r.getChange();
     ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
@@ -129,9 +137,10 @@
     in.onBehalfOf = user.id().toString();
     in.message = "Message on behalf of";
 
-    exception.expect(AuthException.class);
-    exception.expectMessage("label required to post review on behalf of \"" + in.onBehalfOf + '"');
-    revision.review(in);
+    AuthException thrown = assertThrows(AuthException.class, () -> revision.review(in));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("label required to post review on behalf of \"" + in.onBehalfOf + '"');
   }
 
   @Test
@@ -143,9 +152,10 @@
     ReviewInput in = new ReviewInput().label("Not-A-Label", 5);
     in.onBehalfOf = user.id().toString();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("label \"Not-A-Label\" is not a configured label");
-    gApi.changes().id(changeId).current().review(in);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class, () -> gApi.changes().id(changeId).current().review(in));
+    assertThat(thrown).hasMessageThat().contains("label \"Not-A-Label\" is not a configured label");
   }
 
   @Test
@@ -163,7 +173,7 @@
   @Test
   public void voteOnBehalfOfLabelNotPermitted() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType verified = Util.verified();
+      LabelType verified = TestLabels.verified();
       u.getConfig().getLabelSections().put(verified.getName(), verified);
       u.save();
     }
@@ -175,10 +185,11 @@
     in.onBehalfOf = user.id().toString();
     in.label("Verified", 1);
 
-    exception.expect(AuthException.class);
-    exception.expectMessage(
-        "not permitted to modify label \"Verified\" on behalf of \"" + in.onBehalfOf + '"');
-    revision.review(in);
+    AuthException thrown = assertThrows(AuthException.class, () -> revision.review(in));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "not permitted to modify label \"Verified\" on behalf of \"" + in.onBehalfOf + '"');
   }
 
   @Test
@@ -207,11 +218,11 @@
     gApi.changes().id(r.getChangeId()).current().review(in);
 
     PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
-    assertThat(psa.getPatchSetId().get()).isEqualTo(1);
-    assertThat(psa.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa.getAccountId()).isEqualTo(user.id());
-    assertThat(psa.getValue()).isEqualTo(1);
-    assertThat(psa.getRealAccountId()).isEqualTo(admin.id());
+    assertThat(psa.patchSetId().get()).isEqualTo(1);
+    assertThat(psa.label()).isEqualTo("Code-Review");
+    assertThat(psa.accountId()).isEqualTo(user.id());
+    assertThat(psa.value()).isEqualTo(1);
+    assertThat(psa.realAccountId()).isEqualTo(admin.id());
 
     ChangeData cd = r.getChange();
     Comment c = Iterables.getOnlyElement(commentsUtil.publishedByChange(cd.notes()));
@@ -266,9 +277,10 @@
     in.label("Code-Review", 1);
     in.drafts = DraftHandling.PUBLISH;
 
-    exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to modify other user's drafts");
-    gApi.changes().id(r.getChangeId()).current().review(in);
+    AuthException thrown =
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(r.getChangeId()).current().review(in));
+    assertThat(thrown).hasMessageThat().contains("not allowed to modify other user's drafts");
   }
 
   @Test
@@ -281,10 +293,10 @@
     in.onBehalfOf = "doesnotexist";
     in.label("Code-Review", 1);
 
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("not found");
-    exception.expectMessage("doesnotexist");
-    revision.review(in);
+    UnprocessableEntityException thrown =
+        assertThrows(UnprocessableEntityException.class, () -> revision.review(in));
+    assertThat(thrown).hasMessageThat().contains("not found");
+    assertThat(thrown).hasMessageThat().contains("doesnotexist");
   }
 
   @Test
@@ -299,9 +311,11 @@
     in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
 
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("on_behalf_of account " + user.id() + " cannot see change");
-    revision.review(in);
+    UnprocessableEntityException thrown =
+        assertThrows(UnprocessableEntityException.class, () -> revision.review(in));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("on_behalf_of account " + user.id() + " cannot see change");
   }
 
   @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
@@ -318,10 +332,10 @@
     in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
 
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("not found");
-    exception.expectMessage(in.onBehalfOf);
-    revision.review(in);
+    UnprocessableEntityException thrown =
+        assertThrows(UnprocessableEntityException.class, () -> revision.review(in));
+    assertThat(thrown).hasMessageThat().contains("not found");
+    assertThat(thrown).hasMessageThat().contains(in.onBehalfOf);
   }
 
   @Test
@@ -338,8 +352,8 @@
     assertThat(cd.change().isMerged()).isTrue();
     PatchSetApproval submitter =
         approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
-    assertThat(submitter.getAccountId()).isEqualTo(admin2.id());
-    assertThat(submitter.getRealAccountId()).isEqualTo(admin.id());
+    assertThat(submitter.accountId()).isEqualTo(admin2.id());
+    assertThat(submitter.realAccountId()).isEqualTo(admin.id());
   }
 
   @Test
@@ -350,10 +364,12 @@
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
     in.onBehalfOf = "doesnotexist";
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("not found");
-    exception.expectMessage("doesnotexist");
-    gApi.changes().id(changeId).current().submit(in);
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.changes().id(changeId).current().submit(in));
+    assertThat(thrown).hasMessageThat().contains("not found");
+    assertThat(thrown).hasMessageThat().contains("doesnotexist");
   }
 
   @Test
@@ -365,9 +381,15 @@
         .review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
     in.onBehalfOf = admin2.email();
-    exception.expect(AuthException.class);
-    exception.expectMessage("submit on behalf of other users not permitted");
-    gApi.changes().id(project.get() + "~master~" + r.getChangeId()).current().submit(in);
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () ->
+                gApi.changes()
+                    .id(project.get() + "~master~" + r.getChangeId())
+                    .current()
+                    .submit(in));
+    assertThat(thrown).hasMessageThat().contains("submit on behalf of other users not permitted");
   }
 
   @Test
@@ -380,9 +402,13 @@
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
     in.onBehalfOf = user.email();
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("on_behalf_of account " + user.id() + " cannot see change");
-    gApi.changes().id(changeId).current().submit(in);
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.changes().id(changeId).current().submit(in));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("on_behalf_of account " + user.id() + " cannot see change");
   }
 
   @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
@@ -397,10 +423,12 @@
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
     in.onBehalfOf = user.email();
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("not found");
-    exception.expectMessage(in.onBehalfOf);
-    gApi.changes().id(changeId).current().submit(in);
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.changes().id(changeId).current().submit(in));
+    assertThat(thrown).hasMessageThat().contains("not found");
+    assertThat(thrown).hasMessageThat().contains(in.onBehalfOf);
   }
 
   @Test
@@ -510,11 +538,11 @@
     adminRestSession.postWithHeader(endpoint, runAsHeader(user2.id()), in).assertOK();
 
     PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
-    assertThat(psa.getPatchSetId().get()).isEqualTo(1);
-    assertThat(psa.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa.getAccountId()).isEqualTo(user.id());
-    assertThat(psa.getValue()).isEqualTo(1);
-    assertThat(psa.getRealAccountId()).isEqualTo(admin.id()); // not user2
+    assertThat(psa.patchSetId().get()).isEqualTo(1);
+    assertThat(psa.label()).isEqualTo("Code-Review");
+    assertThat(psa.accountId()).isEqualTo(user.id());
+    assertThat(psa.value()).isEqualTo(1);
+    assertThat(psa.realAccountId()).isEqualTo(admin.id()); // not user2
 
     ChangeData cd = r.getChange();
     ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
@@ -546,54 +574,53 @@
   }
 
   private void allowCodeReviewOnBehalfOf() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType codeReviewType = Util.codeReview();
-      String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName());
-      String heads = "refs/heads/*";
-      AccountGroup.UUID uuid = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      Util.allow(u.getConfig(), forCodeReviewAs, -1, 1, uuid, heads);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .impersonation(true)
+                .ref("refs/heads/*")
+                .group(REGISTERED_USERS)
+                .range(-1, 1))
+        .update();
   }
 
   private void allowSubmitOnBehalfOf() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      String heads = "refs/heads/*";
-      AccountGroup.UUID uuid = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      Util.allow(u.getConfig(), Permission.SUBMIT_AS, uuid, heads);
-      Util.allow(u.getConfig(), Permission.SUBMIT, uuid, heads);
-      LabelType codeReviewType = Util.codeReview();
-      Util.allow(u.getConfig(), Permission.forLabel(codeReviewType.getName()), -2, 2, uuid, heads);
-      u.save();
-    }
+    String heads = "refs/heads/*";
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT_AS).ref(heads).group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT).ref(heads).group(REGISTERED_USERS))
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .ref(heads)
+                .group(REGISTERED_USERS)
+                .range(-2, 2))
+        .update();
   }
 
   private void blockRead(GroupInfo group) throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.block(
-          u.getConfig(), Permission.READ, new AccountGroup.UUID(group.id), "refs/heads/master");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/master").group(AccountGroup.uuid(group.id)))
+        .update();
   }
 
   private void allowRunAs() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      Util.allow(
-          u.getConfig(),
-          GlobalCapability.RUN_AS,
-          systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
-      u.save();
-    }
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.RUN_AS).group(ANONYMOUS_USERS))
+        .update();
   }
 
   private void removeRunAs() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      Util.remove(
-          u.getConfig(),
-          GlobalCapability.RUN_AS,
-          systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
-      u.save();
-    }
+    projectOperations
+        .allProjectsForUpdate()
+        .remove(capabilityKey(GlobalCapability.RUN_AS).group(ANONYMOUS_USERS))
+        .update();
   }
 
   private static Header runAsHeader(Object user) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
index d0c1fa4..2c9107c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.account;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -114,9 +115,11 @@
     pwi.notifyNewPatchSets = true;
     projectsToWatch.add(pwi);
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("duplicate entry for project " + projectName);
-    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.accounts().self().setWatchedProjects(projectsToWatch));
+    assertThat(thrown).hasMessageThat().contains("duplicate entry for project " + projectName);
   }
 
   @Test
@@ -146,9 +149,9 @@
     pwi.notifyNewChanges = true;
     pwi.notifyAllComments = true;
     projectsToWatch.add(pwi);
-
-    exception.expect(UnprocessableEntityException.class);
-    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+    assertThrows(
+        UnprocessableEntityException.class,
+        () -> gApi.accounts().self().setWatchedProjects(projectsToWatch));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/auth/AuthenticationCheckIT.java b/javatests/com/google/gerrit/acceptance/rest/auth/AuthenticationCheckIT.java
new file mode 100644
index 0000000..b6ef5a3
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/auth/AuthenticationCheckIT.java
@@ -0,0 +1,35 @@
+// 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.
+
+package com.google.gerrit.acceptance.rest.auth;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import org.junit.Test;
+
+public class AuthenticationCheckIT extends AbstractDaemonTest {
+  @Test
+  public void authCheck_loggedInUser_returnsOk() throws Exception {
+    RestResponse r = adminRestSession.get("/auth-check");
+    r.assertNoContent();
+  }
+
+  @Test
+  public void authCheck_anonymousUser_returnsForbidden() throws Exception {
+    RestSession anonymous = new RestSession(server, null);
+    RestResponse r = anonymous.get("/auth-check");
+    r.assertForbidden();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/auth/BUILD b/javatests/com/google/gerrit/acceptance/rest/auth/BUILD
new file mode 100644
index 0000000..5de1607
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/auth/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "auth",
+    labels = ["rest"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 55744cc..8a284d9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
 import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -35,7 +36,6 @@
 import com.google.gerrit.extensions.common.FixReplacementInfo;
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
-import com.google.gerrit.reviewdb.client.Patch;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
index 5f210cc..00dcb4f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.binding;
 
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.ImmutableList;
@@ -22,10 +23,12 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
 import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
 import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
 import java.util.List;
 import java.util.Optional;
 import org.junit.Test;
@@ -81,10 +84,15 @@
           // Task deletion must be tested last
           RestCall.delete("/config/server/tasks/%s"));
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void configEndpoints() throws Exception {
     // 'Access Database' is needed for the '/config/server/check.consistency' REST endpoint
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     RestApiCallHelper.execute(adminRestSession, CONFIG_ENDPOINTS);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
index 27df565..b53f182 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
@@ -15,15 +15,25 @@
 package com.google.gerrit.acceptance.rest.binding;
 
 import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
+import static com.google.gerrit.server.change.RobotCommentResource.ROBOT_COMMENT_KIND;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
 import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Change.Id;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
+import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
@@ -31,12 +41,16 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.RobotCommentResource;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
 import org.junit.Test;
 
 /**
@@ -53,7 +67,7 @@
 
   private static final String PLUGIN_NAME = "my-plugin";
 
-  private static final ImmutableSet<RestCall> TEST_CALLS =
+  private static final ImmutableSet<RestCall> REVISION_TEST_CALLS =
       ImmutableSet.of(
           // Calls that have the plugin name as part of the collection name
           RestCall.get("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/"),
@@ -69,6 +83,9 @@
           RestCall.post("/changes/%s/revisions/%s/test-collection/"),
           RestCall.post("/changes/%s/revisions/%s/test-collection/1/update"));
 
+  private static final ImmutableSet<RestCall> ROBOTCOMMENT_TEST_CALLS =
+      ImmutableSet.of(RestCall.delete("/changes/%s/revisions/%s/robotcomments/%s"));
+
   /**
    * Module for all sys bindings.
    *
@@ -88,6 +105,7 @@
               postOnCollection(TEST_KIND).to(TestPostOnCollection.class);
               post(TEST_KIND, "update").to(TestPost.class);
               get(TEST_KIND, "detail").to(TestGet.class);
+              delete(ROBOT_COMMENT_KIND).to(TestDelete.class);
             }
           });
     }
@@ -107,7 +125,8 @@
 
     @Override
     public RestView<RevisionResource> list() throws RestApiException {
-      return (RestReadView<RevisionResource>) resource -> ImmutableList.of("one", "two");
+      return (RestReadView<RevisionResource>)
+          resource -> Response.ok(ImmutableList.of("one", "two"));
     }
 
     @Override
@@ -125,36 +144,90 @@
   static class TestPostOnCollection
       implements RestCollectionModifyView<RevisionResource, TestPluginResource, String> {
     @Override
-    public Object apply(RevisionResource parentResource, String input) throws Exception {
-      return "test";
+    public Response<String> apply(RevisionResource parentResource, String input) throws Exception {
+      return Response.ok("test");
     }
   }
 
   @Singleton
   static class TestPost implements RestModifyView<TestPluginResource, String> {
     @Override
-    public String apply(TestPluginResource resource, String input) throws Exception {
-      return "test";
+    public Response<String> apply(TestPluginResource resource, String input) throws Exception {
+      return Response.ok("test");
     }
   }
 
   @Singleton
   static class TestGet implements RestReadView<TestPluginResource> {
     @Override
-    public String apply(TestPluginResource resource) throws Exception {
-      return "test";
+    public Response<String> apply(TestPluginResource resource) throws Exception {
+      return Response.ok("test");
+    }
+  }
+
+  @Singleton
+  static class TestDelete implements RestModifyView<RobotCommentResource, String> {
+    @Override
+    public Response<?> apply(RobotCommentResource resource, String input) throws Exception {
+      return Response.none();
     }
   }
 
   @Test
-  public void testEndpoints() throws Exception {
+  public void testRevisionEndpoints() throws Exception {
     PatchSet.Id patchSetId = createChange().getPatchSetId();
     try (AutoCloseable ignored = installPlugin(PLUGIN_NAME, MyPluginSysModule.class, null, null)) {
       RestApiCallHelper.execute(
           adminRestSession,
-          TEST_CALLS.asList(),
-          String.valueOf(patchSetId.changeId.id),
-          String.valueOf(patchSetId.patchSetId));
+          REVISION_TEST_CALLS.asList(),
+          String.valueOf(patchSetId.changeId().get()),
+          String.valueOf(patchSetId.get()));
     }
   }
+
+  @Test
+  public void testRobotCommentEndpoints() throws Exception {
+    PatchSet.Id patchSetId = createChange().getPatchSetId();
+    String robotCommentUuid = createRobotComment(patchSetId.changeId());
+    try (AutoCloseable ignored = installPlugin(PLUGIN_NAME, MyPluginSysModule.class, null, null)) {
+      RestApiCallHelper.execute(
+          adminRestSession,
+          ROBOTCOMMENT_TEST_CALLS.asList(),
+          String.valueOf(patchSetId.changeId().get()),
+          String.valueOf(patchSetId.get()),
+          robotCommentUuid);
+    }
+  }
+
+  private String createRobotComment(Change.Id changeId) throws Exception {
+    addRobotComment(changeId, createRobotCommentInput(PushOneCommit.FILE_NAME));
+    return Iterables.getOnlyElement(
+            Iterables.getOnlyElement(
+                gApi.changes().id(changeId.get()).current().robotComments().values()))
+        .id;
+  }
+
+  private void addRobotComment(Id changeId, RobotCommentInput robotCommentInput) throws Exception {
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.robotComments =
+        Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
+    reviewInput.message = "Test robot comment";
+    reviewInput.tag = ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX;
+    gApi.changes().id(changeId.get()).current().review(reviewInput);
+  }
+
+  private static RobotCommentInput createRobotCommentInput(
+      String path, FixSuggestionInfo... fixSuggestionInfos) {
+    RobotCommentInput in = new RobotCommentInput();
+    in.robotId = "happyRobot";
+    in.robotRunId = "1";
+    in.message = "nit: trailing whitespace";
+    in.path = path;
+    in.url = "http://www.happy-robot.com";
+    in.properties = new HashMap<>();
+    in.properties.put("key1", "value1");
+    in.properties.put("key2", "value2");
+    in.fixSuggestions = Arrays.asList(fixSuggestionInfos);
+    return in;
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
index 178a326..b447534 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
@@ -157,7 +158,8 @@
 
     @Override
     public RestView<TopLevelResource> list() throws RestApiException {
-      return (RestReadView<TopLevelResource>) resource -> ImmutableList.of("one", "two");
+      return (RestReadView<TopLevelResource>)
+          resource -> Response.ok(ImmutableList.of("one", "two"));
     }
 
     @Override
@@ -175,24 +177,24 @@
   static class TestPostOnCollection
       implements RestCollectionModifyView<TopLevelResource, TestPluginResource, String> {
     @Override
-    public Object apply(TopLevelResource parentResource, String input) throws Exception {
-      return "test";
+    public Response<String> apply(TopLevelResource parentResource, String input) throws Exception {
+      return Response.ok("test");
     }
   }
 
   @Singleton
   static class TestPost implements RestModifyView<TestPluginResource, String> {
     @Override
-    public String apply(TestPluginResource resource, String input) throws Exception {
-      return "test";
+    public Response<String> apply(TestPluginResource resource, String input) throws Exception {
+      return Response.ok("test");
     }
   }
 
   @Singleton
   static class TestGet implements RestReadView<TestPluginResource> {
     @Override
-    public String apply(TestPluginResource resource) throws Exception {
-      return "test";
+    public Response<String> apply(TestPluginResource resource) throws Exception {
+      return Response.ok("test");
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index ed09ddd..f48a603 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -17,7 +17,8 @@
 import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.rest.util.RestCall.Method.GET;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.entities.RefNames.REFS_DASHBOARDS;
 import static com.google.gerrit.server.restapi.project.DashboardsCollection.DEFAULT_DASHBOARD_NAME;
 import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
 import static org.apache.http.HttpStatus.SC_NOT_FOUND;
@@ -29,10 +30,10 @@
 import com.google.gerrit.acceptance.rest.util.RestCall;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.TagInput;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
@@ -216,7 +217,7 @@
         testRepo
             .commit()
             .message("A change")
-            .parent(getRemoteHead())
+            .parent(projectOperations.project(project).getHead("master"))
             .add(filename, "content")
             .insertChangeId()
             .create();
@@ -234,7 +235,11 @@
 
   private void createDefaultDashboard() throws Exception {
     String dashboardRef = REFS_DASHBOARDS + "team";
-    grant(project, "refs/meta/*", Permission.CREATE);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/meta/*").group(adminGroupUuid()))
+        .update();
     gApi.projects().name(project.get()).branch(dashboardRef).create(new BranchInput());
 
     try (Repository r = repoManager.openRepository(project);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index a299b9a..b822750 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -16,15 +16,25 @@
 
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+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.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
 import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static java.util.concurrent.TimeUnit.SECONDS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
+import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -32,14 +42,27 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+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.ChangeApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -52,27 +75,18 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.LabelInfo;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.change.Submit;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -80,7 +94,7 @@
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.TestTimeUtil;
+import com.google.gerrit.testing.GerritJUnit.ThrowingRunnable;
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -103,11 +117,11 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.RefSpec;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
+@UseClockStep
+@UseTimezone(timezone = "US/Eastern")
 public abstract class AbstractSubmit extends AbstractDaemonTest {
   @ConfigSuite.Config
   public static Config submitWholeTopicEnabled() {
@@ -115,57 +129,36 @@
   }
 
   @Inject private ApprovalsUtil approvalsUtil;
-  @Inject private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
   @Inject private IdentifiedUser.GenericFactory userFactory;
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private Submit submitHandler;
-
-  private RegistrationHandle onSubmitValidatorHandle;
-  private String systemTimeZone;
-
-  @Before
-  public void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
-
-  @After
-  public void removeOnSubmitValidator() {
-    if (onSubmitValidatorHandle != null) {
-      onSubmitValidatorHandle.remove();
-    }
-  }
+  @Inject private ExtensionRegistry extensionRegistry;
 
   protected abstract SubmitType getSubmitType();
 
   @Test
   @TestProjectInput(createEmptyCommit = false)
-  public void submitToEmptyRepo() throws Exception {
+  public void submitToEmptyRepo() throws Throwable {
     assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     PushOneCommit.Result change = createChange();
     assertThat(change.getCommit().getParents()).isEmpty();
-    Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
+    Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
     assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     assertThat(actual).hasSize(1);
 
     submit(change.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
+    assertThat(projectOperations.project(project).getHead("master").getId())
+        .isEqualTo(change.getCommit());
     assertTrees(project, actual);
   }
 
   @Test
-  public void submitSingleChange() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitSingleChange() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
-    Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
-    RevCommit headAfterSubmit = getRemoteHead();
+    Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterSubmit).isEqualTo(initialHead);
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
@@ -183,13 +176,13 @@
   }
 
   @Test
-  public void submitMultipleChangesOtherMergeConflictPreview() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitMultipleChangesOtherMergeConflictPreview() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
     PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
@@ -223,7 +216,7 @@
           break;
         case REBASE_IF_NECESSARY:
         case REBASE_ALWAYS:
-          String change2hash = change2.getChange().currentPatchSet().getRevision().get();
+          String change2hash = change2.getChange().currentPatchSet().commitId().name();
           assertThat(e.getMessage())
               .isEqualTo(
                   "Cannot rebase "
@@ -255,11 +248,11 @@
           break;
         case CHERRY_PICK:
         default:
-          fail("Should not reach here.");
+          assertWithMessage("Should not reach here.").fail();
           break;
       }
 
-      RevCommit headAfterSubmit = getRemoteHead();
+      RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
       assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit);
       assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
       assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name());
@@ -267,19 +260,19 @@
   }
 
   @Test
-  public void submitMultipleChangesPreview() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitMultipleChangesPreview() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
     PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
     PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
     // change 2 is not approved, but we ignore labels
     approve(change3.getChangeId());
-    Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change4.getChangeId());
+    Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change4.getChangeId());
     Map<String, Map<String, Integer>> expected = new HashMap<>();
     expected.put(project.get(), new HashMap<>());
     expected.get(project.get()).put("refs/heads/master", 3);
 
-    assertThat(actual).containsKey(new Branch.NameKey(project, "refs/heads/master"));
+    assertThat(actual).containsKey(BranchNameKey.create(project, "refs/heads/master"));
     if (getSubmitType() == SubmitType.CHERRY_PICK) {
       // CherryPick ignores dependencies, thus only change and destination
       // branch refs are modified.
@@ -293,7 +286,7 @@
     }
 
     // check that the submit preview did not actually submit
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterSubmit).isEqualTo(initialHead);
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
@@ -305,10 +298,14 @@
   }
 
   @Test
-  public void submitNoPermission() throws Exception {
+  public void submitNoPermission() throws Throwable {
     // create project where submit is blocked
     Project.NameKey p = projectOperations.newProject().create();
-    block(p, "refs/*", Permission.SUBMIT, REGISTERED_USERS);
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
     PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
@@ -319,16 +316,16 @@
   }
 
   @Test
-  public void noSelfSubmit() throws Exception {
+  public void noSelfSubmit() throws Throwable {
     // create project where submit is blocked for the change owner
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.block(u.getConfig(), Permission.SUBMIT, CHANGE_OWNER, "refs/*");
-      Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.SUBMIT).ref("refs/*").group(CHANGE_OWNER))
+        .add(allow(Permission.SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
     PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
@@ -345,16 +342,16 @@
   }
 
   @Test
-  public void onlySelfSubmit() throws Exception {
+  public void onlySelfSubmit() throws Throwable {
     // create project where only the change owner can submit
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.block(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/*");
-      Util.allow(u.getConfig(), Permission.SUBMIT, CHANGE_OWNER, "refs/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(CHANGE_OWNER))
+        .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
     PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
@@ -372,7 +369,7 @@
   }
 
   @Test
-  public void submitWholeTopicMultipleProjects() throws Exception {
+  public void submitWholeTopicMultipleProjects() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
     String topic = "test-topic";
 
@@ -408,7 +405,7 @@
   }
 
   @Test
-  public void submitWholeTopicMultipleBranchesOnSameProject() throws Exception {
+  public void submitWholeTopicMultipleBranchesOnSameProject() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
     String topic = "test-topic";
 
@@ -416,7 +413,7 @@
     Project.NameKey keyA = createProjectForPush(getSubmitType());
     TestRepository<?> repoA = cloneProject(keyA);
 
-    RevCommit initialHead = getRemoteHead(keyA, "master");
+    RevCommit initialHead = projectOperations.project(keyA).getHead("master");
 
     // Create the dev branch on the test project
     BranchInput in = new BranchInput();
@@ -450,7 +447,7 @@
   }
 
   @Test
-  public void submitWholeTopic() throws Exception {
+  public void submitWholeTopic() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
     String topic = "test-topic";
     PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content", topic);
@@ -488,7 +485,82 @@
   }
 
   @Test
-  public void submitReusingOldTopic() throws Exception {
+  public void submitWholeTopicWithMultipleTopics() throws Throwable {
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+    String topic1 = "test-topic-1";
+    String topic2 = "test-topic-2";
+    PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content", topic1);
+    PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "content", topic1);
+    PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "content", topic2);
+    PushOneCommit.Result change4 = createChange("Change 4", "d.txt", "content", topic2);
+    approve(change1.getChangeId());
+    approve(change2.getChangeId());
+    approve(change3.getChangeId());
+    approve(change4.getChangeId());
+    submit(change4.getChangeId());
+    String expectedTopic1 = name(topic1);
+    String expectedTopic2 = name(topic2);
+    if (getSubmitType() == SubmitType.CHERRY_PICK) {
+      change1.assertChange(Change.Status.NEW, expectedTopic1, admin);
+      change2.assertChange(Change.Status.NEW, expectedTopic1, admin);
+
+    } else {
+      change1.assertChange(Change.Status.MERGED, expectedTopic1, admin);
+      change2.assertChange(Change.Status.MERGED, expectedTopic1, admin);
+    }
+
+    // Check for the exact change to have the correct submitter.
+    assertSubmitter(change4);
+    // Also check submitters for changes submitted via the topic relationship.
+    assertSubmitter(change3);
+    if (getSubmitType() != SubmitType.CHERRY_PICK) {
+      assertSubmitter(change1);
+      assertSubmitter(change2);
+    }
+
+    // Check that the repo has the expected commits
+    List<RevCommit> log = getRemoteLog();
+    List<String> commitsInRepo = log.stream().map(RevCommit::getShortMessage).collect(toList());
+    int expectedCommitCount;
+    switch (getSubmitType()) {
+      case MERGE_ALWAYS:
+        // initial commit + 4 commits + merge commit
+        expectedCommitCount = 6;
+        break;
+      case CHERRY_PICK:
+        // initial commit + 2 commits
+        expectedCommitCount = 3;
+        break;
+      case FAST_FORWARD_ONLY:
+      case INHERIT:
+      case MERGE_IF_NECESSARY:
+      case REBASE_ALWAYS:
+      case REBASE_IF_NECESSARY:
+      default:
+        // initial commit + 4 commits
+        expectedCommitCount = 5;
+        break;
+    }
+    assertThat(log).hasSize(expectedCommitCount);
+
+    if (getSubmitType() == SubmitType.CHERRY_PICK) {
+      assertThat(commitsInRepo).containsAtLeast("Initial empty repository", "Change 3", "Change 4");
+      assertThat(commitsInRepo).doesNotContain("Change 1");
+      assertThat(commitsInRepo).doesNotContain("Change 2");
+    } else if (getSubmitType() == SubmitType.MERGE_ALWAYS) {
+      assertThat(commitsInRepo)
+          .contains(
+              String.format(
+                  "Merge changes from topics \"%s\", \"%s\"", expectedTopic1, expectedTopic2));
+    } else {
+      assertThat(commitsInRepo)
+          .containsAtLeast(
+              "Initial empty repository", "Change 1", "Change 2", "Change 3", "Change 4");
+    }
+  }
+
+  @Test
+  public void submitReusingOldTopic() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
     String topic = "test-topic";
@@ -519,13 +591,13 @@
   }
 
   private void assertSubmittedTogether(String changeId, Iterable<String> expected)
-      throws Exception {
+      throws Throwable {
     assertThat(gApi.changes().id(changeId).submittedTogether().stream().map(i -> i.changeId))
         .containsExactlyElementsIn(expected);
   }
 
   @Test
-  public void submitWorkInProgressChange() throws Exception {
+  public void submitWorkInProgressChange() throws Throwable {
     PushOneCommit.Result change = pushTo("refs/for/master%wip");
     Change.Id num = change.getChange().getId();
     submitWithConflict(
@@ -539,15 +611,19 @@
   }
 
   @Test
-  public void submitWithHiddenBranchInSameTopic() throws Exception {
+  public void submitWithHiddenBranchInSameTopic() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
     PushOneCommit.Result visible = createChange("refs/for/master/" + name("topic"));
     Change.Id num = visible.getChange().getId();
 
-    createBranch(new Branch.NameKey(project, "hidden"));
+    createBranch(BranchNameKey.create(project, "hidden"));
     PushOneCommit.Result hidden = createChange("refs/for/hidden/" + name("topic"));
     approve(hidden.getChangeId());
-    blockRead("refs/heads/hidden");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/hidden").group(REGISTERED_USERS))
+        .update();
 
     submit(
         visible.getChangeId(),
@@ -557,7 +633,7 @@
   }
 
   @Test
-  public void submitChangeWhenParentOfOtherBranchTip() throws Exception {
+  public void submitChangeWhenParentOfOtherBranchTip() throws Throwable {
     // Chain of two commits
     // Push both to topic-branch
     // Push the first commit for review and submit
@@ -588,7 +664,7 @@
   }
 
   @Test
-  public void submitMergeOfNonChangeBranchTip() throws Exception {
+  public void submitMergeOfNonChangeBranchTip() throws Throwable {
     // Merge a branch with commits that have not been submitted as
     // changes.
     //
@@ -598,7 +674,7 @@
     // | /
     // I   -- master
     //
-    RevCommit master = getRemoteHead(project, "master");
+    RevCommit master = projectOperations.project(project).getHead("master");
     PushOneCommit stableTip =
         pushFactory.create(admin.newIdent(), testRepo, "Tip of branch stable", "stable.txt", "");
     PushOneCommit.Result stable = stableTip.to("refs/heads/stable");
@@ -615,7 +691,7 @@
   }
 
   @Test
-  public void submitMergeOfNonChangeBranchNonTip() throws Exception {
+  public void submitMergeOfNonChangeBranchNonTip() throws Throwable {
     // Merge a branch with commits that have not been submitted as
     // changes.
     //
@@ -626,7 +702,7 @@
     // | /
     // I -- master
     //
-    RevCommit initial = getRemoteHead(project, "master");
+    RevCommit initial = projectOperations.project(project).getHead("master");
     // push directly to stable to S1
     PushOneCommit.Result s1 =
         pushFactory
@@ -659,11 +735,11 @@
   }
 
   @Test
-  public void submitChangeWithCommitThatWasAlreadyMerged() throws Exception {
+  public void submitChangeWithCommitThatWasAlreadyMerged() throws Throwable {
     // create and submit a change
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
 
     // set the status of the change back to NEW to simulate a failed submit that
     // merged the commit but failed to update the change status
@@ -672,11 +748,11 @@
     // submitting the change again should detect that the commit was already
     // merged and just fix the change status to be MERGED
     submit(change.getChangeId());
-    assertThat(getRemoteHead()).isEqualTo(headAfterSubmit);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterSubmit);
   }
 
   @Test
-  public void submitChangesWithCommitsThatWereAlreadyMerged() throws Exception {
+  public void submitChangesWithCommitsThatWereAlreadyMerged() throws Throwable {
     // create and submit 2 changes
     PushOneCommit.Result change1 = createChange();
     PushOneCommit.Result change2 = createChange();
@@ -686,7 +762,7 @@
     }
     submit(change2.getChangeId());
     assertMerged(change1.getChangeId());
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
 
     // set the status of the changes back to NEW to simulate a failed submit that
     // merged the commits but failed to update the change status
@@ -696,11 +772,11 @@
     // merged and just fix the change status to be MERGED
     submit(change1.getChangeId());
     submit(change2.getChangeId());
-    assertThat(getRemoteHead()).isEqualTo(headAfterSubmit);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterSubmit);
   }
 
   @Test
-  public void submitTopicWithCommitsThatWereAlreadyMerged() throws Exception {
+  public void submitTopicWithCommitsThatWereAlreadyMerged() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
     // create and submit 2 changes with the same topic
@@ -710,7 +786,7 @@
     approve(change1.getChangeId());
     submit(change2.getChangeId());
     assertMerged(change1.getChangeId());
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
 
     // set the status of the second change back to NEW to simulate a failed
     // submit that merged the commits but failed to update the change status of
@@ -720,33 +796,38 @@
     // submitting the topic again should detect that the commits were already
     // merged and just fix the change status to be MERGED
     submit(change2.getChangeId());
-    assertThat(getRemoteHead()).isEqualTo(headAfterSubmit);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterSubmit);
   }
 
   @Test
-  public void submitWithValidation() throws Exception {
+  public void submitWithValidation() throws Throwable {
     AtomicBoolean called = new AtomicBoolean(false);
-    this.addOnSubmitValidationListener(
-        args -> {
-          called.set(true);
-          HashSet<String> refs = Sets.newHashSet(args.getCommands().keySet());
-          assertThat(refs).contains("refs/heads/master");
-          refs.remove("refs/heads/master");
-          if (!refs.isEmpty()) {
-            // Some submit strategies need to insert new patchset.
-            assertThat(refs).hasSize(1);
-            assertThat(refs.iterator().next()).startsWith(RefNames.REFS_CHANGES);
+    OnSubmitValidationListener listener =
+        new OnSubmitValidationListener() {
+          @Override
+          public void preBranchUpdate(Arguments args) throws ValidationException {
+            called.set(true);
+            HashSet<String> refs = Sets.newHashSet(args.getCommands().keySet());
+            assertThat(refs).contains("refs/heads/master");
+            refs.remove("refs/heads/master");
+            if (!refs.isEmpty()) {
+              // Some submit strategies need to insert new patchset.
+              assertThat(refs).hasSize(1);
+              assertThat(refs.iterator().next()).startsWith(RefNames.REFS_CHANGES);
+            }
           }
-        });
+        };
 
-    PushOneCommit.Result change = createChange();
-    approve(change.getChangeId());
-    submit(change.getChangeId());
-    assertThat(called.get()).isTrue();
+    try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+      PushOneCommit.Result change = createChange();
+      approve(change.getChangeId());
+      submit(change.getChangeId());
+      assertThat(called.get()).isTrue();
+    }
   }
 
   @Test
-  public void submitWithValidationMultiRepo() throws Exception {
+  public void submitWithValidationMultiRepo() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
     String topic = "test-topic";
 
@@ -777,42 +858,47 @@
     // Since there are 2 repos, first submit attempt will fail, the second will
     // succeed.
     List<String> projectsCalled = new ArrayList<>(4);
-    this.addOnSubmitValidationListener(
-        args -> {
-          String master = "refs/heads/master";
-          assertThat(args.getCommands()).containsKey(master);
-          ReceiveCommand cmd = args.getCommands().get(master);
-          ObjectId newMasterId = cmd.getNewId();
-          try (Repository repo = repoManager.openRepository(args.getProject())) {
-            assertThat(repo.exactRef(master).getObjectId()).isEqualTo(cmd.getOldId());
-            assertThat(args.getRef(master)).hasValue(newMasterId);
-            args.getRevWalk().parseBody(args.getRevWalk().parseCommit(newMasterId));
-          } catch (IOException e) {
-            throw new AssertionError("failed checking new ref value", e);
+    OnSubmitValidationListener listener =
+        new OnSubmitValidationListener() {
+          @Override
+          public void preBranchUpdate(Arguments args) throws ValidationException {
+            String master = "refs/heads/master";
+            assertThat(args.getCommands()).containsKey(master);
+            ReceiveCommand cmd = args.getCommands().get(master);
+            ObjectId newMasterId = cmd.getNewId();
+            try (Repository repo = repoManager.openRepository(args.getProject())) {
+              assertThat(repo.exactRef(master).getObjectId()).isEqualTo(cmd.getOldId());
+              assertThat(args.getRef(master)).hasValue(newMasterId);
+              args.getRevWalk().parseBody(args.getRevWalk().parseCommit(newMasterId));
+            } catch (IOException e) {
+              throw new AssertionError("failed checking new ref value", e);
+            }
+            projectsCalled.add(args.getProject().get());
+            if (projectsCalled.size() == 2) {
+              throw new ValidationException("time to fail");
+            }
           }
-          projectsCalled.add(args.getProject().get());
-          if (projectsCalled.size() == 2) {
-            throw new ValidationException("time to fail");
-          }
-        });
-    submitWithConflict(change4.getChangeId(), "time to fail");
-    assertThat(projectsCalled).containsExactly(keyA.get(), keyB.get());
-    for (PushOneCommit.Result change : changes) {
-      change.assertChange(Change.Status.NEW, name(topic), admin);
-    }
+        };
+    try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+      submitWithConflict(change4.getChangeId(), "time to fail");
+      assertThat(projectsCalled).containsExactly(keyA.get(), keyB.get());
+      for (PushOneCommit.Result change : changes) {
+        change.assertChange(Change.Status.NEW, name(topic), admin);
+      }
 
-    submit(change4.getChangeId());
-    assertThat(projectsCalled).containsExactly(keyA.get(), keyB.get(), keyA.get(), keyB.get());
-    for (PushOneCommit.Result change : changes) {
-      change.assertChange(Change.Status.MERGED, name(topic), admin);
+      submit(change4.getChangeId());
+      assertThat(projectsCalled).containsExactly(keyA.get(), keyB.get(), keyA.get(), keyB.get());
+      for (PushOneCommit.Result change : changes) {
+        change.assertChange(Change.Status.MERGED, name(topic), admin);
+      }
     }
   }
 
   @Test
-  public void submitWithCommitAndItsMergeCommitTogether() throws Exception {
+  public void submitWithCommitAndItsMergeCommitTogether() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     // Create a stable branch and bootstrap it.
     gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
@@ -820,8 +906,8 @@
         pushFactory.create(user.newIdent(), testRepo, "initial commit", "a.txt", "a");
     PushOneCommit.Result change = push.to("refs/heads/stable");
 
-    RevCommit stable = getRemoteHead(project, "stable");
-    RevCommit master = getRemoteHead(project, "master");
+    RevCommit stable = projectOperations.project(project).getHead("stable");
+    RevCommit master = projectOperations.project(project).getHead("master");
 
     assertThat(master).isEqualTo(initialHead);
     assertThat(stable).isEqualTo(change.getCommit());
@@ -874,13 +960,13 @@
     assertMerged(mergeId);
     testRepo.git().fetch().call();
     RevWalk rw = testRepo.getRevWalk();
-    master = rw.parseCommit(getRemoteHead(project, "master"));
+    master = rw.parseCommit(projectOperations.project(project).getHead("master"));
     assertThat(rw.isMergedInto(merge, master)).isTrue();
     assertThat(rw.isMergedInto(fix, master)).isTrue();
   }
 
   @Test
-  public void retrySubmitSingleChangeOnLockFailure() throws Exception {
+  public void retrySubmitSingleChangeOnLockFailure() throws Throwable {
     PushOneCommit.Result change = createChange();
     String id = change.getChangeId();
     approve(id);
@@ -897,7 +983,7 @@
 
     testRepo.git().fetch().call();
     RevWalk rw = testRepo.getRevWalk();
-    RevCommit master = rw.parseCommit(getRemoteHead(project, "master"));
+    RevCommit master = rw.parseCommit(projectOperations.project(project).getHead("master"));
     RevCommit patchSet = parseCurrentRevision(rw, change.getChangeId());
     assertThat(rw.isMergedInto(patchSet, master)).isTrue();
 
@@ -905,7 +991,7 @@
   }
 
   @Test
-  public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
+  public void retrySubmitAfterTornTopicOnLockFailure() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
     String topic = "test-topic";
@@ -940,13 +1026,13 @@
 
     repoA.git().fetch().call();
     RevWalk rwA = repoA.getRevWalk();
-    RevCommit masterA = rwA.parseCommit(getRemoteHead(keyA, "master"));
+    RevCommit masterA = rwA.parseCommit(projectOperations.project(keyA).getHead("master"));
     RevCommit change1Ps = parseCurrentRevision(rwA, change1.getChangeId());
     assertThat(rwA.isMergedInto(change1Ps, masterA)).isTrue();
 
     repoB.git().fetch().call();
     RevWalk rwB = repoB.getRevWalk();
-    RevCommit masterB = rwB.parseCommit(getRemoteHead(keyB, "master"));
+    RevCommit masterB = rwB.parseCommit(projectOperations.project(keyB).getHead("master"));
     RevCommit change2Ps = parseCurrentRevision(rwB, change2.getChangeId());
     assertThat(rwB.isMergedInto(change2Ps, masterB)).isTrue();
 
@@ -954,14 +1040,14 @@
   }
 
   @Test
-  public void authorAndCommitDateAreEqual() throws Exception {
+  public void authorAndCommitDateAreEqual() throws Throwable {
     assume().that(getSubmitType()).isNotEqualTo(SubmitType.FAST_FORWARD_ONLY);
 
     ConfigInput ci = new ConfigInput();
     ci.matchAuthorToCommitterDate = InheritableBoolean.TRUE;
     gApi.projects().name(project.get()).config(ci);
 
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
 
@@ -975,12 +1061,12 @@
     }
 
     submit(change2.getChangeId());
-    assertAuthorAndCommitDateEquals(getRemoteHead());
+    assertAuthorAndCommitDateEquals(projectOperations.project(project).getHead("master"));
   }
 
   @Test
   @TestProjectInput(rejectEmptyCommit = InheritableBoolean.FALSE)
-  public void submitEmptyCommitPatchSetCanNotFastForward_emptyCommitAllowed() throws Exception {
+  public void submitEmptyCommitPatchSetCanNotFastForward_emptyCommitAllowed() throws Throwable {
     assume().that(getSubmitType()).isNotEqualTo(SubmitType.FAST_FORWARD_ONLY);
 
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
@@ -997,7 +1083,7 @@
 
   @Test
   @TestProjectInput(rejectEmptyCommit = InheritableBoolean.TRUE)
-  public void submitEmptyCommitPatchSetCanNotFastForward_emptyCommitNotAllowed() throws Exception {
+  public void submitEmptyCommitPatchSetCanNotFastForward_emptyCommitNotAllowed() throws Throwable {
     assume().that(getSubmitType()).isNotEqualTo(SubmitType.FAST_FORWARD_ONLY);
 
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
@@ -1010,18 +1096,20 @@
     ChangeApi revert2 = gApi.changes().id(change.getChangeId()).revert();
     approve(revert2.id());
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        "Change "
-            + revert2.get()._number
-            + ": Change could not be merged because the commit is empty. "
-            + "Project policy requires all commits to contain modifications to at least one file.");
-    revert2.current().submit();
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> revert2.current().submit());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "Change "
+                + revert2.get()._number
+                + ": Change could not be merged because the commit is empty. Project policy"
+                + " requires all commits to contain modifications to at least one file.");
   }
 
   @Test
   @TestProjectInput(rejectEmptyCommit = InheritableBoolean.FALSE)
-  public void submitEmptyCommitPatchSetCanFastForward_emptyCommitAllowed() throws Exception {
+  public void submitEmptyCommitPatchSetCanFastForward_emptyCommitAllowed() throws Throwable {
     ChangeInput ci = new ChangeInput();
     ci.subject = "Empty change";
     ci.project = project.get();
@@ -1033,7 +1121,7 @@
 
   @Test
   @TestProjectInput(rejectEmptyCommit = InheritableBoolean.TRUE)
-  public void submitEmptyCommitPatchSetCanFastForward_emptyCommitNotAllowed() throws Exception {
+  public void submitEmptyCommitPatchSetCanFastForward_emptyCommitNotAllowed() throws Throwable {
     ChangeInput ci = new ChangeInput();
     ci.subject = "Empty change";
     ci.project = project.get();
@@ -1041,53 +1129,55 @@
     ChangeApi change = gApi.changes().create(ci);
     approve(change.id());
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        "Change "
-            + change.get()._number
-            + ": Change could not be merged because the commit is empty. "
-            + "Project policy requires all commits to contain modifications to at least one file.");
-    change.current().submit();
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> change.current().submit());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "Change "
+                + change.get()._number
+                + ": Change could not be merged because the commit is empty. Project policy"
+                + " requires all commits to contain modifications to at least one file.");
   }
 
   @Test
   @TestProjectInput(createEmptyCommit = false, rejectEmptyCommit = InheritableBoolean.TRUE)
-  public void submitNonemptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Exception {
+  public void submitNonemptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Throwable {
     assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     PushOneCommit.Result change = createChange();
     assertThat(change.getCommit().getParents()).isEmpty();
-    Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
+    Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
     assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     assertThat(actual).hasSize(1);
 
     submit(change.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
+    assertThat(projectOperations.project(project).getHead("master").getId())
+        .isEqualTo(change.getCommit());
     assertTrees(project, actual);
   }
 
   @Test
   @TestProjectInput(createEmptyCommit = false, rejectEmptyCommit = InheritableBoolean.TRUE)
-  public void submitEmptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Exception {
+  public void submitEmptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Throwable {
     assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     PushOneCommit.Result change =
         pushFactory
             .create(admin.newIdent(), testRepo, "Change 1", ImmutableMap.of())
             .to("refs/for/master");
     change.assertOkStatus();
-    // TODO(dborowitz): Use EMPTY_TREE_ID after upgrading to https://git.eclipse.org/r/127473
-    assertThat(change.getCommit().getTree())
-        .isEqualTo(ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904"));
+    assertThat(change.getCommit().getTree()).isEqualTo(EMPTY_TREE_ID);
 
-    Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
+    Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
     assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     assertThat(actual).hasSize(1);
 
     submit(change.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
+    assertThat(projectOperations.project(project).getHead("master").getId())
+        .isEqualTo(change.getCommit());
     assertTrees(project, actual);
   }
 
-  private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception {
+  private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Throwable {
     for (PushOneCommit.Result change : changes) {
       try (BatchUpdate bu =
           batchUpdateFactory.create(project, userFactory.create(admin.id()), TimeUtil.nowTs())) {
@@ -1106,7 +1196,61 @@
     }
   }
 
-  private void assertSubmitter(PushOneCommit.Result change) throws Exception {
+  @Test
+  @GerritConfig(name = "index.reindexAfterRefUpdate", value = "true")
+  public void submitSchedulesOpenChangesOfSameBranchForReindexing() throws Throwable {
+    // Create a merged change.
+    PushOneCommit push =
+        pushFactory.create(admin.newIdent(), testRepo, "Merged Change", "foo.txt", "foo");
+    PushOneCommit.Result mergedChange = push.to("refs/for/master");
+    mergedChange.assertOkStatus();
+    approve(mergedChange.getChangeId());
+    submit(mergedChange.getChangeId());
+
+    // Create some open changes.
+    PushOneCommit.Result change1 = createChange();
+    PushOneCommit.Result change2 = createChange();
+    PushOneCommit.Result change3 = createChange();
+
+    // Create a branch with one open change.
+    BranchInput in = new BranchInput();
+    in.revision = projectOperations.project(project).getHead("master").name();
+    gApi.projects().name(project.get()).branch("dev").create(in);
+    PushOneCommit.Result changeOtherBranch = createChange("refs/for/dev");
+
+    ChangeIndexedListener changeIndexedListener = mock(ChangeIndexedListener.class);
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedListener)) {
+      // submit a change, this should trigger asynchronous reindexing of the open changes on the
+      // same branch
+      approve(change1.getChangeId());
+      submit(change1.getChangeId());
+      assertThat(gApi.changes().id(change1.getChangeId()).get().status)
+          .isEqualTo(ChangeStatus.MERGED);
+
+      // on submit the change that is submitted gets reindexed synchronously
+      verify(changeIndexedListener, atLeast(1))
+          .onChangeScheduledForIndexing(project.get(), change1.getChange().getId().get());
+      verify(changeIndexedListener, atLeast(1))
+          .onChangeIndexed(project.get(), change1.getChange().getId().get());
+
+      // the open changes on the same branch get reindexed asynchronously
+      verify(changeIndexedListener, times(1))
+          .onChangeScheduledForIndexing(project.get(), change2.getChange().getId().get());
+      verify(changeIndexedListener, times(1))
+          .onChangeScheduledForIndexing(project.get(), change3.getChange().getId().get());
+
+      // merged changes don't get reindexed
+      verify(changeIndexedListener, times(0))
+          .onChangeScheduledForIndexing(project.get(), mergedChange.getChange().getId().get());
+
+      // open changes on other branches don't get reindexed
+      verify(changeIndexedListener, times(0))
+          .onChangeScheduledForIndexing(project.get(), changeOtherBranch.getChange().getId().get());
+    }
+  }
+
+  private void assertSubmitter(PushOneCommit.Result change) throws Throwable {
     ChangeInfo info = get(change.getChangeId(), ListChangesOption.MESSAGES);
     assertThat(info.messages).isNotNull();
     Iterable<String> messages = Iterables.transform(info.messages, i -> i.message);
@@ -1129,102 +1273,86 @@
     }
   }
 
-  protected void submit(String changeId) throws Exception {
+  protected void submit(String changeId) throws Throwable {
     submit(changeId, new SubmitInput(), null, null);
   }
 
-  protected void submit(String changeId, SubmitInput input) throws Exception {
+  protected void submit(String changeId, SubmitInput input) throws Throwable {
     submit(changeId, input, null, null);
   }
 
-  protected void submitWithConflict(String changeId, String expectedError) throws Exception {
+  protected void submitWithConflict(String changeId, String expectedError) throws Throwable {
     submit(changeId, new SubmitInput(), ResourceConflictException.class, expectedError);
   }
 
   protected void submit(
       String changeId,
       SubmitInput input,
-      Class<? extends RestApiException> expectedExceptionType,
+      @Nullable Class<? extends RestApiException> expectedExceptionType,
       String expectedExceptionMsg)
-      throws Exception {
+      throws Throwable {
     approve(changeId);
     if (expectedExceptionType == null) {
       assertSubmittable(changeId);
+    } else {
+      requireNonNull(expectedExceptionMsg);
     }
-    try {
-      gApi.changes().id(changeId).current().submit(input);
-      if (expectedExceptionType != null) {
-        fail("Expected exception of type " + expectedExceptionType.getSimpleName());
-      }
-    } catch (RestApiException e) {
-      if (expectedExceptionType == null) {
-        throw e;
-      }
-      // More verbose than using assertThat and/or ExpectedException, but gives
-      // us the stack trace.
-      if (!expectedExceptionType.isAssignableFrom(e.getClass())
-          || !e.getMessage().equals(expectedExceptionMsg)) {
-        throw new AssertionError(
-            "Expected exception of type "
-                + expectedExceptionType.getSimpleName()
-                + " with message: \""
-                + expectedExceptionMsg
-                + "\" but got exception of type "
-                + e.getClass().getSimpleName()
-                + " with message \""
-                + e.getMessage()
-                + "\"",
-            e);
-      }
+    ThrowingRunnable submit = () -> gApi.changes().id(changeId).current().submit(input);
+    if (expectedExceptionType != null) {
+      RestApiException thrown = assertThrows(expectedExceptionType, submit);
+      assertThat(thrown).hasMessageThat().isEqualTo(expectedExceptionMsg);
       return;
     }
+    submit.run();
     ChangeInfo change = gApi.changes().id(changeId).info();
     assertMerged(change.changeId);
   }
 
-  protected void assertSubmittable(String changeId) throws Exception {
-    assertThat(get(changeId, SUBMITTABLE).submittable).named("submit bit on ChangeInfo").isTrue();
+  protected void assertSubmittable(String changeId) throws Throwable {
+    assertWithMessage("submit bit on ChangeInfo")
+        .that(get(changeId, SUBMITTABLE).submittable)
+        .isTrue();
     RevisionResource rsrc = parseCurrentRevisionResource(changeId);
     UiAction.Description desc = submitHandler.getDescription(rsrc);
-    assertThat(desc.isVisible()).named("visible bit on submit action").isTrue();
-    assertThat(desc.isEnabled()).named("enabled bit on submit action").isTrue();
+    assertWithMessage("visible bit on submit action").that(desc.isVisible()).isTrue();
+    assertWithMessage("enabled bit on submit action").that(desc.isEnabled()).isTrue();
   }
 
-  protected void assertChangeMergedEvents(String... expected) throws Exception {
+  protected void assertChangeMergedEvents(String... expected) throws Throwable {
     eventRecorder.assertChangeMergedEvents(project.get(), "refs/heads/master", expected);
   }
 
-  protected void assertRefUpdatedEvents(RevCommit... expected) throws Exception {
+  protected void assertRefUpdatedEvents(RevCommit... expected) throws Throwable {
     eventRecorder.assertRefUpdatedEvents(project.get(), "refs/heads/master", expected);
   }
 
   protected void assertCurrentRevision(String changeId, int expectedNum, ObjectId expectedId)
-      throws Exception {
+      throws Throwable {
     ChangeInfo c = get(changeId, CURRENT_REVISION);
     assertThat(c.currentRevision).isEqualTo(expectedId.name());
     assertThat(c.revisions.get(expectedId.name())._number).isEqualTo(expectedNum);
-    try (Repository repo = repoManager.openRepository(new Project.NameKey(c.project))) {
-      String refName = new PatchSet.Id(new Change.Id(c._number), expectedNum).toRefName();
+    try (Repository repo = repoManager.openRepository(Project.nameKey(c.project))) {
+      String refName = PatchSet.id(Change.id(c._number), expectedNum).toRefName();
       Ref ref = repo.exactRef(refName);
-      assertThat(ref).named(refName).isNotNull();
+      assertWithMessage(refName).that(ref).isNotNull();
       assertThat(ref.getObjectId()).isEqualTo(expectedId);
     }
   }
 
-  protected void assertNew(String changeId) throws Exception {
+  protected void assertNew(String changeId) throws Throwable {
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
   }
 
-  protected void assertApproved(String changeId) throws Exception {
+  protected void assertApproved(String changeId) throws Throwable {
     assertApproved(changeId, admin);
   }
 
-  protected void assertApproved(String changeId, TestAccount user) throws Exception {
+  protected void assertApproved(String changeId, TestAccount user) throws Throwable {
     ChangeInfo c = get(changeId, DETAILED_LABELS);
     LabelInfo cr = c.labels.get("Code-Review");
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).value).isEqualTo(2);
-    assertThat(new Account.Id(cr.all.get(0)._accountId)).isEqualTo(user.id());
+    assertThat(Account.id(cr.all.get(0)._accountId)).isEqualTo(user.id());
   }
 
   protected void assertMerged(String changeId) throws RestApiException {
@@ -1244,40 +1372,40 @@
         .isEqualTo(commit.getCommitterIdent().getTimeZone());
   }
 
-  protected void assertSubmitter(String changeId, int psId) throws Exception {
+  protected void assertSubmitter(String changeId, int psId) throws Throwable {
     assertSubmitter(changeId, psId, admin);
   }
 
-  protected void assertSubmitter(String changeId, int psId, TestAccount user) throws Exception {
+  protected void assertSubmitter(String changeId, int psId, TestAccount user) throws Throwable {
     Change c = getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change();
     ChangeNotes cn = notesFactory.createChecked(c);
     PatchSetApproval submitter =
-        approvalsUtil.getSubmitter(cn, new PatchSet.Id(cn.getChangeId(), psId));
+        approvalsUtil.getSubmitter(cn, PatchSet.id(cn.getChangeId(), psId));
     assertThat(submitter).isNotNull();
     assertThat(submitter.isLegacySubmit()).isTrue();
-    assertThat(submitter.getAccountId()).isEqualTo(user.id());
+    assertThat(submitter.accountId()).isEqualTo(user.id());
   }
 
-  protected void assertNoSubmitter(String changeId, int psId) throws Exception {
+  protected void assertNoSubmitter(String changeId, int psId) throws Throwable {
     Change c = getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change();
     ChangeNotes cn = notesFactory.createChecked(c);
     PatchSetApproval submitter =
-        approvalsUtil.getSubmitter(cn, new PatchSet.Id(cn.getChangeId(), psId));
+        approvalsUtil.getSubmitter(cn, PatchSet.id(cn.getChangeId(), psId));
     assertThat(submitter).isNull();
   }
 
   protected void assertCherryPick(TestRepository<?> testRepo, boolean contentMerge)
-      throws Exception {
+      throws Throwable {
     assertRebase(testRepo, contentMerge);
-    RevCommit remoteHead = getRemoteHead();
+    RevCommit remoteHead = projectOperations.project(project).getHead("master");
     assertThat(remoteHead.getFooterLines("Reviewed-On")).isNotEmpty();
     assertThat(remoteHead.getFooterLines("Reviewed-By")).isNotEmpty();
   }
 
-  protected void assertRebase(TestRepository<?> testRepo, boolean contentMerge) throws Exception {
+  protected void assertRebase(TestRepository<?> testRepo, boolean contentMerge) throws Throwable {
     Repository repo = testRepo.getRepository();
     RevCommit localHead = getHead(repo, "HEAD");
-    RevCommit remoteHead = getRemoteHead();
+    RevCommit remoteHead = projectOperations.project(project).getHead("master");
     assertThat(localHead.getId()).isNotEqualTo(remoteHead.getId());
     assertThat(remoteHead.getParentCount()).isEqualTo(1);
     if (!contentMerge) {
@@ -1286,7 +1414,7 @@
     assertThat(remoteHead.getShortMessage()).isEqualTo(localHead.getShortMessage());
   }
 
-  protected List<RevCommit> getRemoteLog(Project.NameKey project, String branch) throws Exception {
+  protected List<RevCommit> getRemoteLog(Project.NameKey project, String branch) throws Throwable {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       rw.markStart(rw.parseCommit(repo.exactRef("refs/heads/" + branch).getObjectId()));
@@ -1294,22 +1422,17 @@
     }
   }
 
-  protected List<RevCommit> getRemoteLog() throws Exception {
+  protected List<RevCommit> getRemoteLog() throws Throwable {
     return getRemoteLog(project, "master");
   }
 
-  protected void addOnSubmitValidationListener(OnSubmitValidationListener listener) {
-    assertThat(onSubmitValidatorHandle).isNull();
-    onSubmitValidatorHandle = onSubmitValidationListeners.add("gerrit", listener);
-  }
-
-  private String getLatestDiff(Repository repo) throws Exception {
+  private String getLatestDiff(Repository repo) throws Throwable {
     ObjectId oldTreeId = repo.resolve("HEAD~1^{tree}");
     ObjectId newTreeId = repo.resolve("HEAD^{tree}");
     return getLatestDiff(repo, oldTreeId, newTreeId);
   }
 
-  private String getLatestRemoteDiff() throws Exception {
+  private String getLatestRemoteDiff() throws Throwable {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       ObjectId oldTreeId = repo.resolve("refs/heads/master~1^{tree}");
@@ -1319,7 +1442,7 @@
   }
 
   private String getLatestDiff(Repository repo, ObjectId oldTreeId, ObjectId newTreeId)
-      throws Exception {
+      throws Throwable {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     try (DiffFormatter fmt = new DiffFormatter(out)) {
       fmt.setRepository(repo);
@@ -1330,15 +1453,19 @@
   }
 
   // TODO(hanwen): the submodule tests have a similar method; maybe we could share code?
-  protected Project.NameKey createProjectForPush(SubmitType submitType) throws Exception {
+  protected Project.NameKey createProjectForPush(SubmitType submitType) throws Throwable {
     Project.NameKey project = projectOperations.newProject().submitType(submitType).create();
-    grant(project, "refs/heads/*", Permission.PUSH);
-    grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid()))
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid()))
+        .update();
     return project;
   }
 
   protected PushOneCommit.Result createChange(
-      String subject, String fileName, String content, String topic) throws Exception {
+      String subject, String fileName, String content, String topic) throws Throwable {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
     return push.to("refs/for/master/" + name(topic));
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index 36a09fd..a4fa84b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -19,23 +19,26 @@
 
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.inject.Inject;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 public abstract class AbstractSubmitByMerge extends AbstractSubmit {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
-  public void submitWithMerge() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithMerge() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
     submit(change2.getChangeId());
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head.getParentCount()).isEqualTo(2);
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertThat(head.getParent(1)).isEqualTo(change2.getCommit());
@@ -43,17 +46,17 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitWithContentMerge() throws Exception {
+  public void submitWithContentMerge() throws Throwable {
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "aaa\nbbb\nccc\n");
     submit(change.getChangeId());
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 = createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head.getParentCount()).isEqualTo(2);
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertThat(head.getParent(1)).isEqualTo(change3.getCommit());
@@ -61,12 +64,12 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitWithContentMerge_Conflict() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithContentMerge_Conflict() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
     submitWithConflict(
@@ -78,22 +81,23 @@
             + "Change could not be merged due to a path conflict. "
             + "Please rebase the change locally "
             + "and upload the rebased commit for review.");
-    assertThat(getRemoteHead()).isEqualTo(oldHead);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(oldHead);
   }
 
   @Test
   @TestProjectInput(createEmptyCommit = false)
-  public void submitMultipleCommitsToEmptyRepoAsFastForward() throws Exception {
+  public void submitMultipleCommitsToEmptyRepoAsFastForward() throws Throwable {
     PushOneCommit.Result change1 = createChange();
     PushOneCommit.Result change2 = createChange();
     approve(change1.getChangeId());
     submit(change2.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change2.getCommit());
+    assertThat(projectOperations.project(project).getHead("master").getId())
+        .isEqualTo(change2.getCommit());
   }
 
   @Test
   @TestProjectInput(createEmptyCommit = false)
-  public void submitMultipleCommitsToEmptyRepoWithOneMerge() throws Exception {
+  public void submitMultipleCommitsToEmptyRepoWithOneMerge() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
     PushOneCommit.Result change1 =
         pushFactory
@@ -108,7 +112,7 @@
     approve(change1.getChangeId());
     submit(change2.getChangeId());
 
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head.getParents()).hasLength(2);
     assertThat(head.getParent(0)).isEqualTo(change1.getCommit());
     assertThat(head.getParent(1)).isEqualTo(change2.getCommit());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index c12adfa..fff67f3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -17,21 +17,25 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+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.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -40,6 +44,7 @@
 import org.junit.Test;
 
 public abstract class AbstractSubmitByRebase extends AbstractSubmit {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Override
@@ -47,42 +52,41 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitWithRebase() throws Exception {
+  public void submitWithRebase() throws Throwable {
     submitWithRebase(admin);
   }
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitWithRebaseWithoutAddPatchSetPermission() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.block(u.getConfig(), Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/*");
-      Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(Util.codeReview().getName()),
-          -2,
-          2,
-          REGISTERED_USERS,
-          "refs/heads/*");
-      u.save();
-    }
+  public void submitWithRebaseWithoutAddPatchSetPermission() throws Throwable {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.ADD_PATCH_SET).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .ref("refs/heads/*")
+                .group(REGISTERED_USERS)
+                .range(-2, 2))
+        .update();
 
     submitWithRebase(user);
   }
 
   protected ImmutableList<PushOneCommit.Result> submitWithRebase(TestAccount submitter)
-      throws Exception {
+      throws Throwable {
     requestScopeOperations.setApiUser(submitter.id());
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
     submit(change2.getChangeId());
     assertRebase(testRepo, false);
-    RevCommit headAfterSecondSubmit = getRemoteHead();
+    RevCommit headAfterSecondSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterSecondSubmit.getParent(0)).isEqualTo(headAfterFirstSubmit);
     assertApproved(change2.getChangeId(), submitter);
     assertCurrentRevision(change2.getChangeId(), 2, headAfterSecondSubmit);
@@ -102,11 +106,11 @@
   }
 
   @Test
-  public void submitWithRebaseMultipleChanges() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithRebaseMultipleChanges() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content");
     submit(change1.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
       assertCurrentRevision(change1.getChangeId(), 2, headAfterFirstSubmit);
     } else {
@@ -127,7 +131,7 @@
     assertApproved(change3.getChangeId());
     assertApproved(change4.getChangeId());
 
-    RevCommit headAfterSecondSubmit = parse(getRemoteHead());
+    RevCommit headAfterSecondSubmit = parse(projectOperations.project(project).getHead("master"));
     assertThat(headAfterSecondSubmit.getShortMessage()).isEqualTo("Change 4");
     assertThat(headAfterSecondSubmit).isNotEqualTo(change4.getCommit());
     assertCurrentRevision(change4.getChangeId(), 2, headAfterSecondSubmit);
@@ -163,7 +167,7 @@
   }
 
   @Test
-  public void submitWithRebaseMergeCommit() throws Exception {
+  public void submitWithRebaseMergeCommit() throws Throwable {
     /*
        *  (HEAD, origin/master, origin/HEAD) Merge changes X,Y
        |\
@@ -175,7 +179,7 @@
        |/
        * Initial empty repository
     */
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 = createChange("Added a", "a.txt", "");
 
     PushOneCommit change2Push =
@@ -193,7 +197,7 @@
     approve(change2.getChangeId());
     submit(change2.getChangeId());
 
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
     assertThat(newHead.getParentCount()).isEqualTo(2);
 
     RevCommit headParent1 = parse(newHead.getParent(0).getId());
@@ -219,12 +223,12 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitWithContentMerge_Conflict() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithContentMerge_Conflict() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
     submitWithConflict(
@@ -232,7 +236,7 @@
         "Cannot rebase "
             + change2.getCommit().name()
             + ": The change could not be rebased due to a conflict during merge.");
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head).isEqualTo(headAfterFirstSubmit);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
     assertNoSubmitter(change2.getChangeId(), 1);
@@ -241,7 +245,7 @@
     assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name());
   }
 
-  protected RevCommit parse(ObjectId id) throws Exception {
+  protected RevCommit parse(ObjectId id) throws Throwable {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       RevCommit c = rw.parseCommit(id);
@@ -251,8 +255,8 @@
   }
 
   @Test
-  public void submitAfterReorderOfCommits() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitAfterReorderOfCommits() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     // Create two commits and push.
     RevCommit c1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
@@ -271,15 +275,15 @@
     approve(id1);
     approve(id2);
     submit(id1);
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
 
     assertRefUpdatedEvents(initialHead, headAfterSubmit);
     assertChangeMergedEvents(id2, headAfterSubmit.name(), id1, headAfterSubmit.name());
   }
 
   @Test
-  public void submitChangesAfterBranchOnSecond() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitChangesAfterBranchOnSecond() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     PushOneCommit.Result change = createChange();
     approve(change.getChangeId());
@@ -287,13 +291,13 @@
     PushOneCommit.Result change2 = createChange();
     approve(change2.getChangeId());
     Project.NameKey project = change2.getChange().change().getProject();
-    Branch.NameKey branch = new Branch.NameKey(project, "branch");
+    BranchNameKey branch = BranchNameKey.create(project, "branch");
     createBranchWithRevision(branch, change2.getCommit().getName());
     gApi.changes().id(change2.getChangeId()).current().submit();
     assertMerged(change2.getChangeId());
     assertMerged(change.getChangeId());
 
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(this.project).getHead("master");
     assertRefUpdatedEvents(initialHead, newHead);
     assertChangeMergedEvents(
         change.getChangeId(), newHead.name(), change2.getChangeId(), newHead.name());
@@ -301,8 +305,8 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitFastForwardIdenticalTree() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitFastForwardIdenticalTree() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "a");
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "a");
 
@@ -313,18 +317,18 @@
     testRepo.reset(initialHead);
     PushOneCommit.Result change0 = createChange("Change 0", "b.txt", "b");
     submit(change0.getChangeId());
-    RevCommit headAfterChange0 = getRemoteHead();
+    RevCommit headAfterChange0 = projectOperations.project(project).getHead("master");
     assertThat(headAfterChange0.getShortMessage()).isEqualTo("Change 0");
 
     submit(change1.getChangeId());
-    RevCommit headAfterChange1 = getRemoteHead();
+    RevCommit headAfterChange1 = projectOperations.project(project).getHead("master");
     assertThat(headAfterChange1.getShortMessage()).isEqualTo("Change 1");
     assertThat(headAfterChange0).isEqualTo(headAfterChange1.getParent(0));
 
     // Do manual rebase first.
     gApi.changes().id(change2.getChangeId()).current().rebase();
     submit(change2.getChangeId());
-    RevCommit headAfterChange2 = getRemoteHead();
+    RevCommit headAfterChange2 = projectOperations.project(project).getHead("master");
     assertThat(headAfterChange2.getShortMessage()).isEqualTo("Change 2");
     assertThat(headAfterChange1).isEqualTo(headAfterChange2.getParent(0));
 
@@ -334,7 +338,7 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitChainOneByOne() throws Exception {
+  public void submitChainOneByOne() throws Throwable {
     PushOneCommit.Result change1 = createChange("subject 1", "fileName 1", "content 1");
     PushOneCommit.Result change2 = createChange("subject 2", "fileName 2", "content 2");
     submit(change1.getChangeId());
@@ -343,7 +347,7 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitChainFailsOnRework() throws Exception {
+  public void submitChainFailsOnRework() throws Throwable {
     PushOneCommit.Result change1 = createChange("subject 1", "fileName 1", "content 1");
     RevCommit headAfterChange1 = change1.getCommit();
     PushOneCommit.Result change2 = createChange("subject 2", "fileName 2", "content 2");
@@ -351,7 +355,7 @@
     change1 =
         amendChange(change1.getChangeId(), "subject 1 amend", "fileName 2", "rework content 2");
     submit(change1.getChangeId());
-    headAfterChange1 = getRemoteHead();
+    headAfterChange1 = projectOperations.project(project).getHead("master");
 
     submitWithConflict(
         change2.getChangeId(),
@@ -359,13 +363,13 @@
             + change2.getCommit().getName()
             + ": "
             + "The change could not be rebased due to a conflict during merge.");
-    assertThat(getRemoteHead()).isEqualTo(headAfterChange1);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterChange1);
   }
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitChainOneByOneManualRebase() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitChainOneByOneManualRebase() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 = createChange("subject 1", "fileName 1", "content 1");
     PushOneCommit.Result change2 = createChange("subject 2", "fileName 2", "content 2");
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index e7f3d54..dda7bbd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -22,9 +22,14 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.ActionVisitor;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -32,11 +37,8 @@
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.change.RevisionJson;
+import com.google.gerrit.server.change.testing.TestChangeETagComputation;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
@@ -45,8 +47,6 @@
 import java.util.Set;
 import java.util.TreeSet;
 import org.eclipse.jgit.lib.Config;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 public class ActionsIT extends AbstractDaemonTest {
@@ -55,23 +55,9 @@
     return submitWholeTopicEnabledConfig();
   }
 
-  @Inject private DynamicSet<ActionVisitor> actionVisitors;
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private RevisionJson.Factory revisionJsonFactory;
-
-  private RegistrationHandle visitorHandle;
-
-  @Before
-  public void setUp() {
-    visitorHandle = null;
-  }
-
-  @After
-  public void tearDown() {
-    if (visitorHandle != null) {
-      visitorHandle.remove();
-    }
-  }
+  @Inject private ExtensionRegistry extensionRegistry;
 
   protected Map<String, ActionInfo> getActions(String id) throws Exception {
     return gApi.changes().id(id).revision(1).actions();
@@ -206,6 +192,45 @@
   }
 
   @Test
+  public void pluginCanContributeToETagComputation() throws Exception {
+    String change = createChange().getChangeId();
+    String oldETag = getETag(change);
+
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(TestChangeETagComputation.withETag("foo"))) {
+      assertThat(getETag(change)).isNotEqualTo(oldETag);
+    }
+
+    assertThat(getETag(change)).isEqualTo(oldETag);
+  }
+
+  @Test
+  public void returningNullFromETagComputationDoesNotBreakGerrit() throws Exception {
+    String change = createChange().getChangeId();
+    String oldETag = getETag(change);
+
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(TestChangeETagComputation.withETag(null))) {
+      assertThat(getETag(change)).isEqualTo(oldETag);
+    }
+  }
+
+  @Test
+  public void throwingExceptionFromETagComputationDoesNotBreakGerrit() throws Exception {
+    String change = createChange().getChangeId();
+    String oldETag = getETag(change);
+
+    try (Registration registration =
+        extensionRegistry
+            .newRegistration()
+            .add(
+                TestChangeETagComputation.withException(
+                    new StorageException("exception during test")))) {
+      assertThat(getETag(change)).isEqualTo(oldETag);
+    }
+  }
+
+  @Test
   public void revisionActionsTwoChangesInTopic_conflicting() throws Exception {
     String changeId = createChangeWithTopic().getChangeId();
     approve(changeId);
@@ -331,19 +356,18 @@
     assertThat(origActions.keySet()).containsAtLeast("followup", "abandon");
     assertThat(origActions.get("abandon").label).isEqualTo("Abandon");
 
-    Visitor v = new Visitor();
-    visitorHandle = actionVisitors.add("gerrit", v);
+    try (Registration registration = extensionRegistry.newRegistration().add(new Visitor())) {
+      Map<String, ActionInfo> newActions =
+          gApi.changes().id(id).get(EnumSet.of(ListChangesOption.CHANGE_ACTIONS)).actions;
 
-    Map<String, ActionInfo> newActions =
-        gApi.changes().id(id).get(EnumSet.of(ListChangesOption.CHANGE_ACTIONS)).actions;
+      Set<String> expectedNames = new TreeSet<>(origActions.keySet());
+      expectedNames.remove("followup");
+      assertThat(newActions.keySet()).isEqualTo(expectedNames);
 
-    Set<String> expectedNames = new TreeSet<>(origActions.keySet());
-    expectedNames.remove("followup");
-    assertThat(newActions.keySet()).isEqualTo(expectedNames);
-
-    ActionInfo abandon = newActions.get("abandon");
-    assertThat(abandon).isNotNull();
-    assertThat(abandon.label).isEqualTo("Abandon All Hope");
+      ActionInfo abandon = newActions.get("abandon");
+      assertThat(abandon).isNotNull();
+      assertThat(abandon.label).isEqualTo("Abandon All Hope");
+    }
   }
 
   @Test
@@ -351,7 +375,7 @@
     String id = createChange().getChangeId();
     amendChange(id);
     ChangeInfo origChange = gApi.changes().id(id).get(CHANGE_ACTIONS);
-    Change.Id changeId = new Change.Id(origChange._number);
+    Change.Id changeId = Change.id(origChange._number);
 
     class Visitor implements ActionVisitor {
       @Override
@@ -380,22 +404,22 @@
     assertThat(origActions.keySet()).containsAtLeast("cherrypick", "rebase");
     assertThat(origActions.get("rebase").label).isEqualTo("Rebase");
 
-    Visitor v = new Visitor();
-    visitorHandle = actionVisitors.add("gerrit", v);
+    try (Registration registration = extensionRegistry.newRegistration().add(new Visitor())) {
+      // Test different codepaths within ActionJson...
+      // ...via revision API.
+      visitedCurrentRevisionActionsAssertions(
+          origActions, gApi.changes().id(id).current().actions());
 
-    // Test different codepaths within ActionJson...
-    // ...via revision API.
-    visitedCurrentRevisionActionsAssertions(origActions, gApi.changes().id(id).current().actions());
+      // ...via change API with option.
+      EnumSet<ListChangesOption> opts = EnumSet.of(CURRENT_ACTIONS, CURRENT_REVISION);
+      ChangeInfo changeInfo = gApi.changes().id(id).get(opts);
+      RevisionInfo revisionInfo = Iterables.getOnlyElement(changeInfo.revisions.values());
+      visitedCurrentRevisionActionsAssertions(origActions, revisionInfo.actions);
 
-    // ...via change API with option.
-    EnumSet<ListChangesOption> opts = EnumSet.of(CURRENT_ACTIONS, CURRENT_REVISION);
-    ChangeInfo changeInfo = gApi.changes().id(id).get(opts);
-    RevisionInfo revisionInfo = Iterables.getOnlyElement(changeInfo.revisions.values());
-    visitedCurrentRevisionActionsAssertions(origActions, revisionInfo.actions);
-
-    // ...via ChangeJson directly.
-    ChangeData cd = changeDataFactory.create(project, changeId);
-    revisionJsonFactory.create(opts).getRevisionInfo(cd, cd.patchSet(new PatchSet.Id(changeId, 1)));
+      // ...via ChangeJson directly.
+      ChangeData cd = changeDataFactory.create(project, changeId);
+      revisionJsonFactory.create(opts).getRevisionInfo(cd, cd.patchSet(PatchSet.id(changeId, 1)));
+    }
   }
 
   private void visitedCurrentRevisionActionsAssertions(
@@ -440,18 +464,17 @@
     assertThat(origActions.keySet()).containsExactly("description");
     assertThat(origActions.get("description").label).isEqualTo("Edit Description");
 
-    Visitor v = new Visitor();
-    visitorHandle = actionVisitors.add("gerrit", v);
+    try (Registration registration = extensionRegistry.newRegistration().add(new Visitor())) {
+      // Unlike for the current revision, actions for old revisions are only available via the
+      // revision API.
+      Map<String, ActionInfo> newActions = gApi.changes().id(id).revision(1).actions();
+      assertThat(newActions).isNotNull();
+      assertThat(newActions.keySet()).isEqualTo(origActions.keySet());
 
-    // Unlike for the current revision, actions for old revisions are only available via the
-    // revision API.
-    Map<String, ActionInfo> newActions = gApi.changes().id(id).revision(1).actions();
-    assertThat(newActions).isNotNull();
-    assertThat(newActions.keySet()).isEqualTo(origActions.keySet());
-
-    ActionInfo description = newActions.get("description");
-    assertThat(description).isNotNull();
-    assertThat(description.label).isEqualTo("Describify");
+      ActionInfo description = newActions.get("description");
+      assertThat(description).isNotNull();
+      assertThat(description.label).isEqualTo("Describify");
+    }
   }
 
   private void commonActionsAssertions(Map<String, ActionInfo> actions) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index 2d6227b..0f5def6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -15,47 +15,38 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static java.util.concurrent.TimeUnit.SECONDS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
+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.RefNames;
 import com.google.gerrit.extensions.api.changes.AssigneeInput;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.Iterator;
 import java.util.List;
 import org.eclipse.jgit.transport.RefSpec;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
 @NoHttpd
+@UseClockStep
 public class AssigneeIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Test
   public void getNoAssignee() throws Exception {
     PushOneCommit.Result r = createChange();
@@ -93,16 +84,32 @@
   }
 
   @Test
-  public void assigneeAddedAsReviewer() throws Exception {
-    ReviewerState state = ReviewerState.CC;
+  public void assigneeAddedAsCc() throws Exception {
     PushOneCommit.Result r = createChange();
-    Iterable<AccountInfo> reviewers = getReviewers(r, state);
+    Iterable<AccountInfo> reviewers = getReviewers(r, ReviewerState.CC);
     assertThat(reviewers).isNull();
+
     assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
-    reviewers = getReviewers(r, state);
+    reviewers = getReviewers(r, ReviewerState.CC);
     assertThat(reviewers).hasSize(1);
-    AccountInfo reviewer = Iterables.getFirst(reviewers, null);
-    assertThat(reviewer._accountId).isEqualTo(user.id().get());
+    assertThat(Iterables.getFirst(reviewers, null)._accountId).isEqualTo(user.id().get());
+    assertThat(getReviewers(r, ReviewerState.REVIEWER)).isNull();
+  }
+
+  @Test
+  public void assigneeStaysReviewer() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
+    Iterable<AccountInfo> reviewers = getReviewers(r, ReviewerState.REVIEWER);
+    assertThat(reviewers).hasSize(1);
+    assertThat(Iterables.getFirst(reviewers, null)._accountId).isEqualTo(user.id().get());
+    assertThat(getReviewers(r, ReviewerState.CC)).isNull();
+
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
+    reviewers = getReviewers(r, ReviewerState.REVIEWER);
+    assertThat(reviewers).hasSize(1);
+    assertThat(Iterables.getFirst(reviewers, null)._accountId).isEqualTo(user.id().get());
+    assertThat(getReviewers(r, ReviewerState.CC)).isNull();
   }
 
   @Test
@@ -130,20 +137,17 @@
   public void setAssigneeToInactiveUser() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.accounts().id(user.id().get()).setActive(false);
-    try {
-      setAssignee(r, user.email());
-      assert_().fail("expected UnresolvableAccountException");
-    } catch (UnresolvableAccountException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(
-              "Account '"
-                  + user.email()
-                  + "' only matches inactive accounts. To use an inactive account, retry with one"
-                  + " of the following exact account IDs:\n"
-                  + user.id()
-                  + ": User <user@example.com>");
-    }
+    UnresolvableAccountException thrown =
+        assertThrows(UnresolvableAccountException.class, () -> setAssignee(r, user.email()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            "Account '"
+                + user.email()
+                + "' only matches inactive accounts. To use an inactive account, retry with one"
+                + " of the following exact account IDs:\n"
+                + user.id()
+                + ": User <user@example.com>");
   }
 
   @Test
@@ -159,24 +163,26 @@
     git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
     testRepo.reset(RefNames.REFS_CONFIG);
     PushOneCommit.Result r = createChange("refs/for/refs/meta/config");
-    exception.expect(AuthException.class);
-    exception.expectMessage("read not permitted");
-    setAssignee(r, user.email());
+    AuthException thrown = assertThrows(AuthException.class, () -> setAssignee(r, user.email()));
+    assertThat(thrown).hasMessageThat().contains("read not permitted");
   }
 
   @Test
   public void setAssigneeNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("not permitted");
-    setAssignee(r, user.email());
+    AuthException thrown = assertThrows(AuthException.class, () -> setAssignee(r, user.email()));
+    assertThat(thrown).hasMessageThat().contains("not permitted");
   }
 
   @Test
   public void setAssigneeAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
-    grant(project, "refs/heads/master", Permission.EDIT_ASSIGNEE, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.EDIT_ASSIGNEE).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/BUILD b/javatests/com/google/gerrit/acceptance/rest/change/BUILD
index 7ccf10f..1eddcce 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/change/BUILD
@@ -20,14 +20,14 @@
     ],
 )
 
-acceptance_tests(
-    srcs = SUBMIT_TESTS,
-    group = "rest_change_submit",
+[acceptance_tests(
+    srcs = [f],
+    group = f[:f.index(".")],
     labels = ["rest"],
     deps = [
         ":submit_util",
     ],
-)
+) for f in SUBMIT_TESTS]
 
 java_library(
     name = "submit_util",
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java
index 3f1608c..bc52681 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java
@@ -17,8 +17,8 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
-import com.google.gerrit.reviewdb.client.Change;
 import org.junit.Test;
 
 public class ChangeIdIT extends AbstractDaemonTest {
@@ -98,14 +98,14 @@
     // This test tests a redirect that is primarily intended for the UI (though the backend doesn't
     // really care who the caller is). The redirect rewrites a shorthand change number URL (/123) to
     // it's canonical long form (/c/project/+/123).
-    int changeId = createChange().getChange().getId().id;
+    int changeId = createChange().getChange().getId().get();
     RestResponse res = anonymousRestSession.get("/" + changeId);
     res.assertTemporaryRedirect("/c/" + project.get() + "/+/" + changeId + "/");
   }
 
   @Test
   public void changeNumberRedirectsWithTrailingSlash() throws Exception {
-    int changeId = createChange().getChange().getId().id;
+    int changeId = createChange().getChange().getId().get();
     RestResponse res = anonymousRestSession.get("/" + changeId + "/");
     res.assertTemporaryRedirect("/c/" + project.get() + "/+/" + changeId + "/");
   }
@@ -125,8 +125,8 @@
   @Test
   public void hiddenChangeNotFound() throws Exception {
     Change.Id changeId = createChange().getChange().getId();
-    gApi.changes().id(changeId.id).setPrivate(true, null);
-    RestResponse res = anonymousRestSession.get("/" + changeId.id);
+    gApi.changes().id(changeId.get()).setPrivate(true, null);
+    RestResponse res = anonymousRestSession.get("/" + changeId.get());
     res.assertNotFound();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
index 59b6e29..47fb20a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
@@ -15,20 +15,25 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.TagInput;
-import com.google.gerrit.reviewdb.client.Branch;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 @NoHttpd
 public class ChangeIncludedInIT extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void includedInOpenChange() throws Exception {
     Result result = createChange();
@@ -49,13 +54,17 @@
         .containsExactly("master");
     assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags).isEmpty();
 
-    grant(project, R_TAGS + "*", Permission.CREATE_TAG);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE_TAG).ref(R_TAGS + "*").group(adminGroupUuid()))
+        .update();
     gApi.projects().name(project.get()).tag("test-tag").create(new TagInput());
 
     assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags)
         .containsExactly("test-tag");
 
-    createBranch(new Branch.NameKey(project.get(), "test-branch"));
+    createBranch(BranchNameKey.create(project, "test-branch"));
 
     assertThat(gApi.changes().id(result.getChangeId()).includedIn().branches)
         .containsExactly("master", "test-branch");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 51c5fc8..8cfcbab 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -14,12 +14,15 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
 import static com.google.gerrit.server.restapi.change.DeleteChangeMessage.createNewChangeMessage;
-import static java.util.concurrent.TimeUnit.SECONDS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toSet;
 import static org.eclipse.jgit.util.RawParseUtils.decode;
 
@@ -28,8 +31,12 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseTimezone;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.DeleteChangeMessageInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -37,10 +44,8 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
@@ -49,29 +54,16 @@
 import java.util.Optional;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.util.RawParseUtils;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@UseClockStep
+@UseTimezone(timezone = "US/Eastern")
 @RunWith(ConfigSuite.class)
 public class ChangeMessagesIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  private String systemTimeZone;
-
-  @Before
-  public void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
-
   @Test
   public void messagesNotReturnedByDefault() throws Exception {
     String changeId = createChange().getChangeId();
@@ -159,17 +151,17 @@
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
     requestScopeOperations.setApiUser(user.id());
 
-    try {
-      deleteOneChangeMessage(changeNum, 0, user, "spam");
-      fail("expected AuthException");
-    } catch (AuthException e) {
-      assertThat(e.getMessage()).isEqualTo("administrate server not permitted");
-    }
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> deleteOneChangeMessage(changeNum, 0, user, "spam"));
+    assertThat(thrown).hasMessageThat().isEqualTo("administrate server not permitted");
   }
 
   @Test
   public void deleteCanBeAppliedWithAdministrateServerCapability() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+        .update();
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
     requestScopeOperations.setApiUser(user.id());
     deleteOneChangeMessage(changeNum, 0, user, "spam");
@@ -179,12 +171,15 @@
   public void deleteCannotBeAppliedWithEmptyChangeMessageUuid() throws Exception {
     String changeId = createChange().getChangeId();
 
-    try {
-      gApi.changes().id(changeId).message("").delete(new DeleteChangeMessageInput("spam"));
-      fail("expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).isEqualTo("change message  not found");
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () ->
+                gApi.changes()
+                    .id(changeId)
+                    .message("")
+                    .delete(new DeleteChangeMessageInput("spam")));
+    assertThat(thrown).hasMessageThat().isEqualTo("change message  not found");
   }
 
   @Test
@@ -194,12 +189,11 @@
     String id = "8473b95934b5732ac55d26311a706c9c2bde9941";
     input.reason = "spam";
 
-    try {
-      gApi.changes().id(changeId).message(id).delete(input);
-      fail("expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).isEqualTo(String.format("change message %s not found", id));
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id(changeId).message(id).delete(input));
+    assertThat(thrown).hasMessageThat().isEqualTo(String.format("change message %s not found", id));
   }
 
   @Test
@@ -279,7 +273,7 @@
     List<ChangeMessageInfo> messagesBeforeDeletion = gApi.changes().id(changeNum).messages();
 
     List<CommentInfo> commentsBefore = getChangeSortedComments(changeNum);
-    List<RevCommit> commitsBefore = getChangeMetaCommitsInReverseOrder(new Change.Id(changeNum));
+    List<RevCommit> commitsBefore = getChangeMetaCommitsInReverseOrder(Change.id(changeNum));
 
     String id = messagesBeforeDeletion.get(deletedMessageIndex).id;
     DeleteChangeMessageInput input = new DeleteChangeMessageInput(reason);
@@ -306,8 +300,8 @@
       int deletedMessageIndex,
       TestAccount deletedBy,
       String deleteReason) {
-    assertThat(messagesAfterDeletion)
-        .named("after: %s; before: %s", messagesAfterDeletion, messagesBeforeDeletion)
+    assertWithMessage("after: %s; before: %s", messagesAfterDeletion, messagesBeforeDeletion)
+        .that(messagesAfterDeletion)
         .hasSize(messagesBeforeDeletion.size());
 
     for (int i = 0; i < messagesAfterDeletion.size(); ++i) {
@@ -340,8 +334,7 @@
       TestAccount deletedBy,
       String deleteReason)
       throws Exception {
-    List<RevCommit> commitsAfterDeletion =
-        getChangeMetaCommitsInReverseOrder(new Change.Id(changeNum));
+    List<RevCommit> commitsAfterDeletion = getChangeMetaCommitsInReverseOrder(Change.id(changeNum));
     assertThat(commitsAfterDeletion).hasSize(commitsBeforeDeletion.size());
 
     for (int i = 0; i < commitsBeforeDeletion.size(); i++) {
@@ -356,8 +349,8 @@
             parseCommitMessageRange(commitBefore);
         Optional<ChangeNoteUtil.CommitMessageRange> rangeAfter =
             parseCommitMessageRange(commitAfter);
-        assertThat(rangeBefore.isPresent()).isTrue();
-        assertThat(rangeAfter.isPresent()).isTrue();
+        assertThat(rangeBefore).isPresent();
+        assertThat(rangeAfter).isPresent();
 
         String subjectBefore =
             decode(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index d51221e..10194eb 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -14,6 +14,11 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -21,10 +26,10 @@
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -121,8 +126,7 @@
   }
 
   private void assertApproveFails(TestAccount a, String changeId) throws Exception {
-    exception.expect(AuthException.class);
-    approve(a, changeId);
+    assertThrows(AuthException.class, () -> approve(a, changeId));
   }
 
   private void grantApproveToChangeOwner(Project.NameKey project) throws Exception {
@@ -139,11 +143,24 @@
 
   private void grantApprove(Project.NameKey project, AccountGroup.UUID groupUUID, boolean exclusive)
       throws Exception {
-    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", false, groupUUID, exclusive);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(groupUUID).range(-2, 2))
+        .setExclusiveGroup(labelPermissionKey("Code-Review").ref("refs/heads/*"), exclusive)
+        .update();
   }
 
   private void blockApproveForChangeOwner(Project.NameKey project) throws Exception {
-    blockLabel("Code-Review", -2, 2, SystemGroupBackend.CHANGE_OWNER, "refs/heads/*", project);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            blockLabel("Code-Review")
+                .ref("refs/heads/*")
+                .group(SystemGroupBackend.CHANGE_OWNER)
+                .range(-2, 2))
+        .update();
   }
 
   private String createMyChange(TestRepository<InMemoryRepository> testRepo) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 173b78d..fbe6533 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -15,11 +15,14 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+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.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ReviewerState.CC;
 import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
 import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 
@@ -31,8 +34,10 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+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.RefNames;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -49,7 +54,6 @@
 import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.change.ReviewerAdder;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gson.stream.JsonReader;
@@ -66,6 +70,7 @@
 public class ChangeReviewersIT extends AbstractDaemonTest {
 
   @Inject private GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -332,20 +337,18 @@
     assertThat(result.reviewers).isNotNull();
     assertThat(result.reviewers).hasSize(1);
 
-    // Verify reviewer state. Both admin and user should be REVIEWERs now,
-    // because admin gets forced into REVIEWER state by virtue of being owner.
+    // Verify reviewer state.
     c = gApi.changes().id(r.getChangeId()).get();
-    assertReviewers(c, REVIEWER, admin, user);
+    assertReviewers(c, REVIEWER, user);
     assertReviewers(c, CC);
     label = c.labels.get("Code-Review");
     assertThat(label).isNotNull();
     assertThat(label.all).isNotNull();
-    assertThat(label.all).hasSize(2);
+    assertThat(label.all).hasSize(1);
     Map<Integer, Integer> approvals = new HashMap<>();
     for (ApprovalInfo approval : label.all) {
       approvals.put(approval._accountId, approval.value);
     }
-    assertThat(approvals).containsEntry(admin.id().get(), 0);
     assertThat(approvals).containsEntry(user.id().get(), 0);
 
     // Comment as user without voting. This should delete the approval and
@@ -360,17 +363,16 @@
 
     // Verify reviewer state.
     c = gApi.changes().id(r.getChangeId()).get();
-    assertReviewers(c, REVIEWER, admin, user);
+    assertReviewers(c, REVIEWER, user);
     assertReviewers(c, CC);
     label = c.labels.get("Code-Review");
     assertThat(label).isNotNull();
     assertThat(label.all).isNotNull();
-    assertThat(label.all).hasSize(2);
+    assertThat(label.all).hasSize(1);
     approvals.clear();
     for (ApprovalInfo approval : label.all) {
       approvals.put(approval._accountId, approval.value);
     }
-    assertThat(approvals).containsEntry(admin.id().get(), 0);
     assertThat(approvals).containsEntry(user.id().get(), 0);
   }
 
@@ -386,8 +388,7 @@
     assertThat(result.reviewers).isNotNull();
     assertThat(result.reviewers).hasSize(2);
 
-    // Verify reviewer and CC were added. If not in NoteDb read mode, both
-    // parties will be returned as CCed.
+    // Verify reviewer and CC were added.
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     assertReviewers(c, REVIEWER, admin, user);
     assertReviewers(c, CC, observer);
@@ -488,7 +489,7 @@
   }
 
   @Test
-  public void noteDbAddReviewerToReviewerChangeInfo() throws Exception {
+  public void addReviewerToReviewerChangeInfo() throws Exception {
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     AddReviewerInput in = new AddReviewerInput();
@@ -502,7 +503,7 @@
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
     requestScopeOperations.setApiUser(user.id());
-    // NoteDb adds reviewer to a change on every review.
+    // By posting a review the user is added as reviewer.
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
     deleteReviewer(changeId, user).assertNoContent();
@@ -662,9 +663,11 @@
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).current().review(new ReviewInput().label("Code-Review", 1));
     requestScopeOperations.setApiUser(newUser.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove());
+    assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted");
   }
 
   @Test
@@ -674,7 +677,11 @@
     // This test creates a new user so that it can explicitly check the REMOVE_REVIEWER permission
     // rather than bypassing the check because of project or ref ownership.
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
-    grant(project, RefNames.REFS + "*", Permission.REMOVE_REVIEWER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.REMOVE_REVIEWER).ref(RefNames.REFS + "*").group(REGISTERED_USERS))
+        .update();
 
     gApi.changes().id(r.getChangeId()).addReviewer(user.email());
     assertThatUserIsOnlyReviewer(r.getChangeId());
@@ -690,9 +697,11 @@
 
     gApi.changes().id(r.getChangeId()).addReviewer(user.email());
     requestScopeOperations.setApiUser(newUser.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove());
+    assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted");
   }
 
   @Test
@@ -705,9 +714,11 @@
     input.state = ReviewerState.CC;
     gApi.changes().id(r.getChangeId()).addReviewer(input);
     requestScopeOperations.setApiUser(newUser.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove());
+    assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted");
   }
 
   @Test
@@ -742,6 +753,81 @@
     assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).ccs).isEmpty();
   }
 
+  @Test
+  public void moveCcToReviewer() throws Exception {
+    // Create a change and add 'user' as CC.
+    String changeId = createChange().getChangeId();
+    AddReviewerInput reviewerInput = new AddReviewerInput();
+    reviewerInput.reviewer = user.email();
+    reviewerInput.state = ReviewerState.CC;
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+
+    // Verify that 'user' is a CC on the change and that there are no reviewers.
+    ChangeInfo c = gApi.changes().id(changeId).get();
+    Collection<AccountInfo> ccs = c.reviewers.get(CC);
+    assertThat(ccs).isNotNull();
+    assertThat(ccs).hasSize(1);
+    assertThat(ccs.iterator().next()._accountId).isEqualTo(user.id().get());
+    assertThat(c.reviewers.get(REVIEWER)).isNull();
+
+    // Move 'user' from CC to reviewer.
+    gApi.changes().id(changeId).addReviewer(user.id().toString());
+
+    // Verify that 'user' is a reviewer on the change now and that there are no CCs.
+    c = gApi.changes().id(changeId).get();
+    Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
+    assertThat(c.reviewers.get(CC)).isNull();
+  }
+
+  @Test
+  public void moveReviewerToCc() throws Exception {
+    // Allow everyone to approve changes.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .update();
+
+    // Create a change and add 'user' as reviewer.
+    String changeId = createChange().getChangeId();
+    gApi.changes().id(changeId).addReviewer(user.id().toString());
+
+    // Verify that 'user' is a reviewer on the change and that there are no CCs.
+    ChangeInfo c = gApi.changes().id(changeId).get();
+    Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
+    assertThat(c.reviewers.get(CC)).isNull();
+
+    // Let 'user' approve the change and verify that the change has the approval.
+    requestScopeOperations.setApiUser(user.id());
+    approve(changeId);
+    c = gApi.changes().id(changeId).get();
+    assertThat(c.labels.get("Code-Review").approved._accountId).isEqualTo(user.id().get());
+
+    // Move 'user' from reviewer to CC.
+    requestScopeOperations.setApiUser(admin.id());
+    AddReviewerInput reviewerInput = new AddReviewerInput();
+    reviewerInput.reviewer = user.id().toString();
+    reviewerInput.state = CC;
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+
+    // Verify that 'user' is a CC on the change now and that there are no reviewers.
+    c = gApi.changes().id(changeId).get();
+    Collection<AccountInfo> ccs = c.reviewers.get(CC);
+    assertThat(ccs).isNotNull();
+    assertThat(ccs).hasSize(1);
+    assertThat(ccs.iterator().next()._accountId).isEqualTo(user.id().get());
+    assertThat(c.reviewers.get(REVIEWER)).isNull();
+
+    // Verify that the approval of 'user' is still there.
+    assertThat(c.labels.get("Code-Review").approved._accountId).isEqualTo(user.id().get());
+  }
+
   private void assertThatUserIsOnlyReviewer(String changeId) throws Exception {
     AccountInfo userInfo = new AccountInfo(user.fullName(), user.getEmailAddress().getEmail());
     userInfo._accountId = user.id().get();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index 9a907aa..243991b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -15,47 +15,46 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+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.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Before;
 import org.junit.Test;
 
 public class ConfigChangeIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
   public void setUp() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.OWNER, REGISTERED_USERS, "refs/*");
-      Util.allow(u.getConfig(), Permission.PUSH, REGISTERED_USERS, "refs/for/refs/meta/config");
-      Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, RefNames.REFS_CONFIG);
-      u.save();
-    }
-
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/for/refs/meta/config").group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     fetchRefsMetaConfig();
   }
@@ -75,8 +74,8 @@
   }
 
   private String testUpdateProjectConfig() throws Exception {
-    Config cfg = readProjectConfig();
-    assertThat(cfg.getString("project", null, "description")).isNull();
+    Config cfg = projectOperations.project(project).getConfig();
+    assertThat(cfg).stringValue("project", null, "description").isNull();
     String desc = "new project description";
     cfg.setString("project", null, "description", desc);
 
@@ -89,7 +88,12 @@
     assertThat(gApi.changes().id(id).info().status).isEqualTo(ChangeStatus.MERGED);
     assertThat(gApi.projects().name(project.get()).get().description).isEqualTo(desc);
     fetchRefsMetaConfig();
-    assertThat(readProjectConfig().getString("project", null, "description")).isEqualTo(desc);
+    assertThat(
+            projectOperations
+                .project(project)
+                .getConfig()
+                .getString("project", null, "description"))
+        .isEqualTo(desc);
     String changeRev = gApi.changes().id(id).get().currentRevision;
     String branchRev =
         gApi.projects().name(project.get()).branch(RefNames.REFS_CONFIG).get().revision;
@@ -107,33 +111,31 @@
     gApi.projects().create(parent);
 
     requestScopeOperations.setApiUser(user.id());
-    Config cfg = readProjectConfig();
-    assertThat(cfg.getString("access", null, "inheritFrom")).isAnyOf(null, allProjects.get());
+    Config cfg = projectOperations.project(project).getConfig();
+    assertThat(cfg).stringValue("access", null, "inheritFrom").isAnyOf(null, allProjects.get());
     cfg.setString("access", null, "inheritFrom", parent.name);
 
     PushOneCommit.Result r = createConfigChange(cfg);
     String id = r.getChangeId();
 
     gApi.changes().id(id).current().review(ReviewInput.approve());
-    try {
-      gApi.changes().id(id).current().submit();
-      fail("expected submit to fail");
-    } catch (ResourceConflictException e) {
-      int n = gApi.changes().id(id).info()._number;
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(
-              "Failed to submit 1 change due to the following problems:\n"
-                  + "Change "
-                  + n
-                  + ": Change contains a project configuration that"
-                  + " changes the parent project.\n"
-                  + "The change must be submitted by a Gerrit administrator.");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(id).current().submit());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            "Failed to submit 1 change due to the following problems:\n"
+                + "Change "
+                + gApi.changes().id(id).info()._number
+                + ": Change contains a project configuration that"
+                + " changes the parent project.\n"
+                + "The change must be submitted by a Gerrit administrator.");
 
     assertThat(gApi.projects().name(project.get()).get().parent).isEqualTo(allProjects.get());
     fetchRefsMetaConfig();
-    assertThat(readProjectConfig().getString("access", null, "inheritFrom"))
+    assertThat(
+            projectOperations.project(project).getConfig().getString("access", null, "inheritFrom"))
         .isAnyOf(null, allProjects.get());
 
     requestScopeOperations.setApiUser(admin.id());
@@ -141,7 +143,9 @@
     assertThat(gApi.changes().id(id).info().status).isEqualTo(ChangeStatus.MERGED);
     assertThat(gApi.projects().name(project.get()).get().parent).isEqualTo(parent.name);
     fetchRefsMetaConfig();
-    assertThat(readProjectConfig().getString("access", null, "inheritFrom")).isEqualTo(parent.name);
+    assertThat(
+            projectOperations.project(project).getConfig().getString("access", null, "inheritFrom"))
+        .isEqualTo(parent.name);
   }
 
   @Test
@@ -179,17 +183,6 @@
     testRepo.reset(RefNames.REFS_CONFIG);
   }
 
-  private Config readProjectConfig() throws Exception {
-    RevWalk rw = testRepo.getRevWalk();
-    RevTree tree = rw.parseTree(testRepo.getRepository().resolve("HEAD"));
-    RevObject obj = rw.parseAny(testRepo.get(tree, "project.config"));
-    ObjectLoader loader = rw.getObjectReader().open(obj);
-    String text = new String(loader.getCachedBytes(), UTF_8);
-    Config cfg = new Config();
-    cfg.fromText(text);
-    return cfg;
-  }
-
   private PushOneCommit.Result createConfigChange(Config cfg) throws Exception {
     PushOneCommit.Result r =
         pushFactory
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
index 9c42542..3b26459 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
@@ -26,6 +26,7 @@
 import static com.google.common.net.HttpHeaders.ORIGIN;
 import static com.google.common.net.HttpHeaders.VARY;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
@@ -80,11 +81,11 @@
     String allowMethods = r.getHeader(ACCESS_CONTROL_ALLOW_METHODS);
     String allowHeaders = r.getHeader(ACCESS_CONTROL_ALLOW_HEADERS);
 
-    assertThat(allowOrigin).named(ACCESS_CONTROL_ALLOW_ORIGIN).isNull();
-    assertThat(allowCred).named(ACCESS_CONTROL_ALLOW_CREDENTIALS).isNull();
-    assertThat(maxAge).named(ACCESS_CONTROL_MAX_AGE).isNull();
-    assertThat(allowMethods).named(ACCESS_CONTROL_ALLOW_METHODS).isNull();
-    assertThat(allowHeaders).named(ACCESS_CONTROL_ALLOW_HEADERS).isNull();
+    assertWithMessage(ACCESS_CONTROL_ALLOW_ORIGIN).that(allowOrigin).isNull();
+    assertWithMessage(ACCESS_CONTROL_ALLOW_CREDENTIALS).that(allowCred).isNull();
+    assertWithMessage(ACCESS_CONTROL_MAX_AGE).that(maxAge).isNull();
+    assertWithMessage(ACCESS_CONTROL_ALLOW_METHODS).that(allowMethods).isNull();
+    assertWithMessage(ACCESS_CONTROL_ALLOW_HEADERS).that(allowHeaders).isNull();
   }
 
   @Test
@@ -162,7 +163,7 @@
     res.assertOK();
 
     String vary = res.getHeader(VARY);
-    assertThat(vary).named(VARY).isNotNull();
+    assertWithMessage(VARY).that(vary).isNotNull();
     assertThat(Splitter.on(", ").splitToList(vary))
         .containsExactly(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS);
     checkCors(res, true, origin);
@@ -213,7 +214,7 @@
         auth = c.getValue();
       }
     }
-    assertThat(auth).named("GerritAccount cookie").isNotNull();
+    assertWithMessage("GerritAccount cookie").that(auth).isNotNull();
     cookies.clear();
 
     UrlEncoded url =
@@ -232,16 +233,18 @@
     assertThat(r.getStatusLine().getStatusCode()).isEqualTo(200);
 
     Header vary = r.getFirstHeader(VARY);
-    assertThat(vary).named(VARY).isNotNull();
-    assertThat(Splitter.on(", ").splitToList(vary.getValue())).named(VARY).contains(ORIGIN);
+    assertWithMessage(VARY).that(vary).isNotNull();
+    assertWithMessage(VARY).that(Splitter.on(", ").splitToList(vary.getValue())).contains(ORIGIN);
 
     Header allowOrigin = r.getFirstHeader(ACCESS_CONTROL_ALLOW_ORIGIN);
-    assertThat(allowOrigin).named(ACCESS_CONTROL_ALLOW_ORIGIN).isNotNull();
-    assertThat(allowOrigin.getValue()).named(ACCESS_CONTROL_ALLOW_ORIGIN).isEqualTo(origin);
+    assertWithMessage(ACCESS_CONTROL_ALLOW_ORIGIN).that(allowOrigin).isNotNull();
+    assertWithMessage(ACCESS_CONTROL_ALLOW_ORIGIN).that(allowOrigin.getValue()).isEqualTo(origin);
 
     Header allowAuth = r.getFirstHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS);
-    assertThat(allowAuth).named(ACCESS_CONTROL_ALLOW_CREDENTIALS).isNotNull();
-    assertThat(allowAuth.getValue()).named(ACCESS_CONTROL_ALLOW_CREDENTIALS).isEqualTo("true");
+    assertWithMessage(ACCESS_CONTROL_ALLOW_CREDENTIALS).that(allowAuth).isNotNull();
+    assertWithMessage(ACCESS_CONTROL_ALLOW_CREDENTIALS)
+        .that(allowAuth.getValue())
+        .isEqualTo("true");
 
     checkTopic(change, "test-xd");
   }
@@ -264,7 +267,7 @@
 
   private void checkTopic(Result change, @Nullable String topic) throws RestApiException {
     ChangeInfo info = gApi.changes().id(change.getChangeId()).get();
-    StringSubject t = assertThat(info.topic).named("topic");
+    StringSubject t = assertWithMessage("topic").that(info.topic);
     if (topic != null) {
       t.isEqualTo(topic);
     } else {
@@ -287,8 +290,8 @@
 
   private void checkCors(RestResponse r, boolean accept, String origin) {
     String vary = r.getHeader(VARY);
-    assertThat(vary).named(VARY).isNotNull();
-    assertThat(Splitter.on(", ").splitToList(vary)).named(VARY).contains(ORIGIN);
+    assertWithMessage(VARY).that(vary).isNotNull();
+    assertWithMessage(VARY).that(Splitter.on(", ").splitToList(vary)).contains(ORIGIN);
 
     String allowOrigin = r.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN);
     String allowCred = r.getHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS);
@@ -296,28 +299,28 @@
     String allowMethods = r.getHeader(ACCESS_CONTROL_ALLOW_METHODS);
     String allowHeaders = r.getHeader(ACCESS_CONTROL_ALLOW_HEADERS);
     if (accept) {
-      assertThat(allowOrigin).named(ACCESS_CONTROL_ALLOW_ORIGIN).isEqualTo(origin);
-      assertThat(allowCred).named(ACCESS_CONTROL_ALLOW_CREDENTIALS).isEqualTo("true");
-      assertThat(maxAge).named(ACCESS_CONTROL_MAX_AGE).isEqualTo("600");
+      assertWithMessage(ACCESS_CONTROL_ALLOW_ORIGIN).that(allowOrigin).isEqualTo(origin);
+      assertWithMessage(ACCESS_CONTROL_ALLOW_CREDENTIALS).that(allowCred).isEqualTo("true");
+      assertWithMessage(ACCESS_CONTROL_MAX_AGE).that(maxAge).isEqualTo("600");
 
-      assertThat(allowMethods).named(ACCESS_CONTROL_ALLOW_METHODS).isNotNull();
-      assertThat(Splitter.on(", ").splitToList(allowMethods))
-          .named(ACCESS_CONTROL_ALLOW_METHODS)
+      assertWithMessage(ACCESS_CONTROL_ALLOW_METHODS).that(allowMethods).isNotNull();
+      assertWithMessage(ACCESS_CONTROL_ALLOW_METHODS)
+          .that(Splitter.on(", ").splitToList(allowMethods))
           .containsExactly("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
 
-      assertThat(allowHeaders).named(ACCESS_CONTROL_ALLOW_HEADERS).isNotNull();
-      assertThat(Splitter.on(", ").splitToList(allowHeaders))
-          .named(ACCESS_CONTROL_ALLOW_HEADERS)
+      assertWithMessage(ACCESS_CONTROL_ALLOW_HEADERS).that(allowHeaders).isNotNull();
+      assertWithMessage(ACCESS_CONTROL_ALLOW_HEADERS)
+          .that(Splitter.on(", ").splitToList(allowHeaders))
           .containsExactlyElementsIn(
               Stream.of(AUTHORIZATION, CONTENT_TYPE, "X-Gerrit-Auth", "X-Requested-With")
                   .map(s -> s.toLowerCase(Locale.US))
                   .collect(ImmutableSet.toImmutableSet()));
     } else {
-      assertThat(allowOrigin).named(ACCESS_CONTROL_ALLOW_ORIGIN).isNull();
-      assertThat(allowCred).named(ACCESS_CONTROL_ALLOW_CREDENTIALS).isNull();
-      assertThat(maxAge).named(ACCESS_CONTROL_MAX_AGE).isNull();
-      assertThat(allowMethods).named(ACCESS_CONTROL_ALLOW_METHODS).isNull();
-      assertThat(allowHeaders).named(ACCESS_CONTROL_ALLOW_HEADERS).isNull();
+      assertWithMessage(ACCESS_CONTROL_ALLOW_ORIGIN).that(allowOrigin).isNull();
+      assertWithMessage(ACCESS_CONTROL_ALLOW_CREDENTIALS).that(allowCred).isNull();
+      assertWithMessage(ACCESS_CONTROL_MAX_AGE).that(maxAge).isNull();
+      assertWithMessage(ACCESS_CONTROL_ALLOW_METHODS).that(allowMethods).isNull();
+      assertWithMessage(ACCESS_CONTROL_ALLOW_HEADERS).that(allowHeaders).isNull();
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index bc675e5..a1167ed 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -15,10 +15,11 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.common.data.Permission.READ;
-import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.gerrit.entities.RefNames.changeMetaRef;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static java.util.concurrent.TimeUnit.SECONDS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
 
 import com.google.common.base.Strings;
@@ -27,7 +28,13 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseSystemTime;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -37,17 +44,14 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.List;
@@ -64,23 +68,13 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
+@UseClockStep
 public class CreateChangeIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Test
   public void createEmptyChange_MissingBranch() throws Exception {
     ChangeInput ci = new ChangeInput();
@@ -135,6 +129,13 @@
   }
 
   @Test
+  public void createNewChange_RequiresAuthentication() throws Exception {
+    requestScopeOperations.setApiUserAnonymous();
+    assertCreateFails(
+        newChangeInput(ChangeStatus.NEW), AuthException.class, "Authentication required");
+  }
+
+  @Test
   public void createNewChange() throws Exception {
     ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
     assertThat(info.revisions.get(info.currentRevision).commit.message)
@@ -162,6 +163,31 @@
   }
 
   @Test
+  public void cannotCreateChangeWithChangeIfOfExistingChangeOnSameBranch() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    ci.subject = "Subject\n\nChange-Id: " + changeId;
+    assertCreateFails(
+        ci,
+        ResourceConflictException.class,
+        "A change with Change-Id " + changeId + " already exists for this branch.");
+  }
+
+  @Test
+  public void canCreateChangeWithChangeIfOfExistingChangeOnOtherBranch() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    createBranch(BranchNameKey.create(project, "other"));
+
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    ci.subject = "Subject\n\nChange-Id: " + changeId;
+    ci.branch = "other";
+    ChangeInfo info = assertCreateSucceeds(ci);
+    assertThat(info.changeId).isEqualTo(changeId);
+  }
+
+  @Test
   public void notificationsOnChangeCreation() throws Exception {
     requestScopeOperations.setApiUser(user.id());
     watch(project.get());
@@ -300,7 +326,11 @@
   public void createChangeWithoutAccessToParentCommitFails() throws Exception {
     Map<String, PushOneCommit.Result> results =
         changeInTwoBranches("invisible-branch", "a.txt", "visible-branch", "b.txt");
-    block(project, "refs/heads/invisible-branch", READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref("refs/heads/invisible-branch").group(REGISTERED_USERS))
+        .update();
 
     ChangeInput in = newChangeInput(ChangeStatus.NEW);
     in.branch = "visible-branch";
@@ -315,7 +345,7 @@
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       RevCommit commit =
-          rw.parseCommit(repo.exactRef(changeMetaRef(new Change.Id(c._number))).getObjectId());
+          rw.parseCommit(repo.exactRef(changeMetaRef(Change.id(c._number))).getObjectId());
 
       assertThat(commit.getShortMessage()).isEqualTo("Create change");
 
@@ -379,8 +409,8 @@
     // Create 2 branches.
     String branchA = "branchA";
     String branchB = "branchB";
-    createBranch(new Branch.NameKey(project, branchA));
-    createBranch(new Branch.NameKey(project, branchB));
+    createBranch(BranchNameKey.create(project, branchA));
+    createBranch(BranchNameKey.create(project, branchB));
 
     // Push an octopus merge to both of the branches.
     Result octopusA =
@@ -483,7 +513,7 @@
     cherry.current().review(ReviewInput.approve());
     cherry.current().submit();
 
-    ObjectId remoteId = getRemoteHead();
+    ObjectId remoteId = projectOperations.project(project).getHead("master");
     assertThat(remoteId).isNotEqualTo(commitId);
 
     ChangeInput in = newMergeChangeInput("master", commitId.getName(), "");
@@ -491,45 +521,13 @@
   }
 
   @Test
-  public void sha1sOfTwoNewChangesDiffer() throws Exception {
-    ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
-    ChangeInfo info1 = assertCreateSucceeds(changeInput);
-    ChangeInfo info2 = assertCreateSucceeds(changeInput);
-    assertThat(info1.currentRevision).isNotEqualTo(info2.currentRevision);
-    assertThat(info1.changeId).isNotEqualTo(info2.changeId);
-  }
-
-  @Test
-  public void sha1sOfTwoNewChangesDifferIfCreatedConcurrently() throws Exception {
-    ExecutorService executor = Executors.newFixedThreadPool(2);
-    try {
-      for (int i = 0; i < 10; i++) {
-        ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
-
-        CyclicBarrier sync = new CyclicBarrier(2);
-        Callable<ChangeInfo> createChange =
-            () -> {
-              requestScopeOperations.setApiUser(admin.id());
-              sync.await();
-              return assertCreateSucceeds(changeInput);
-            };
-
-        Future<ChangeInfo> changeInfo1 = executor.submit(createChange);
-        Future<ChangeInfo> changeInfo2 = executor.submit(createChange);
-        assertThat(changeInfo1.get().currentRevision)
-            .isNotEqualTo(changeInfo2.get().currentRevision);
-        assertThat(changeInfo1.get().changeId).isNotEqualTo(changeInfo2.get().changeId);
-      }
-    } finally {
-      executor.shutdown();
-      executor.awaitTermination(5, TimeUnit.SECONDS);
-    }
-  }
-
-  @Test
   public void createChangeOnExistingBranchNotPermitted() throws Exception {
-    createBranch(new Branch.NameKey(project, "foo"));
-    blockRead("refs/heads/*");
+    createBranch(BranchNameKey.create(project, "foo"));
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     ChangeInput input = newChangeInput(ChangeStatus.NEW);
     input.branch = "foo";
@@ -548,7 +546,11 @@
 
   @Test
   public void createChangeOnNonExistingBranchNotPermitted() throws Exception {
-    blockRead("refs/heads/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     ChangeInput input = newChangeInput(ChangeStatus.NEW);
     input.branch = "foo";
@@ -568,6 +570,42 @@
         input, BadRequestException.class, "Cannot create merge: destination branch does not exist");
   }
 
+  @Test
+  @UseSystemTime
+  public void sha1sOfTwoNewChangesDiffer() throws Exception {
+    ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
+    ChangeInfo info1 = assertCreateSucceeds(changeInput);
+    ChangeInfo info2 = assertCreateSucceeds(changeInput);
+    assertThat(info1.currentRevision).isNotEqualTo(info2.currentRevision);
+  }
+
+  @Test
+  @UseSystemTime
+  public void sha1sOfTwoNewChangesDifferIfCreatedConcurrently() throws Exception {
+    ExecutorService executor = Executors.newFixedThreadPool(2);
+    try {
+      for (int i = 0; i < 10; i++) {
+        ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
+
+        CyclicBarrier sync = new CyclicBarrier(2);
+        Callable<ChangeInfo> createChange =
+            () -> {
+              requestScopeOperations.setApiUser(admin.id());
+              sync.await();
+              return assertCreateSucceeds(changeInput);
+            };
+
+        Future<ChangeInfo> changeInfo1 = executor.submit(createChange);
+        Future<ChangeInfo> changeInfo2 = executor.submit(createChange);
+        assertThat(changeInfo1.get().currentRevision)
+            .isNotEqualTo(changeInfo2.get().currentRevision);
+      }
+    } finally {
+      executor.shutdown();
+      executor.awaitTermination(5, TimeUnit.SECONDS);
+    }
+  }
+
   private ChangeInput newChangeInput(ChangeStatus status) {
     ChangeInput in = new ChangeInput();
     in.project = project.get();
@@ -604,9 +642,8 @@
   private void assertCreateFails(
       ChangeInput in, Class<? extends RestApiException> errType, String errSubstring)
       throws Exception {
-    exception.expect(errType);
-    exception.expectMessage(errSubstring);
-    gApi.changes().create(in);
+    Throwable thrown = assertThrows(errType, () -> gApi.changes().create(in));
+    assertThat(thrown).hasMessageThat().contains(errSubstring);
   }
 
   // TODO(davido): Expose setting of account preferences in the API
@@ -665,8 +702,8 @@
     initialCommit.assertOkStatus();
 
     // create two new branches
-    createBranch(new Branch.NameKey(project, branchA));
-    createBranch(new Branch.NameKey(project, branchB));
+    createBranch(BranchNameKey.create(project, branchA));
+    createBranch(BranchNameKey.create(project, branchB));
 
     // create a commit in branchA
     Result changeA =
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
index 286980f..37fa2ce 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -23,11 +23,11 @@
 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.Account;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
@@ -104,6 +104,6 @@
   }
 
   private Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
-    return Iterables.transform(r, a -> new Account.Id(a._accountId));
+    return Iterables.transform(r, a -> Account.id(a._accountId));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index c13df49..c57a035 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -15,9 +15,11 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.Objects.requireNonNull;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
@@ -25,29 +27,21 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
+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.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
 @NoHttpd
+@UseClockStep
 public class HashtagsIT extends AbstractDaemonTest {
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
+  @Inject private ProjectOperations projectOperations;
 
   @Inject private RequestScopeOperations requestScopeOperations;
 
@@ -78,9 +72,9 @@
   public void addInvalidHashtag() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("hashtags may not contain commas");
-    addHashtags(r, "invalid,hashtag");
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> addHashtags(r, "invalid,hashtag"));
+    assertThat(thrown).hasMessageThat().contains("hashtags may not contain commas");
   }
 
   @Test
@@ -258,15 +252,18 @@
   public void addHashtagWithoutPermissionNotAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("edit hashtags not permitted");
-    addHashtags(r, "MyHashtag");
+    AuthException thrown = assertThrows(AuthException.class, () -> addHashtags(r, "MyHashtag"));
+    assertThat(thrown).hasMessageThat().contains("edit hashtags not permitted");
   }
 
   @Test
   public void addHashtagWithPermissionAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
-    grant(project, "refs/heads/master", Permission.EDIT_HASHTAGS, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.EDIT_HASHTAGS).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     addHashtags(r, "MyHashtag");
     assertThatGet(r).containsExactly("MyHashtag");
@@ -304,7 +301,7 @@
   private ChangeMessageInfo getLastMessage(PushOneCommit.Result r) throws Exception {
     ChangeMessageInfo lastMessage =
         Iterables.getLast(gApi.changes().id(r.getChange().getId().get()).get().messages, null);
-    assertThat(lastMessage).named(lastMessage.message).isNotNull();
+    assertWithMessage(lastMessage.message).that(lastMessage).isNotNull();
     return lastMessage;
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index f49d1fb..3030b02 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -24,10 +26,9 @@
 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.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -48,7 +49,11 @@
   @Test
   public void indexChangeOnNonVisibleBranch() throws Exception {
     String changeId = createChange().getChangeId();
-    blockRead("refs/heads/master");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     userRestSession.post("/changes/" + changeId + "/index/").assertNotFound();
   }
 
@@ -62,15 +67,15 @@
 
     // Create a project and restrict its visibility to the group
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.allow(
-          u.getConfig(),
-          Permission.READ,
-          groupCache.get(new AccountGroup.NameKey(group)).get().getGroupUUID(),
-          "refs/*");
-      Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(
+            allow(Permission.READ)
+                .ref("refs/*")
+                .group(groupCache.get(AccountGroup.nameKey(group)).get().getGroupUUID()))
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // Clone it and push a change as a regular user
     TestRepository<InMemoryRepository> repo = cloneProject(p, user);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index 2448ff881c..063f1a0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -16,17 +16,22 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.api.changes.MoveInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -35,10 +40,7 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import org.eclipse.jgit.junit.TestRepository;
@@ -48,15 +50,16 @@
 
 @NoHttpd
 public class MoveChangeIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void moveChangeWithShortRef() throws Exception {
     // Move change to a different branch using short ref name
     PushOneCommit.Result r = createChange();
-    Branch.NameKey newBranch = new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchNameKey newBranch = BranchNameKey.create(r.getChange().change().getProject(), "moveTest");
     createBranch(newBranch);
-    move(r.getChangeId(), newBranch.getShortName());
+    move(r.getChangeId(), newBranch.shortName());
     assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
   }
 
@@ -64,9 +67,9 @@
   public void moveChangeWithFullRef() throws Exception {
     // Move change to a different branch using full ref name
     PushOneCommit.Result r = createChange();
-    Branch.NameKey newBranch = new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchNameKey newBranch = BranchNameKey.create(r.getChange().change().getProject(), "moveTest");
     createBranch(newBranch);
-    move(r.getChangeId(), newBranch.get());
+    move(r.getChangeId(), newBranch.branch());
     assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
   }
 
@@ -74,10 +77,10 @@
   public void moveChangeWithMessage() throws Exception {
     // Provide a message using --message flag
     PushOneCommit.Result r = createChange();
-    Branch.NameKey newBranch = new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchNameKey newBranch = BranchNameKey.create(r.getChange().change().getProject(), "moveTest");
     createBranch(newBranch);
     String moveMessage = "Moving for the move test";
-    move(r.getChangeId(), newBranch.get(), moveMessage);
+    move(r.getChangeId(), newBranch.branch(), moveMessage);
     assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
     StringBuilder expectedMessage = new StringBuilder();
     expectedMessage.append("Change destination moved from master to moveTest");
@@ -90,49 +93,59 @@
   public void moveChangeToSameRefAsCurrent() throws Exception {
     // Move change to the branch same as change's destination
     PushOneCommit.Result r = createChange();
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Change is already destined for the specified branch");
-    move(r.getChangeId(), r.getChange().change().getDest().get());
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> move(r.getChangeId(), r.getChange().change().getDest().branch()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Change is already destined for the specified branch");
   }
 
   @Test
   public void moveChangeToSameChangeId() throws Exception {
     // Move change to a branch with existing change with same change ID
     PushOneCommit.Result r = createChange();
-    Branch.NameKey newBranch = new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchNameKey newBranch = BranchNameKey.create(r.getChange().change().getProject(), "moveTest");
     createBranch(newBranch);
     int changeNum = r.getChange().change().getChangeId();
-    createChange(newBranch.get(), r.getChangeId());
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        "Destination "
-            + newBranch.getShortName()
-            + " has a different change with same change key "
-            + r.getChangeId());
-    move(changeNum, newBranch.get());
+    createChange(newBranch.branch(), r.getChangeId());
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> move(changeNum, newBranch.branch()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "Destination "
+                + newBranch.shortName()
+                + " has a different change with same change key "
+                + r.getChangeId());
   }
 
   @Test
   public void moveChangeToNonExistentRef() throws Exception {
     // Move change to a non-existing branch
     PushOneCommit.Result r = createChange();
-    Branch.NameKey newBranch =
-        new Branch.NameKey(r.getChange().change().getProject(), "does_not_exist");
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Destination " + newBranch.get() + " not found in the project");
-    move(r.getChangeId(), newBranch.get());
+    BranchNameKey newBranch =
+        BranchNameKey.create(r.getChange().change().getProject(), "does_not_exist");
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> move(r.getChangeId(), newBranch.branch()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Destination " + newBranch.branch() + " not found in the project");
   }
 
   @Test
   public void moveClosedChange() throws Exception {
     // Move a change which is not open
     PushOneCommit.Result r = createChange();
-    Branch.NameKey newBranch = new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchNameKey newBranch = BranchNameKey.create(r.getChange().change().getProject(), "moveTest");
     createBranch(newBranch);
     merge(r);
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Change is merged");
-    move(r.getChangeId(), newBranch.get());
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> move(r.getChangeId(), newBranch.branch()));
+    assertThat(thrown).hasMessageThat().contains("Change is merged");
   }
 
   @Test
@@ -153,43 +166,51 @@
     pushHead(testRepo, "refs/for/master", false, false);
 
     // Try to move the merge commit to another branch
-    Branch.NameKey newBranch = new Branch.NameKey(r1.getChange().change().getProject(), "moveTest");
+    BranchNameKey newBranch =
+        BranchNameKey.create(r1.getChange().change().getProject(), "moveTest");
     createBranch(newBranch);
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Merge commit cannot be moved");
-    move(GitUtil.getChangeId(testRepo, c).get(), newBranch.get());
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> move(GitUtil.getChangeId(testRepo, c).get(), newBranch.branch()));
+    assertThat(thrown).hasMessageThat().contains("Merge commit cannot be moved");
   }
 
   @Test
   public void moveChangeToBranchWithoutUploadPerms() throws Exception {
     // Move change to a destination where user doesn't have upload permissions
     PushOneCommit.Result r = createChange();
-    Branch.NameKey newBranch =
-        new Branch.NameKey(r.getChange().change().getProject(), "blocked_branch");
+    BranchNameKey newBranch =
+        BranchNameKey.create(r.getChange().change().getProject(), "blocked_branch");
     createBranch(newBranch);
-    block(
-        "refs/for/" + newBranch.get(),
-        Permission.PUSH,
-        systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
-    exception.expect(AuthException.class);
-    exception.expectMessage("move not permitted");
-    move(r.getChangeId(), newBranch.get());
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.PUSH).ref("refs/for/" + newBranch.branch()).group(REGISTERED_USERS))
+        .update();
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> move(r.getChangeId(), newBranch.branch()));
+    assertThat(thrown).hasMessageThat().contains("move not permitted");
   }
 
   @Test
   public void moveChangeFromBranchWithoutAbandonPerms() throws Exception {
     // Move change for which user does not have abandon permissions
     PushOneCommit.Result r = createChange();
-    Branch.NameKey newBranch = new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchNameKey newBranch = BranchNameKey.create(r.getChange().change().getProject(), "moveTest");
     createBranch(newBranch);
-    block(
-        r.getChange().change().getDest().get(),
-        Permission.ABANDON,
-        systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            block(Permission.ABANDON)
+                .ref(r.getChange().change().getDest().branch())
+                .group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("move not permitted");
-    move(r.getChangeId(), newBranch.get());
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> move(r.getChangeId(), newBranch.branch()));
+    assertThat(thrown).hasMessageThat().contains("move not permitted");
   }
 
   @Test
@@ -202,52 +223,56 @@
     int changeNum = r.getChange().change().getChangeId();
 
     // Create a branch with that same commit
-    Branch.NameKey newBranch = new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchNameKey newBranch = BranchNameKey.create(r.getChange().change().getProject(), "moveTest");
     BranchInput bi = new BranchInput();
     bi.revision = r.getCommit().name();
-    gApi.projects().name(newBranch.getParentKey().get()).branch(newBranch.get()).create(bi);
+    gApi.projects().name(newBranch.project().get()).branch(newBranch.branch()).create(bi);
 
     // Try to move the change to the branch with the same commit
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        "Current patchset revision is reachable from tip of " + newBranch.get());
-    move(changeNum, newBranch.get());
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> move(changeNum, newBranch.branch()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Current patchset revision is reachable from tip of " + newBranch.branch());
   }
 
   @Test
   public void moveChangeWithCurrentPatchSetLocked() throws Exception {
     // Move change that is locked
     PushOneCommit.Result r = createChange();
-    Branch.NameKey newBranch = new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchNameKey newBranch = BranchNameKey.create(r.getChange().change().getProject(), "moveTest");
     createBranch(newBranch);
 
+    LabelType patchSetLock = TestLabels.patchSetLock();
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType patchSetLock = Util.patchSetLock();
       u.getConfig().getLabelSections().put(patchSetLock.getName(), patchSetLock);
-      AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(patchSetLock.getName()),
-          0,
-          1,
-          registeredUsers,
-          "refs/heads/*");
       u.save();
     }
-    grant(project, "refs/heads/*", Permission.LABEL + "Patch-Set-Lock");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(patchSetLock.getName())
+                .ref("refs/heads/*")
+                .group(REGISTERED_USERS)
+                .range(0, 1))
+        .update();
     revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1));
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        String.format("The current patch set of change %s is locked", r.getChange().getId()));
-    move(r.getChangeId(), newBranch.get());
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> move(r.getChangeId(), newBranch.branch()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            String.format("The current patch set of change %s is locked", r.getChange().getId()));
   }
 
   @Test
   public void moveChangeOnlyKeepVetoVotes() throws Exception {
     // A vote for a label will be kept after moving if the label's function is *WithBlock and the
     // vote holds the minimum value.
-    createBranch(new Branch.NameKey(project, "foo"));
+    createBranch(BranchNameKey.create(project, "foo"));
 
     String codeReviewLabel = "Code-Review"; // 'Code-Review' uses 'MaxWithBlock' function.
     String testLabelA = "Label-A";
@@ -257,16 +282,13 @@
     configLabel(testLabelB, LabelFunction.MAX_NO_BLOCK);
     configLabel(testLabelC, LabelFunction.NO_BLOCK);
 
-    AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(
-          u.getConfig(), Permission.forLabel(testLabelA), -1, +1, registered, "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel(testLabelB), -1, +1, registered, "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel(testLabelC), -1, +1, registered, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(testLabelA).ref("refs/heads/*").group(REGISTERED_USERS).range(-1, +1))
+        .add(allowLabel(testLabelB).ref("refs/heads/*").group(REGISTERED_USERS).range(-1, +1))
+        .add(allowLabel(testLabelC).ref("refs/heads/*").group(REGISTERED_USERS).range(-1, +1))
+        .update();
 
     String changeId = createChange().getChangeId();
     gApi.changes().id(changeId).current().review(ReviewInput.reject());
@@ -301,17 +323,16 @@
   }
 
   @Test
-  public void moveToBranchWithoutLabel() throws Exception {
-    createBranch(new Branch.NameKey(project, "foo"));
+  public void moveToBranchThatDoesNotHaveCustomLabel() throws Exception {
+    createBranch(BranchNameKey.create(project, "foo"));
     String testLabelA = "Label-A";
     configLabel(testLabelA, LabelFunction.MAX_WITH_BLOCK, Arrays.asList("refs/heads/master"));
 
-    AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(
-          u.getConfig(), Permission.forLabel(testLabelA), -1, +1, registered, "refs/heads/master");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(testLabelA).ref("refs/heads/master").group(REGISTERED_USERS).range(-1, +1))
+        .update();
 
     String changeId = createChange().getChangeId();
 
@@ -326,16 +347,26 @@
 
     move(changeId, "foo");
 
-    // TODO(dpursehouse): Assert about state of labels after move
+    // "foo" branch does not have the custom label
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().keySet())
+        .isEmpty();
+
+    // Move back to master and confirm that the custom label score is still there
+    move(changeId, "master");
+
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().keySet())
+        .containsExactly(testLabelA);
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().values())
+        .containsExactly((short) -1);
   }
 
   @Test
   public void moveNoDestinationBranchSpecified() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("destination branch is required");
-    move(r.getChangeId(), null);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> move(r.getChangeId(), null));
+    assertThat(thrown).hasMessageThat().contains("destination branch is required");
   }
 
   @Test
@@ -343,9 +374,9 @@
   public void moveCanBeDisabledByConfig() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("move changes endpoint is disabled");
-    move(r.getChangeId(), null);
+    MethodNotAllowedException thrown =
+        assertThrows(MethodNotAllowedException.class, () -> move(r.getChangeId(), null));
+    assertThat(thrown).hasMessageThat().contains("move changes endpoint is disabled");
   }
 
   private void move(int changeNum, String destination) throws RestApiException {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
index 649c7ae..7649316 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
@@ -21,8 +21,8 @@
 import com.google.gerrit.acceptance.AbstractPluginFieldsTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 import java.util.List;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
index ca4288f6..1a3c10f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
@@ -15,21 +15,21 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -81,9 +81,9 @@
     setPrivateByDefault(project2, InheritableBoolean.TRUE);
 
     ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("private changes are disabled");
-    gApi.changes().create(input);
+    MethodNotAllowedException thrown =
+        assertThrows(MethodNotAllowedException.class, () -> gApi.changes().create(input));
+    assertThat(thrown).hasMessageThat().contains("private changes are disabled");
   }
 
   @Test
@@ -125,22 +125,6 @@
     result.assertErrorStatus();
   }
 
-  @Test
-  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
-  public void pushDraftsWithPrivateByDefaultAndDisablePrivateChangesTrue() throws Exception {
-    setPrivateByDefault(project2, InheritableBoolean.TRUE);
-
-    RevCommit initialHead = getRemoteHead(project2, "master");
-    TestRepository<InMemoryRepository> testRepo = cloneProject(project2);
-    PushOneCommit.Result result =
-        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
-    result.assertErrorStatus();
-
-    testRepo.reset(initialHead);
-    result = pushFactory.create(admin.newIdent(), testRepo).to("refs/drafts/master");
-    result.assertErrorStatus();
-  }
-
   private void setPrivateByDefault(Project.NameKey proj, InheritableBoolean value)
       throws Exception {
     ConfigInput input = new ConfigInput();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index 2ad4aca..6f519f1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -19,8 +19,11 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 
 import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -28,9 +31,6 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
-import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.submit.CommitMergeStatus;
 import com.google.inject.Inject;
 import java.util.List;
@@ -39,7 +39,8 @@
 import org.junit.Test;
 
 public class SubmitByCherryPickIT extends AbstractSubmit {
-  @Inject private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -47,12 +48,12 @@
   }
 
   @Test
-  public void submitWithCherryPickIfFastForwardPossible() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithCherryPickIfFastForwardPossible() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     assertCherryPick(testRepo, false);
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
     assertThat(newHead.getParent(0)).isEqualTo(change.getCommit().getParent(0));
 
     assertRefUpdatedEvents(initialHead, newHead);
@@ -60,17 +61,17 @@
   }
 
   @Test
-  public void submitWithCherryPick() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithCherryPick() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
     submit(change2.getChangeId());
     assertCherryPick(testRepo, false);
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
     assertThat(newHead.getParentCount()).isEqualTo(1);
     assertThat(newHead.getParent(0)).isEqualTo(headAfterFirstSubmit);
     assertCurrentRevision(change2.getChangeId(), 2, newHead);
@@ -85,17 +86,15 @@
   }
 
   @Test
-  public void changeMessageOnSubmit() throws Exception {
+  public void changeMessageOnSubmit() throws Throwable {
     PushOneCommit.Result change = createChange();
-    RegistrationHandle handle =
-        changeMessageModifiers.add(
-            "gerrit",
-            (newCommitMessage, original, mergeTip, destination) ->
-                newCommitMessage + "Custom: " + destination.get());
-    try {
+    try (Registration registration =
+        extensionRegistry
+            .newRegistration()
+            .add(
+                (newCommitMessage, original, mergeTip, destination) ->
+                    newCommitMessage + "Custom: " + destination.branch())) {
       submit(change.getChangeId());
-    } finally {
-      handle.remove();
     }
     testRepo.git().fetch().setRemote("origin").call();
     ChangeInfo info = get(change.getChangeId(), CURRENT_REVISION);
@@ -107,20 +106,20 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitWithContentMerge() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithContentMerge() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "aaa\nbbb\nccc\n");
     submit(change.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
-    RevCommit headAfterSecondSubmit = getRemoteHead();
+    RevCommit headAfterSecondSubmit = projectOperations.project(project).getHead("master");
 
     testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 = createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
     assertCherryPick(testRepo, true);
-    RevCommit headAfterThirdSubmit = getRemoteHead();
+    RevCommit headAfterThirdSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterThirdSubmit.getParent(0)).isEqualTo(headAfterSecondSubmit);
     assertApproved(change3.getChangeId());
     assertCurrentRevision(change3.getChangeId(), 2, headAfterThirdSubmit);
@@ -145,12 +144,12 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitWithContentMerge_Conflict() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithContentMerge_Conflict() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
     submitWithConflict(
@@ -162,7 +161,7 @@
             + "merged due to a path conflict. Please rebase the change locally and "
             + "upload the rebased commit for review.");
 
-    assertThat(getRemoteHead()).isEqualTo(newHead);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(newHead);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
     assertNoSubmitter(change2.getChangeId(), 1);
 
@@ -171,18 +170,18 @@
   }
 
   @Test
-  public void submitOutOfOrder() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitOutOfOrder() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     createChange("Change 2", "b.txt", "other content");
     PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "different content");
     submit(change3.getChangeId());
     assertCherryPick(testRepo, false);
-    RevCommit headAfterSecondSubmit = getRemoteHead();
+    RevCommit headAfterSecondSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterSecondSubmit.getParent(0)).isEqualTo(headAfterFirstSubmit);
     assertApproved(change3.getChangeId());
     assertCurrentRevision(change3.getChangeId(), 2, headAfterSecondSubmit);
@@ -199,12 +198,12 @@
   }
 
   @Test
-  public void submitOutOfOrder_Conflict() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitOutOfOrder_Conflict() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     createChange("Change 2", "b.txt", "other content");
     PushOneCommit.Result change3 = createChange("Change 3", "b.txt", "different content");
@@ -217,7 +216,7 @@
             + "merged due to a path conflict. Please rebase the change locally and "
             + "upload the rebased commit for review.");
 
-    assertThat(getRemoteHead()).isEqualTo(newHead);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(newHead);
     assertCurrentRevision(change3.getChangeId(), 1, change3.getCommit());
     assertNoSubmitter(change3.getChangeId(), 1);
 
@@ -226,8 +225,8 @@
   }
 
   @Test
-  public void submitMultipleChanges() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitMultipleChanges() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
@@ -254,8 +253,8 @@
   }
 
   @Test
-  public void submitDependentNonConflictingChangesOutOfOrder() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitDependentNonConflictingChangesOutOfOrder() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
@@ -264,11 +263,11 @@
 
     // Submit succeeds; change2 is successfully cherry-picked onto head.
     submit(change2.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     // Submit succeeds; change is successfully cherry-picked onto head
     // (which was change2's cherry-pick).
     submit(change.getChangeId());
-    RevCommit headAfterSecondSubmit = getRemoteHead();
+    RevCommit headAfterSecondSubmit = projectOperations.project(project).getHead("master");
 
     // change is the new tip.
     List<RevCommit> log = getRemoteLog();
@@ -290,8 +289,8 @@
   }
 
   @Test
-  public void submitDependentConflictingChangesOutOfOrder() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitDependentConflictingChangesOutOfOrder() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b1");
@@ -322,8 +321,8 @@
   }
 
   @Test
-  public void submitSubsetOfDependentChanges() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitSubsetOfDependentChanges() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
@@ -334,7 +333,7 @@
     // related to change 3 by topic or ancestor (due to cherrypicking!)
     approve(change2.getChangeId());
     submit(change3.getChangeId());
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
 
     assertNew(change.getChangeId());
     assertNew(change2.getChangeId());
@@ -345,8 +344,8 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitIdenticalTree() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitIdenticalTree() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "a");
 
@@ -354,12 +353,13 @@
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "a");
 
     submit(change1.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterFirstSubmit.getShortMessage()).isEqualTo("Change 1");
 
     submit(change2.getChangeId(), new SubmitInput(), null, null);
 
-    assertThat(getRemoteHead()).isEqualTo(headAfterFirstSubmit);
+    assertThat(projectOperations.project(project).getHead("master"))
+        .isEqualTo(headAfterFirstSubmit);
 
     ChangeInfo info2 = get(change2.getChangeId(), MESSAGES);
     assertThat(info2.status).isEqualTo(ChangeStatus.MERGED);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index 974180c..670cff2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -16,19 +16,23 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.inject.Inject;
 import java.util.Map;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
 import org.junit.Test;
 
 public class SubmitByFastForwardIT extends AbstractSubmit {
+  @Inject private ProjectOperations projectOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -36,11 +40,11 @@
   }
 
   @Test
-  public void submitWithFastForward() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithFastForward() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit updatedHead = getRemoteHead();
+    RevCommit updatedHead = projectOperations.project(project).getHead("master");
     assertThat(updatedHead.getId()).isEqualTo(change.getCommit());
     assertThat(updatedHead.getParent(0)).isEqualTo(initialHead);
     assertSubmitter(change.getChangeId(), 1);
@@ -50,8 +54,8 @@
   }
 
   @Test
-  public void submitMultipleChangesWithFastForward() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitMultipleChangesWithFastForward() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     PushOneCommit.Result change = createChange();
     PushOneCommit.Result change2 = createChange();
@@ -64,7 +68,7 @@
     approve(id2);
     submit(id3);
 
-    RevCommit updatedHead = getRemoteHead();
+    RevCommit updatedHead = projectOperations.project(project).getHead("master");
     assertThat(updatedHead.getId()).isEqualTo(change3.getCommit());
     assertThat(updatedHead.getParent(0).getId()).isEqualTo(change2.getCommit());
     assertSubmitter(change.getChangeId(), 1);
@@ -82,12 +86,12 @@
   }
 
   @Test
-  public void submitTwoChangesWithFastForward_missingDependency() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitTwoChangesWithFastForward_missingDependency() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 = createChange();
     PushOneCommit.Result change2 = createChange();
 
-    Change.Id id1 = change1.getPatchSetId().getParentKey();
+    Change.Id id1 = change1.getPatchSetId().changeId();
     submitWithConflict(
         change2.getChangeId(),
         "Failed to submit 2 changes due to the following problems:\n"
@@ -95,19 +99,19 @@
             + id1
             + ": needs Code-Review");
 
-    RevCommit updatedHead = getRemoteHead();
+    RevCommit updatedHead = projectOperations.project(project).getHead("master");
     assertThat(updatedHead.getId()).isEqualTo(initialHead.getId());
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
   }
 
   @Test
-  public void submitFastForwardNotPossible_Conflict() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitFastForwardNotPossible_Conflict() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
 
@@ -128,7 +132,8 @@
             + ": Project policy requires "
             + "all submissions to be a fast-forward. Please rebase the change "
             + "locally and upload again for review.");
-    assertThat(getRemoteHead()).isEqualTo(headAfterFirstSubmit);
+    assertThat(projectOperations.project(project).getHead("master"))
+        .isEqualTo(headAfterFirstSubmit);
     assertSubmitter(change.getChangeId(), 1);
 
     assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
@@ -136,11 +141,15 @@
   }
 
   @Test
-  public void submitSameCommitsAsInExperimentalBranch() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitSameCommitsAsInExperimentalBranch() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
-    grant(project, "refs/heads/*", Permission.CREATE);
-    grant(project, "refs/heads/experimental", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/heads/*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref("refs/heads/experimental").group(adminGroupUuid()))
+        .update();
 
     RevCommit c1 = commitBuilder().add("b.txt", "1").message("commit at tip").create();
     String id1 = GitUtil.getChangeId(testRepo, c1).get();
@@ -153,9 +162,9 @@
         .isEqualTo(c1.getId());
 
     submit(id1);
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
 
-    assertThat(getRemoteHead().getId()).isEqualTo(c1.getId());
+    assertThat(projectOperations.project(project).getHead("master").getId()).isEqualTo(c1.getId());
     assertSubmitter(id1, 1);
 
     assertRefUpdatedEvents(initialHead, headAfterSubmit);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
index 9bc5a2f..f80bdca 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -17,11 +17,14 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.inject.Inject;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 public class SubmitByMergeAlwaysIT extends AbstractSubmitByMerge {
+  @Inject private ProjectOperations projectOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -29,11 +32,11 @@
   }
 
   @Test
-  public void submitWithMergeIfFastForwardPossible() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithMergeIfFastForwardPossible() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterSubmit.getParentCount()).isEqualTo(2);
     assertThat(headAfterSubmit.getParent(0)).isEqualTo(initialHead);
     assertThat(headAfterSubmit.getParent(1)).isEqualTo(change.getCommit());
@@ -46,8 +49,8 @@
   }
 
   @Test
-  public void submitMultipleChanges() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitMultipleChanges() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     // Submit a change so that the remote head advances
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 5ebcd85..b259d90 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -16,14 +16,22 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+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.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 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.BranchNameKey;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -33,9 +41,6 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import java.io.File;
 import java.io.InputStream;
@@ -63,11 +68,11 @@
   }
 
   @Test
-  public void submitWithFastForward() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithFastForward() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit updatedHead = getRemoteHead();
+    RevCommit updatedHead = projectOperations.project(project).getHead("master");
     assertThat(updatedHead.getId()).isEqualTo(change.getCommit());
     assertThat(updatedHead.getParent(0)).isEqualTo(initialHead);
     assertSubmitter(change.getChangeId(), 1);
@@ -79,8 +84,8 @@
   }
 
   @Test
-  public void submitMultipleChanges() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitMultipleChanges() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
@@ -136,13 +141,13 @@
   }
 
   @Test
-  public void submitChangesAcrossRepos() throws Exception {
+  public void submitChangesAcrossRepos() throws Throwable {
     Project.NameKey p1 = projectOperations.newProject().create();
     Project.NameKey p2 = projectOperations.newProject().create();
     Project.NameKey p3 = projectOperations.newProject().create();
 
-    RevCommit initialHead2 = getRemoteHead(p2, "master");
-    RevCommit initialHead3 = getRemoteHead(p3, "master");
+    RevCommit initialHead2 = projectOperations.project(p2).getHead("master");
+    RevCommit initialHead3 = projectOperations.project(p3).getHead("master");
 
     TestRepository<?> repo1 = cloneProject(p1);
     TestRepository<?> repo2 = cloneProject(p2);
@@ -180,7 +185,7 @@
     approve(change3.getChangeId());
 
     // get a preview before submitting:
-    Map<Branch.NameKey, ObjectId> preview = fetchFromSubmitPreview(change1b.getChangeId());
+    Map<BranchNameKey, ObjectId> preview = fetchFromSubmitPreview(change1b.getChangeId());
     submit(change1b.getChangeId());
 
     RevCommit tip1 = getRemoteLog(p1, "master").get(0);
@@ -196,24 +201,24 @@
       // check that the preview matched what happened:
       assertThat(preview).hasSize(3);
 
-      assertThat(preview).containsKey(new Branch.NameKey(p1, "refs/heads/master"));
+      assertThat(preview).containsKey(BranchNameKey.create(p1, "refs/heads/master"));
       assertTrees(p1, preview);
 
-      assertThat(preview).containsKey(new Branch.NameKey(p2, "refs/heads/master"));
+      assertThat(preview).containsKey(BranchNameKey.create(p2, "refs/heads/master"));
       assertTrees(p2, preview);
 
-      assertThat(preview).containsKey(new Branch.NameKey(p3, "refs/heads/master"));
+      assertThat(preview).containsKey(BranchNameKey.create(p3, "refs/heads/master"));
       assertTrees(p3, preview);
     } else {
       assertThat(tip2.getShortMessage()).isEqualTo(initialHead2.getShortMessage());
       assertThat(tip3.getShortMessage()).isEqualTo(initialHead3.getShortMessage());
       assertThat(preview).hasSize(1);
-      assertThat(preview.get(new Branch.NameKey(p1, "refs/heads/master"))).isNotNull();
+      assertThat(preview.get(BranchNameKey.create(p1, "refs/heads/master"))).isNotNull();
     }
   }
 
   @Test
-  public void submitChangesAcrossReposBlocked() throws Exception {
+  public void submitChangesAcrossReposBlocked() throws Throwable {
     Project.NameKey p1 = projectOperations.newProject().create();
     Project.NameKey p2 = projectOperations.newProject().create();
     Project.NameKey p3 = projectOperations.newProject().create();
@@ -222,9 +227,9 @@
     TestRepository<?> repo2 = cloneProject(p2);
     TestRepository<?> repo3 = cloneProject(p3);
 
-    RevCommit initialHead1 = getRemoteHead(p1, "master");
-    RevCommit initialHead2 = getRemoteHead(p2, "master");
-    RevCommit initialHead3 = getRemoteHead(p3, "master");
+    RevCommit initialHead1 = projectOperations.project(p1).getHead("master");
+    RevCommit initialHead2 = projectOperations.project(p2).getHead("master");
+    RevCommit initialHead3 = projectOperations.project(p3).getHead("master");
 
     PushOneCommit.Result change1a =
         createChange(
@@ -277,15 +282,12 @@
               + "and upload the rebased commit for review.";
 
       // Get a preview before submitting:
-      try (BinaryResult r = gApi.changes().id(change1b.getChangeId()).current().submitPreview()) {
-        // We cannot just use the ExpectedException infrastructure as provided
-        // by AbstractDaemonTest, as then we'd stop early and not test the
-        // actual submit.
+      RestApiException thrown =
+          assertThrows(
+              RestApiException.class,
+              () -> gApi.changes().id(change1b.getChangeId()).current().submitPreview().close());
+      assertThat(thrown.getMessage()).isEqualTo(msg);
 
-        fail("expected failure");
-      } catch (RestApiException e) {
-        assertThat(e.getMessage()).isEqualTo(msg);
-      }
       submitWithConflict(change1b.getChangeId(), msg);
     } else {
       submit(change1b.getChangeId());
@@ -313,13 +315,13 @@
   }
 
   @Test
-  public void submitWithMergedAncestorsOnOtherBranch() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithMergedAncestorsOnOtherBranch() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     PushOneCommit.Result change1 =
         createChange(testRepo, "master", "base commit", "a.txt", "1", "");
     submit(change1.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
 
     gApi.projects().name(project.get()).branch("branch").create(new BranchInput());
 
@@ -362,12 +364,12 @@
   }
 
   @Test
-  public void submitWithOpenAncestorsOnOtherBranch() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithOpenAncestorsOnOtherBranch() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 =
         createChange(testRepo, "master", "base commit", "a.txt", "1", "");
     submit(change1.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
 
     gApi.projects().name(project.get()).branch("branch").create(new BranchInput());
 
@@ -395,7 +397,7 @@
 
     Project.NameKey p3 = projectOperations.newProject().create();
     TestRepository<?> repo3 = cloneProject(p3);
-    RevCommit repo3Head = getRemoteHead(p3, "master");
+    RevCommit repo3Head = projectOperations.project(p3).getHead("master");
     PushOneCommit.Result change3b =
         createChange(
             repo3,
@@ -435,8 +437,8 @@
   }
 
   @Test
-  public void gerritWorkflow() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void gerritWorkflow() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     // We'll setup a master and a stable branch.
     // Then we create a change to be applied to master, which is
@@ -462,8 +464,8 @@
     gApi.changes().id(cherryId).current().submit();
 
     // Create the merge locally
-    RevCommit stable = getRemoteHead(project, "stable");
-    RevCommit master = getRemoteHead(project, "master");
+    RevCommit stable = projectOperations.project(project).getHead("stable");
+    RevCommit master = projectOperations.project(project).getHead("master");
     testRepo.git().fetch().call();
     testRepo.git().branchCreate().setName("stable").setStartPoint(stable).call();
     testRepo.git().branchCreate().setName("master").setStartPoint(master).call();
@@ -493,7 +495,7 @@
   }
 
   @Test
-  public void openChangeForTargetBranchPreventsMerge() throws Exception {
+  public void openChangeForTargetBranchPreventsMerge() throws Throwable {
     gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
 
     // Propose a change for master, but leave it open for master!
@@ -517,7 +519,7 @@
         change3.getChangeId(),
         "Failed to submit 1 change due to the following problems:\n"
             + "Change "
-            + change3.getPatchSetId().getParentKey().get()
+            + change3.getPatchSetId().changeId().get()
             + ": Depends on change that was not submitted."
             + " Commit "
             + change3.getCommit().name()
@@ -532,7 +534,7 @@
   }
 
   @Test
-  public void dependencyOnOutdatedPatchSetPreventsMerge() throws Exception {
+  public void dependencyOnOutdatedPatchSetPreventsMerge() throws Throwable {
     // Create a change
     PushOneCommit change = pushFactory.create(user.newIdent(), testRepo, "fix", "a.txt", "foo");
     PushOneCommit.Result changeResult = change.to("refs/for/master");
@@ -574,7 +576,7 @@
   }
 
   @Test
-  public void dependencyOnDeletedChangePreventsMerge() throws Exception {
+  public void dependencyOnDeletedChangePreventsMerge() throws Throwable {
     // Create a change
     PushOneCommit change = pushFactory.create(user.newIdent(), testRepo, "fix", "a.txt", "foo");
     PushOneCommit.Result changeResult = change.to("refs/for/master");
@@ -608,9 +610,13 @@
   }
 
   @Test
-  public void dependencyOnChangeForNonVisibleBranchPreventsMerge() throws Exception {
-    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", false, REGISTERED_USERS, false);
-    grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
+  public void dependencyOnChangeForNonVisibleBranchPreventsMerge() throws Throwable {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // Create a change
     PushOneCommit change = pushFactory.create(admin.newIdent(), testRepo, "fix", "a.txt", "foo");
@@ -624,23 +630,26 @@
 
     // Move the first change to a destination branch that is non-visible to user so that user cannot
     // this change anymore.
-    Branch.NameKey secretBranch = new Branch.NameKey(project, "secretBranch");
+    BranchNameKey secretBranch = BranchNameKey.create(project, "secretBranch");
     gApi.projects()
-        .name(secretBranch.getParentKey().get())
-        .branch(secretBranch.get())
+        .name(secretBranch.project().get())
+        .branch(secretBranch.branch())
         .create(new BranchInput());
-    gApi.changes().id(changeResult.getChangeId()).move(secretBranch.get());
-    block(secretBranch.get(), "read", ANONYMOUS_USERS);
+    gApi.changes().id(changeResult.getChangeId()).move(secretBranch.branch());
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref(secretBranch.branch()).group(ANONYMOUS_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see the first change.
-    try {
-      gApi.changes().id(changeResult.getChangeId()).get();
-      fail("expected failure");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).isEqualTo("Not found: " + changeResult.getChangeId());
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id(changeResult.getChangeId()).get());
+    assertThat(thrown).hasMessageThat().isEqualTo("Not found: " + changeResult.getChangeId());
 
     // Submit is expected to fail.
     submitWithConflict(
@@ -663,9 +672,13 @@
   }
 
   @Test
-  public void dependencyOnHiddenChangePreventsMerge() throws Exception {
-    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", false, REGISTERED_USERS, false);
-    grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
+  public void dependencyOnHiddenChangePreventsMerge() throws Throwable {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // Create a change
     PushOneCommit change = pushFactory.create(admin.newIdent(), testRepo, "fix", "a.txt", "foo");
@@ -684,30 +697,29 @@
     requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see the first change.
-    try {
-      gApi.changes().id(changeResult.getChangeId()).get();
-      fail("expected failure");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).isEqualTo("Not found: " + changeResult.getChangeId());
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id(changeResult.getChangeId()).get());
+    assertThat(thrown).hasMessageThat().isEqualTo("Not found: " + changeResult.getChangeId());
 
     // Submit is expected to fail.
-    try {
-      gApi.changes().id(change2Result.getChangeId()).current().submit();
-      fail("expected failure");
-    } catch (AuthException e) {
-      assertThat(e.getMessage())
-          .isEqualTo(
-              "A change to be submitted with "
-                  + change2Result.getChange().getId().id
-                  + " is not visible");
-    }
+    AuthException thrown2 =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(change2Result.getChangeId()).current().submit());
+    assertThat(thrown2)
+        .hasMessageThat()
+        .isEqualTo(
+            "A change to be submitted with "
+                + change2Result.getChange().getId().get()
+                + " is not visible");
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
   }
 
   @Test
-  public void dependencyOnHiddenChangeUsingTopicPreventsMerge() throws Exception {
+  public void dependencyOnHiddenChangeUsingTopicPreventsMerge() throws Throwable {
     // Construct a topic where a change included by topic depends on a private change that is not
     // visible to the submitting user
     // (c1) --- topic --- (c2b)
@@ -718,10 +730,18 @@
     Project.NameKey p1 = projectOperations.newProject().create();
     Project.NameKey p2 = projectOperations.newProject().create();
 
-    grantLabel("Code-Review", -2, 2, p1, "refs/heads/*", false, REGISTERED_USERS, false);
-    grant(p1, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
-    grantLabel("Code-Review", -2, 2, p2, "refs/heads/*", false, REGISTERED_USERS, false);
-    grant(p2, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
+    projectOperations
+        .project(p1)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(p2)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<?> repo1 = cloneProject(p1);
     TestRepository<?> repo2 = cloneProject(p2);
@@ -744,30 +764,27 @@
     requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see change2a
-    try {
-      gApi.changes().id(change2a.getChangeId()).get();
-      fail("expected failure");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).isEqualTo("Not found: " + change2a.getChangeId());
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class, () -> gApi.changes().id(change2a.getChangeId()).get());
+    assertThat(thrown).hasMessageThat().isEqualTo("Not found: " + change2a.getChangeId());
 
     // Submit is expected to fail.
-    try {
-      gApi.changes().id(change1.getChangeId()).current().submit();
-      fail("expected failure");
-    } catch (AuthException e) {
-      assertThat(e.getMessage())
-          .isEqualTo(
-              "A change to be submitted with "
-                  + change1.getChange().getId().id
-                  + " is not visible");
-    }
+    AuthException thrown2 =
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(change1.getChangeId()).current().submit());
+    assertThat(thrown2)
+        .hasMessageThat()
+        .isEqualTo(
+            "A change to be submitted with "
+                + change1.getChange().getId().get()
+                + " is not visible");
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
   }
 
   @Test
-  public void testPreviewSubmitTgz() throws Exception {
+  public void testPreviewSubmitTgz() throws Throwable {
     Project.NameKey p1 = projectOperations.newProject().create();
 
     TestRepository<?> repo1 = cloneProject(p1);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
index eb8dea5..388c4f4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -15,35 +15,35 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.config.UrlFormatter;
 import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
-import java.util.ArrayDeque;
-import java.util.Deque;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 public class SubmitByRebaseAlwaysIT extends AbstractSubmitByRebase {
-  @Inject private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
   @Inject private DynamicItem<UrlFormatter> urlFormatter;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -52,12 +52,12 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitWithPossibleFastForward() throws Exception {
-    RevCommit oldHead = getRemoteHead();
+  public void submitWithPossibleFastForward() throws Throwable {
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
 
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head.getId()).isNotEqualTo(change.getCommit());
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change.getChangeId());
@@ -72,7 +72,7 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void alwaysAddFooters() throws Exception {
+  public void alwaysAddFooters() throws Throwable {
     PushOneCommit.Result change1 = createChange();
     PushOneCommit.Result change2 = createChange();
 
@@ -89,21 +89,22 @@
   }
 
   @Test
-  public void rebaseInvokesChangeMessageModifiers() throws Exception {
+  public void rebaseInvokesChangeMessageModifiers() throws Throwable {
     ChangeMessageModifier modifier1 =
         (msg, orig, tip, dest) -> msg + "This-change-before-rebase: " + orig.name() + "\n";
     ChangeMessageModifier modifier2 =
         (msg, orig, tip, dest) -> msg + "Previous-step-tip: " + tip.name() + "\n";
     ChangeMessageModifier modifier3 =
-        (msg, orig, tip, dest) -> msg + "Dest: " + dest.getShortName() + "\n";
+        (msg, orig, tip, dest) -> msg + "Dest: " + dest.shortName() + "\n";
 
-    try (AutoCloseable ignored = installChangeMessageModifiers(modifier1, modifier2, modifier3)) {
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(modifier1).add(modifier2).add(modifier3)) {
       ImmutableList<PushOneCommit.Result> changes = submitWithRebase(admin);
       ChangeData cd1 = changes.get(0).getChange();
       ChangeData cd2 = changes.get(1).getChange();
       assertThat(cd2.patchSets()).hasSize(2);
-      String change1CurrentCommit = cd1.currentPatchSet().getRevision().get();
-      String change2Ps1Commit = cd2.patchSet(new PatchSet.Id(cd2.getId(), 1)).getRevision().get();
+      String change1CurrentCommit = cd1.currentPatchSet().commitId().name();
+      String change2Ps1Commit = cd2.patchSet(PatchSet.id(cd2.getId(), 1)).commitId().name();
 
       assertThat(gApi.changes().id(cd2.getId().get()).revision(2).commit(false).message)
           .isEqualTo(
@@ -120,65 +121,52 @@
   }
 
   @Test
-  public void failingChangeMessageModifierShortCircuits() throws Exception {
+  public void failingChangeMessageModifierShortCircuits() throws Throwable {
     ChangeMessageModifier modifier1 =
         (msg, orig, tip, dest) -> {
           throw new IllegalStateException("boom");
         };
     ChangeMessageModifier modifier2 = (msg, orig, tip, dest) -> msg + "A-footer: value\n";
-    try (AutoCloseable ignored = installChangeMessageModifiers(modifier1, modifier2)) {
-      try {
-        submitWithRebase();
-        assert_().fail("expected ResourceConflictException");
-      } catch (ResourceConflictException e) {
-        Throwable cause = Throwables.getRootCause(e);
-        assertThat(cause).isInstanceOf(RuntimeException.class);
-        assertThat(cause).hasMessageThat().isEqualTo("boom");
-      }
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(modifier1).add(modifier2)) {
+      ResourceConflictException thrown =
+          assertThrows(ResourceConflictException.class, () -> submitWithRebase());
+      Throwable cause = Throwables.getRootCause(thrown);
+      assertThat(cause).isInstanceOf(RuntimeException.class);
+      assertThat(cause).hasMessageThat().isEqualTo("boom");
     }
   }
 
   @Test
-  public void changeMessageModifierReturningNullShortCircuits() throws Exception {
+  public void changeMessageModifierReturningNullShortCircuits() throws Throwable {
     ChangeMessageModifier modifier1 = (msg, orig, tip, dest) -> null;
     ChangeMessageModifier modifier2 = (msg, orig, tip, dest) -> msg + "A-footer: value\n";
-    try (AutoCloseable ignored = installChangeMessageModifiers(modifier1, modifier2)) {
-      try {
-        submitWithRebase();
-        assert_().fail("expected ResourceConflictException");
-      } catch (ResourceConflictException e) {
-        Throwable cause = Throwables.getRootCause(e);
-        assertThat(cause).isInstanceOf(RuntimeException.class);
-        assertThat(cause)
-            .hasMessageThat()
-            .isEqualTo(
-                modifier1.getClass().getName()
-                    + ".onSubmit from plugin modifier-1 returned null instead of new commit"
-                    + " message");
-      }
+    try (Registration registration =
+        extensionRegistry
+            .newRegistration()
+            .add(modifier1, "modifier-1")
+            .add(modifier2, "modifier-2")) {
+      ResourceConflictException thrown =
+          assertThrows(ResourceConflictException.class, () -> submitWithRebase());
+      Throwable cause = Throwables.getRootCause(thrown);
+      assertThat(cause).isInstanceOf(RuntimeException.class);
+      assertThat(cause)
+          .hasMessageThat()
+          .isEqualTo(
+              modifier1.getClass().getName()
+                  + ".onSubmit from plugin modifier-1 returned null instead of new commit"
+                  + " message");
     }
   }
 
-  private AutoCloseable installChangeMessageModifiers(ChangeMessageModifier... modifiers) {
-    Deque<RegistrationHandle> handles = new ArrayDeque<>(modifiers.length);
-    for (int i = 0; i < modifiers.length; i++) {
-      handles.push(changeMessageModifiers.add("modifier-" + (i + 1), modifiers[i]));
-    }
-    return () -> {
-      while (!handles.isEmpty()) {
-        handles.pop().remove();
-      }
-    };
-  }
-
-  private void assertLatestRevisionHasFooters(PushOneCommit.Result change) throws Exception {
+  private void assertLatestRevisionHasFooters(PushOneCommit.Result change) throws Throwable {
     RevCommit c = getCurrentCommit(change);
     assertThat(c.getFooterLines(FooterConstants.CHANGE_ID)).isNotEmpty();
     assertThat(c.getFooterLines(FooterConstants.REVIEWED_BY)).isNotEmpty();
     assertThat(c.getFooterLines(FooterConstants.REVIEWED_ON)).isNotEmpty();
   }
 
-  private RevCommit getCurrentCommit(PushOneCommit.Result change) throws Exception {
+  private RevCommit getCurrentCommit(PushOneCommit.Result change) throws Throwable {
     testRepo.git().fetch().setRemote("origin").call();
     ChangeInfo info = get(change.getChangeId(), CURRENT_REVISION);
     RevCommit c = testRepo.getRevWalk().parseCommit(ObjectId.fromString(info.currentRevision));
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
index 7bb31a0..01b58ee 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -18,12 +18,15 @@
 
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.inject.Inject;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 public class SubmitByRebaseIfNecessaryIT extends AbstractSubmitByRebase {
+  @Inject private ProjectOperations projectOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -32,11 +35,11 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitWithFastForward() throws Exception {
-    RevCommit oldHead = getRemoteHead();
+  public void submitWithFastForward() throws Throwable {
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head.getId()).isEqualTo(change.getCommit());
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change.getChangeId());
@@ -50,20 +53,20 @@
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
-  public void submitWithContentMerge() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+  public void submitWithContentMerge() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "aaa\nbbb\nccc\n");
     submit(change.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
 
-    RevCommit headAfterSecondSubmit = getRemoteHead();
+    RevCommit headAfterSecondSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 = createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
     assertRebase(testRepo, true);
-    RevCommit headAfterThirdSubmit = getRemoteHead();
+    RevCommit headAfterThirdSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterThirdSubmit.getParent(0)).isEqualTo(headAfterSecondSubmit);
     assertApproved(change3.getChangeId());
     assertCurrentRevision(change3.getChangeId(), 2, headAfterThirdSubmit);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index 87fc9f6..73f10e5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -21,8 +21,8 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.ChangeStatus;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.restapi.change.Submit;
@@ -186,8 +186,8 @@
     String project2Name = name("Project2");
     gApi.projects().create(project1Name);
     gApi.projects().create(project2Name);
-    TestRepository<InMemoryRepository> project1 = cloneProject(new Project.NameKey(project1Name));
-    TestRepository<InMemoryRepository> project2 = cloneProject(new Project.NameKey(project2Name));
+    TestRepository<InMemoryRepository> project1 = cloneProject(Project.nameKey(project1Name));
+    TestRepository<InMemoryRepository> project2 = cloneProject(Project.nameKey(project2Name));
 
     PushOneCommit.Result a = createChange(project1, "A");
     PushOneCommit.Result b =
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 9bbe1dd..8e0042c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -16,7 +16,12 @@
 
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
@@ -29,14 +34,17 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 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.ReviewerState;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import java.util.List;
 import java.util.Set;
@@ -130,6 +138,48 @@
   }
 
   @Test
+  @GerritConfig(name = "index.maxTerms", value = "10")
+  public void suggestReviewersTooManyQueryTerms() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // Do a query which doesn't exceed index.maxTerms succeeds (add only 9 terms, since on
+    // 'inactive:1' term is implicitly added) and assert that a result is returned
+    StringBuilder query = new StringBuilder();
+    for (int i = 1; i <= 9; i++) {
+      query.append(name("u")).append(" ");
+    }
+    assertThat(suggestReviewers(changeId, query.toString())).isNotEmpty();
+
+    // Do a query which exceed index.maxTerms succeeds (10 terms plus 'inactive:1' term which is
+    // implicitly added).
+    query.append(name("u"));
+    BadRequestException exception =
+        assertThrows(BadRequestException.class, () -> suggestReviewers(changeId, query.toString()));
+    assertThat(exception).hasMessageThat().isEqualTo("too many terms in query");
+  }
+
+  @Test
+  public void suggestReviewersWithExcludeGroups() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // by default groups are included
+    List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, name("user"));
+    assertReviewers(
+        reviewers, ImmutableList.of(user1, user2, user3), ImmutableList.of(group1, group2, group3));
+
+    // exclude groups
+    reviewers =
+        gApi.changes().id(changeId).suggestReviewers(name("user")).excludeGroups(true).get();
+    assertReviewers(reviewers, ImmutableList.of(user1, user2, user3), ImmutableList.of());
+
+    // explicitly include groups
+    reviewers =
+        gApi.changes().id(changeId).suggestReviewers(name("user")).excludeGroups(false).get();
+    assertReviewers(
+        reviewers, ImmutableList.of(user1, user2, user3), ImmutableList.of(group1, group2, group3));
+  }
+
+  @Test
   @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
   public void suggestReviewersSameGroupVisibility() throws Exception {
     String changeId = createChange().getChangeId();
@@ -160,8 +210,12 @@
     List<SuggestedReviewerInfo> reviewers;
 
     requestScopeOperations.setApiUser(user3.id());
-    block("refs/*", "read", ANONYMOUS_USERS);
-    allow("refs/*", "read", group1);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow(READ).ref("refs/*").group(group1))
+        .update();
     reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).isEmpty();
   }
@@ -178,7 +232,10 @@
 
     // Clear cached group info.
     requestScopeOperations.setApiUser(user1.id());
-    allowGlobalCapabilities(group1, GlobalCapability.VIEW_ALL_ACCOUNTS);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.VIEW_ALL_ACCOUNTS).group(group1))
+        .update();
     reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName());
@@ -376,135 +433,119 @@
   }
 
   @Test
-  @GerritConfig(name = "suggest.maxSuggestedReviewers", value = "10")
-  public void reviewerRanking() throws Exception {
-    // Assert that user are ranked by the number of times they have applied a
-    // a label to a change (highest), added comments (medium) or owned a
-    // change (low).
-    String fullName = "Primum Finalis";
-    TestAccount userWhoOwns = user("customuser1", fullName);
-    TestAccount reviewer1 = user("customuser2", fullName);
-    TestAccount reviewer2 = user("customuser3", fullName);
-    TestAccount userWhoComments = user("customuser4", fullName);
-    TestAccount userWhoLooksForSuggestions = user("customuser5", fullName);
-
-    // Create a change as userWhoOwns and add some reviews
-    requestScopeOperations.setApiUser(userWhoOwns.id());
-    String changeId1 = createChangeFromApi();
-
-    requestScopeOperations.setApiUser(reviewer1.id());
-    reviewChange(changeId1);
-
-    requestScopeOperations.setApiUser(user1.id());
-    String changeId2 = createChangeFromApi();
-
-    requestScopeOperations.setApiUser(reviewer1.id());
-    reviewChange(changeId2);
-
-    requestScopeOperations.setApiUser(reviewer2.id());
-    reviewChange(changeId2);
-
-    // Create a comment as a different user
-    requestScopeOperations.setApiUser(userWhoComments.id());
-    ReviewInput ri = new ReviewInput();
-    ri.message = "Test";
-    gApi.changes().id(changeId1).revision(1).review(ri);
-
-    // Create a change as a new user to assert that we receive the correct
-    // ranking
-
-    requestScopeOperations.setApiUser(userWhoLooksForSuggestions.id());
-    List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), "Pri", 4);
-    assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
-        .containsExactly(
-            reviewer1.id().get(),
-            reviewer2.id().get(),
-            userWhoOwns.id().get(),
-            userWhoComments.id().get())
-        .inOrder();
-  }
-
-  @Test
-  public void reviewerRankingProjectIsolation() throws Exception {
-    // Create new project
-    Project.NameKey newProject = projectOperations.newProject().create();
-
-    // Create users who review changes in both the default and the new project
-    String fullName = "Primum Finalis";
-    TestAccount userWhoOwns = user("customuser1", fullName);
-    TestAccount reviewer1 = user("customuser2", fullName);
-    TestAccount reviewer2 = user("customuser3", fullName);
-
-    requestScopeOperations.setApiUser(userWhoOwns.id());
-    String changeId1 = createChangeFromApi();
-
-    requestScopeOperations.setApiUser(reviewer1.id());
-    reviewChange(changeId1);
-
-    requestScopeOperations.setApiUser(userWhoOwns.id());
-    String changeId2 = createChangeFromApi(newProject);
-
-    requestScopeOperations.setApiUser(reviewer2.id());
-    reviewChange(changeId2);
-
-    requestScopeOperations.setApiUser(userWhoOwns.id());
-    String changeId3 = createChangeFromApi(newProject);
-
-    requestScopeOperations.setApiUser(reviewer2.id());
-    reviewChange(changeId3);
-
-    requestScopeOperations.setApiUser(userWhoOwns.id());
-    List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), "Prim", 4);
-
-    // Assert that reviewer1 is on top, even though reviewer2 has more reviews
-    // in other projects
-    assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
-        .containsExactly(reviewer1.id().get(), reviewer2.id().get())
-        .inOrder();
-  }
-
-  @Test
   public void suggestNoInactiveAccounts() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    String changeIdReviewed = createChangeFromApi();
+    String changeId = createChangeFromApi();
+
     String name = name("foo");
     TestAccount foo1 = accountCreator.create(name + "-1");
+    requestScopeOperations.setApiUser(foo1.id());
+    reviewChange(changeIdReviewed);
     assertThat(gApi.accounts().id(foo1.username()).getActive()).isTrue();
 
     TestAccount foo2 = accountCreator.create(name + "-2");
+    requestScopeOperations.setApiUser(foo2.id());
+    reviewChange(changeIdReviewed);
     assertThat(gApi.accounts().id(foo2.username()).getActive()).isTrue();
 
-    String changeId = createChange().getChangeId();
     assertReviewers(
         suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
 
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().id(foo2.username()).setActive(false);
     assertThat(gApi.accounts().id(foo2.id().get()).getActive()).isFalse();
     assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo1), ImmutableList.of());
   }
 
   @Test
+  public void suggestNoExistingReviewers() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    String changeId = createChangeFromApi();
+    String changeIdReviewed = createChangeFromApi();
+
+    String name = name("foo");
+    TestAccount foo1 = accountCreator.create(name + "-1");
+    requestScopeOperations.setApiUser(foo1.id());
+    reviewChange(changeIdReviewed);
+
+    TestAccount foo2 = accountCreator.create(name + "-2");
+    requestScopeOperations.setApiUser(foo2.id());
+    reviewChange(changeIdReviewed);
+
+    assertReviewers(
+        suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
+
+    gApi.changes().id(changeId).addReviewer(foo2.id().toString());
+    assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo1), ImmutableList.of());
+  }
+
+  @Test
+  public void suggestCcAsReviewer() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    String changeId = createChangeFromApi();
+    String changeIdReviewed = createChangeFromApi();
+
+    String name = name("foo");
+    TestAccount foo1 = accountCreator.create(name + "-1");
+    requestScopeOperations.setApiUser(foo1.id());
+    reviewChange(changeIdReviewed);
+
+    TestAccount foo2 = accountCreator.create(name + "-2");
+    requestScopeOperations.setApiUser(foo2.id());
+    reviewChange(changeIdReviewed);
+
+    assertReviewers(
+        suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
+
+    AddReviewerInput reviewerInput = new AddReviewerInput();
+    reviewerInput.reviewer = foo2.id().toString();
+    reviewerInput.state = ReviewerState.CC;
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+    assertReviewers(
+        suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
+  }
+
+  @Test
+  public void suggestReviewerAsCc() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    String changeId = createChangeFromApi();
+    String changeIdReviewed = createChangeFromApi();
+
+    String name = name("foo");
+    TestAccount foo1 = accountCreator.create(name + "-1");
+    requestScopeOperations.setApiUser(foo1.id());
+    reviewChange(changeIdReviewed);
+
+    TestAccount foo2 = accountCreator.create(name + "-2");
+    requestScopeOperations.setApiUser(foo2.id());
+    reviewChange(changeIdReviewed);
+
+    assertReviewers(suggestCcs(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
+
+    AddReviewerInput reviewerInput = new AddReviewerInput();
+    reviewerInput.reviewer = foo2.id().toString();
+    reviewerInput.state = ReviewerState.REVIEWER;
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+    assertReviewers(suggestCcs(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
+  }
+
+  @Test
   public void suggestBySecondaryEmailWithModifyAccount() throws Exception {
     String secondaryEmail = "foo.secondary@example.com";
     TestAccount foo = createAccountWithSecondaryEmail("foo", secondaryEmail);
 
-    List<SuggestedReviewerInfo> reviewers =
-        suggestReviewers(createChange().getChangeId(), secondaryEmail, 4);
-    assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
-
-    reviewers = suggestReviewers(createChange().getChangeId(), "secondary", 4);
+    List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), "secondary", 4);
     assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
   }
 
   @Test
   public void cannotSuggestBySecondaryEmailWithoutModifyAccount() throws Exception {
-    String secondaryEmail = "foo.secondary@example.com";
-    createAccountWithSecondaryEmail("foo", secondaryEmail);
-
+    // Test that even if the account exists, the result is still empty since
+    // it shouldn't match to that account based only on the secondary email.
+    createAccountWithSecondaryEmail("foo", "foo.secondary@example.com");
     requestScopeOperations.setApiUser(user.id());
-    List<SuggestedReviewerInfo> reviewers =
-        suggestReviewers(createChange().getChangeId(), secondaryEmail, 4);
-    assertThat(reviewers).isEmpty();
-
-    reviewers = suggestReviewers(createChange().getChangeId(), "secondary2", 4);
+    List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), "secondary", 4);
     assertThat(reviewers).isEmpty();
   }
 
@@ -525,6 +566,24 @@
     assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails).isNull();
   }
 
+  @Test
+  public void suggestsPeopleWithNoReviewsWhenExplicitlyQueried() throws Exception {
+    TestAccount newTeamMember = accountCreator.create("newTeamMember");
+
+    requestScopeOperations.setApiUser(user.id());
+    String changeId = createChangeFromApi();
+    String changeIdReviewed = createChangeFromApi();
+
+    TestAccount reviewer = accountCreator.create("newReviewer");
+    requestScopeOperations.setApiUser(reviewer.id());
+    reviewChange(changeIdReviewed);
+
+    List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "new", 4);
+    assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
+        .containsExactly(reviewer.id().get(), newTeamMember.id().get())
+        .inOrder();
+  }
+
   private TestAccount createAccountWithSecondaryEmail(String name, String secondaryEmail)
       throws Exception {
     TestAccount foo = accountCreator.create(name(name), "foo.primary@example.com", "Foo");
@@ -545,6 +604,10 @@
     return gApi.changes().id(changeId).suggestReviewers(query).withLimit(n).get();
   }
 
+  private List<SuggestedReviewerInfo> suggestCcs(String changeId, String query) throws Exception {
+    return gApi.changes().id(changeId).suggestCcs(query).get();
+  }
+
   private AccountGroup.UUID createGroupWithArbitraryMembers(int numMembers) {
     Set<Account.Id> members =
         IntStream.rangeClosed(1, numMembers)
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
index 49692dd..525f5d5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
@@ -15,127 +15,194 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
-import org.junit.After;
-import org.junit.Before;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PushResult;
 import org.junit.Test;
 
 public class WorkInProgressByDefaultIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  private Project.NameKey project1;
-  private Project.NameKey project2;
-
-  @Before
-  public void setUp() throws Exception {
-    project1 = projectOperations.newProject().create();
-    project2 = projectOperations.newProject().parent(project1).create();
-  }
-
-  @After
-  public void tearDown() throws Exception {
-    requestScopeOperations.setApiUser(admin.id());
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
-    prefs.workInProgressByDefault = false;
-    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
-  }
-
   @Test
   public void createChangeWithWorkInProgressByDefaultForProjectDisabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
     ChangeInfo info =
-        gApi.changes().create(new ChangeInput(project2.get(), "master", "empty change")).get();
+        gApi.changes().create(new ChangeInput(project.get(), "master", "empty change")).get();
     assertThat(info.workInProgress).isNull();
   }
 
   @Test
   public void createChangeWithWorkInProgressByDefaultForProjectEnabled() throws Exception {
-    setWorkInProgressByDefaultForProject(project2);
-    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    Project.NameKey project = projectOperations.newProject().create();
+    setWorkInProgressByDefaultForProject(project);
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
     assertThat(gApi.changes().create(input).get().workInProgress).isTrue();
   }
 
   @Test
   public void createChangeWithWorkInProgressByDefaultForUserEnabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
     setWorkInProgressByDefaultForUser();
-    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
     assertThat(gApi.changes().create(input).get().workInProgress).isTrue();
   }
 
   @Test
   public void createChangeBypassWorkInProgressByDefaultForProjectEnabled() throws Exception {
-    setWorkInProgressByDefaultForProject(project2);
-    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    Project.NameKey project = projectOperations.newProject().create();
+    setWorkInProgressByDefaultForProject(project);
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
     input.workInProgress = false;
     assertThat(gApi.changes().create(input).get().workInProgress).isNull();
   }
 
   @Test
   public void createChangeBypassWorkInProgressByDefaultForUserEnabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
     setWorkInProgressByDefaultForUser();
-    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
     input.workInProgress = false;
     assertThat(gApi.changes().create(input).get().workInProgress).isNull();
   }
 
   @Test
   public void createChangeWithWorkInProgressByDefaultForProjectInherited() throws Exception {
-    setWorkInProgressByDefaultForProject(project1);
+    Project.NameKey parentProject = projectOperations.newProject().create();
+    Project.NameKey childProject = projectOperations.newProject().parent(parentProject).create();
+    setWorkInProgressByDefaultForProject(parentProject);
     ChangeInfo info =
-        gApi.changes().create(new ChangeInput(project2.get(), "master", "empty change")).get();
+        gApi.changes().create(new ChangeInput(childProject.get(), "master", "empty change")).get();
     assertThat(info.workInProgress).isTrue();
   }
 
   @Test
   public void pushWithWorkInProgressByDefaultForProjectEnabled() throws Exception {
-    setWorkInProgressByDefaultForProject(project2);
-    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+    Project.NameKey project = projectOperations.newProject().create();
+    setWorkInProgressByDefaultForProject(project);
+    assertThat(createChange(project).getChange().change().isWorkInProgress()).isTrue();
   }
 
   @Test
   public void pushWithWorkInProgressByDefaultForUserEnabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
     setWorkInProgressByDefaultForUser();
-    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+    assertThat(createChange(project).getChange().change().isWorkInProgress()).isTrue();
   }
 
   @Test
   public void pushBypassWorkInProgressByDefaultForProjectEnabled() throws Exception {
-    setWorkInProgressByDefaultForProject(project2);
+    Project.NameKey project = projectOperations.newProject().create();
+    setWorkInProgressByDefaultForProject(project);
     assertThat(
-            createChange(project2, "refs/for/master%ready").getChange().change().isWorkInProgress())
+            createChange(project, "refs/for/master%ready").getChange().change().isWorkInProgress())
         .isFalse();
   }
 
   @Test
   public void pushBypassWorkInProgressByDefaultForUserEnabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
     setWorkInProgressByDefaultForUser();
     assertThat(
-            createChange(project2, "refs/for/master%ready").getChange().change().isWorkInProgress())
+            createChange(project, "refs/for/master%ready").getChange().change().isWorkInProgress())
         .isFalse();
   }
 
   @Test
   public void pushWithWorkInProgressByDefaultForProjectDisabled() throws Exception {
-    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isFalse();
+    Project.NameKey project = projectOperations.newProject().create();
+    assertThat(createChange(project).getChange().change().isWorkInProgress()).isFalse();
   }
 
   @Test
   public void pushWorkInProgressByDefaultForProjectInherited() throws Exception {
-    setWorkInProgressByDefaultForProject(project1);
-    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+    Project.NameKey parentProject = projectOperations.newProject().create();
+    Project.NameKey childProject = projectOperations.newProject().parent(parentProject).create();
+    setWorkInProgressByDefaultForProject(parentProject);
+    assertThat(createChange(childProject).getChange().change().isWorkInProgress()).isTrue();
+  }
+
+  @Test
+  public void pushNewPatchSetWithWorkInProgressByDefaultForUserEnabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
+
+    // Create change.
+    TestRepository<InMemoryRepository> testRepo = cloneProject(project);
+    PushOneCommit.Result result =
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
+    result.assertOkStatus();
+
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
+
+    setWorkInProgressByDefaultForUser();
+
+    // Create new patch set on existing change, this shouldn't mark the change as WIP.
+    result = pushFactory.create(admin.newIdent(), testRepo, changeId).to("refs/for/master");
+    result.assertOkStatus();
+    assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
+  }
+
+  @Test
+  public void pushNewPatchSetAndNewChangeAtOnceWithWorkInProgressByDefaultForUserEnabled()
+      throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
+
+    // Create change.
+    TestRepository<InMemoryRepository> testRepo = cloneProject(project);
+    RevCommit initialHead = getHead(testRepo.getRepository(), "HEAD");
+    RevCommit commit1a =
+        testRepo.commit().parent(initialHead).message("Change 1").insertChangeId().create();
+    String changeId1 = GitUtil.getChangeId(testRepo, commit1a).get();
+    testRepo.reset(commit1a);
+    PushResult result = pushHead(testRepo, "refs/for/master", false);
+    assertPushOk(result, "refs/for/master");
+    assertThat(gApi.changes().id(changeId1).get().workInProgress).isNull();
+
+    setWorkInProgressByDefaultForUser();
+
+    // Clone the repo again. The test connection keeps an AccountState internally, so we need to
+    // create a new connection after changing account properties.
+    PatchSet.Id ps1OfChange1 =
+        PatchSet.id(Change.id(gApi.changes().id(changeId1).get()._number), 1);
+    testRepo = cloneProject(project);
+    testRepo.git().fetch().setRefSpecs(RefNames.patchSetRef(ps1OfChange1) + ":c1").call();
+    testRepo.reset("c1");
+
+    // Create a new patch set on the existing change and in the same push create a new successor
+    // change.
+    RevCommit commit1b = testRepo.amend(commit1a).create();
+    testRepo.reset(commit1b);
+    RevCommit commit2 =
+        testRepo.commit().parent(commit1b).message("Change 2").insertChangeId().create();
+    String changeId2 = GitUtil.getChangeId(testRepo, commit2).get();
+    testRepo.reset(commit2);
+    result = pushHead(testRepo, "refs/for/master", false);
+    assertPushOk(result, "refs/for/master");
+
+    // Check that the existing change (changeId1) is not marked as WIP, but only the newly created
+    // change (changeId2).
+    assertThat(gApi.changes().id(changeId1).get().workInProgress).isNull();
+    assertThat(gApi.changes().id(changeId2).get().workInProgress).isTrue();
   }
 
   private void setWorkInProgressByDefaultForProject(Project.NameKey p) throws Exception {
@@ -145,9 +212,12 @@
   }
 
   private void setWorkInProgressByDefaultForUser() throws Exception {
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
+    GeneralPreferencesInfo prefs = new GeneralPreferencesInfo();
     prefs.workInProgressByDefault = true;
     gApi.accounts().id(admin.id().get()).setPreferences(prefs);
+    // Generate a new API scope. User preferences are stored in IdentifiedUser, so we need to flush
+    // that entity.
+    requestScopeOperations.resetCurrentApiUser();
   }
 
   private PushOneCommit.Result createChange(Project.NameKey p) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java b/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
index 7ef915b..daeb032 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
@@ -15,19 +15,24 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.restapi.config.PostCaches.Operation.FLUSH;
 import static com.google.gerrit.server.restapi.config.PostCaches.Operation.FLUSH_ALL;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.server.restapi.config.ListCaches.CacheInfo;
 import com.google.gerrit.server.restapi.config.PostCaches;
+import com.google.inject.Inject;
 import java.util.Arrays;
 import org.junit.Test;
 
 public class CacheOperationsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void flushAll() throws Exception {
@@ -124,8 +129,11 @@
 
   @Test
   public void flushWebSessions_Forbidden() throws Exception {
-    allowGlobalCapabilities(
-        REGISTERED_USERS, GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.FLUSH_CACHES).group(REGISTERED_USERS))
+        .add(allowCapability(GlobalCapability.VIEW_CACHES).group(REGISTERED_USERS))
+        .update();
     try {
       RestResponse r =
           userRestSession.post(
@@ -138,8 +146,11 @@
               "/config/server/caches/", new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")))
           .assertForbidden();
     } finally {
-      removeGlobalCapabilities(
-          REGISTERED_USERS, GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES);
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(capabilityKey(GlobalCapability.FLUSH_CACHES).group(REGISTERED_USERS))
+          .remove(capabilityKey(GlobalCapability.VIEW_CACHES).group(REGISTERED_USERS))
+          .update();
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index caecefa..a161ec4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -15,15 +15,20 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.server.restapi.config.ListCaches.CacheInfo;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 public class FlushCacheIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void flushCache() throws Exception {
@@ -65,8 +70,11 @@
 
   @Test
   public void flushWebSessionsCache_Forbidden() throws Exception {
-    allowGlobalCapabilities(
-        REGISTERED_USERS, GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.FLUSH_CACHES).group(REGISTERED_USERS))
+        .add(allowCapability(GlobalCapability.VIEW_CACHES).group(REGISTERED_USERS))
+        .update();
     try {
       RestResponse r = userRestSession.post("/config/server/caches/accounts/flush");
       r.assertOK();
@@ -74,8 +82,11 @@
 
       userRestSession.post("/config/server/caches/web_sessions/flush").assertForbidden();
     } finally {
-      removeGlobalCapabilities(
-          REGISTERED_USERS, GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(capabilityKey(GlobalCapability.FLUSH_CACHES).group(REGISTERED_USERS))
+          .remove(capabilityKey(GlobalCapability.VIEW_CACHES).group(REGISTERED_USERS))
+          .update();
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/IndexChangesIT.java b/javatests/com/google/gerrit/acceptance/rest/config/IndexChangesIT.java
index fa35d19..a3c1722 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/IndexChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/IndexChangesIT.java
@@ -15,84 +15,90 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.ChangeIndexedCounter;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.events.ChangeIndexedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.server.restapi.config.IndexChanges;
 import com.google.inject.Inject;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 public class IndexChangesIT extends AbstractDaemonTest {
 
-  @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
-
-  private ChangeIndexedCounter changeIndexedCounter;
-  private RegistrationHandle changeIndexedCounterHandle;
-
-  @Before
-  public void addChangeIndexedCounter() {
-    changeIndexedCounter = new ChangeIndexedCounter();
-    changeIndexedCounterHandle = changeIndexedListeners.add("gerrit", changeIndexedCounter);
-  }
-
-  @After
-  public void removeChangeIndexedCounter() {
-    if (changeIndexedCounterHandle != null) {
-      changeIndexedCounterHandle.remove();
-    }
-  }
+  @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   @Test
   public void indexRequestFromNonAdminRejected() throws Exception {
-    String changeId = createChange().getChangeId();
-    IndexChanges.Input in = new IndexChanges.Input();
-    in.changes = ImmutableSet.of(changeId);
-    changeIndexedCounter.clear();
-    userRestSession.post("/config/server/index.changes", in).assertForbidden();
-    assertThat(changeIndexedCounter.getCount(info(changeId))).isEqualTo(0);
+    ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedCounter)) {
+      String changeId = createChange().getChangeId();
+      IndexChanges.Input in = new IndexChanges.Input();
+      in.changes = ImmutableSet.of(changeId);
+      changeIndexedCounter.clear();
+      userRestSession.post("/config/server/index.changes", in).assertForbidden();
+      assertThat(changeIndexedCounter.getCount(info(changeId))).isEqualTo(0);
+    }
   }
 
   @Test
   public void indexVisibleChange() throws Exception {
-    String changeId = createChange().getChangeId();
-    IndexChanges.Input in = new IndexChanges.Input();
-    in.changes = ImmutableSet.of(changeId);
-    changeIndexedCounter.clear();
-    adminRestSession.post("/config/server/index.changes", in).assertOK();
-    assertThat(changeIndexedCounter.getCount(info(changeId))).isEqualTo(1);
+    ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedCounter)) {
+      String changeId = createChange().getChangeId();
+      IndexChanges.Input in = new IndexChanges.Input();
+      in.changes = ImmutableSet.of(changeId);
+      changeIndexedCounter.clear();
+      adminRestSession.post("/config/server/index.changes", in).assertOK();
+      assertThat(changeIndexedCounter.getCount(info(changeId))).isEqualTo(1);
+    }
   }
 
   @Test
   public void indexNonVisibleChange() throws Exception {
-    String changeId = createChange().getChangeId();
-    ChangeInfo changeInfo = info(changeId);
-    blockRead("refs/heads/master");
-    IndexChanges.Input in = new IndexChanges.Input();
-    changeIndexedCounter.clear();
-    in.changes = ImmutableSet.of(changeId);
-    adminRestSession.post("/config/server/index.changes", in).assertOK();
-    assertThat(changeIndexedCounter.getCount(changeInfo)).isEqualTo(1);
+    ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedCounter)) {
+      String changeId = createChange().getChangeId();
+      ChangeInfo changeInfo = info(changeId);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+          .update();
+      IndexChanges.Input in = new IndexChanges.Input();
+      changeIndexedCounter.clear();
+      in.changes = ImmutableSet.of(changeId);
+      adminRestSession.post("/config/server/index.changes", in).assertOK();
+      assertThat(changeIndexedCounter.getCount(changeInfo)).isEqualTo(1);
+    }
   }
 
   @Test
   public void indexMultipleChanges() throws Exception {
-    ImmutableSet.Builder<String> changeIds = ImmutableSet.builder();
-    for (int i = 0; i < 10; i++) {
-      changeIds.add(createChange().getChangeId());
-    }
-    IndexChanges.Input in = new IndexChanges.Input();
-    in.changes = changeIds.build();
-    changeIndexedCounter.clear();
-    adminRestSession.post("/config/server/index.changes", in).assertOK();
-    for (String changeId : in.changes) {
-      assertThat(changeIndexedCounter.getCount(info(changeId))).isEqualTo(1);
+    ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedCounter)) {
+      ImmutableSet.Builder<String> changeIds = ImmutableSet.builder();
+      for (int i = 0; i < 10; i++) {
+        changeIds.add(createChange().getChangeId());
+      }
+      IndexChanges.Input in = new IndexChanges.Input();
+      in.changes = changeIds.build();
+      changeIndexedCounter.clear();
+      adminRestSession.post("/config/server/index.changes", in).assertOK();
+      for (String changeId : in.changes) {
+        assertThat(changeIndexedCounter.getCount(info(changeId))).isEqualTo(1);
+      }
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 4a74018..1d87ca1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -168,6 +168,8 @@
     assertThat(i.change.replyLabel).isEqualTo("Reply\u2026");
     assertThat(i.change.updateDelay).isEqualTo(300);
     assertThat(i.change.disablePrivateChanges).isNull();
+    assertThat(i.change.submitWholeTopic).isNull();
+    assertThat(i.change.excludeMergeableInChangeInfo).isNull();
 
     // download
     assertThat(i.download.archives).containsExactly("tar", "tbz2", "tgz", "txz");
@@ -190,4 +192,18 @@
     // user
     assertThat(i.user.anonymousCowardName).isEqualTo(AnonymousCowardNameProvider.DEFAULT);
   }
+
+  @Test
+  @GerritConfig(name = "change.submitWholeTopic", value = "true")
+  public void serverConfigWithSubmitWholeTopic() throws Exception {
+    ServerInfo i = gApi.config().server().getInfo();
+    assertThat(i.change.submitWholeTopic).isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "change.api.excludeMergeableInChangeInfo", value = "true")
+  public void serverConfigWithExcludeMergeableInChangeInfo() throws Exception {
+    ServerInfo i = gApi.config().server().getInfo();
+    assertThat(i.change.excludeMergeableInChangeInfo).isTrue();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
index 35812fd..9276b9a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
@@ -14,21 +14,25 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.GitUtil.createAnnotatedTag;
 import static com.google.gerrit.acceptance.GitUtil.deleteRef;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.GitUtil.updateAnnotatedTag;
 import static com.google.gerrit.acceptance.rest.project.AbstractPushTag.TagType.ANNOTATED;
 import static com.google.gerrit.acceptance.rest.project.AbstractPushTag.TagType.LIGHTWEIGHT;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -57,12 +61,14 @@
     return config;
   }
 
+  @Inject private ProjectOperations projectOperations;
+
   private RevCommit initialHead;
   private TagType tagType;
 
   @Before
   public void setUpTestEnvironment() throws Exception {
-    initialHead = getRemoteHead();
+    initialHead = projectOperations.project(project).getHead("master");
     tagType = getTagType();
     blockAnonymousRead();
   }
@@ -235,7 +241,11 @@
     }
 
     if (!newCommit) {
-      grant(project, "refs/for/refs/heads/master", Permission.SUBMIT, false, REGISTERED_USERS);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(REGISTERED_USERS))
+          .update();
       pushHead(testRepo, "refs/for/master%submit");
     }
 
@@ -245,7 +255,7 @@
             ? pushHead(testRepo, tagRef, false, force)
             : GitUtil.pushTag(testRepo, tagName, !createTag);
     RemoteRefUpdate refUpdate = r.getRemoteUpdate(tagRef);
-    assertThat(refUpdate.getStatus()).named(tagType.name()).isEqualTo(expectedStatus);
+    assertWithMessage(tagType.name()).that(refUpdate.getStatus()).isEqualTo(expectedStatus);
     return tagName;
   }
 
@@ -253,16 +263,28 @@
     String tagRef = tagRef(tagName);
     PushResult r = deleteRef(testRepo, tagRef);
     RemoteRefUpdate refUpdate = r.getRemoteUpdate(tagRef);
-    assertThat(refUpdate.getStatus()).named(tagType.name()).isEqualTo(expectedStatus);
+    assertWithMessage(tagType.name()).that(refUpdate.getStatus()).isEqualTo(expectedStatus);
   }
 
-  private void removeReadAccessOnRefsStar() throws Exception {
-    removePermission(allProjects, "refs/heads/*", Permission.READ);
-    removePermission(project, "refs/heads/*", Permission.READ);
+  private void removeReadAccessOnRefsStar() {
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .remove(permissionKey(Permission.READ).ref("refs/*"))
+        .update();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(permissionKey(Permission.READ).ref("refs/*"))
+        .update();
   }
 
-  private void grantReadAccessOnRefsHeadsStar() throws Exception {
-    grant(project, "refs/heads/*", Permission.READ, false, REGISTERED_USERS);
+  private void grantReadAccessOnRefsHeadsStar() {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
   }
 
   private void allowReadingAllTag() throws Exception {
@@ -280,30 +302,54 @@
     // visible since by default READ access to it is exclusively granted to the project owners only.
     // This means to make all refs, and thus all tags, visible, we must allow registered users to
     // see the refs/meta/config branch.
-    allow(project, "refs/meta/config", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS))
+        .update();
   }
 
   private void allowTagCreation() throws Exception {
-    grant(project, "refs/tags/*", tagType.createPermission, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(tagType.createPermission).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
   }
 
   private void allowPushOnRefsTags() throws Exception {
     removePushFromRefsTags();
-    grant(project, "refs/tags/*", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
   }
 
   private void allowForcePushOnRefsTags() throws Exception {
     removePushFromRefsTags();
-    grant(project, "refs/tags/*", Permission.PUSH, true, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/tags/*").group(REGISTERED_USERS).force(true))
+        .update();
   }
 
   private void allowTagDeletion() throws Exception {
     removePushFromRefsTags();
-    grant(project, "refs/tags/*", Permission.DELETE, true, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE).ref("refs/tags/*").group(REGISTERED_USERS).force(true))
+        .update();
   }
 
   private void removePushFromRefsTags() throws Exception {
-    removePermission(project, "refs/tags/*", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(permissionKey(Permission.PUSH).ref("refs/tags/*"))
+        .update();
   }
 
   private void commit(PersonIdent ident, String subject) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index b199e32..eb9a81a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -15,13 +15,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.util.Arrays.asList;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -30,26 +34,23 @@
 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.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
 import com.google.gerrit.extensions.api.access.PermissionInfo;
 import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.webui.FileHistoryWebLink;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectConfig;
@@ -75,9 +76,9 @@
 
   private static final String LABEL_CODE_REVIEW = "Code-Review";
 
-  @Inject private DynamicSet<FileHistoryWebLink> fileHistoryWebLinkDynamicSet;
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   private Project.NameKey newProjectName;
 
@@ -92,45 +93,45 @@
     assertThat(inheritedName).isEqualTo(AllProjectsNameProvider.DEFAULT);
   }
 
+  private Registration newFileHistoryWebLink() {
+    FileHistoryWebLink weblink =
+        new FileHistoryWebLink() {
+          @Override
+          public WebLinkInfo getFileHistoryWebLink(
+              String projectName, String revision, String fileName) {
+            return new WebLinkInfo(
+                "name", "imageURL", "http://view/" + projectName + "/" + fileName);
+          }
+        };
+    return extensionRegistry.newRegistration().add(weblink);
+  }
+
   @Test
   public void webLink() throws Exception {
-    RegistrationHandle handle =
-        fileHistoryWebLinkDynamicSet.add(
-            "gerrit",
-            (projectName, revision, fileName) ->
-                new WebLinkInfo("name", "imageURL", "http://view/" + projectName + "/" + fileName));
-    try {
+    try (Registration registration = newFileHistoryWebLink()) {
       ProjectAccessInfo info = pApi().access();
       assertThat(info.configWebLinks).hasSize(1);
       assertThat(info.configWebLinks.get(0).url)
           .isEqualTo("http://view/" + newProjectName + "/project.config");
-    } finally {
-      handle.remove();
     }
   }
 
   @Test
   public void webLinkNoRefsMetaConfig() throws Exception {
-    RegistrationHandle handle =
-        fileHistoryWebLinkDynamicSet.add(
-            "gerrit",
-            (projectName, revision, fileName) ->
-                new WebLinkInfo("name", "imageURL", "http://view/" + projectName + "/" + fileName));
-    try (Repository repo = repoManager.openRepository(newProjectName)) {
+    try (Repository repo = repoManager.openRepository(newProjectName);
+        Registration registration = newFileHistoryWebLink()) {
       RefUpdate u = repo.updateRef(RefNames.REFS_CONFIG);
       u.setForceUpdate(true);
       assertThat(u.delete()).isEqualTo(Result.FORCED);
 
       // This should not crash.
       pApi().access();
-    } finally {
-      handle.remove();
     }
   }
 
   @Test
   public void addAccessSection() throws Exception {
-    RevCommit initialHead = getRemoteHead(newProjectName, RefNames.REFS_CONFIG);
+    RevCommit initialHead = projectOperations.project(newProjectName).getHead(RefNames.REFS_CONFIG);
 
     ProjectAccessInput accessInput = newProjectAccessInput();
     AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
@@ -140,7 +141,7 @@
 
     assertThat(pApi().access().local).isEqualTo(accessInput.add);
 
-    RevCommit updatedHead = getRemoteHead(newProjectName, RefNames.REFS_CONFIG);
+    RevCommit updatedHead = projectOperations.project(newProjectName).getHead(RefNames.REFS_CONFIG);
     eventRecorder.assertRefUpdatedEvents(
         newProjectName.get(), RefNames.REFS_CONFIG, null, initialHead, initialHead, updatedHead);
   }
@@ -148,8 +149,7 @@
   @Test
   public void createAccessChangeNop() throws Exception {
     ProjectAccessInput accessInput = newProjectAccessInput();
-    exception.expect(BadRequestException.class);
-    pApi().accessChange(accessInput);
+    assertThrows(BadRequestException.class, () -> pApi().accessChange(accessInput));
   }
 
   @Test
@@ -174,7 +174,11 @@
 
   @Test
   public void createAccessChange() throws Exception {
-    allow(newProjectName, RefNames.REFS_CONFIG, Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(newProjectName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+        .update();
     // User can see the branch
     requestScopeOperations.setApiUser(user.id());
     pApi().branch("refs/heads/master").get();
@@ -211,12 +215,7 @@
 
     // check that the change took effect.
     requestScopeOperations.setApiUser(user.id());
-    try {
-      BranchInfo info = pApi().branch("refs/heads/master").get();
-      fail("wanted failure, got " + newGson().toJson(info));
-    } catch (ResourceNotFoundException e) {
-      // OK.
-    }
+    assertThrows(ResourceNotFoundException.class, () -> pApi().branch("refs/heads/master").get());
 
     // Restore.
     accessInput.add.clear();
@@ -331,8 +330,7 @@
     pApi().access(accessInput);
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(ResourceNotFoundException.class);
-    pApi().access();
+    assertThrows(ResourceNotFoundException.class, () -> pApi().access());
   }
 
   @Test
@@ -351,8 +349,7 @@
     accessInfoToApply.add.put(REFS_HEADS, accessSectionInfoToApply);
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(ResourceNotFoundException.class);
-    pApi().access();
+    assertThrows(ResourceNotFoundException.class, () -> pApi().access());
   }
 
   @Test
@@ -414,9 +411,8 @@
     accessInput.parent = newParentProjectName;
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    exception.expectMessage("administrate server not permitted");
-    pApi().access(accessInput);
+    AuthException thrown = assertThrows(AuthException.class, () -> pApi().access(accessInput));
+    assertThat(thrown).hasMessageThat().contains("administrate server not permitted");
   }
 
   @Test
@@ -441,8 +437,8 @@
     accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.projects().name(allProjects.get()).access(accessInput);
+    assertThrows(
+        AuthException.class, () -> gApi.projects().name(allProjects.get()).access(accessInput));
   }
 
   @Test
@@ -470,8 +466,7 @@
 
     accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
-    exception.expect(BadRequestException.class);
-    pApi().access(accessInput);
+    assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
   }
 
   @Test
@@ -484,9 +479,9 @@
     accessSectionInfo.permissions.put(Permission.PUSH, permissionInfo);
 
     accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
-
-    exception.expect(BadRequestException.class);
-    gApi.projects().name(allProjects.get()).access(accessInput);
+    assertThrows(
+        BadRequestException.class,
+        () -> gApi.projects().name(allProjects.get()).access(accessInput));
   }
 
   @Test
@@ -497,8 +492,8 @@
     accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.projects().name(allProjects.get()).access(accessInput);
+    assertThrows(
+        AuthException.class, () -> gApi.projects().name(allProjects.get()).access(accessInput));
   }
 
   @Test
@@ -577,7 +572,7 @@
             .file(ProjectConfig.PROJECT_CONFIG)
             .asString();
     cfg.fromText(config);
-    assertThat(cfg.getString(access, refsFor, unknownPermission)).isEqualTo(registeredUsers);
+    assertThat(cfg).stringValue(access, refsFor, unknownPermission).isEqualTo(registeredUsers);
 
     // Make permission change through API
     ProjectAccessInput accessInput = newProjectAccessInput();
@@ -596,16 +591,20 @@
             .file(ProjectConfig.PROJECT_CONFIG)
             .asString();
     cfg.fromText(config);
-    assertThat(cfg.getString(access, refsFor, unknownPermission)).isEqualTo(registeredUsers);
+    assertThat(cfg).stringValue(access, refsFor, unknownPermission).isEqualTo(registeredUsers);
   }
 
   @Test
   public void allUsersCanOnlyInheritFromAllProjects() throws Exception {
     ProjectAccessInput accessInput = newProjectAccessInput();
     accessInput.parent = project.get();
-    exception.expect(BadRequestException.class);
-    exception.expectMessage(allUsers.get() + " must inherit from " + allProjects.get());
-    gApi.projects().name(allUsers.get()).access(accessInput);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(allUsers.get()).access(accessInput));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(allUsers.get() + " must inherit from " + allProjects.get());
   }
 
   @Test
@@ -655,9 +654,9 @@
     String invalidRef = Constants.R_HEADS + "stable_*";
     accessInput.add.put(invalidRef, accessSectionInfo);
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("Invalid Name: " + invalidRef);
-    pApi().access(accessInput);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
+    assertThat(thrown).hasMessageThat().contains("Invalid Name: " + invalidRef);
   }
 
   @Test
@@ -669,9 +668,9 @@
     String invalidRef = Constants.R_HEADS + "stable_*";
     accessInput.add.put(invalidRef, accessSectionInfo);
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("Invalid Name: " + invalidRef);
-    pApi().accessChange(accessInput);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> pApi().accessChange(accessInput));
+    assertThat(thrown).hasMessageThat().contains("Invalid Name: " + invalidRef);
   }
 
   private ProjectApi pApi() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/BUILD b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
index 6e66aab..ca187a3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
@@ -1,9 +1,9 @@
 load("@rules_java//java:defs.bzl", "java_library")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
-acceptance_tests(
-    srcs = glob(["*IT.java"]),
-    group = "rest_project",
+[acceptance_tests(
+    srcs = [f],
+    group = f[:f.index(".")],
     labels = ["rest"],
     deps = [
         ":project",
@@ -11,7 +11,7 @@
         ":refassert",
         "//lib/commons:lang",
     ],
-)
+) for f in glob(["*IT.java"])]
 
 java_library(
     name = "refassert",
@@ -31,8 +31,8 @@
         "ProjectAssert.java",
     ],
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:guava",
         "//lib/truth",
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java
index 7667fc0..a2f976a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java
@@ -20,12 +20,14 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.common.MergeableInfo;
-import com.google.gerrit.reviewdb.client.Branch;
+import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.RefSpec;
@@ -34,20 +36,19 @@
 
 public class CheckMergeabilityIT extends AbstractDaemonTest {
 
-  private Branch.NameKey branch;
+  @Inject private ProjectOperations projectOperations;
+
+  private BranchNameKey branch;
 
   @Before
   public void setUp() throws Exception {
-    branch = new Branch.NameKey(project, "test");
-    gApi.projects()
-        .name(branch.getParentKey().get())
-        .branch(branch.get())
-        .create(new BranchInput());
+    branch = BranchNameKey.create(project, "test");
+    gApi.projects().name(branch.project().get()).branch(branch.branch()).create(new BranchInput());
   }
 
   @Test
   public void checkMergeableCommit() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     testRepo
         .branch("HEAD")
         .commit()
@@ -82,7 +83,7 @@
 
   @Test
   public void checkUnMergeableCommit() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     testRepo
         .branch("HEAD")
         .commit()
@@ -117,7 +118,7 @@
 
   @Test
   public void checkOursMergeStrategy() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     testRepo
         .branch("HEAD")
         .commit()
@@ -211,7 +212,7 @@
     cherry.current().review(ReviewInput.approve());
     cherry.current().submit();
 
-    ObjectId remoteId = getRemoteHead();
+    ObjectId remoteId = projectOperations.project(project).getHead("master");
     assertThat(remoteId).isNotEqualTo(commitId);
     assertContentMerged("master", commitId.getName(), "recursive");
   }
@@ -237,7 +238,7 @@
 
   @Test
   public void checkInvalidStrategy() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     testRepo
         .branch("HEAD")
         .commit()
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 698f7e0..85d383e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -16,14 +16,22 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_HEADS;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.entities.RefNames.REFS_HEADS;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+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.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -31,23 +39,20 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.inject.Inject;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
 
 public class CreateBranchIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  private Branch.NameKey testBranch;
+  private BranchNameKey testBranch;
 
   @Before
   public void setUp() throws Exception {
-    testBranch = new Branch.NameKey(project, "test");
+    testBranch = BranchNameKey.create(project, "test");
   }
 
   @Test
@@ -104,17 +109,25 @@
   @Test
   public void createMetaBranch() throws Exception {
     String metaRef = RefNames.REFS_META + "foo";
-    allow(metaRef, Permission.CREATE, REGISTERED_USERS);
-    allow(metaRef, Permission.PUSH, REGISTERED_USERS);
-    assertCreateSucceeds(new Branch.NameKey(project, metaRef));
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(metaRef).group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(metaRef).group(REGISTERED_USERS))
+        .update();
+    assertCreateSucceeds(BranchNameKey.create(project, metaRef));
   }
 
   @Test
   public void createUserBranch_Conflict() throws Exception {
-    allow(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE, REGISTERED_USERS);
-    allow(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(REGISTERED_USERS))
+        .update();
     assertCreateFails(
-        new Branch.NameKey(allUsers, RefNames.refsUsers(new Account.Id(1))),
+        BranchNameKey.create(allUsers, RefNames.refsUsers(Account.id(1))),
         RefNames.refsUsers(admin.id()),
         ResourceConflictException.class,
         "Not allowed to create user branch.");
@@ -122,10 +135,14 @@
 
   @Test
   public void createGroupBranch_Conflict() throws Exception {
-    allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE, REGISTERED_USERS);
-    allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
     assertCreateFails(
-        new Branch.NameKey(allUsers, RefNames.refsGroups(new AccountGroup.UUID("foo"))),
+        BranchNameKey.create(allUsers, RefNames.refsGroups(AccountGroup.uuid("foo"))),
         RefNames.refsGroups(adminGroupUuid()),
         ResourceConflictException.class,
         "Not allowed to create group branch.");
@@ -133,81 +150,85 @@
 
   @Test
   public void createWithRevision() throws Exception {
-    RevCommit revision = getRemoteHead(project, "master");
+    RevCommit revision = projectOperations.project(project).getHead("master");
 
     // update master so that points to a different revision than the revision on which we create the
     // new branch
     pushTo("refs/heads/master");
-    assertThat(getRemoteHead(project, "master")).isNotEqualTo(revision);
+    assertThat(projectOperations.project(project).getHead("master")).isNotEqualTo(revision);
 
     BranchInput input = new BranchInput();
     input.revision = revision.name();
     BranchInfo created = branch(testBranch).create(input).get();
-    assertThat(created.ref).isEqualTo(testBranch.get());
+    assertThat(created.ref).isEqualTo(testBranch.branch());
     assertThat(created.revision).isEqualTo(revision.name());
-    assertThat(getRemoteHead(project, testBranch.getShortName())).isEqualTo(revision);
+    assertThat(projectOperations.project(project).getHead(testBranch.branch())).isEqualTo(revision);
   }
 
   @Test
   public void createWithoutSpecifyingRevision() throws Exception {
     // If revision is not specified, the branch is created based on HEAD, which points to master.
-    RevCommit expectedRevision = getRemoteHead(project, "master");
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
 
     BranchInput input = new BranchInput();
     input.revision = null;
     BranchInfo created = branch(testBranch).create(input).get();
-    assertThat(created.ref).isEqualTo(testBranch.get());
+    assertThat(created.ref).isEqualTo(testBranch.branch());
     assertThat(created.revision).isEqualTo(expectedRevision.name());
-    assertThat(getRemoteHead(project, testBranch.getShortName())).isEqualTo(expectedRevision);
+    assertThat(projectOperations.project(project).getHead(testBranch.branch()))
+        .isEqualTo(expectedRevision);
   }
 
   @Test
   public void createWithEmptyRevision() throws Exception {
     // If revision is not specified, the branch is created based on HEAD, which points to master.
-    RevCommit expectedRevision = getRemoteHead(project, "master");
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
 
     BranchInput input = new BranchInput();
     input.revision = "";
     BranchInfo created = branch(testBranch).create(input).get();
-    assertThat(created.ref).isEqualTo(testBranch.get());
+    assertThat(created.ref).isEqualTo(testBranch.branch());
     assertThat(created.revision).isEqualTo(expectedRevision.name());
-    assertThat(getRemoteHead(project, testBranch.getShortName())).isEqualTo(expectedRevision);
+    assertThat(projectOperations.project(project).getHead(testBranch.branch()))
+        .isEqualTo(expectedRevision);
   }
 
   @Test
   public void createRevisionIsTrimmed() throws Exception {
-    RevCommit revision = getRemoteHead(project, "master");
+    RevCommit revision = projectOperations.project(project).getHead("master");
 
     BranchInput input = new BranchInput();
     input.revision = "\t" + revision.name();
     BranchInfo created = branch(testBranch).create(input).get();
-    assertThat(created.ref).isEqualTo(testBranch.get());
+    assertThat(created.ref).isEqualTo(testBranch.branch());
     assertThat(created.revision).isEqualTo(revision.name());
-    assertThat(getRemoteHead(project, testBranch.getShortName())).isEqualTo(revision);
+    assertThat(projectOperations.project(project).getHead(testBranch.branch())).isEqualTo(revision);
   }
 
   @Test
   public void createWithBranchNameAsRevision() throws Exception {
-    RevCommit expectedRevision = getRemoteHead(project, "master");
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
 
     BranchInput input = new BranchInput();
     input.revision = "master";
     BranchInfo created = branch(testBranch).create(input).get();
-    assertThat(created.ref).isEqualTo(testBranch.get());
+    assertThat(created.ref).isEqualTo(testBranch.branch());
     assertThat(created.revision).isEqualTo(expectedRevision.name());
-    assertThat(getRemoteHead(project, testBranch.getShortName())).isEqualTo(expectedRevision);
+    assertThat(projectOperations.project(project).getHead(testBranch.branch()))
+        .isEqualTo(expectedRevision);
   }
 
   @Test
   public void createWithFullBranchNameAsRevision() throws Exception {
-    RevCommit expectedRevision = getRemoteHead(project, "master");
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
 
     BranchInput input = new BranchInput();
     input.revision = "refs/heads/master";
     BranchInfo created = branch(testBranch).create(input).get();
-    assertThat(created.ref).isEqualTo(testBranch.get());
+    assertThat(created.ref).isEqualTo(testBranch.branch());
     assertThat(created.revision).isEqualTo(expectedRevision.name());
-    assertThat(getRemoteHead(project, testBranch.getShortName())).isEqualTo(expectedRevision);
+    assertThat(projectOperations.project(project).getHead(testBranch.branch()))
+        .isEqualTo(expectedRevision);
   }
 
   @Test
@@ -238,44 +259,51 @@
   }
 
   private void blockCreateReference() throws Exception {
-    block("refs/*", Permission.CREATE, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.CREATE).ref("refs/*").group(ANONYMOUS_USERS))
+        .update();
   }
 
   private void grantOwner() throws Exception {
-    allow("refs/*", Permission.OWNER, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
   }
 
-  private BranchApi branch(Branch.NameKey branch) throws Exception {
-    return gApi.projects().name(branch.getParentKey().get()).branch(branch.get());
+  private BranchApi branch(BranchNameKey branch) throws Exception {
+    return gApi.projects().name(branch.project().get()).branch(branch.branch());
   }
 
-  private void assertCreateSucceeds(Branch.NameKey branch) throws Exception {
+  private void assertCreateSucceeds(BranchNameKey branch) throws Exception {
     BranchInfo created = branch(branch).create(new BranchInput()).get();
-    assertThat(created.ref).isEqualTo(branch.get());
+    assertThat(created.ref).isEqualTo(branch.branch());
   }
 
   private void assertCreateFails(
-      Branch.NameKey branch, Class<? extends RestApiException> errType, String errMsg)
+      BranchNameKey branch, Class<? extends RestApiException> errType, String errMsg)
       throws Exception {
     assertCreateFails(branch, null, errType, errMsg);
   }
 
   private void assertCreateFails(
-      Branch.NameKey branch,
+      BranchNameKey branch,
       String revision,
       Class<? extends RestApiException> errType,
       String errMsg)
       throws Exception {
     BranchInput in = new BranchInput();
     in.revision = revision;
+    RestApiException thrown = assertThrows(errType, () -> branch(branch).create(in));
     if (errMsg != null) {
-      exception.expectMessage(errMsg);
+      assertThat(thrown).hasMessageThat().contains(errMsg);
     }
-    exception.expect(errType);
-    branch(branch).create(in);
   }
 
-  private void assertCreateFails(Branch.NameKey branch, Class<? extends RestApiException> errType)
+  private void assertCreateFails(BranchNameKey branch, Class<? extends RestApiException> errType)
       throws Exception {
     assertCreateFails(branch, errType, null);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 463532f..6b2baa7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -19,7 +19,10 @@
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectOwners;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.server.project.ProjectConfig.PROJECT_CONFIG;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableList;
@@ -30,8 +33,13 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -43,10 +51,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
@@ -72,6 +76,7 @@
 import org.junit.Test;
 
 public class CreateProjectIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -86,7 +91,7 @@
     // for more extensive coverage of the LabelTypeInfo.
     assertThat(p.labels).hasSize(1);
 
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    ProjectState projectState = projectCache.get(Project.nameKey(newProjectName));
     assertThat(projectState).isNotNull();
     assertProjectInfo(projectState.getProject(), p);
     assertHead(newProjectName, "refs/heads/master");
@@ -162,7 +167,7 @@
     String newProjectName = name("newProject");
     ProjectInfo p = gApi.projects().create(newProjectName).get();
     assertThat(p.name).isEqualTo(newProjectName);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    ProjectState projectState = projectCache.get(Project.nameKey(newProjectName));
     assertThat(projectState).isNotNull();
     assertProjectInfo(projectState.getProject(), p);
     assertHead(newProjectName, "refs/heads/master");
@@ -175,7 +180,7 @@
     String newProjectName = name("newProject");
     ProjectInfo p = gApi.projects().create(newProjectName + ".git").get();
     assertThat(p.name).isEqualTo(newProjectName);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    ProjectState projectState = projectCache.get(Project.nameKey(newProjectName));
     assertThat(projectState).isNotNull();
     assertProjectInfo(projectState.getProject(), p);
     assertHead(newProjectName, "refs/heads/master");
@@ -186,7 +191,7 @@
     String newProjectName = name("newProject");
     ProjectInfo p = gApi.projects().create(newProjectName + "/").get();
     assertThat(p.name).isEqualTo(newProjectName);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    ProjectState projectState = projectCache.get(Project.nameKey(newProjectName));
     assertThat(projectState).isNotNull();
     assertProjectInfo(projectState.getProject(), p);
     assertHead(newProjectName, "refs/heads/master");
@@ -197,7 +202,7 @@
     String newProjectName = name("newProject/newProject");
     ProjectInfo p = gApi.projects().create(newProjectName).get();
     assertThat(p.name).isEqualTo(newProjectName);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    ProjectState projectState = projectCache.get(Project.nameKey(newProjectName));
     assertThat(projectState).isNotNull();
     assertProjectInfo(projectState.getProject(), p);
     assertHead(newProjectName, "refs/heads/master");
@@ -216,7 +221,7 @@
     in.requireChangeId = InheritableBoolean.TRUE;
     ProjectInfo p = gApi.projects().create(in).get();
     assertThat(p.name).isEqualTo(newProjectName);
-    Project project = projectCache.get(new Project.NameKey(newProjectName)).getProject();
+    Project project = projectCache.get(Project.nameKey(newProjectName)).getProject();
     assertProjectInfo(project, p);
     assertThat(project.getDescription()).isEqualTo(in.description);
     assertThat(project.getConfiguredSubmitType()).isEqualTo(in.submitType);
@@ -242,7 +247,7 @@
     in.name = childName;
     in.parent = parentName;
     gApi.projects().create(in);
-    Project project = projectCache.get(new Project.NameKey(childName)).getProject();
+    Project project = projectCache.get(Project.nameKey(childName)).getProject();
     assertThat(project.getParentName()).isEqualTo(in.parent);
   }
 
@@ -265,12 +270,12 @@
     in.owners.add(
         Integer.toString(
             groupCache
-                .get(new AccountGroup.NameKey("Administrators"))
+                .get(AccountGroup.nameKey("Administrators"))
                 .orElse(null)
                 .getId()
                 .get())); // by ID
     gApi.projects().create(in);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    ProjectState projectState = projectCache.get(Project.nameKey(newProjectName));
     Set<AccountGroup.UUID> expectedOwnerIds = Sets.newHashSetWithExpectedSize(3);
     expectedOwnerIds.add(SystemGroupBackend.ANONYMOUS_USERS);
     expectedOwnerIds.add(SystemGroupBackend.REGISTERED_USERS);
@@ -323,7 +328,12 @@
 
   @Test
   public void createProjectWithCapability() throws Exception {
-    allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(
+            allowCapability(GlobalCapability.CREATE_PROJECT)
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
     try {
       requestScopeOperations.setApiUser(user.id());
       ProjectInput in = new ProjectInput();
@@ -331,8 +341,12 @@
       ProjectInfo p = gApi.projects().create(in).get();
       assertThat(p.name).isEqualTo(in.name);
     } finally {
-      removeGlobalCapabilities(
-          SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(
+              capabilityKey(GlobalCapability.CREATE_PROJECT)
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
     }
   }
 
@@ -355,7 +369,12 @@
   public void createProjectWithCreateProjectCapabilityAndParentNotVisible() throws Exception {
     Project parent = projectCache.get(allProjects).getProject();
     parent.setState(com.google.gerrit.extensions.client.ProjectState.HIDDEN);
-    allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(
+            allowCapability(GlobalCapability.CREATE_PROJECT)
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
     try {
       requestScopeOperations.setApiUser(user.id());
       ProjectInput in = new ProjectInput();
@@ -364,8 +383,12 @@
       assertThat(p.name).isEqualTo(in.name);
     } finally {
       parent.setState(com.google.gerrit.extensions.client.ProjectState.ACTIVE);
-      removeGlobalCapabilities(
-          SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(
+              capabilityKey(GlobalCapability.CREATE_PROJECT)
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
     }
   }
 
@@ -447,13 +470,13 @@
   }
 
   private void assertHead(String projectName, String expectedRef) throws Exception {
-    try (Repository repo = repoManager.openRepository(new Project.NameKey(projectName))) {
+    try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
       assertThat(repo.exactRef(Constants.HEAD).getTarget().getName()).isEqualTo(expectedRef);
     }
   }
 
   private void assertEmptyCommit(String projectName, String... refs) throws Exception {
-    Project.NameKey projectKey = new Project.NameKey(projectName);
+    Project.NameKey projectKey = Project.nameKey(projectName);
     try (Repository repo = repoManager.openRepository(projectKey);
         RevWalk rw = new RevWalk(repo);
         TreeWalk tw = new TreeWalk(rw.getObjectReader())) {
@@ -469,12 +492,11 @@
 
   private void assertCreateFails(ProjectInput in, Class<? extends RestApiException> errType)
       throws Exception {
-    exception.expect(errType);
-    gApi.projects().create(in);
+    assertThrows(errType, () -> gApi.projects().create(in));
   }
 
   private Optional<String> readProjectConfig(String projectName) throws Exception {
-    try (Repository repo = repoManager.openRepository(new Project.NameKey(projectName));
+    try (Repository repo = repoManager.openRepository(Project.nameKey(projectName));
         TestRepository<Repository> tr = new TestRepository<>(repo)) {
       RevWalk rw = tr.getRevWalk();
       Ref ref = repo.exactRef(RefNames.REFS_CONFIG);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index ccdc497..5636014 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -25,6 +27,8 @@
 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.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -32,8 +36,6 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
@@ -42,12 +44,12 @@
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  private Branch.NameKey testBranch;
+  private BranchNameKey testBranch;
 
   @Before
   public void setUp() throws Exception {
     project = projectOperations.newProject().create();
-    testBranch = new Branch.NameKey(project, "test");
+    testBranch = BranchNameKey.create(project, "test");
     branch(testBranch).create(new BranchInput());
   }
 
@@ -100,7 +102,7 @@
   @Test
   public void deleteBranchByRestWithoutRefsHeadsPrefix() throws Exception {
     grantDelete();
-    String ref = testBranch.getShortName();
+    String ref = testBranch.shortName();
     assertThat(ref).doesNotMatch(R_HEADS);
     assertDeleteByRestSucceeds(testBranch, ref);
   }
@@ -108,14 +110,14 @@
   @Test
   public void deleteBranchByRestWithFullName() throws Exception {
     grantDelete();
-    assertDeleteByRestSucceeds(testBranch, testBranch.get());
+    assertDeleteByRestSucceeds(testBranch, testBranch.branch());
   }
 
   @Test
   public void deleteBranchByRestFailsWithUnencodedFullName() throws Exception {
     grantDelete();
     RestResponse r =
-        userRestSession.delete("/projects/" + project.get() + "/branches/" + testBranch.get());
+        userRestSession.delete("/projects/" + project.get() + "/branches/" + testBranch.branch());
     r.assertNotFound();
     branch(testBranch).get();
   }
@@ -123,10 +125,14 @@
   @Test
   public void deleteMetaBranch() throws Exception {
     String metaRef = RefNames.REFS_META + "foo";
-    allow(metaRef, Permission.CREATE, REGISTERED_USERS);
-    allow(metaRef, Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(metaRef).group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(metaRef).group(REGISTERED_USERS))
+        .update();
 
-    Branch.NameKey metaBranch = new Branch.NameKey(project, metaRef);
+    BranchNameKey metaBranch = BranchNameKey.create(project, metaRef);
     branch(metaBranch).create(new BranchInput());
 
     grantDelete();
@@ -135,22 +141,36 @@
 
   @Test
   public void deleteUserBranch_Conflict() throws Exception {
-    allow(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE, REGISTERED_USERS);
-    allow(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(REGISTERED_USERS))
+        .update();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Not allowed to delete user branch.");
-    branch(new Branch.NameKey(allUsers, RefNames.refsUsers(admin.id()))).delete();
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> branch(BranchNameKey.create(allUsers, RefNames.refsUsers(admin.id()))).delete());
+    assertThat(thrown).hasMessageThat().contains("Not allowed to delete user branch.");
   }
 
   @Test
   public void deleteGroupBranch_Conflict() throws Exception {
-    allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE, REGISTERED_USERS);
-    allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
 
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Not allowed to delete group branch.");
-    branch(new Branch.NameKey(allUsers, RefNames.refsGroups(adminGroupUuid()))).delete();
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () ->
+                branch(BranchNameKey.create(allUsers, RefNames.refsGroups(adminGroupUuid())))
+                    .delete());
+    assertThat(thrown).hasMessageThat().contains("Not allowed to delete group branch.");
   }
 
   @Test
@@ -158,7 +178,7 @@
     MethodNotAllowedException thrown =
         assertThrows(
             MethodNotAllowedException.class,
-            () -> branch(new Branch.NameKey(allUsers, RefNames.REFS_CONFIG)).delete());
+            () -> branch(BranchNameKey.create(allUsers, RefNames.REFS_CONFIG)).delete());
     assertThat(thrown).hasMessageThat().contains("not allowed to delete branch refs/meta/config");
   }
 
@@ -167,31 +187,47 @@
     MethodNotAllowedException thrown =
         assertThrows(
             MethodNotAllowedException.class,
-            () -> branch(new Branch.NameKey(allUsers, RefNames.HEAD)).delete());
+            () -> branch(BranchNameKey.create(allUsers, RefNames.HEAD)).delete());
     assertThat(thrown).hasMessageThat().contains("not allowed to delete HEAD");
   }
 
   private void blockForcePush() throws Exception {
-    block("refs/heads/*", Permission.PUSH, ANONYMOUS_USERS).setForce(true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS).force(true))
+        .update();
   }
 
   private void grantForcePush() throws Exception {
-    grant(project, "refs/heads/*", Permission.PUSH, true, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS).force(true))
+        .update();
   }
 
   private void grantDelete() throws Exception {
-    allow("refs/*", Permission.DELETE, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE).ref("refs/*").group(ANONYMOUS_USERS))
+        .update();
   }
 
   private void grantOwner() throws Exception {
-    allow("refs/*", Permission.OWNER, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
   }
 
-  private BranchApi branch(Branch.NameKey branch) throws Exception {
-    return gApi.projects().name(branch.getParentKey().get()).branch(branch.get());
+  private BranchApi branch(BranchNameKey branch) throws Exception {
+    return gApi.projects().name(branch.project().get()).branch(branch.branch());
   }
 
-  private void assertDeleteByRestSucceeds(Branch.NameKey branch, String ref) throws Exception {
+  private void assertDeleteByRestSucceeds(BranchNameKey branch, String ref) throws Exception {
     RestResponse r =
         userRestSession.delete(
             "/projects/"
@@ -199,24 +235,21 @@
                 + "/branches/"
                 + IdString.fromDecoded(ref).encoded());
     r.assertNoContent();
-    exception.expect(ResourceNotFoundException.class);
-    branch(branch).get();
+    assertThrows(ResourceNotFoundException.class, () -> branch(branch).get());
   }
 
-  private void assertDeleteSucceeds(Branch.NameKey branch) throws Exception {
+  private void assertDeleteSucceeds(BranchNameKey branch) throws Exception {
     assertThat(branch(branch).get().canDelete).isTrue();
     String branchRev = branch(branch).get().revision;
     branch(branch).delete();
     eventRecorder.assertRefUpdatedEvents(
-        project.get(), branch.get(), null, branchRev, branchRev, null);
-    exception.expect(ResourceNotFoundException.class);
-    branch(branch).get();
+        project.get(), branch.branch(), null, branchRev, branchRev, null);
+    assertThrows(ResourceNotFoundException.class, () -> branch(branch).get());
   }
 
-  private void assertDeleteForbidden(Branch.NameKey branch) throws Exception {
+  private void assertDeleteForbidden(BranchNameKey branch) throws Exception {
     assertThat(branch(branch).get().canDelete).isNull();
-    exception.expect(AuthException.class);
-    exception.expectMessage("not permitted: delete");
-    branch(branch).delete();
+    AuthException thrown = assertThrows(AuthException.class, () -> branch(branch).delete());
+    assertThat(thrown).hasMessageThat().contains("not permitted: delete");
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index 4d72123..ad90109 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
@@ -25,8 +26,10 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+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.RefNames;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
@@ -34,7 +37,6 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.inject.Inject;
 import java.util.HashMap;
 import java.util.List;
@@ -48,12 +50,17 @@
   private static final ImmutableList<String> BRANCHES =
       ImmutableList.of("refs/heads/test-1", "refs/heads/test-2", "test-3", "refs/meta/foo");
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
   public void setUp() throws Exception {
-    allow("refs/*", Permission.CREATE, REGISTERED_USERS);
-    allow("refs/*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     for (String name : BRANCHES) {
       project().branch(name).create(new BranchInput());
     }
@@ -77,12 +84,8 @@
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = branchToDelete;
     requestScopeOperations.setApiUser(user.id());
-    try {
-      project().deleteBranches(input);
-      fail("Expected AuthException");
-    } catch (AuthException e) {
-      assertThat(e).hasMessageThat().isEqualTo("not permitted: delete on refs/heads/test-1");
-    }
+    AuthException thrown = assertThrows(AuthException.class, () -> project().deleteBranches(input));
+    assertThat(thrown).hasMessageThat().isEqualTo("not permitted: delete on refs/heads/test-1");
     requestScopeOperations.setApiUser(admin.id());
     assertBranches(BRANCHES);
   }
@@ -92,12 +95,9 @@
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = BRANCHES;
     requestScopeOperations.setApiUser(user.id());
-    try {
-      project().deleteBranches(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e).hasMessageThat().isEqualTo(errorMessageForBranches(BRANCHES));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteBranches(input));
+    assertThat(thrown).hasMessageThat().isEqualTo(errorMessageForBranches(BRANCHES));
     requestScopeOperations.setApiUser(admin.id());
     assertBranches(BRANCHES);
   }
@@ -108,14 +108,11 @@
     List<String> branches = Lists.newArrayList(BRANCHES);
     branches.add("refs/heads/does-not-exist");
     input.branches = branches;
-    try {
-      project().deleteBranches(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(errorMessageForBranches(ImmutableList.of("refs/heads/does-not-exist")));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteBranches(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(errorMessageForBranches(ImmutableList.of("refs/heads/does-not-exist")));
     assertBranchesDeleted(BRANCHES);
   }
 
@@ -127,40 +124,37 @@
     List<String> branches = Lists.newArrayList("refs/heads/does-not-exist");
     branches.addAll(BRANCHES);
     input.branches = branches;
-    try {
-      project().deleteBranches(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(errorMessageForBranches(ImmutableList.of("refs/heads/does-not-exist")));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteBranches(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(errorMessageForBranches(ImmutableList.of("refs/heads/does-not-exist")));
     assertBranchesDeleted(BRANCHES);
   }
 
   @Test
   public void missingInput() throws Exception {
     DeleteBranchesInput input = null;
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("branches must be specified");
-    project().deleteBranches(input);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> project().deleteBranches(input));
+    assertThat(thrown).hasMessageThat().contains("branches must be specified");
   }
 
   @Test
   public void missingBranchList() throws Exception {
     DeleteBranchesInput input = new DeleteBranchesInput();
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("branches must be specified");
-    project().deleteBranches(input);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> project().deleteBranches(input));
+    assertThat(thrown).hasMessageThat().contains("branches must be specified");
   }
 
   @Test
   public void emptyBranchList() throws Exception {
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = Lists.newArrayList();
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("branches must be specified");
-    project().deleteBranches(input);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> project().deleteBranches(input));
+    assertThat(thrown).hasMessageThat().contains("branches must be specified");
   }
 
   @Test
@@ -198,7 +192,7 @@
   private HashMap<String, RevCommit> initialRevisions(List<String> branches) throws Exception {
     HashMap<String, RevCommit> result = new HashMap<>();
     for (String branch : branches) {
-      result.put(branch, getRemoteHead(project, branch));
+      result.put(branch, projectOperations.project(project).getHead(branch));
     }
     return result;
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
index 07bb2b1..9770031 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -15,12 +15,16 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+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.extensions.api.projects.TagApi;
@@ -35,6 +39,7 @@
 public class DeleteTagIT extends AbstractDaemonTest {
   private static final String TAG = "refs/tags/test";
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
@@ -97,19 +102,35 @@
   }
 
   private void blockForcePush() throws Exception {
-    block("refs/tags/*", Permission.PUSH, ANONYMOUS_USERS).setForce(true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.PUSH).ref("refs/tags/*").group(ANONYMOUS_USERS).force(true))
+        .update();
   }
 
   private void grantForcePush() throws Exception {
-    grant(project, "refs/tags/*", Permission.PUSH, true, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/tags/*").group(ANONYMOUS_USERS).force(true))
+        .update();
   }
 
   private void grantDelete() throws Exception {
-    allow("refs/tags/*", Permission.DELETE, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE).ref("refs/tags/*").group(ANONYMOUS_USERS))
+        .update();
   }
 
   private void grantOwner() throws Exception {
-    allow("refs/tags/*", Permission.OWNER, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
   }
 
   private TagApi tag() throws Exception {
@@ -122,14 +143,12 @@
     String tagRev = tagInfo.revision;
     tag().delete();
     eventRecorder.assertRefUpdatedEvents(project.get(), TAG, null, tagRev, tagRev, null);
-    exception.expect(ResourceNotFoundException.class);
-    tag().get();
+    assertThrows(ResourceNotFoundException.class, () -> tag().get());
   }
 
   private void assertDeleteForbidden() throws Exception {
     assertThat(tag().get().canDelete).isNull();
-    exception.expect(AuthException.class);
-    exception.expectMessage("not permitted: delete");
-    tag().delete();
+    AuthException thrown = assertThrows(AuthException.class, () -> tag().delete());
+    assertThat(thrown).hasMessageThat().contains("not permitted: delete");
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
index fae9d00..46e2345 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
@@ -23,6 +24,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.projects.DeleteTagsInput;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
@@ -41,6 +43,7 @@
   private static final ImmutableList<String> TAGS =
       ImmutableList.of("refs/tags/test-1", "refs/tags/test-2", "refs/tags/test-3", "test-4");
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
@@ -66,12 +69,9 @@
     DeleteTagsInput input = new DeleteTagsInput();
     input.tags = TAGS;
     requestScopeOperations.setApiUser(user.id());
-    try {
-      project().deleteTags(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e).hasMessageThat().isEqualTo(errorMessageForTags(TAGS));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteTags(input));
+    assertThat(thrown).hasMessageThat().isEqualTo(errorMessageForTags(TAGS));
     requestScopeOperations.setApiUser(admin.id());
     assertTags(TAGS);
   }
@@ -82,14 +82,11 @@
     List<String> tags = Lists.newArrayList(TAGS);
     tags.add("refs/tags/does-not-exist");
     input.tags = tags;
-    try {
-      project().deleteTags(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(errorMessageForTags(ImmutableList.of("refs/tags/does-not-exist")));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteTags(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(errorMessageForTags(ImmutableList.of("refs/tags/does-not-exist")));
     assertTagsDeleted();
   }
 
@@ -101,14 +98,11 @@
     List<String> tags = Lists.newArrayList("refs/tags/does-not-exist");
     tags.addAll(TAGS);
     input.tags = tags;
-    try {
-      project().deleteTags(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(errorMessageForTags(ImmutableList.of("refs/tags/does-not-exist")));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteTags(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(errorMessageForTags(ImmutableList.of("refs/tags/does-not-exist")));
     assertTagsDeleted();
   }
 
@@ -128,7 +122,7 @@
     HashMap<String, RevCommit> result = new HashMap<>();
     for (String tag : tags) {
       String ref = prefixRef(tag);
-      result.put(ref, getRemoteHead(project, ref));
+      result.put(ref, projectOperations.project(project).getHead(ref));
     }
     return result;
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/FileBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/FileBranchIT.java
index 63f41ad..a7f3174 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/FileBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/FileBranchIT.java
@@ -15,25 +15,26 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Branch;
 import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class FileBranchIT extends AbstractDaemonTest {
 
-  private Branch.NameKey branch;
+  private BranchNameKey branch;
 
   @Before
   public void setUp() throws Exception {
-    branch = new Branch.NameKey(project, "master");
+    branch = BranchNameKey.create(project, "master");
     PushOneCommit.Result change = createChange();
     approve(change.getChangeId());
     revision(change).submit();
@@ -45,12 +46,12 @@
     assertThat(content.asString()).isEqualTo(PushOneCommit.FILE_CONTENT);
   }
 
-  @Test(expected = ResourceNotFoundException.class)
+  @Test
   public void getNonExistingFile() throws Exception {
-    branch().file("does-not-exist");
+    assertThrows(ResourceNotFoundException.class, () -> branch().file("does-not-exist"));
   }
 
   private BranchApi branch() throws Exception {
-    return gApi.projects().name(branch.getParentKey().get()).branch(branch.get());
+    return gApi.projects().name(branch.project().get()).branch(branch.branch());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
index 48527af..0bdaad0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
index 56265c6..964cff7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
@@ -15,6 +15,10 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -26,17 +30,18 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
@@ -75,22 +80,30 @@
   }
 
   @Test
-  public void cannotGetNonVisibleBranch() throws Exception {
+  public void cannotGetNonVisibleBranch() {
     String branchName = "master";
 
     // block read access to the branch
-    block(project, RefNames.fullName(branchName), Permission.READ, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName(branchName)).group(ANONYMOUS_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     assertBranchNotFound(project, RefNames.fullName(branchName));
   }
 
   @Test
-  public void cannotGetNonVisibleBranchByShortName() throws Exception {
+  public void cannotGetNonVisibleBranchByShortName() {
     String branchName = "master";
 
     // block read access to the branch
-    block(project, RefNames.fullName(branchName), Permission.READ, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName(branchName)).group(ANONYMOUS_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     assertBranchNotFound(project, branchName);
@@ -114,7 +127,11 @@
     Change.Id changeId = createChange("refs/for/" + branchName).getPatchSetId().changeId();
 
     // block read access to the branch
-    block(project, RefNames.fullName(branchName), Permission.READ, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName(branchName)).group(ANONYMOUS_USERS))
+        .update();
 
     // a user without the 'Access Database' capability cannot see the change ref
     requestScopeOperations.setApiUser(user.id());
@@ -160,7 +177,11 @@
     gApi.changes().id(changeId.get()).edit().create();
 
     // make the change non-visible by blocking read access on the destination
-    block(project, RefNames.fullName(branchName), Permission.READ, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName(branchName)).group(ANONYMOUS_USERS))
+        .update();
 
     // user cannot see their own change edit refs if the change is no longer visible
     String changeEditRef = RefNames.refsEdit(user.id(), changeId, PatchSet.id(changeId, 1));
@@ -193,17 +214,30 @@
 
     // a non-project owner cannot get the refs/meta/config branch even with the 'Access Database'
     // capability
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     try {
       assertBranchNotFound(project, RefNames.REFS_CONFIG);
     } finally {
-      removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(
+              capabilityKey(GlobalCapability.ACCESS_DATABASE)
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
     }
 
     requestScopeOperations.setApiUser(user.id());
 
     // a project owner can get the refs/meta/config branch
-    allow(project, "refs/*", Permission.OWNER, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     assertBranchFound(project, RefNames.REFS_CONFIG);
   }
 
@@ -265,7 +299,11 @@
     // This is because READ access for refs/groups/* on All-Users is by default granted to
     // REGISTERED_USERS, and if an ALLOW rule and a BLOCK rule are on the same project and ref,
     // the ALLOW rule takes precedence.
-    block(allProjects, "refs/groups/*", Permission.READ, ANONYMOUS_USERS);
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/groups/*").group(ANONYMOUS_USERS))
+        .update();
     assertBranchNotFound(allUsers, groupRef);
   }
 
@@ -378,7 +416,11 @@
     TagInfo tagInfo = gApi.projects().name(project.get()).tag("my-tag").create(input).get();
 
     // block read access to the branch
-    block(allProjects, RefNames.fullName("master"), Permission.READ, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName("master")).group(ANONYMOUS_USERS))
+        .update();
 
     // if the user cannot see the project, the tag is not visible
     requestScopeOperations.setApiUser(user.id());
@@ -393,9 +435,13 @@
   }
 
   @Test
-  public void cannotGetSymbolicRefThatPointsToNonVisibleBranch() throws Exception {
+  public void cannotGetSymbolicRefThatPointsToNonVisibleBranch() {
     // block read access to the branch to which HEAD points by default
-    block(allProjects, RefNames.fullName("master"), Permission.READ, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName("master")).group(ANONYMOUS_USERS))
+        .update();
 
     // since 'master' is not visible, 'HEAD' which points to 'master' is also not visible
     requestScopeOperations.setApiUser(user.id());
@@ -445,13 +491,23 @@
     testGetRefWithAccessDatabase(allProjects, RefNames.REFS_VERSION);
   }
 
-  private void testGetRefWithAccessDatabase(Project.NameKey project, String ref) throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+  private void testGetRefWithAccessDatabase(Project.NameKey project, String ref)
+      throws RestApiException {
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     try {
       requestScopeOperations.setApiUser(user.id());
       assertBranchFound(project, ref);
     } finally {
-      removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(
+              capabilityKey(GlobalCapability.ACCESS_DATABASE)
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
index d736578..8911163 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
@@ -14,14 +14,16 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import org.junit.Test;
 
@@ -69,8 +71,10 @@
   }
 
   private void assertChildNotFound(Project.NameKey parent, String child) throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage(child);
-    gApi.projects().name(parent.get()).child(child).get();
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.projects().name(parent.get()).child(child).get());
+    assertThat(thrown).hasMessageThat().contains(child);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index 18c706b..b18db81 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -15,14 +15,18 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.inject.Inject;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -32,12 +36,14 @@
 import org.junit.Test;
 
 public class GetCommitIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   private TestRepository<Repository> repo;
 
   @Before
   public void setUp() throws Exception {
     repo = GitUtil.newTestRepository(repoManager.openRepository(project));
-    blockRead("refs/*");
+    blockRead();
   }
 
   @After
@@ -107,8 +113,17 @@
 
   @Test
   public void getOpenChange_NotFound() throws Exception {
+    // Need to unblock read to allow the push operation to succeed if not, when retrieving the
+    // advertised refs during
+    // the push, the client won't be sent the initial commit and will send it again as part of the
+    // change.
+    unblockRead();
+
     PushOneCommit.Result r = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
     r.assertOkStatus();
+
+    // Re-blocking the read
+    blockRead();
     assertNotFound(r.getCommit());
   }
 
@@ -119,6 +134,14 @@
     }
   }
 
+  private void blockRead() {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+  }
+
   private void assertNotFound(ObjectId id) throws Exception {
     userRestSession.get("/projects/" + project.get() + "/commits/" + id.name()).assertNotFound();
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetProjectIT.java
index 989050c..e9aa589 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetProjectIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -54,8 +55,9 @@
     assertThat(p.name).isEqualTo(name);
   }
 
-  @Test(expected = ResourceNotFoundException.class)
+  @Test
   public void getProjectNotExisting() throws Exception {
-    gApi.projects().name("does-not-exist").get();
+    assertThrows(
+        ResourceNotFoundException.class, () -> gApi.projects().name("does-not-exist").get());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index ec1c708..91a2c4b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -15,36 +15,48 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.gerrit.acceptance.rest.project.RefAssert.assertRefs;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestProjectInput;
+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.RefNames;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.inject.Inject;
 import org.junit.Test;
 
 @NoHttpd
 public class ListBranchesIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void listBranchesOfNonExistingProject_NotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.projects().name("non-existing").branches().get();
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.projects().name("non-existing").branches().get());
   }
 
   @Test
   public void listBranchesOfNonVisibleProject_NotFound() throws Exception {
-    blockRead("refs/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(ResourceNotFoundException.class);
-    gApi.projects().name(project.get()).branches().get();
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.projects().name(project.get()).branches().get());
   }
 
   @Test
@@ -59,7 +71,7 @@
   public void listBranches() throws Exception {
     String master = pushTo("refs/heads/master").getCommit().name();
     String dev = pushTo("refs/heads/dev").getCommit().name();
-    String refsConfig = getRemoteHead(project, RefNames.REFS_CONFIG).name();
+    String refsConfig = projectOperations.project(project).getHead(RefNames.REFS_CONFIG).name();
     assertRefs(
         ImmutableList.of(
             branch("HEAD", "master", false),
@@ -71,7 +83,11 @@
 
   @Test
   public void listBranchesSomeHidden() throws Exception {
-    blockRead("refs/heads/dev");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/dev").group(REGISTERED_USERS))
+        .update();
     String master = pushTo("refs/heads/master").getCommit().name();
     pushTo("refs/heads/dev");
     requestScopeOperations.setApiUser(user.id());
@@ -84,7 +100,11 @@
 
   @Test
   public void listBranchesHeadHidden() throws Exception {
-    blockRead("refs/heads/master");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     pushTo("refs/heads/master");
     String dev = pushTo("refs/heads/dev").getCommit().name();
     requestScopeOperations.setApiUser(user.id());
@@ -96,7 +116,10 @@
   public void listBranchesUsingPagination() throws Exception {
     BranchInfo head = branch("HEAD", "master", false);
     BranchInfo refsConfig =
-        branch(RefNames.REFS_CONFIG, getRemoteHead(project, RefNames.REFS_CONFIG).name(), false);
+        branch(
+            RefNames.REFS_CONFIG,
+            projectOperations.project(project).getHead(RefNames.REFS_CONFIG).name(),
+            false);
     BranchInfo master =
         branch("refs/heads/master", pushTo("refs/heads/master").getCommit().getName(), false);
     BranchInfo branch1 =
@@ -170,11 +193,6 @@
   }
 
   private void assertBadRequest(ListRefsRequest<BranchInfo> req) throws Exception {
-    try {
-      req.get();
-      fail("Expected BadRequestException");
-    } catch (BadRequestException e) {
-      // Expected
-    }
+    assertThrows(BadRequestException.class, () -> req.get());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index 7746820..7535dea 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -14,13 +14,15 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import org.apache.commons.lang.RandomStringUtils;
 import org.junit.Test;
@@ -31,9 +33,11 @@
 
   @Test
   public void listChildrenOfNonExistingProject_NotFound() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("non-existing");
-    gApi.projects().name(name("non-existing")).child("children");
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.projects().name(name("non-existing")).child("children"));
+    assertThat(thrown).hasMessageThat().contains("non-existing");
   }
 
   @Test
@@ -47,7 +51,7 @@
     Project.NameKey child1_1 = projectOperations.newProject().parent(child1).create();
     Project.NameKey child1_2 = projectOperations.newProject().parent(child1).create();
 
-    assertThatNameList(gApi.projects().name(child1.get()).children()).isOrdered();
+    assertThatNameList(gApi.projects().name(child1.get()).children()).isInOrder();
     assertThatNameList(gApi.projects().name(child1.get()).children())
         .containsExactly(child1_1, child1_2);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 9dba8cf..29d3eb2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -16,7 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
 
@@ -31,6 +33,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.Project;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.Projects.ListRequest;
@@ -39,9 +42,7 @@
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.ProjectCacheImpl;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.project.ListProjects;
 import com.google.gson.Gson;
 import com.google.gson.JsonObject;
@@ -65,7 +66,7 @@
     Project.NameKey someProject = projectOperations.newProject().create();
     assertThatNameList(gApi.projects().list().get())
         .containsExactly(allProjects, allUsers, project, someProject);
-    assertThatNameList(gApi.projects().list().get()).isOrdered();
+    assertThatNameList(gApi.projects().list().get()).isInOrder();
   }
 
   @Test
@@ -73,10 +74,11 @@
     requestScopeOperations.setApiUser(user.id());
     assertThatNameList(gApi.projects().list().get()).contains(project);
 
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     assertThatNameList(gApi.projects().list().get()).doesNotContain(project);
   }
@@ -243,7 +245,7 @@
     int n = 5;
     assertThat(all).hasSize(n);
     assertThatNameList(gApi.projects().list().withPrefix(pre).withStart(n - 1).get())
-        .containsExactly(new Project.NameKey(Iterables.getLast(all).name));
+        .containsExactly(Project.nameKey(Iterables.getLast(all).name));
   }
 
   @Test
@@ -337,11 +339,6 @@
   }
 
   private void assertBadRequest(ListRequest req) throws Exception {
-    try {
-      req.get();
-      fail("Expected BadRequestException");
-    } catch (BadRequestException expected) {
-      // Expected.
-    }
+    assertThrows(BadRequestException.class, () -> req.get());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/javatests/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
index 3b5a3a4..3f583a2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -21,10 +21,10 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.truth.IterableSubject;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.ProjectState;
 import java.util.List;
 import java.util.Set;
@@ -38,7 +38,7 @@
           .that(Url.decode(info.id))
           .isEqualTo(info.name);
     }
-    return assertThat(Iterables.transform(actual, p -> new Project.NameKey(p.name)));
+    return assertThat(Iterables.transform(actual, p -> Project.nameKey(p.name)));
   }
 
   public static void assertProjectInfo(Project project, ProjectInfo info) {
@@ -47,7 +47,7 @@
       assertThat(info.name).isEqualTo(project.getName());
     }
     assertThat(Url.decode(info.id)).isEqualTo(project.getName());
-    Project.NameKey parentName = project.getParent(new Project.NameKey("All-Projects"));
+    Project.NameKey parentName = project.getParent(Project.nameKey("All-Projects"));
     if (parentName != null) {
       assertThat(info.parent).isEqualTo(parentName.get());
     } else {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
index bf2a534..76c30a9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
@@ -20,8 +20,8 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import java.util.Arrays;
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/RefAssert.java b/javatests/com/google/gerrit/acceptance/rest/project/RefAssert.java
index b3e3d2f..a93fc0f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/RefAssert.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/RefAssert.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.api.projects.RefInfo;
@@ -38,10 +39,12 @@
   public static void assertRefInfo(RefInfo expected, RefInfo actual) {
     assertThat(actual.ref).isEqualTo(expected.ref);
     if (expected.revision != null) {
-      assertThat(actual.revision).named("revision of " + actual.ref).isEqualTo(expected.revision);
+      assertWithMessage("revision of " + actual.ref)
+          .that(actual.revision)
+          .isEqualTo(expected.revision);
     }
-    assertThat(toBoolean(actual.canDelete))
-        .named("can delete " + actual.ref)
+    assertWithMessage("can delete " + actual.ref)
+        .that(toBoolean(actual.canDelete))
         .isEqualTo(toBoolean(expected.canDelete));
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index c945a2b..f5d2db4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -15,7 +15,10 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.common.collect.FluentIterable;
@@ -23,6 +26,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+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.extensions.api.projects.ProjectApi.ListRefsRequest;
@@ -62,6 +66,7 @@
           + "=XFeC\n"
           + "-----END PGP SIGNATURE-----";
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
@@ -76,29 +81,39 @@
 
   @Test
   public void listTagsOfNonExistingProject() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.projects().name("does-not-exist").tags().get();
+    assertThrows(
+        ResourceNotFoundException.class, () -> gApi.projects().name("does-not-exist").tags().get());
   }
 
   @Test
   public void getTagOfNonExistingProject() throws Exception {
-    exception.expect(ResourceNotFoundException.class);
-    gApi.projects().name("does-not-exist").tag("tag").get();
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.projects().name("does-not-exist").tag("tag").get());
   }
 
   @Test
   public void listTagsOfNonVisibleProject() throws Exception {
-    blockRead("refs/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(ResourceNotFoundException.class);
-    gApi.projects().name(project.get()).tags().get();
+    assertThrows(
+        ResourceNotFoundException.class, () -> gApi.projects().name(project.get()).tags().get());
   }
 
   @Test
   public void getTagOfNonVisibleProject() throws Exception {
-    blockRead("refs/*");
-    exception.expect(ResourceNotFoundException.class);
-    gApi.projects().name(project.get()).tag("tag").get();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.projects().name(project.get()).tag("tag").get());
   }
 
   @Test
@@ -173,7 +188,11 @@
     assertThat(tags.get(1).ref).isEqualTo(R_TAGS + tag2.ref);
     assertThat(tags.get(1).revision).isEqualTo(tag2.revision);
 
-    blockRead("refs/heads/hidden");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/hidden").group(REGISTERED_USERS))
+        .update();
     tags = getTags().get();
     assertThat(tags).hasSize(1);
     assertThat(tags.get(0).ref).isEqualTo(R_TAGS + tag1.ref);
@@ -182,7 +201,7 @@
 
   @Test
   public void lightweightTag() throws Exception {
-    grant(project, R_TAGS + "*", Permission.CREATE);
+    grantTagPermissions();
 
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
@@ -214,7 +233,7 @@
 
   @Test
   public void annotatedTag() throws Exception {
-    grant(project, R_TAGS + "*", Permission.CREATE_TAG);
+    grantTagPermissions();
 
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
@@ -261,30 +280,38 @@
     assertThat(result.ref).isEqualTo(R_TAGS + "test");
 
     input.ref = "refs/tags/test";
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("tag \"" + R_TAGS + "test\" already exists");
-    tag(input.ref).create(input);
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> tag(input.ref).create(input));
+    assertThat(thrown).hasMessageThat().contains("tag \"" + R_TAGS + "test\" already exists");
   }
 
   @Test
   public void createTagNotAllowed() throws Exception {
-    block(R_TAGS + "*", Permission.CREATE, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.CREATE).ref(R_TAGS + "*").group(REGISTERED_USERS))
+        .update();
     TagInput input = new TagInput();
     input.ref = "test";
-    exception.expect(AuthException.class);
-    exception.expectMessage("not permitted: create");
-    tag(input.ref).create(input);
+    AuthException thrown = assertThrows(AuthException.class, () -> tag(input.ref).create(input));
+    assertThat(thrown).hasMessageThat().contains("not permitted: create");
   }
 
   @Test
   public void createAnnotatedTagNotAllowed() throws Exception {
-    block(R_TAGS + "*", Permission.CREATE_TAG, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.CREATE_TAG).ref(R_TAGS + "*").group(REGISTERED_USERS))
+        .update();
     TagInput input = new TagInput();
     input.ref = "test";
     input.message = "annotation";
-    exception.expect(AuthException.class);
-    exception.expectMessage("Cannot create annotated tag \"" + R_TAGS + "test\"");
-    tag(input.ref).create(input);
+    AuthException thrown = assertThrows(AuthException.class, () -> tag(input.ref).create(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Cannot create annotated tag \"" + R_TAGS + "test\"");
   }
 
   @Test
@@ -292,9 +319,9 @@
     TagInput input = new TagInput();
     input.ref = "test";
     input.message = SIGNED_ANNOTATION;
-    exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage("Cannot create signed tag \"" + R_TAGS + "test\"");
-    tag(input.ref).create(input);
+    MethodNotAllowedException thrown =
+        assertThrows(MethodNotAllowedException.class, () -> tag(input.ref).create(input));
+    assertThat(thrown).hasMessageThat().contains("Cannot create signed tag \"" + R_TAGS + "test\"");
   }
 
   @Test
@@ -302,9 +329,9 @@
     TagInput input = new TagInput();
     input.ref = "test";
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("ref must match URL");
-    tag("TEST").create(input);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> tag("TEST").create(input));
+    assertThat(thrown).hasMessageThat().contains("ref must match URL");
   }
 
   @Test
@@ -314,9 +341,9 @@
     TagInput input = new TagInput();
     input.ref = "refs/heads/test";
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("invalid tag name \"" + input.ref + "\"");
-    tag(input.ref).create(input);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> tag(input.ref).create(input));
+    assertThat(thrown).hasMessageThat().contains("invalid tag name \"" + input.ref + "\"");
   }
 
   @Test
@@ -326,9 +353,9 @@
     TagInput input = new TagInput();
     input.ref = "//";
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("invalid tag name \"refs/tags/\"");
-    tag(input.ref).create(input);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> tag(input.ref).create(input));
+    assertThat(thrown).hasMessageThat().contains("invalid tag name \"refs/tags/\"");
   }
 
   @Test
@@ -339,9 +366,9 @@
     input.ref = "test";
     input.revision = "abcdefg";
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("Invalid base revision");
-    tag(input.ref).create(input);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> tag(input.ref).create(input));
+    assertThat(thrown).hasMessageThat().contains("Invalid base revision");
   }
 
   @Test
@@ -349,7 +376,7 @@
     grantTagPermissions();
 
     // If revision is not specified, the tag is created based on HEAD, which points to master.
-    RevCommit expectedRevision = getRemoteHead(project, "master");
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
 
     TagInput input = new TagInput();
     input.ref = "test";
@@ -365,7 +392,7 @@
     grantTagPermissions();
 
     // If revision is not specified, the tag is created based on HEAD, which points to master.
-    RevCommit expectedRevision = getRemoteHead(project, "master");
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
 
     TagInput input = new TagInput();
     input.ref = "test";
@@ -380,7 +407,7 @@
   public void baseRevisionIsTrimmed() throws Exception {
     grantTagPermissions();
 
-    RevCommit revision = getRemoteHead(project, "master");
+    RevCommit revision = projectOperations.project(project).getHead("master");
 
     TagInput input = new TagInput();
     input.ref = "test";
@@ -428,19 +455,18 @@
   }
 
   private void assertBadRequest(ListRefsRequest<TagInfo> req) throws Exception {
-    try {
-      req.get();
-      fail("Expected BadRequestException");
-    } catch (BadRequestException e) {
-      // Expected
-    }
+    assertThrows(BadRequestException.class, () -> req.get());
   }
 
   private void grantTagPermissions() throws Exception {
-    grant(project, R_TAGS + "*", Permission.CREATE);
-    grant(project, R_TAGS + "", Permission.DELETE);
-    grant(project, R_TAGS + "*", Permission.CREATE_TAG);
-    grant(project, R_TAGS + "*", Permission.CREATE_SIGNED_TAG);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(R_TAGS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.DELETE).ref(R_TAGS + "").group(adminGroupUuid()))
+        .add(allow(Permission.CREATE_TAG).ref(R_TAGS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.CREATE_SIGNED_TAG).ref(R_TAGS + "*").group(adminGroupUuid()))
+        .update();
   }
 
   private static void removeAllBranchPermissions(ProjectConfig cfg, String... permissions) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
index 52e72fe..f98fb45 100644
--- a/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
+++ b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.util;
 
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth.assert_;
 import static org.apache.http.HttpStatus.SC_FORBIDDEN;
 import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
 import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
@@ -76,7 +75,8 @@
         response = restSession.delete(uri);
         break;
       default:
-        assert_().fail(String.format("unsupported method: %s", restCall.httpMethod().name()));
+        assertWithMessage(String.format("unsupported method: %s", restCall.httpMethod().name()))
+            .fail();
         throw new IllegalStateException();
     }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
index fa1b467..e924143 100644
--- a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
@@ -15,8 +15,9 @@
 package com.google.gerrit.acceptance.server.account;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
@@ -25,8 +26,8 @@
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountVisibility;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountResolver;
@@ -81,26 +82,16 @@
   }
 
   private void checkBySelfFails() throws Exception {
-    Result result = resolveAsResult("self");
-    assertThat(result.asIdSet()).isEmpty();
-    assertThat(result.isSelf()).isTrue();
-    try {
-      result.asUnique();
-      assert_().fail("expected UnresolvableAccountException");
-    } catch (UnresolvableAccountException e) {
-      assertThat(e).hasMessageThat().isEqualTo("Resolving account 'self' requires login");
-      assertThat(e.isSelf()).isTrue();
-    }
-
-    result = resolveAsResult("me");
-    assertThat(result.asIdSet()).isEmpty();
-    assertThat(result.isSelf()).isTrue();
-    try {
-      result.asUnique();
-      assert_().fail("expected UnresolvableAccountException");
-    } catch (UnresolvableAccountException e) {
-      assertThat(e).hasMessageThat().isEqualTo("Resolving account 'me' requires login");
-      assertThat(e.isSelf()).isTrue();
+    for (String input : ImmutableList.of("self", "me")) {
+      Result result = resolveAsResult(input);
+      assertThat(result.asIdSet()).isEmpty();
+      assertThat(result.isSelf()).isTrue();
+      UnresolvableAccountException thrown =
+          assertThrows(UnresolvableAccountException.class, () -> result.asUnique());
+      assertThat(thrown)
+          .hasMessageThat()
+          .isEqualTo(String.format("Resolving account '%s' requires login", input));
+      assertThat(thrown.isSelf()).isTrue();
     }
   }
 
@@ -123,7 +114,7 @@
     Account.Id idWithExistingIdAsFullname =
         accountOperations.newAccount().fullname(existingId.toString()).create();
 
-    Account.Id nonexistentId = new Account.Id(sequences.nextAccountId());
+    Account.Id nonexistentId = Account.id(sequences.nextAccountId());
     accountOperations.newAccount().fullname(nonexistentId.toString()).create();
 
     assertThat(resolve(existingId)).containsExactly(existingId);
@@ -137,7 +128,7 @@
     Account.Id existingId = accountOperations.newAccount().fullname("Test User").create();
     accountOperations.newAccount().fullname(existingId.toString()).create();
 
-    Account.Id nonexistentId = new Account.Id(sequences.nextAccountId());
+    Account.Id nonexistentId = Account.id(sequences.nextAccountId());
     accountOperations.newAccount().fullname("Any Name (" + nonexistentId + ")").create();
     accountOperations.newAccount().fullname(nonexistentId.toString()).create();
 
@@ -259,31 +250,28 @@
 
     assertThat(resolve(account.accountId())).containsExactly(id);
     for (String input : inputs) {
-      assertThat(resolve(input)).named("results for %s (active)", input).containsExactly(id);
+      assertWithMessage("results for %s (active)", input).that(resolve(input)).containsExactly(id);
     }
 
     gApi.accounts().id(id.get()).setActive(false);
     assertThat(resolve(account.accountId())).containsExactly(id);
     for (String input : inputs) {
       Result result = accountResolver.resolve(input);
-      assertThat(result.asIdSet()).named("results for %s (inactive)", input).isEmpty();
-      try {
-        result.asUnique();
-        assert_().fail("expected UnresolvableAccountException");
-      } catch (UnresolvableAccountException e) {
-        assertThat(e)
-            .hasMessageThat()
-            .isEqualTo(
-                "Account '"
-                    + input
-                    + "' only matches inactive accounts. To use an inactive account, retry"
-                    + " with one of the following exact account IDs:\n"
-                    + id
-                    + ": "
-                    + nameEmail);
-      }
-      assertThat(resolveByNameOrEmail(input))
-          .named("results by name or email for %s (inactive)", input)
+      assertWithMessage("results for %s (inactive)", input).that(result.asIdSet()).isEmpty();
+      UnresolvableAccountException thrown =
+          assertThrows(UnresolvableAccountException.class, () -> result.asUnique());
+      assertThat(thrown)
+          .hasMessageThat()
+          .isEqualTo(
+              "Account '"
+                  + input
+                  + "' only matches inactive accounts. To use an inactive account, retry"
+                  + " with one of the following exact account IDs:\n"
+                  + id
+                  + ": "
+                  + nameEmail);
+      assertWithMessage("results by name or email for %s (inactive)", input)
+          .that(resolveByNameOrEmail(input))
           .isEmpty();
     }
   }
@@ -352,6 +340,6 @@
         accountsUpdateProvider
             .get()
             .update("Force set preferred email", id, (s, u) -> u.setPreferredEmail(email));
-    assertThat(result.map(a -> a.getAccount().getPreferredEmail())).hasValue(email);
+    assertThat(result.map(a -> a.account().preferredEmail())).hasValue(email);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 35fe48f..7ac803e 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -18,10 +18,10 @@
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.toList;
 
-import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
@@ -31,6 +31,9 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -45,9 +48,6 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
@@ -61,11 +61,11 @@
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -98,8 +98,9 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     String revId = r.getCommit().getName();
-    exception.expect(ResourceNotFoundException.class);
-    getPublishedComment(changeId, revId, "non-existing");
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> getPublishedComment(changeId, revId, "non-existing"));
   }
 
   @Test
@@ -140,12 +141,11 @@
       addDraft(changeId, revId, c4);
       Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
       assertThat(result).hasSize(1);
-      assertThat(Lists.transform(result.get(path), infoToDraft(path)))
-          .containsExactly(c1, c2, c3, c4);
+      assertThat(result.get(path).stream().map(infoToDraft(path))).containsExactly(c1, c2, c3, c4);
 
       List<CommentInfo> list = getDraftCommentsAsList(changeId);
       assertThat(list).hasSize(4);
-      assertThat(Lists.transform(list, infoToDraft(path))).containsExactly(c1, c2, c3, c4);
+      assertThat(list.stream().map(infoToDraft(path))).containsExactly(c1, c2, c3, c4);
     }
   }
 
@@ -246,11 +246,10 @@
       revision(r).review(input);
       Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
       assertThat(result).isNotEmpty();
-      assertThat(Lists.transform(result.get(file), infoToInput(file)))
-          .containsExactly(c1, c2, c3, c4);
+      assertThat(result.get(file).stream().map(infoToInput(file))).containsExactly(c1, c2, c3, c4);
 
       List<CommentInfo> list = getPublishedCommentsAsList(changeId);
-      assertThat(Lists.transform(list, infoToInput(file))).containsExactly(c1, c2, c3, c4);
+      assertThat(list.stream().map(infoToInput(file))).containsExactly(c1, c2, c3, c4);
     }
 
     // for the commit message comments on the auto-merge are not possible
@@ -268,10 +267,10 @@
       revision(r).review(input);
       Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
       assertThat(result).isNotEmpty();
-      assertThat(Lists.transform(result.get(file), infoToInput(file))).containsExactly(c1, c2, c3);
+      assertThat(result.get(file).stream().map(infoToInput(file))).containsExactly(c1, c2, c3);
 
       List<CommentInfo> list = getPublishedCommentsAsList(changeId);
-      assertThat(Lists.transform(list, infoToInput(file))).containsExactly(c1, c2, c3);
+      assertThat(list.stream().map(infoToInput(file))).containsExactly(c1, c2, c3);
     }
   }
 
@@ -282,9 +281,40 @@
     CommentInput c = newComment(Patch.COMMIT_MSG, Side.PARENT, 0, "comment on auto-merge", false);
     input.comments = new HashMap<>();
     input.comments.put(Patch.COMMIT_MSG, ImmutableList.of(c));
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("cannot comment on " + Patch.COMMIT_MSG + " on auto-merge");
-    revision(r).review(input);
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> revision(r).review(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("cannot comment on " + Patch.COMMIT_MSG + " on auto-merge");
+  }
+
+  @Test
+  public void postCommentsReplacingDrafts() throws Exception {
+    String file = "file";
+    PushOneCommit push =
+        pushFactory.create(admin.newIdent(), testRepo, "first subject", file, "contents");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+
+    DraftInput draft = newDraft(file, Side.REVISION, 0, "comment");
+    addDraft(changeId, revId, draft);
+    Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
+    CommentInfo draftInfo = Iterables.getOnlyElement(drafts.get(draft.path));
+
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.drafts = DraftHandling.KEEP;
+    reviewInput.message = "foo";
+    CommentInput comment = newComment(file, Side.REVISION, 0, "comment", false);
+    // Replace the existing draft.
+    comment.id = draftInfo.id;
+    reviewInput.comments = new HashMap<>();
+    reviewInput.comments.put(comment.path, ImmutableList.of(comment));
+    revision(r).review(reviewInput);
+
+    // DraftHandling.KEEP is ignored on publishing a comment.
+    drafts = getDraftComments(changeId, revId);
+    assertThat(drafts).isEmpty();
   }
 
   @Test
@@ -350,12 +380,11 @@
     Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
     assertThat(result).isNotEmpty();
     List<CommentInfo> actualComments = result.get(file);
-    assertThat(Lists.transform(actualComments, infoToInput(file)))
+    assertThat(actualComments.stream().map(infoToInput(file)))
         .containsExactlyElementsIn(expectedComments);
 
     List<CommentInfo> list = getPublishedCommentsAsList(changeId);
-    assertThat(Lists.transform(list, infoToInput(file)))
-        .containsExactlyElementsIn(expectedComments);
+    assertThat(list.stream().map(infoToInput(file))).containsExactlyElementsIn(expectedComments);
   }
 
   /**
@@ -434,7 +463,7 @@
     Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
     assertThat(result).isNotEmpty();
     List<CommentInfo> actualComments = result.get(file);
-    assertThat(Lists.transform(actualComments, infoToDraft(file)))
+    assertThat(actualComments.stream().map(infoToDraft(file)))
         .containsExactlyElementsIn(expectedDrafts);
   }
 
@@ -888,8 +917,9 @@
     DeleteCommentInput input = new DeleteCommentInput("contains confidential information");
 
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.changes().id(result.getChangeId()).current().comment(uuid).delete(input);
+    assertThrows(
+        AuthException.class,
+        () -> gApi.changes().id(result.getChangeId()).current().comment(uuid).delete(input));
   }
 
   @Test
@@ -1047,20 +1077,6 @@
     assertThat(getChangeSortedComments(id.get())).hasSize(3);
   }
 
-  @Test
-  public void jsonCommentHasLegacyFormatFalse() throws Exception {
-    PushOneCommit.Result result = createChange();
-    Change.Id changeId = result.getChange().getId();
-    addComment(result.getChangeId(), "comment");
-
-    Collection<com.google.gerrit.reviewdb.client.Comment> comments =
-        notesFactory.createChecked(project, changeId).getComments().values();
-    assertThat(comments).hasSize(1);
-    com.google.gerrit.reviewdb.client.Comment comment = comments.iterator().next();
-    assertThat(comment.message).isEqualTo("comment");
-    assertThat(comment.legacyFormat).isFalse();
-  }
-
   private List<CommentInfo> getRevisionComments(String changeId, String revId) throws Exception {
     return getPublishedComments(changeId, revId).values().stream()
         .flatMap(List::stream)
@@ -1101,17 +1117,16 @@
         RevCommit commitBefore = beforeDelete.get(i);
         RevCommit commitAfter = afterDelete.get(i);
 
-        Map<String, com.google.gerrit.reviewdb.client.Comment> commentMapBefore =
+        Map<String, com.google.gerrit.entities.Comment> commentMapBefore =
             DeleteCommentRewriter.getPublishedComments(
-                noteUtil, changeId, reader, NoteMap.read(reader, commitBefore));
-        Map<String, com.google.gerrit.reviewdb.client.Comment> commentMapAfter =
+                noteUtil, reader, NoteMap.read(reader, commitBefore));
+        Map<String, com.google.gerrit.entities.Comment> commentMapAfter =
             DeleteCommentRewriter.getPublishedComments(
-                noteUtil, changeId, reader, NoteMap.read(reader, commitAfter));
+                noteUtil, reader, NoteMap.read(reader, commitAfter));
 
         if (commentMapBefore.containsKey(targetCommentUuid)) {
           assertThat(commentMapAfter).containsKey(targetCommentUuid);
-          com.google.gerrit.reviewdb.client.Comment comment =
-              commentMapAfter.get(targetCommentUuid);
+          com.google.gerrit.entities.Comment comment = commentMapAfter.get(targetCommentUuid);
           assertThat(comment.message).isEqualTo(expectedMessage);
           comment.message = commentMapBefore.get(targetCommentUuid).message;
           commentMapAfter.put(targetCommentUuid, comment);
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 7c375bd..1e2d1ba 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -25,14 +25,14 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ProblemInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.ChangeInserter;
@@ -131,7 +131,7 @@
     assertProblems(
         notes,
         null,
-        problem("Ref missing: " + ps.getId().toRefName()),
+        problem("Ref missing: " + ps.id().toRefName()),
         problem("Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
   }
 
@@ -142,7 +142,7 @@
     PatchSet ps = insertMissingPatchSet(notes, rev);
     notes = reload(notes);
 
-    String refName = ps.getId().toRefName();
+    String refName = ps.id().toRefName();
     assertProblems(
         notes,
         new FixInput(),
@@ -153,8 +153,7 @@
   @Test
   public void patchSetRefMissing() throws Exception {
     ChangeNotes notes = insertChange();
-    serverSideTestRepo.update(
-        "refs/other/foo", ObjectId.fromString(psUtil.current(notes).getRevision().get()));
+    serverSideTestRepo.update("refs/other/foo", psUtil.current(notes).commitId());
     String refName = notes.getChange().currentPatchSetId().toRefName();
     deleteRef(refName);
 
@@ -164,15 +163,15 @@
   @Test
   public void patchSetRefMissingWithFix() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(notes).getRevision().get();
-    serverSideTestRepo.update("refs/other/foo", ObjectId.fromString(rev));
+    ObjectId commitId = psUtil.current(notes).commitId();
+    serverSideTestRepo.update("refs/other/foo", commitId);
     String refName = notes.getChange().currentPatchSetId().toRefName();
     deleteRef(refName);
 
     assertProblems(
         notes, new FixInput(), problem("Ref missing: " + refName, FIXED, "Repaired patch set ref"));
-    assertThat(serverSideTestRepo.getRepository().exactRef(refName).getObjectId().name())
-        .isEqualTo(rev);
+    assertThat(serverSideTestRepo.getRepository().exactRef(refName).getObjectId())
+        .isEqualTo(commitId);
   }
 
   @Test
@@ -189,13 +188,13 @@
     assertProblems(
         notes,
         fix,
-        problem("Ref missing: " + ps2.getId().toRefName()),
+        problem("Ref missing: " + ps2.id().toRefName()),
         problem("Object missing: patch set 2: " + rev2, FIXED, "Deleted patch set"));
 
     notes = reload(notes);
     assertThat(notes.getChange().currentPatchSetId().get()).isEqualTo(1);
-    assertThat(psUtil.get(notes, ps1.getId())).isNotNull();
-    assertThat(psUtil.get(notes, ps2.getId())).isNull();
+    assertThat(psUtil.get(notes, ps1.id())).isNotNull();
+    assertThat(psUtil.get(notes, ps2.id())).isNull();
   }
 
   @Test
@@ -218,17 +217,17 @@
     assertProblems(
         notes,
         fix,
-        problem("Ref missing: " + ps2.getId().toRefName()),
+        problem("Ref missing: " + ps2.id().toRefName()),
         problem("Object missing: patch set 2: " + rev2, FIXED, "Deleted patch set"),
-        problem("Ref missing: " + ps4.getId().toRefName()),
+        problem("Ref missing: " + ps4.id().toRefName()),
         problem("Object missing: patch set 4: " + rev4, FIXED, "Deleted patch set"));
 
     notes = reload(notes);
     assertThat(notes.getChange().currentPatchSetId().get()).isEqualTo(3);
-    assertThat(psUtil.get(notes, ps1.getId())).isNotNull();
-    assertThat(psUtil.get(notes, ps2.getId())).isNull();
-    assertThat(psUtil.get(notes, ps3.getId())).isNotNull();
-    assertThat(psUtil.get(notes, ps4.getId())).isNull();
+    assertThat(psUtil.get(notes, ps1.id())).isNotNull();
+    assertThat(psUtil.get(notes, ps2.id())).isNull();
+    assertThat(psUtil.get(notes, ps3.id())).isNotNull();
+    assertThat(psUtil.get(notes, ps4.id())).isNull();
   }
 
   @Test
@@ -245,7 +244,7 @@
             + "\n"
             + "Patch-set: 1\n"
             + "Branch: "
-            + c.getDest().get()
+            + c.getDest().branch()
             + "\n"
             + "Change-id: "
             + c.getKey().get()
@@ -265,7 +264,7 @@
     assertProblems(
         notes,
         fix,
-        problem("Ref missing: " + ps.getId().toRefName()),
+        problem("Ref missing: " + ps.id().toRefName()),
         problem(
             "Object missing: patch set 1: " + rev,
             FIX_FAILED,
@@ -280,13 +279,13 @@
   public void duplicatePatchSetRevisions() throws Exception {
     ChangeNotes notes = insertChange();
     PatchSet ps1 = psUtil.current(notes);
-    String rev = ps1.getRevision().get();
 
-    notes =
-        incrementPatchSet(
-            notes, serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
+    notes = incrementPatchSet(notes, serverSideTestRepo.getRevWalk().parseCommit(ps1.commitId()));
 
-    assertProblems(notes, null, problem("Multiple patch sets pointing to " + rev + ": [1, 2]"));
+    assertProblems(
+        notes,
+        null,
+        problem("Multiple patch sets pointing to " + ps1.commitId().name() + ": [1, 2]"));
   }
 
   @Test
@@ -322,14 +321,13 @@
     }
     notes = reload(notes);
 
-    String rev = psUtil.current(notes).getRevision().get();
     ObjectId tip = getDestRef(notes);
     assertProblems(
         notes,
         null,
         problem(
             "Patch set 1 ("
-                + rev
+                + psUtil.current(notes).commitId().name()
                 + ") is not merged into destination ref"
                 + " refs/heads/master ("
                 + tip.name()
@@ -339,40 +337,40 @@
   @Test
   public void newChangeIsMerged() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(notes).getRevision().get();
+    ObjectId commitId = psUtil.current(notes).commitId();
     serverSideTestRepo
-        .branch(notes.getChange().getDest().get())
-        .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
+        .branch(notes.getChange().getDest().branch())
+        .update(serverSideTestRepo.getRevWalk().parseCommit(commitId));
 
     assertProblems(
         notes,
         null,
         problem(
             "Patch set 1 ("
-                + rev
+                + commitId.name()
                 + ") is merged into destination ref"
                 + " refs/heads/master ("
-                + rev
+                + commitId.name()
                 + "), but change status is NEW"));
   }
 
   @Test
   public void newChangeIsMergedWithFix() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(notes).getRevision().get();
+    ObjectId commitId = psUtil.current(notes).commitId();
     serverSideTestRepo
-        .branch(notes.getChange().getDest().get())
-        .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
+        .branch(notes.getChange().getDest().branch())
+        .update(serverSideTestRepo.getRevWalk().parseCommit(commitId));
 
     assertProblems(
         notes,
         new FixInput(),
         problem(
             "Patch set 1 ("
-                + rev
+                + commitId.name()
                 + ") is merged into destination ref"
                 + " refs/heads/master ("
-                + rev
+                + commitId.name()
                 + "), but change status is NEW",
             FIXED,
             "Marked change as merged"));
@@ -385,10 +383,10 @@
   @Test
   public void extensionApiReturnsUpdatedValueAfterFix() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(notes).getRevision().get();
+    ObjectId commitId = psUtil.current(notes).commitId();
     serverSideTestRepo
-        .branch(notes.getChange().getDest().get())
-        .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
+        .branch(notes.getChange().getDest().branch())
+        .update(serverSideTestRepo.getRevWalk().parseCommit(commitId));
 
     ChangeInfo info = gApi.changes().id(notes.getChangeId().get()).info();
     assertThat(info.status).isEqualTo(ChangeStatus.NEW);
@@ -400,22 +398,22 @@
   @Test
   public void expectedMergedCommitIsLatestPatchSet() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(notes).getRevision().get();
+    ObjectId commitId = psUtil.current(notes).commitId();
     serverSideTestRepo
-        .branch(notes.getChange().getDest().get())
-        .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
+        .branch(notes.getChange().getDest().branch())
+        .update(serverSideTestRepo.getRevWalk().parseCommit(commitId));
 
     FixInput fix = new FixInput();
-    fix.expectMergedAs = rev;
+    fix.expectMergedAs = commitId.name();
     assertProblems(
         notes,
         fix,
         problem(
             "Patch set 1 ("
-                + rev
+                + commitId.name()
                 + ") is merged into destination ref"
                 + " refs/heads/master ("
-                + rev
+                + commitId.name()
                 + "), but change status is NEW",
             FIXED,
             "Marked change as merged"));
@@ -428,9 +426,9 @@
   @Test
   public void expectedMergedCommitNotMergedIntoDestination() throws Exception {
     ChangeNotes notes = insertChange();
-    String rev = psUtil.current(notes).getRevision().get();
-    RevCommit commit = serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
-    serverSideTestRepo.branch(notes.getChange().getDest().get()).update(commit);
+    RevCommit commit =
+        serverSideTestRepo.getRevWalk().parseCommit(psUtil.current(notes).commitId());
+    serverSideTestRepo.branch(notes.getChange().getDest().branch()).update(commit);
 
     FixInput fix = new FixInput();
     RevCommit other = serverSideTestRepo.commit().message(commit.getFullMessage()).create();
@@ -450,9 +448,9 @@
   @Test
   public void createNewPatchSetForExpectedMergeCommitWithNoChangeId() throws Exception {
     ChangeNotes notes = insertChange();
-    String dest = notes.getChange().getDest().get();
-    String rev = psUtil.current(notes).getRevision().get();
-    RevCommit commit = serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
+    String dest = notes.getChange().getDest().branch();
+    RevCommit commit =
+        serverSideTestRepo.getRevWalk().parseCommit(psUtil.current(notes).commitId());
 
     RevCommit mergedAs =
         serverSideTestRepo
@@ -481,9 +479,9 @@
             "Inserted as patch set 2"));
 
     notes = reload(notes);
-    PatchSet.Id psId2 = new PatchSet.Id(notes.getChangeId(), 2);
+    PatchSet.Id psId2 = PatchSet.id(notes.getChangeId(), 2);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId2);
-    assertThat(psUtil.get(notes, psId2).getRevision().get()).isEqualTo(mergedAs.name());
+    assertThat(psUtil.get(notes, psId2).commitId()).isEqualTo(mergedAs);
 
     assertNoProblems(notes, null);
   }
@@ -491,9 +489,9 @@
   @Test
   public void createNewPatchSetForExpectedMergeCommitWithChangeId() throws Exception {
     ChangeNotes notes = insertChange();
-    String dest = notes.getChange().getDest().get();
-    String rev = psUtil.current(notes).getRevision().get();
-    RevCommit commit = serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
+    String dest = notes.getChange().getDest().branch();
+    RevCommit commit =
+        serverSideTestRepo.getRevWalk().parseCommit(psUtil.current(notes).commitId());
 
     RevCommit mergedAs =
         serverSideTestRepo
@@ -529,9 +527,9 @@
             "Inserted as patch set 2"));
 
     notes = reload(notes);
-    PatchSet.Id psId2 = new PatchSet.Id(notes.getChangeId(), 2);
+    PatchSet.Id psId2 = PatchSet.id(notes.getChangeId(), 2);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId2);
-    assertThat(psUtil.get(notes, psId2).getRevision().get()).isEqualTo(mergedAs.name());
+    assertThat(psUtil.get(notes, psId2).commitId()).isEqualTo(mergedAs);
 
     assertNoProblems(notes, null);
   }
@@ -539,41 +537,43 @@
   @Test
   public void expectedMergedCommitIsOldPatchSetOfSameChange() throws Exception {
     ChangeNotes notes = insertChange();
-    PatchSet ps1 = psUtil.current(notes);
-    String rev1 = ps1.getRevision().get();
+    ObjectId commitId1 = psUtil.current(notes).commitId();
     notes = incrementPatchSet(notes);
     PatchSet ps2 = psUtil.current(notes);
     serverSideTestRepo
-        .branch(notes.getChange().getDest().get())
-        .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev1)));
+        .branch(notes.getChange().getDest().branch())
+        .update(serverSideTestRepo.getRevWalk().parseCommit(commitId1));
 
     FixInput fix = new FixInput();
-    fix.expectMergedAs = rev1;
+    fix.expectMergedAs = commitId1.name();
     assertProblems(
         notes,
         fix,
-        problem("No patch set found for merged commit " + rev1, FIXED, "Marked change as merged"),
+        problem(
+            "No patch set found for merged commit " + commitId1.name(),
+            FIXED,
+            "Marked change as merged"),
         problem(
             "Expected merge commit "
-                + rev1
+                + commitId1.name()
                 + " corresponds to patch set 1,"
                 + " not the current patch set 2",
             FIXED,
             "Deleted patch set"),
         problem(
             "Expected merge commit "
-                + rev1
+                + commitId1.name()
                 + " corresponds to patch set 1,"
                 + " not the current patch set 2",
             FIXED,
             "Inserted as patch set 3"));
 
     notes = reload(notes);
-    PatchSet.Id psId3 = new PatchSet.Id(notes.getChangeId(), 3);
+    PatchSet.Id psId3 = PatchSet.id(notes.getChangeId(), 3);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId3);
     assertThat(notes.getChange().isMerged()).isTrue();
-    assertThat(psUtil.byChangeAsMap(notes).keySet()).containsExactly(ps2.getId(), psId3);
-    assertThat(psUtil.get(notes, psId3).getRevision().get()).isEqualTo(rev1);
+    assertThat(psUtil.byChangeAsMap(notes).keySet()).containsExactly(ps2.id(), psId3);
+    assertThat(psUtil.get(notes, psId3).commitId()).isEqualTo(commitId1);
   }
 
   @Test
@@ -582,47 +582,46 @@
     PatchSet ps1 = psUtil.current(notes);
 
     // Create dangling ref so next ID in the database becomes 3.
-    PatchSet.Id psId2 = new PatchSet.Id(notes.getChangeId(), 2);
+    PatchSet.Id psId2 = PatchSet.id(notes.getChangeId(), 2);
     RevCommit commit2 = patchSetCommit(psId2);
-    String rev2 = commit2.name();
     serverSideTestRepo.branch(psId2.toRefName()).update(commit2);
 
     notes = incrementPatchSet(notes);
     PatchSet ps3 = psUtil.current(notes);
-    assertThat(ps3.getId().get()).isEqualTo(3);
+    assertThat(ps3.id().get()).isEqualTo(3);
 
-    serverSideTestRepo
-        .branch(notes.getChange().getDest().get())
-        .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev2)));
+    serverSideTestRepo.branch(notes.getChange().getDest().branch()).update(commit2);
 
     FixInput fix = new FixInput();
-    fix.expectMergedAs = rev2;
+    fix.expectMergedAs = commit2.name();
     assertProblems(
         notes,
         fix,
-        problem("No patch set found for merged commit " + rev2, FIXED, "Marked change as merged"),
+        problem(
+            "No patch set found for merged commit " + commit2.name(),
+            FIXED,
+            "Marked change as merged"),
         problem(
             "Expected merge commit "
-                + rev2
+                + commit2.name()
                 + " corresponds to patch set 2,"
                 + " not the current patch set 3",
             FIXED,
             "Deleted patch set"),
         problem(
             "Expected merge commit "
-                + rev2
+                + commit2.name()
                 + " corresponds to patch set 2,"
                 + " not the current patch set 3",
             FIXED,
             "Inserted as patch set 4"));
 
     notes = reload(notes);
-    PatchSet.Id psId4 = new PatchSet.Id(notes.getChangeId(), 4);
+    PatchSet.Id psId4 = PatchSet.id(notes.getChangeId(), 4);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId4);
     assertThat(notes.getChange().isMerged()).isTrue();
-    assertThat(psUtil.byChangeAsMap(notes).keySet())
-        .containsExactly(ps1.getId(), ps3.getId(), psId4);
-    assertThat(psUtil.get(notes, psId4).getRevision().get()).isEqualTo(rev2);
+    assertThat(psUtil.byChangeAsMap(notes).keySet()).containsExactly(ps1.id(), ps3.id(), psId4);
+    assertThat(psUtil.get(notes, psId4).commitId()).isEqualTo(commit2);
   }
 
   @Test
@@ -631,24 +630,24 @@
     PatchSet ps1 = psUtil.current(notes);
 
     // Create dangling ref with no patch set.
-    PatchSet.Id psId2 = new PatchSet.Id(notes.getChangeId(), 2);
+    PatchSet.Id psId2 = PatchSet.id(notes.getChangeId(), 2);
     RevCommit commit2 = patchSetCommit(psId2);
-    String rev2 = commit2.name();
     serverSideTestRepo.branch(psId2.toRefName()).update(commit2);
 
-    serverSideTestRepo
-        .branch(notes.getChange().getDest().get())
-        .update(serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev2)));
+    serverSideTestRepo.branch(notes.getChange().getDest().branch()).update(commit2);
 
     FixInput fix = new FixInput();
-    fix.expectMergedAs = rev2;
+    fix.expectMergedAs = commit2.name();
     assertProblems(
         notes,
         fix,
-        problem("No patch set found for merged commit " + rev2, FIXED, "Marked change as merged"),
+        problem(
+            "No patch set found for merged commit " + commit2.name(),
+            FIXED,
+            "Marked change as merged"),
         problem(
             "Expected merge commit "
-                + rev2
+                + commit2.name()
                 + " corresponds to patch set 2,"
                 + " not the current patch set 1",
             FIXED,
@@ -657,17 +656,17 @@
     notes = reload(notes);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId2);
     assertThat(notes.getChange().isMerged()).isTrue();
-    assertThat(psUtil.byChangeAsMap(notes).keySet()).containsExactly(ps1.getId(), psId2);
-    assertThat(psUtil.get(notes, psId2).getRevision().get()).isEqualTo(rev2);
+    assertThat(psUtil.byChangeAsMap(notes).keySet()).containsExactly(ps1.id(), psId2);
+    assertThat(psUtil.get(notes, psId2).commitId()).isEqualTo(commit2);
   }
 
   @Test
   public void expectedMergedCommitWithMismatchedChangeId() throws Exception {
     ChangeNotes notes = insertChange();
-    String dest = notes.getChange().getDest().get();
+    String dest = notes.getChange().getDest().branch();
     RevCommit parent = serverSideTestRepo.branch(dest).commit().message("parent").create();
-    String rev = psUtil.current(notes).getRevision().get();
-    RevCommit commit = serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
+    RevCommit commit =
+        serverSideTestRepo.getRevWalk().parseCommit(psUtil.current(notes).commitId());
     serverSideTestRepo.branch(dest).update(commit);
 
     String badId = "I0000000000000000000000000000000000000000";
@@ -700,19 +699,19 @@
   @Test
   public void expectedMergedCommitMatchesMultiplePatchSets() throws Exception {
     ChangeNotes notes1 = insertChange();
-    PatchSet.Id psId1 = psUtil.current(notes1).getId();
-    String dest = notes1.getChange().getDest().get();
-    String rev = psUtil.current(notes1).getRevision().get();
-    RevCommit commit = serverSideTestRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
+    PatchSet.Id psId1 = psUtil.current(notes1).id();
+    String dest = notes1.getChange().getDest().branch();
+    RevCommit commit =
+        serverSideTestRepo.getRevWalk().parseCommit(psUtil.current(notes1).commitId());
     serverSideTestRepo.branch(dest).update(commit);
 
     ChangeNotes notes2 = insertChange();
     notes2 = incrementPatchSet(notes2, commit);
-    PatchSet.Id psId2 = psUtil.current(notes2).getId();
+    PatchSet.Id psId2 = psUtil.current(notes2).id();
 
     ChangeNotes notes3 = insertChange();
     notes3 = incrementPatchSet(notes3, commit);
-    PatchSet.Id psId3 = psUtil.current(notes3).getId();
+    PatchSet.Id psId3 = psUtil.current(notes3).id();
 
     FixInput fix = new FixInput();
     fix.expectMergedAs = commit.name();
@@ -744,10 +743,10 @@
   }
 
   private ChangeNotes insertChange(TestAccount owner, String dest) throws Exception {
-    Change.Id id = new Change.Id(sequences.nextChangeId());
+    Change.Id id = Change.id(sequences.nextChangeId());
     ChangeInserter ins;
     try (BatchUpdate bu = newUpdate(owner.id())) {
-      RevCommit commit = patchSetCommit(new PatchSet.Id(id, 1));
+      RevCommit commit = patchSetCommit(PatchSet.id(id, 1));
       bu.setNotify(NotifyResolver.Result.none());
       ins =
           changeInserterFactory
@@ -842,14 +841,14 @@
   private ObjectId getDestRef(ChangeNotes notes) throws Exception {
     return serverSideTestRepo
         .getRepository()
-        .exactRef(notes.getChange().getDest().get())
+        .exactRef(notes.getChange().getDest().branch())
         .getObjectId();
   }
 
   private ChangeNotes mergeChange(ChangeNotes notes) throws Exception {
-    final ObjectId oldId = getDestRef(notes);
-    final ObjectId newId = ObjectId.fromString(psUtil.current(notes).getRevision().get());
-    final String dest = notes.getChange().getDest().get();
+    ObjectId oldId = getDestRef(notes);
+    ObjectId newId = psUtil.current(notes).commitId();
+    String dest = notes.getChange().getDest().branch();
 
     try (BatchUpdate bu = newUpdate(adminId)) {
       bu.addOp(
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index b9b7ab3..e369d1b 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -15,35 +15,36 @@
 package com.google.gerrit.acceptance.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.truth.Correspondence;
-import com.google.common.truth.Correspondence.BinaryPredicate;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.change.GetRelated;
@@ -52,7 +53,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -64,11 +64,11 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
+@UseClockStep
+@UseTimezone(timezone = "US/Eastern")
 public class GetRelatedIT extends AbstractDaemonTest {
   private static final int MAX_TERMS = 10;
 
@@ -81,22 +81,9 @@
 
   @Inject private AccountOperations accountOperations;
   @Inject private GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  private String systemTimeZone;
-
-  @Before
-  public void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
-
   @Inject private IndexConfig indexConfig;
   @Inject private ChangesCollection changes;
 
@@ -129,14 +116,14 @@
     testRepo.reset(c1_1);
     pushHead(testRepo, "refs/for/master", false);
     PatchSet.Id ps1_1 = getPatchSetId(c1_1);
-    String oldETag = changes.parse(ps1_1.getParentKey()).getETag();
+    String oldETag = changes.parse(ps1_1.changeId()).getETag();
 
     testRepo.reset(c2_1);
     pushHead(testRepo, "refs/for/master", false);
     PatchSet.Id ps2_1 = getPatchSetId(c2_1);
 
     // Push of change 2 should not affect groups (or anything else) of change 1.
-    assertThat(changes.parse(ps1_1.getParentKey()).getETag()).isEqualTo(oldETag);
+    assertThat(changes.parse(ps1_1.changeId()).getETag()).isEqualTo(oldETag);
 
     for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
       assertRelated(ps, changeAndCommit(ps2_1, c2_1, 1), changeAndCommit(ps1_1, c1_1, 1));
@@ -498,7 +485,7 @@
 
     PatchSet.Id ps1_1 = getPatchSetId(c1_1);
     PatchSet.Id ps2_1 = getPatchSetId(c2_1);
-    PatchSet.Id ps2_edit = new PatchSet.Id(ch2.getId(), 0);
+    PatchSet.Id ps2_edit = PatchSet.id(ch2.getId(), 0);
     PatchSet.Id ps3_1 = getPatchSetId(c3_1);
 
     for (PatchSet.Id ps : ImmutableList.of(ps1_1, ps2_1, ps3_1)) {
@@ -512,7 +499,7 @@
     assertRelated(
         ps2_edit,
         changeAndCommit(ps3_1, c3_1, 1),
-        changeAndCommit(new PatchSet.Id(ch2.getId(), 0), editRev, 1),
+        changeAndCommit(PatchSet.id(ch2.getId(), 0), editRev, 1),
         changeAndCommit(ps1_1, c1_1, 1));
   }
 
@@ -533,7 +520,7 @@
 
     // Pretend PS1,1 was pushed before the groups field was added.
     clearGroups(psId1_1);
-    indexer.index(changeDataFactory.create(project, psId1_1.getParentKey()));
+    indexer.index(changeDataFactory.create(project, psId1_1.changeId()));
 
     // PS1,1 has no groups, so disappeared from related changes.
     assertRelated(psId2_1);
@@ -568,7 +555,7 @@
 
     PatchSet.Id psId1_1 = getPatchSetId(c1_1);
     PatchSet.Id psId2_1 = getPatchSetId(c2_1);
-    PatchSet.Id psId2_2 = new PatchSet.Id(psId2_1.changeId, psId2_1.get() + 1);
+    PatchSet.Id psId2_2 = PatchSet.id(psId2_1.changeId(), psId2_1.get() + 1);
 
     assertRelated(psId2_2, changeAndCommit(psId2_2, c2_2, 2), changeAndCommit(psId1_1, c1_1, 1));
   }
@@ -611,11 +598,10 @@
 
     Account.Id accountId = accountOperations.newAccount().create();
     AccountGroup.UUID groupUuid = groupOperations.newGroup().addMember(accountId).create();
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      PermissionRule rule = Util.allow(u.getConfig(), GlobalCapability.QUERY_LIMIT, groupUuid);
-      rule.setRange(0, 2);
-      u.save();
-    }
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.QUERY_LIMIT).group(groupUuid).range(0, 2))
+        .update();
     requestScopeOperations.setApiUser(accountId);
 
     assertRelated(lastPsId, expected);
@@ -651,13 +637,8 @@
   private static Correspondence<RelatedChangeAndCommitInfo, String>
       getRelatedChangeToStatusCorrespondence() {
     return Correspondence.from(
-        new BinaryPredicate<RelatedChangeAndCommitInfo, String>() {
-          @Override
-          public boolean apply(
-              RelatedChangeAndCommitInfo relatedChangeAndCommitInfo, String status) {
-            return Objects.equals(relatedChangeAndCommitInfo.status, status);
-          }
-        },
+        (relatedChangeAndCommitInfo, status) ->
+            Objects.equals(relatedChangeAndCommitInfo.status, status),
         "has status");
   }
 
@@ -678,7 +659,7 @@
       PatchSet.Id psId, ObjectId commitId, int currentRevisionNum) {
     RelatedChangeAndCommitInfo result = new RelatedChangeAndCommitInfo();
     result.project = project.get();
-    result._changeNumber = psId.getParentKey().get();
+    result._changeNumber = psId.changeId().get();
     result.commit = new CommitInfo();
     result.commit.commit = commitId.name();
     result._revisionNumber = psId.get();
@@ -690,12 +671,11 @@
   private void clearGroups(PatchSet.Id psId) throws Exception {
     try (BatchUpdate bu = batchUpdateFactory.create(project, user(user), TimeUtil.nowTs())) {
       bu.addOp(
-          psId.getParentKey(),
+          psId.changeId(),
           new BatchUpdateOp() {
             @Override
             public boolean updateChange(ChangeContext ctx) {
-              PatchSet ps = psUtil.get(ctx.getNotes(), psId);
-              psUtil.setGroups(ctx.getUpdate(psId), ps, ImmutableList.of());
+              ctx.getUpdate(psId).setGroups(ImmutableList.of());
               return true;
             }
           });
@@ -711,19 +691,19 @@
   private void assertRelated(PatchSet.Id psId, List<RelatedChangeAndCommitInfo> expected)
       throws Exception {
     List<RelatedChangeAndCommitInfo> actual =
-        gApi.changes().id(psId.getParentKey().get()).revision(psId.get()).related().changes;
-    assertThat(actual).named("related to " + psId).hasSize(expected.size());
+        gApi.changes().id(psId.changeId().get()).revision(psId.get()).related().changes;
+    assertWithMessage("related to " + psId).that(actual).hasSize(expected.size());
     for (int i = 0; i < actual.size(); i++) {
       String name = "index " + i + " related to " + psId;
       RelatedChangeAndCommitInfo a = actual.get(i);
       RelatedChangeAndCommitInfo e = expected.get(i);
-      assertThat(a.project).named("project of " + name).isEqualTo(e.project);
-      assertThat(a._changeNumber).named("change ID of " + name).isEqualTo(e._changeNumber);
+      assertWithMessage("project of " + name).that(a.project).isEqualTo(e.project);
+      assertWithMessage("change ID of " + name).that(a._changeNumber).isEqualTo(e._changeNumber);
       // Don't bother checking changeId; assume _changeNumber is sufficient.
-      assertThat(a._revisionNumber).named("revision of " + name).isEqualTo(e._revisionNumber);
-      assertThat(a.commit.commit).named("commit of " + name).isEqualTo(e.commit.commit);
-      assertThat(a._currentRevisionNumber)
-          .named("current revision of " + name)
+      assertWithMessage("revision of " + name).that(a._revisionNumber).isEqualTo(e._revisionNumber);
+      assertWithMessage("commit of " + name).that(a.commit.commit).isEqualTo(e.commit.commit);
+      assertWithMessage("current revision of " + name)
+          .that(a._currentRevisionNumber)
           .isEqualTo(e._currentRevisionNumber);
       assertThat(a.status).isEqualTo(e.status);
     }
diff --git a/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
index 42fdae4..b23f9a3 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -25,9 +25,9 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Patch.ChangeType;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.server.patch.IntraLineDiff;
 import com.google.gerrit.server.patch.IntraLineDiffArgs;
 import com.google.gerrit.server.patch.IntraLineDiffKey;
diff --git a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index 389859c..445f787 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -23,13 +23,13 @@
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
 import java.util.EnumSet;
@@ -113,7 +113,7 @@
 
   @Test
   public void respectWholeTopic() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     // Create two independent commits and push.
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
     String id1 = getChangeId(c1_1);
@@ -135,7 +135,7 @@
 
   @Test
   public void anonymousWholeTopic() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     RevCommit a = commitBuilder().add("a", "1").message("change 1").create();
     pushHead(testRepo, "refs/for/master/" + name("topic"), false);
     String id1 = getChangeId(a);
@@ -157,7 +157,7 @@
 
   @Test
   public void topicChaining() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
     String id1 = getChangeId(c1_1);
@@ -185,7 +185,7 @@
 
   @Test
   public void respectTopicsOnAncestors() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
     String id1 = getChangeId(c1_1);
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/event/CommentAddedEventIT.java b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
index 4f98dd0..8469fff 100644
--- a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
@@ -15,71 +15,48 @@
 package com.google.gerrit.acceptance.server.event;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.events.CommentAddedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class CommentAddedEventIT extends AbstractDaemonTest {
 
-  @Inject private DynamicSet<CommentAddedListener> source;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   private final LabelType label =
-      category("CustomLabel", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+      label("CustomLabel", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
 
   private final LabelType pLabel =
-      category("CustomLabel2", value(1, "Positive"), value(0, "No score"));
-
-  private RegistrationHandle eventListenerRegistration;
-  private CommentAddedListener.Event lastCommentAddedEvent;
+      label("CustomLabel2", value(1, "Positive"), value(0, "No score"));
 
   @Before
   public void setUp() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(label.getName()),
-          -1,
-          1,
-          anonymousUsers,
-          "refs/heads/*");
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(pLabel.getName()),
-          0,
-          1,
-          anonymousUsers,
-          "refs/heads/*");
-      u.save();
-    }
-
-    eventListenerRegistration = source.add("gerrit", event -> lastCommentAddedEvent = event);
-  }
-
-  @After
-  public void cleanup() {
-    eventListenerRegistration.remove();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(label.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .add(allowLabel(pLabel.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(0, 1))
+        .update();
   }
 
   private void saveLabelConfig() throws Exception {
@@ -90,16 +67,30 @@
     }
   }
 
+  private static class TestListener implements CommentAddedListener {
+    private CommentAddedListener.Event lastCommentAddedEvent;
+
+    @Override
+    public void onCommentAdded(Event event) {
+      lastCommentAddedEvent = event;
+    }
+
+    public CommentAddedListener.Event getLastCommentAddedEvent() {
+      assertThat(lastCommentAddedEvent).isNotNull();
+      return lastCommentAddedEvent;
+    }
+  }
+
   /* Need to lookup info for the label under test since there can be multiple
    * labels defined.  By default Gerrit already has a Code-Review label.
    */
-  private ApprovalValues getApprovalValues(LabelType label) {
+  private ApprovalValues getApprovalValues(LabelType label, TestListener listener) {
     ApprovalValues res = new ApprovalValues();
-    ApprovalInfo info = lastCommentAddedEvent.getApprovals().get(label.getName());
+    ApprovalInfo info = listener.getLastCommentAddedEvent().getApprovals().get(label.getName());
     if (info != null) {
       res.value = info.value;
     }
-    info = lastCommentAddedEvent.getOldApprovals().get(label.getName());
+    info = listener.getLastCommentAddedEvent().getOldApprovals().get(label.getName());
     if (info != null) {
       res.oldValue = info.value;
     }
@@ -110,15 +101,18 @@
   public void newChangeWithVote() throws Exception {
     saveLabelConfig();
 
-    // push a new change with -1 vote
-    PushOneCommit.Result r = createChange();
-    ReviewInput reviewInput = new ReviewInput().label(label.getName(), (short) -1);
-    revision(r).review(reviewInput);
-    ApprovalValues attr = getApprovalValues(label);
-    assertThat(attr.oldValue).isEqualTo(0);
-    assertThat(attr.value).isEqualTo(-1);
-    assertThat(lastCommentAddedEvent.getComment())
-        .isEqualTo(String.format("Patch Set 1: %s-1", label.getName()));
+    TestListener listener = new TestListener();
+    try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+      // push a new change with -1 vote
+      PushOneCommit.Result r = createChange();
+      ReviewInput reviewInput = new ReviewInput().label(label.getName(), (short) -1);
+      revision(r).review(reviewInput);
+      ApprovalValues attr = getApprovalValues(label, listener);
+      assertThat(attr.oldValue).isEqualTo(0);
+      assertThat(attr.value).isEqualTo(-1);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1: %s-1", label.getName()));
+    }
   }
 
   @Test
@@ -129,17 +123,19 @@
     PushOneCommit.Result r = createChange();
     ReviewInput reviewInput = new ReviewInput().message(label.getName());
     revision(r).review(reviewInput);
-
-    // push a new revision with +1 vote
-    ChangeInfo c = info(r.getChangeId());
-    r = amendChange(c.changeId);
-    reviewInput = new ReviewInput().label(label.getName(), (short) 1);
-    revision(r).review(reviewInput);
-    ApprovalValues attr = getApprovalValues(label);
-    assertThat(attr.oldValue).isEqualTo(0);
-    assertThat(attr.value).isEqualTo(1);
-    assertThat(lastCommentAddedEvent.getComment())
-        .isEqualTo(String.format("Patch Set 2: %s+1", label.getName()));
+    TestListener listener = new TestListener();
+    try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+      // push a new revision with +1 vote
+      ChangeInfo c = info(r.getChangeId());
+      r = amendChange(c.changeId);
+      reviewInput = new ReviewInput().label(label.getName(), (short) 1);
+      revision(r).review(reviewInput);
+      ApprovalValues attr = getApprovalValues(label, listener);
+      assertThat(attr.oldValue).isEqualTo(0);
+      assertThat(attr.value).isEqualTo(1);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 2: %s+1", label.getName()));
+    }
   }
 
   @Test
@@ -149,114 +145,120 @@
     // push a change
     PushOneCommit.Result r = createChange();
 
-    // review with message only, do not apply votes
-    ReviewInput reviewInput = new ReviewInput().message(label.getName());
-    revision(r).review(reviewInput);
-    // reply message only so vote is shown as 0
-    ApprovalValues attr = getApprovalValues(label);
-    assertThat(attr.oldValue).isNull();
-    assertThat(attr.value).isEqualTo(0);
-    assertThat(lastCommentAddedEvent.getComment())
-        .isEqualTo(String.format("Patch Set 1:\n\n%s", label.getName()));
+    TestListener listener = new TestListener();
+    try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+      // review with message only, do not apply votes
+      ReviewInput reviewInput = new ReviewInput().message(label.getName());
+      revision(r).review(reviewInput);
+      // reply message only so vote is shown as 0
+      ApprovalValues attr = getApprovalValues(label, listener);
+      assertThat(attr.oldValue).isNull();
+      assertThat(attr.value).isEqualTo(0);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1:\n\n%s", label.getName()));
 
-    // transition from un-voted to -1 vote
-    reviewInput = new ReviewInput().label(label.getName(), -1);
-    revision(r).review(reviewInput);
-    attr = getApprovalValues(label);
-    assertThat(attr.oldValue).isEqualTo(0);
-    assertThat(attr.value).isEqualTo(-1);
-    assertThat(lastCommentAddedEvent.getComment())
-        .isEqualTo(String.format("Patch Set 1: %s-1", label.getName()));
+      // transition from un-voted to -1 vote
+      reviewInput = new ReviewInput().label(label.getName(), -1);
+      revision(r).review(reviewInput);
+      attr = getApprovalValues(label, listener);
+      assertThat(attr.oldValue).isEqualTo(0);
+      assertThat(attr.value).isEqualTo(-1);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1: %s-1", label.getName()));
 
-    // transition vote from -1 to 0
-    reviewInput = new ReviewInput().label(label.getName(), 0);
-    revision(r).review(reviewInput);
-    attr = getApprovalValues(label);
-    assertThat(attr.oldValue).isEqualTo(-1);
-    assertThat(attr.value).isEqualTo(0);
-    assertThat(lastCommentAddedEvent.getComment())
-        .isEqualTo(String.format("Patch Set 1: -%s", label.getName()));
+      // transition vote from -1 to 0
+      reviewInput = new ReviewInput().label(label.getName(), 0);
+      revision(r).review(reviewInput);
+      attr = getApprovalValues(label, listener);
+      assertThat(attr.oldValue).isEqualTo(-1);
+      assertThat(attr.value).isEqualTo(0);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1: -%s", label.getName()));
 
-    // transition vote from 0 to 1
-    reviewInput = new ReviewInput().label(label.getName(), 1);
-    revision(r).review(reviewInput);
-    attr = getApprovalValues(label);
-    assertThat(attr.oldValue).isEqualTo(0);
-    assertThat(attr.value).isEqualTo(1);
-    assertThat(lastCommentAddedEvent.getComment())
-        .isEqualTo(String.format("Patch Set 1: %s+1", label.getName()));
+      // transition vote from 0 to 1
+      reviewInput = new ReviewInput().label(label.getName(), 1);
+      revision(r).review(reviewInput);
+      attr = getApprovalValues(label, listener);
+      assertThat(attr.oldValue).isEqualTo(0);
+      assertThat(attr.value).isEqualTo(1);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1: %s+1", label.getName()));
 
-    // transition vote from 1 to -1
-    reviewInput = new ReviewInput().label(label.getName(), -1);
-    revision(r).review(reviewInput);
-    attr = getApprovalValues(label);
-    assertThat(attr.oldValue).isEqualTo(1);
-    assertThat(attr.value).isEqualTo(-1);
-    assertThat(lastCommentAddedEvent.getComment())
-        .isEqualTo(String.format("Patch Set 1: %s-1", label.getName()));
+      // transition vote from 1 to -1
+      reviewInput = new ReviewInput().label(label.getName(), -1);
+      revision(r).review(reviewInput);
+      attr = getApprovalValues(label, listener);
+      assertThat(attr.oldValue).isEqualTo(1);
+      assertThat(attr.value).isEqualTo(-1);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1: %s-1", label.getName()));
 
-    // review with message only, do not apply votes
-    reviewInput = new ReviewInput().message(label.getName());
-    revision(r).review(reviewInput);
-    attr = getApprovalValues(label);
-    assertThat(attr.oldValue).isNull(); // no vote change so not included
-    assertThat(attr.value).isEqualTo(-1);
-    assertThat(lastCommentAddedEvent.getComment())
-        .isEqualTo(String.format("Patch Set 1:\n\n%s", label.getName()));
+      // review with message only, do not apply votes
+      reviewInput = new ReviewInput().message(label.getName());
+      revision(r).review(reviewInput);
+      attr = getApprovalValues(label, listener);
+      assertThat(attr.oldValue).isNull(); // no vote change so not included
+      assertThat(attr.value).isEqualTo(-1);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1:\n\n%s", label.getName()));
+    }
   }
 
   @Test
   public void reviewChange_MultipleVotes() throws Exception {
-    saveLabelConfig();
-    PushOneCommit.Result r = createChange();
-    ReviewInput reviewInput = new ReviewInput().label(label.getName(), -1);
-    reviewInput.message = label.getName();
-    revision(r).review(reviewInput);
+    TestListener listener = new TestListener();
+    try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+      saveLabelConfig();
+      PushOneCommit.Result r = createChange();
+      ReviewInput reviewInput = new ReviewInput().label(label.getName(), -1);
+      reviewInput.message = label.getName();
+      revision(r).review(reviewInput);
 
-    ChangeInfo c = get(r.getChangeId(), DETAILED_LABELS);
-    LabelInfo q = c.labels.get(label.getName());
-    assertThat(q.all).hasSize(1);
-    ApprovalValues labelAttr = getApprovalValues(label);
-    assertThat(labelAttr.oldValue).isEqualTo(0);
-    assertThat(labelAttr.value).isEqualTo(-1);
-    assertThat(lastCommentAddedEvent.getComment())
-        .isEqualTo(String.format("Patch Set 1: %s-1\n\n%s", label.getName(), label.getName()));
+      ChangeInfo c = get(r.getChangeId(), DETAILED_LABELS);
+      LabelInfo q = c.labels.get(label.getName());
+      assertThat(q.all).hasSize(1);
+      ApprovalValues labelAttr = getApprovalValues(label, listener);
+      assertThat(labelAttr.oldValue).isEqualTo(0);
+      assertThat(labelAttr.value).isEqualTo(-1);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1: %s-1\n\n%s", label.getName(), label.getName()));
 
-    // there should be 3 approval labels (label, pLabel, and CRVV)
-    assertThat(lastCommentAddedEvent.getApprovals()).hasSize(3);
+      // there should be 3 approval labels (label, pLabel, and CRVV)
+      assertThat(listener.getLastCommentAddedEvent().getApprovals()).hasSize(3);
 
-    // check the approvals that were not voted on
-    ApprovalValues pLabelAttr = getApprovalValues(pLabel);
-    assertThat(pLabelAttr.oldValue).isNull();
-    assertThat(pLabelAttr.value).isEqualTo(0);
+      // check the approvals that were not voted on
+      ApprovalValues pLabelAttr = getApprovalValues(pLabel, listener);
+      assertThat(pLabelAttr.oldValue).isNull();
+      assertThat(pLabelAttr.value).isEqualTo(0);
 
-    LabelType crLabel = LabelType.withDefaultValues("Code-Review");
-    ApprovalValues crlAttr = getApprovalValues(crLabel);
-    assertThat(crlAttr.oldValue).isNull();
-    assertThat(crlAttr.value).isEqualTo(0);
+      LabelType crLabel = LabelType.withDefaultValues("Code-Review");
+      ApprovalValues crlAttr = getApprovalValues(crLabel, listener);
+      assertThat(crlAttr.oldValue).isNull();
+      assertThat(crlAttr.value).isEqualTo(0);
 
-    // update pLabel approval
-    reviewInput = new ReviewInput().label(pLabel.getName(), 1);
-    reviewInput.message = pLabel.getName();
-    revision(r).review(reviewInput);
+      // update pLabel approval
+      reviewInput = new ReviewInput().label(pLabel.getName(), 1);
+      reviewInput.message = pLabel.getName();
+      revision(r).review(reviewInput);
 
-    c = get(r.getChangeId(), DETAILED_LABELS);
-    q = c.labels.get(label.getName());
-    assertThat(q.all).hasSize(1);
-    pLabelAttr = getApprovalValues(pLabel);
-    assertThat(pLabelAttr.oldValue).isEqualTo(0);
-    assertThat(pLabelAttr.value).isEqualTo(1);
-    assertThat(lastCommentAddedEvent.getComment())
-        .isEqualTo(String.format("Patch Set 1: %s+1\n\n%s", pLabel.getName(), pLabel.getName()));
+      c = get(r.getChangeId(), DETAILED_LABELS);
+      q = c.labels.get(label.getName());
+      assertThat(q.all).hasSize(1);
+      pLabelAttr = getApprovalValues(pLabel, listener);
+      assertThat(pLabelAttr.oldValue).isEqualTo(0);
+      assertThat(pLabelAttr.value).isEqualTo(1);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1: %s+1\n\n%s", pLabel.getName(), pLabel.getName()));
 
-    // check the approvals that were not voted on
-    labelAttr = getApprovalValues(label);
-    assertThat(labelAttr.oldValue).isNull();
-    assertThat(labelAttr.value).isEqualTo(-1);
+      // check the approvals that were not voted on
+      labelAttr = getApprovalValues(label, listener);
+      assertThat(labelAttr.oldValue).isNull();
+      assertThat(labelAttr.value).isEqualTo(-1);
 
-    crlAttr = getApprovalValues(crLabel);
-    assertThat(crlAttr.oldValue).isNull();
-    assertThat(crlAttr.value).isEqualTo(0);
+      crlAttr = getApprovalValues(crLabel, listener);
+      assertThat(crlAttr.oldValue).isNull();
+      assertThat(crlAttr.value).isEqualTo(0);
+    }
   }
 
   private static class ApprovalValues {
diff --git a/javatests/com/google/gerrit/acceptance/server/event/EventPayloadIT.java b/javatests/com/google/gerrit/acceptance/server/event/EventPayloadIT.java
new file mode 100644
index 0000000..8744cfad
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/event/EventPayloadIT.java
@@ -0,0 +1,64 @@
+// 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.server.event;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.events.RevisionCreatedListener;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+@NoHttpd
+public class EventPayloadIT extends AbstractDaemonTest {
+  @Inject private ExtensionRegistry extensionRegistry;
+
+  @Test
+  public void defaultOptions() throws Exception {
+    RevisionCreatedListener listener =
+        new RevisionCreatedListener() {
+          @Override
+          public void onRevisionCreated(Event event) {
+            assertThat(event.getChange().submittable).isNotNull();
+            assertThat(event.getRevision().files).isNotEmpty();
+          }
+        };
+    try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+      createChange();
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "event.payload.listChangeOptions", value = "SKIP_MERGEABLE")
+  public void configuredOptions() throws Exception {
+    RevisionCreatedListener listener =
+        new RevisionCreatedListener() {
+          @Override
+          public void onRevisionCreated(Event event) {
+            assertThat(event.getChange().submittable).isNull();
+            assertThat(event.getChange().mergeable).isNull();
+            assertThat(event.getRevision().files).isNull();
+            assertThat(event.getChange().subject).isNotEmpty();
+          }
+        };
+    try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+      createChange();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/git/receive/BUILD b/javatests/com/google/gerrit/acceptance/server/git/receive/BUILD
new file mode 100644
index 0000000..760e7f4
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/git/receive/BUILD
@@ -0,0 +1,8 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "receive",
+    labels = ["server"],
+    deps = [],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
new file mode 100644
index 0000000..6677583
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
@@ -0,0 +1,136 @@
+// 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.server.git.receive;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentForValidation.CommentType;
+import com.google.gerrit.extensions.validators.CommentValidator;
+import com.google.gerrit.testing.TestCommentHelper;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+
+/**
+ * Tests for comment validation when publishing drafts via the {@code --publish-comments} option.
+ */
+public class ReceiveCommitsCommentValidationIT extends AbstractDaemonTest {
+  @Inject private CommentValidator mockCommentValidator;
+  @Inject private TestCommentHelper testCommentHelper;
+
+  private static final String COMMENT_TEXT = "The comment text";
+
+  @Captor private ArgumentCaptor<ImmutableList<CommentForValidation>> capture;
+
+  @Override
+  public Module createModule() {
+    return new FactoryModule() {
+      @Override
+      public void configure() {
+        CommentValidator mockCommentValidator = mock(CommentValidator.class);
+        bind(CommentValidator.class)
+            .annotatedWith(Exports.named(mockCommentValidator.getClass()))
+            .toInstance(mockCommentValidator);
+        bind(CommentValidator.class).toInstance(mockCommentValidator);
+      }
+    };
+  }
+
+  @Before
+  public void resetMock() {
+    initMocks(this);
+    clearInvocations(mockCommentValidator);
+  }
+
+  @Test
+  public void validateComments_commentOK() throws Exception {
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(
+                    CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of());
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String revId = result.getCommit().getName();
+    DraftInput comment = testCommentHelper.newDraft(COMMENT_TEXT);
+    testCommentHelper.addDraft(changeId, revId, comment);
+    assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty();
+    Result amendResult = amendChange(changeId, "refs/for/master%publish-comments", admin, testRepo);
+    amendResult.assertOkStatus();
+    amendResult.assertNotMessage("Comment validation failure:");
+    assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).hasSize(1);
+  }
+
+  @Test
+  public void validateComments_commentRejected() throws Exception {
+    CommentForValidation commentForValidation =
+        CommentForValidation.create(CommentType.FILE_COMMENT, COMMENT_TEXT);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(
+                    CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String revId = result.getCommit().getName();
+    DraftInput comment = testCommentHelper.newDraft(COMMENT_TEXT);
+    testCommentHelper.addDraft(changeId, revId, comment);
+    assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty();
+    Result amendResult = amendChange(changeId, "refs/for/master%publish-comments", admin, testRepo);
+    amendResult.assertOkStatus();
+    amendResult.assertMessage("Comment validation failure:");
+    assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty();
+  }
+
+  @Test
+  public void validateComments_inlineVsFileComments_allOK() throws Exception {
+    when(mockCommentValidator.validateComments(capture.capture())).thenReturn(ImmutableList.of());
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String revId = result.getCommit().getName();
+    DraftInput draftFile = testCommentHelper.newDraft(COMMENT_TEXT);
+    testCommentHelper.addDraft(changeId, revId, draftFile);
+    DraftInput draftInline =
+        testCommentHelper.newDraft(
+            result.getChange().currentFilePaths().get(0), Side.REVISION, 1, COMMENT_TEXT);
+    testCommentHelper.addDraft(changeId, revId, draftInline);
+    assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty();
+    amendChange(changeId, "refs/for/master%publish-comments", admin, testRepo);
+    assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).hasSize(2);
+    assertThat(capture.getAllValues()).hasSize(1);
+    assertThat(capture.getValue())
+        .containsExactly(
+            CommentForValidation.create(
+                CommentForValidation.CommentType.INLINE_COMMENT, draftInline.message),
+            CommentForValidation.create(
+                CommentForValidation.CommentType.FILE_COMMENT, draftFile.message));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/httpd/BUILD b/javatests/com/google/gerrit/acceptance/server/httpd/BUILD
new file mode 100644
index 0000000..d1a64c0
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/httpd/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "server_httpd",
+    labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/httpd/HttpLogoutServletIT.java b/javatests/com/google/gerrit/acceptance/server/httpd/HttpLogoutServletIT.java
new file mode 100644
index 0000000..1dea800
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/httpd/HttpLogoutServletIT.java
@@ -0,0 +1,112 @@
+// 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.httpd;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.URIish;
+import org.junit.Before;
+import org.junit.Test;
+
+public class HttpLogoutServletIT extends StandaloneSiteTest {
+  private static final String LOCALHOST = InetAddress.getLoopbackAddress().getHostName();
+
+  @ConfigSuite.Config
+  public static Config secondConfig() throws IOException {
+    Config cfg = new Config();
+    cfg.setString("auth", null, "logouturl", "/test-logout");
+    cfg.setString("gerrit", null, "canonicalWebUrl", "https://" + LOCALHOST + ":8443/");
+    cfg.setString("httpd", null, "listenUrl", "proxy-https://" + LOCALHOST + ":" + getFreePort());
+    return cfg;
+  }
+
+  @Inject @GerritServerConfig private Config gerritConfig;
+
+  private HttpClient httpClient;
+
+  @Before
+  public void setUp() {
+    httpClient = HttpClientBuilder.create().disableRedirectHandling().build();
+  }
+
+  @Test
+  public void shouldHonourCanonicalWebUrlProxyWhenRedirectAfterLogout() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+
+      URIish listenUrl = new URIish(gerritConfig.getString("httpd", null, "listenUrl"));
+      URIish canonicalWebUrl =
+          new URIish(gerritConfig.getString("gerrit", null, "canonicalWebUrl"));
+
+      String logoutPath =
+          Optional.ofNullable(baseConfig.getString("auth", null, "logouturl")).orElse("/");
+
+      HttpGet getLogout = new HttpGet("/logout");
+      getLogout.addHeader("X-Forwarded-Host", canonicalWebUrl.getHost());
+      getLogout.addHeader("X-Forwarded-Port", "" + canonicalWebUrl.getPort());
+      getLogout.addHeader("X-Forwarded-Proto", canonicalWebUrl.getScheme());
+
+      HttpResponse logoutResponse =
+          httpClient.execute(new HttpHost(listenUrl.getHost(), listenUrl.getPort()), getLogout);
+
+      assertThat(logoutResponse.getStatusLine().getStatusCode())
+          .isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY);
+      assertThat(getLocationHeaderURIish(logoutResponse))
+          .containsExactly(canonicalWebUrl.setPath(logoutPath));
+    }
+  }
+
+  private List<URIish> getLocationHeaderURIish(HttpResponse logoutResponse) {
+    return Arrays.stream(logoutResponse.getHeaders("Location"))
+        .map(h -> h.getValue())
+        .map(HttpLogoutServletIT::unsafeNewURIish)
+        .filter(u -> u.isPresent())
+        .map(u -> u.get())
+        .collect(Collectors.toList());
+  }
+
+  private static Optional<URIish> unsafeNewURIish(String uri) {
+    try {
+      return Optional.of(new URIish(uri));
+    } catch (URISyntaxException e) {
+      return Optional.empty();
+    }
+  }
+
+  private static int getFreePort() throws IOException {
+    try (ServerSocket s = new ServerSocket(0)) {
+      return s.getLocalPort();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/BUILD b/javatests/com/google/gerrit/acceptance/server/mail/BUILD
index 5d7e65e..f90924b 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/mail/BUILD
@@ -7,18 +7,18 @@
     "//java/com/google/gerrit/mail",
 ]
 
-acceptance_tests(
-    srcs = glob(
-        ["*IT.java"],
-        exclude = ["AbstractMailIT.java"],
-    ),
-    group = "server_mail",
+[acceptance_tests(
+    srcs = [f],
+    group = f[:f.index(".")],
     labels = [
         "no_windows",
         "server",
     ],
     deps = DEPS + [":util"],
-)
+) for f in glob(
+    ["*IT.java"],
+    exclude = ["AbstractMailIT.java"],
+)]
 
 java_library(
     name = "util",
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 25e0708..2dc1e24 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.acceptance.server.mail;
 
+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.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;
@@ -31,9 +34,11 @@
 import com.google.common.truth.Truth;
 import com.google.gerrit.acceptance.AbstractNotificationTest;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AssigneeInput;
@@ -50,9 +55,6 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.CommitMessageInput;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.change.PostReview;
 import com.google.inject.Inject;
 import org.eclipse.jgit.junit.TestRepository;
@@ -61,6 +63,7 @@
 import org.junit.Test;
 
 public class ChangeNotificationsIT extends AbstractNotificationTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   /*
@@ -80,14 +83,14 @@
 
   @Before
   public void grantPermissions() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.allow(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, "refs/*");
-      Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/*");
-      Util.allow(cfg, Permission.ABANDON, REGISTERED_USERS, "refs/*");
-      Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.FORGE_COMMITTER).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.ABANDON).ref("refs/*").group(REGISTERED_USERS))
+        .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
   }
 
   /*
@@ -599,7 +602,7 @@
       if (notify != null) {
         in.notify = notify;
       }
-      gApi.changes().id(changeId).revision("current").review(in);
+      gApi.changes().id(changeId).current().review(in);
     };
   }
 
@@ -858,7 +861,7 @@
   public void noCommentAndSetWorkInProgress() throws Exception {
     StagedChange sc = stageReviewableChange();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    gApi.changes().id(sc.changeId).revision("current").review(in);
+    gApi.changes().id(sc.changeId).current().review(in);
     assertThat(sender).didNotSend();
   }
 
@@ -866,7 +869,7 @@
   public void commentAndSetWorkInProgress() throws Exception {
     StagedChange sc = stageReviewableChange();
     ReviewInput in = ReviewInput.noScore().message("ok").setWorkInProgress(true);
-    gApi.changes().id(sc.changeId).revision("current").review(in);
+    gApi.changes().id(sc.changeId).current().review(in);
     assertThat(sender)
         .sent("comment", sc)
         .cc(sc.reviewer, sc.ccer)
@@ -881,7 +884,7 @@
   public void commentOnWipChangeAndStartReview() throws Exception {
     StagedChange sc = stageWipChange();
     ReviewInput in = ReviewInput.noScore().message("ok").setWorkInProgress(false);
-    gApi.changes().id(sc.changeId).revision("current").review(in);
+    gApi.changes().id(sc.changeId).current().review(in);
     assertThat(sender)
         .sent("comment", sc)
         .cc(sc.reviewer, sc.ccer)
@@ -896,7 +899,7 @@
   public void addReviewerOnWipChangeAndStartReview() throws Exception {
     StagedChange sc = stageWipChange();
     ReviewInput in = ReviewInput.noScore().reviewer(other.email()).setWorkInProgress(false);
-    gApi.changes().id(sc.changeId).revision("current").review(in);
+    gApi.changes().id(sc.changeId).current().review(in);
     assertThat(sender)
         .sent("comment", sc)
         .cc(sc.reviewer, sc.ccer, other)
@@ -920,7 +923,7 @@
     StagedChange sc = stageWipChange();
     ReviewInput in =
         ReviewInput.noScore().message(PostReview.START_REVIEW_MESSAGE).setWorkInProgress(false);
-    gApi.changes().id(sc.changeId).revision("current").review(in);
+    gApi.changes().id(sc.changeId).current().review(in);
     Truth.assertThat(sender.getMessages()).isNotEmpty();
     String body = sender.getMessages().get(0).body();
     int idx = body.indexOf(PostReview.START_REVIEW_MESSAGE);
@@ -950,7 +953,7 @@
     ReviewInput in = ReviewInput.recommend();
     in.notify = notify;
     in.tag = tag;
-    gApi.changes().id(changeId).revision("current").review(in);
+    gApi.changes().id(changeId).current().review(in);
   }
 
   /*
@@ -1253,7 +1256,7 @@
 
   private void recommend(StagedChange sc, TestAccount by) throws Exception {
     requestScopeOperations.setApiUser(by.id());
-    gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.recommend());
+    gApi.changes().id(sc.changeId).current().review(ReviewInput.recommend());
   }
 
   private interface Stager {
@@ -1267,7 +1270,7 @@
             .reviewer(extraReviewer.email())
             .reviewer(extraCcer.email(), ReviewerState.CC, false);
     requestScopeOperations.setApiUser(extraReviewer.id());
-    gApi.changes().id(sc.changeId).revision("current").review(in);
+    gApi.changes().id(sc.changeId).current().review(in);
     sender.clear();
     return sc;
   }
@@ -1516,15 +1519,16 @@
       }
 
       merge(sc.changeId, sc.owner);
-      assertThat(sender)
-          .named(name)
+      assertWithMessage(name)
+          .about(fakeEmailSenders())
+          .that(sender)
           .sent("merged", sc)
           .cc(sc.reviewer, sc.ccer)
           .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
           .bcc(sc.starrer)
           .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
           .noOneElse();
-      assertThat(sender).named(name).didNotSend();
+      assertWithMessage(name).about(fakeEmailSenders()).that(sender).didNotSend();
     }
   }
 
@@ -1626,7 +1630,7 @@
       throws Exception {
     setEmailStrategy(by, emailStrategy);
     requestScopeOperations.setApiUser(by.id());
-    gApi.changes().id(changeId).revision("current").submit();
+    gApi.changes().id(changeId).current().submit();
   }
 
   private void merge(String changeId, TestAccount by, NotifyHandling notify) throws Exception {
@@ -1640,13 +1644,13 @@
     requestScopeOperations.setApiUser(by.id());
     SubmitInput in = new SubmitInput();
     in.notify = notify;
-    gApi.changes().id(changeId).revision("current").submit(in);
+    gApi.changes().id(changeId).current().submit(in);
   }
 
   private StagedChange stageChangeReadyForMerge() throws Exception {
     StagedChange sc = stageReviewableChange();
     requestScopeOperations.setApiUser(sc.reviewer.id());
-    gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve());
+    gApi.changes().id(sc.changeId).current().review(ReviewInput.approve());
     sender.clear();
     return sc;
   }
@@ -2039,7 +2043,7 @@
       StagedChange sc, TestAccount by, @Nullable NotifyHandling notify, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    CommitInfo commit = gApi.changes().id(sc.changeId).revision("current").commit(false);
+    CommitInfo commit = gApi.changes().id(sc.changeId).current().commit(false);
     CommitMessageInput in = new CommitMessageInput();
     in.message = "update\n" + commit.message;
     in.notify = notify;
@@ -2254,8 +2258,8 @@
   private StagedChange stageChange() throws Exception {
     StagedChange sc = stageReviewableChange();
     requestScopeOperations.setApiUser(admin.id());
-    gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve());
-    gApi.changes().id(sc.changeId).revision("current").submit();
+    gApi.changes().id(sc.changeId).current().review(ReviewInput.approve());
+    gApi.changes().id(sc.changeId).current().submit();
     sender.clear();
     return sc;
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index 1386aec..0826c166 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.acceptance.server.mail;
 
 import static com.google.common.truth.Truth.assertThat;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -27,7 +28,6 @@
 import com.google.gerrit.mail.MailProcessingUtil;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
 import java.time.ZoneId;
@@ -37,28 +37,14 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 /** Tests the presence of required metadata in email headers, text and html. */
+@UseClockStep
+@UseTimezone(timezone = "US/Eastern")
 public class MailMetadataIT extends AbstractDaemonTest {
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  private String systemTimeZone;
-
-  @Before
-  public void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
-
   @Test
   public void metadataOnNewChange() throws Exception {
     PushOneCommit.Result newChange = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index f917fd8..5531709 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -15,26 +15,68 @@
 package com.google.gerrit.acceptance.server.mail;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.mail.MailMessage;
 import com.google.gerrit.mail.MailProcessingUtil;
 import com.google.gerrit.server.mail.receive.MailProcessor;
 import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.gerrit.testing.TestCommentHelper;
 import com.google.inject.Inject;
+import com.google.inject.Module;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.Collection;
 import java.util.List;
+import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 public class MailProcessorIT extends AbstractMailIT {
   @Inject private MailProcessor mailProcessor;
   @Inject private AccountOperations accountOperations;
+  @Inject private TestCommentHelper testCommentHelper;
+
+  private static final CommentValidator mockCommentValidator = mock(CommentValidator.class);
+
+  private static final String COMMENT_TEXT = "The comment text";
+
+  @Override
+  public Module createModule() {
+    return new FactoryModule() {
+      @Override
+      public void configure() {
+        bind(CommentValidator.class)
+            .annotatedWith(Exports.named(mockCommentValidator.getClass()))
+            .toInstance(mockCommentValidator);
+        bind(CommentValidator.class).toInstance(mockCommentValidator);
+      }
+    };
+  }
+
+  @BeforeClass
+  public static void setUpMock() {
+    // Let the mock comment validator accept all comments during test setup.
+    when(mockCommentValidator.validateComments(any())).thenReturn(ImmutableList.of());
+  }
+
+  @Before
+  public void setUp() {
+    clearInvocations(mockCommentValidator);
+  }
 
   @Test
   public void parseAndPersistChangeMessage() throws Exception {
@@ -223,7 +265,87 @@
     assertThat(message.headers()).containsKey("Subject");
   }
 
+  @Test
+  public void validateChangeMessage_rejected() throws Exception {
+    String changeId = createChangeWithReview();
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
+    String ts =
+        MailProcessingUtil.rfcDateformatter.format(
+            ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
+
+    setupFailValidation(CommentForValidation.CommentType.CHANGE_MESSAGE);
+
+    MailMessage.Builder b = messageBuilderWithDefaultFields();
+    String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", COMMENT_TEXT, null, null, null);
+    b.textContent(txt + textFooterForChange(changeInfo._number, ts));
+
+    Collection<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
+    mailProcessor.process(b.build());
+    assertThat(testCommentHelper.getPublishedComments(changeId)).isEqualTo(commentsBefore);
+
+    assertNotifyTo(user);
+    Message message = sender.nextMessage();
+    assertThat(message.body()).contains("rejected one or more comments");
+  }
+
+  @Test
+  public void validateInlineComment_rejected() throws Exception {
+    String changeId = createChangeWithReview();
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
+    String ts =
+        MailProcessingUtil.rfcDateformatter.format(
+            ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
+
+    setupFailValidation(CommentForValidation.CommentType.INLINE_COMMENT);
+
+    MailMessage.Builder b = messageBuilderWithDefaultFields();
+    String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", null, COMMENT_TEXT, null, null);
+    b.textContent(txt + textFooterForChange(changeInfo._number, ts));
+
+    Collection<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
+    mailProcessor.process(b.build());
+    assertThat(testCommentHelper.getPublishedComments(changeId)).isEqualTo(commentsBefore);
+
+    assertNotifyTo(user);
+    Message message = sender.nextMessage();
+    assertThat(message.body()).contains("rejected one or more comments");
+  }
+
+  @Test
+  public void validateFileComment_rejected() throws Exception {
+    String changeId = createChangeWithReview();
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
+    String ts =
+        MailProcessingUtil.rfcDateformatter.format(
+            ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
+
+    setupFailValidation(CommentForValidation.CommentType.FILE_COMMENT);
+
+    MailMessage.Builder b = messageBuilderWithDefaultFields();
+    String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", null, null, COMMENT_TEXT, null);
+    b.textContent(txt + textFooterForChange(changeInfo._number, ts));
+
+    Collection<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
+    mailProcessor.process(b.build());
+    assertThat(testCommentHelper.getPublishedComments(changeId)).isEqualTo(commentsBefore);
+
+    assertNotifyTo(user);
+    Message message = sender.nextMessage();
+    assertThat(message.body()).contains("rejected one or more comments");
+  }
+
   private String getChangeUrl(ChangeInfo changeInfo) {
     return canonicalWebUrl.get() + "c/" + changeInfo.project + "/+/" + changeInfo._number;
   }
+
+  private void setupFailValidation(CommentForValidation.CommentType type) {
+    CommentForValidation commentForValidation = CommentForValidation.create(type, COMMENT_TEXT);
+
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(CommentForValidation.create(type, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/BUILD b/javatests/com/google/gerrit/acceptance/server/notedb/BUILD
index bdb3f3b..ee3bcb0 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/BUILD
@@ -7,9 +7,6 @@
         "notedb",
         "server",
     ],
-    # TODO(dborowitz): Fix leaks in local disk tests so we can reduce heap size.
-    # http://crbug.com/gerrit/8567
-    vm_args = ["-Xmx1024m"],
     deps = [
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index 748c4ea..c502c79 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -15,17 +15,19 @@
 package com.google.gerrit.acceptance.server.notedb;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateListener;
@@ -126,12 +128,9 @@
               throw new ResourceConflictException(msg);
             }
           });
-      try {
-        bu.execute();
-        fail("expected ResourceConflictException");
-      } catch (ResourceConflictException e) {
-        assertThat(e).hasMessageThat().isEqualTo(msg);
-      }
+      ResourceConflictException thrown =
+          assertThrows(ResourceConflictException.class, () -> bu.execute());
+      assertThat(thrown).hasMessageThat().isEqualTo(msg);
     }
 
     // If updateChange hadn't failed, backup would have been updated to master2.
@@ -188,18 +187,13 @@
 
   @Test
   public void missingChange() throws Exception {
-    Change.Id changeId = new Change.Id(1234567);
+    Change.Id changeId = Change.id(1234567);
     assertNoSuchChangeException(() -> notesFactory.create(project, changeId));
     assertNoSuchChangeException(() -> notesFactory.createChecked(project, changeId));
   }
 
   private void assertNoSuchChangeException(Callable<?> callable) throws Exception {
-    try {
-      callable.call();
-      fail("expected NoSuchChangeException");
-    } catch (NoSuchChangeException e) {
-      // Expected.
-    }
+    assertThrows(NoSuchChangeException.class, () -> callable.call());
   }
 
   private class ConcurrentWritingListener implements BatchUpdateListener {
@@ -306,8 +300,8 @@
     if (repo instanceof InMemoryRepository) {
       ((InMemoryRepository) repo).setPerformsAtomicTransactions(true);
     } else {
-      assertThat(repo.getRefDatabase().performsAtomicTransactions())
-          .named("performsAtomicTransactions on %s", repo)
+      assertWithMessage("performsAtomicTransactions on %s", repo)
+          .that(repo.getRefDatabase().performsAtomicTransactions())
           .isTrue();
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
index 2919e5f..12cae84 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
@@ -19,9 +19,9 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -122,7 +122,7 @@
 
   @Test
   public void refPermissions_sameResourceAndUserEquals() throws Exception {
-    Branch.NameKey branch = new Branch.NameKey(project, "branch");
+    BranchNameKey branch = BranchNameKey.create(project, "branch");
     BooleanCondition cond1 = pb.user(user()).ref(branch).testCond(RefPermission.READ);
     BooleanCondition cond2 = pb.user(user()).ref(branch).testCond(RefPermission.READ);
 
@@ -132,7 +132,7 @@
 
   @Test
   public void refPermissions_sameResourceAndDifferentUserDoesNotEqual() throws Exception {
-    Branch.NameKey branch = new Branch.NameKey(project, "branch");
+    BranchNameKey branch = BranchNameKey.create(project, "branch");
     BooleanCondition cond1 = pb.user(user()).ref(branch).testCond(RefPermission.READ);
     BooleanCondition cond2 = pb.user(admin()).ref(branch).testCond(RefPermission.READ);
 
@@ -142,8 +142,8 @@
 
   @Test
   public void refPermissions_differentResourceAndSameUserDoesNotEqual() throws Exception {
-    Branch.NameKey branch1 = new Branch.NameKey(project, "branch");
-    Branch.NameKey branch2 = new Branch.NameKey(project, "branch2");
+    BranchNameKey branch1 = BranchNameKey.create(project, "branch");
+    BranchNameKey branch2 = BranchNameKey.create(project, "branch2");
     BooleanCondition cond1 = pb.user(user()).ref(branch1).testCond(RefPermission.READ);
     BooleanCondition cond2 = pb.user(user()).ref(branch2).testCond(RefPermission.READ);
 
@@ -153,8 +153,8 @@
 
   @Test
   public void refPermissions_differentResourceAndSameUserDoesNotEqual2() throws Exception {
-    Branch.NameKey branch1 = new Branch.NameKey(project, "branch");
-    Branch.NameKey branch2 = new Branch.NameKey(projectOperations.newProject().create(), "branch");
+    BranchNameKey branch1 = BranchNameKey.create(project, "branch");
+    BranchNameKey branch2 = BranchNameKey.create(projectOperations.newProject().create(), "branch");
     BooleanCondition cond1 = pb.user(user()).ref(branch1).testCond(RefPermission.READ);
     BooleanCondition cond2 = pb.user(user()).ref(branch2).testCond(RefPermission.READ);
 
diff --git a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index 6cbe40e..3b7d826 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.server.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
 import static com.google.gerrit.common.data.LabelFunction.ANY_WITH_BLOCK;
 import static com.google.gerrit.common.data.LabelFunction.MAX_NO_BLOCK;
 import static com.google.gerrit.common.data.LabelFunction.MAX_WITH_BLOCK;
@@ -24,68 +26,50 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.events.CommentAddedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.util.Arrays;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class CustomLabelIT extends AbstractDaemonTest {
 
-  @Inject private DynamicSet<CommentAddedListener> source;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   private final LabelType label =
-      category("CustomLabel", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+      label("CustomLabel", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
 
-  private final LabelType P = category("CustomLabel2", value(1, "Positive"), value(0, "No score"));
-
-  private RegistrationHandle eventListenerRegistration;
-  private CommentAddedListener.Event lastCommentAddedEvent;
+  private final LabelType P = label("CustomLabel2", value(1, "Positive"), value(0, "No score"));
 
   @Before
   public void setUp() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(label.getName()),
-          -1,
-          1,
-          anonymousUsers,
-          "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel(P.getName()), 0, 1, anonymousUsers, "refs/heads/*");
-      u.save();
-    }
-
-    eventListenerRegistration = source.add("gerrit", event -> lastCommentAddedEvent = event);
-  }
-
-  @After
-  public void cleanup() {
-    eventListenerRegistration.remove();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(label.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .add(allowLabel(P.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(0, 1))
+        .update();
   }
 
   @Test
@@ -172,28 +156,41 @@
     assertThat(q.blocking).isTrue();
   }
 
+  private static class TestListener implements CommentAddedListener {
+    public CommentAddedListener.Event lastCommentAddedEvent;
+
+    @Override
+    public void onCommentAdded(Event event) {
+      lastCommentAddedEvent = event;
+    }
+  }
+
   @Test
   public void customLabelAnyWithBlock_Addreviewer_ZeroVote() throws Exception {
-    P.setFunction(ANY_WITH_BLOCK);
-    saveLabelConfig();
-    PushOneCommit.Result r = createChange();
-    AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email();
-    gApi.changes().id(r.getChangeId()).addReviewer(in);
+    TestListener testListener = new TestListener();
+    try (Registration registration = extensionRegistry.newRegistration().add(testListener)) {
+      P.setFunction(ANY_WITH_BLOCK);
+      saveLabelConfig();
+      PushOneCommit.Result r = createChange();
+      AddReviewerInput in = new AddReviewerInput();
+      in.reviewer = user.email();
+      gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    ReviewInput input = new ReviewInput().label(P.getName(), 0);
-    input.message = "foo";
+      ReviewInput input = new ReviewInput().label(P.getName(), 0);
+      input.message = "foo";
 
-    revision(r).review(input);
-    ChangeInfo c = getWithLabels(r);
-    LabelInfo q = c.labels.get(P.getName());
-    assertThat(q.all).hasSize(2);
-    assertThat(q.approved).isNull();
-    assertThat(q.recommended).isNull();
-    assertThat(q.disliked).isNull();
-    assertThat(q.rejected).isNull();
-    assertThat(q.blocking).isNull();
-    assertThat(lastCommentAddedEvent.getComment()).isEqualTo("Patch Set 1:\n\n" + input.message);
+      revision(r).review(input);
+      ChangeInfo c = getWithLabels(r);
+      LabelInfo q = c.labels.get(P.getName());
+      assertThat(q.all).hasSize(1);
+      assertThat(q.approved).isNull();
+      assertThat(q.recommended).isNull();
+      assertThat(q.disliked).isNull();
+      assertThat(q.rejected).isNull();
+      assertThat(q.blocking).isNull();
+      assertThat(testListener.lastCommentAddedEvent.getComment())
+          .isEqualTo("Patch Set 1:\n\n" + input.message);
+    }
   }
 
   @Test
@@ -265,15 +262,17 @@
     assertPermitted(info, P.getName(), 0, 1);
     assertPermitted(info, label.getName());
 
-    ReviewInput in = new ReviewInput();
-    in.label(P.getName(), P.getMax().getValue());
-    revision(r).review(in);
+    ReviewInput postSubmitReview1 = new ReviewInput();
+    postSubmitReview1.label(P.getName(), P.getMax().getValue());
+    revision(r).review(postSubmitReview1);
 
-    in = new ReviewInput();
-    in.label(label.getName(), label.getMax().getValue());
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("Voting on labels disallowed after submit: " + label.getName());
-    revision(r).review(in);
+    ReviewInput postSubmitReview2 = new ReviewInput();
+    postSubmitReview2.label(label.getName(), label.getMax().getValue());
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> revision(r).review(postSubmitReview2));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Voting on labels disallowed after submit: " + label.getName());
   }
 
   @Test
@@ -289,11 +288,11 @@
         value(-1, "I would prefer this is not merged as is"),
         value(-2, "This shall not be merged"));
 
-    AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.forLabel(testLabel), -2, +2, registered, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(testLabel).ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
 
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
@@ -311,11 +310,12 @@
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
     // Update admin's permitted range for 'Test-Label' to be -1...+1.
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.remove(u.getConfig(), Permission.forLabel(testLabel), registered, "refs/heads/*");
-      Util.allow(u.getConfig(), Permission.forLabel(testLabel), -1, +1, registered, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(labelPermissionKey(testLabel).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allowLabel(testLabel).ref("refs/heads/*").group(REGISTERED_USERS).range(-1, +1))
+        .update();
 
     // Verify admin doesn't have +2 permission any more.
     assertPermitted(gApi.changes().id(changeId).get(), testLabel, -1, 0, 1);
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 4ed16ee..29574c4 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.server.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.StarredChangesUtil.IGNORE_LABEL;
 
 import com.google.common.collect.ImmutableSet;
@@ -25,12 +26,12 @@
 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.AccountGroup;
+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.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.testing.FakeEmailSender.Message;
@@ -218,7 +219,7 @@
     // push a change to watched project -> should trigger email notification
     requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
-        cloneProject(new Project.NameKey(watchedProject), admin);
+        cloneProject(Project.nameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
             .create(admin.newIdent(), watchedRepo, "TRIGGER", "a", "a1")
@@ -229,7 +230,7 @@
     // notification
     String notWatchedProject = projectOperations.newProject().create().get();
     TestRepository<InMemoryRepository> notWatchedRepo =
-        cloneProject(new Project.NameKey(notWatchedProject), admin);
+        cloneProject(Project.nameKey(notWatchedProject), admin);
     r =
         pushFactory
             .create(admin.newIdent(), notWatchedRepo, "DONT_TRIGGER", "a", "a1")
@@ -261,7 +262,7 @@
     // user
     requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
-        cloneProject(new Project.NameKey(watchedProject), admin);
+        cloneProject(Project.nameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
             .create(admin.newIdent(), watchedRepo, "TRIGGER", "a.txt", "a1")
@@ -305,15 +306,15 @@
     requestScopeOperations.setApiUser(user.id());
 
     // watch keyword in project as user
-    watch(watchedProject, "multimaster");
+    watch(watchedProject, "multiprimary");
 
     // push a change with keyword -> should trigger email notification
     requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
-        cloneProject(new Project.NameKey(watchedProject), admin);
+        cloneProject(Project.nameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.newIdent(), watchedRepo, "Document multimaster setup", "a.txt", "a1")
+            .create(admin.newIdent(), watchedRepo, "Document multiprimary setup", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -322,7 +323,7 @@
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
     assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
-    assertThat(m.body()).contains("Change subject: Document multimaster setup\n");
+    assertThat(m.body()).contains("Change subject: Document multiprimary setup\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
     sender.clear();
 
@@ -347,8 +348,7 @@
 
     // push a change to any project -> should trigger email notification
     requestScopeOperations.setApiUser(admin.id());
-    TestRepository<InMemoryRepository> anyRepo =
-        cloneProject(new Project.NameKey(anyProject), admin);
+    TestRepository<InMemoryRepository> anyRepo = cloneProject(Project.nameKey(anyProject), admin);
     PushOneCommit.Result r =
         pushFactory.create(admin.newIdent(), anyRepo, "TRIGGER", "a", "a1").to("refs/for/master");
     r.assertOkStatus();
@@ -374,8 +374,7 @@
     // push a change to watched file in any project -> should trigger email
     // notification for user
     requestScopeOperations.setApiUser(admin.id());
-    TestRepository<InMemoryRepository> anyRepo =
-        cloneProject(new Project.NameKey(anyProject), admin);
+    TestRepository<InMemoryRepository> anyRepo = cloneProject(Project.nameKey(anyProject), admin);
     PushOneCommit.Result r =
         pushFactory
             .create(admin.newIdent(), anyRepo, "TRIGGER", "a.txt", "a1")
@@ -419,16 +418,15 @@
     requestScopeOperations.setApiUser(user.id());
 
     // watch keyword in project as user
-    watch(allProjects.get(), "multimaster");
+    watch(allProjects.get(), "multiprimary");
 
     // push a change with keyword to any project -> should trigger email
     // notification
     requestScopeOperations.setApiUser(admin.id());
-    TestRepository<InMemoryRepository> anyRepo =
-        cloneProject(new Project.NameKey(anyProject), admin);
+    TestRepository<InMemoryRepository> anyRepo = cloneProject(Project.nameKey(anyProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.newIdent(), anyRepo, "Document multimaster setup", "a.txt", "a1")
+            .create(admin.newIdent(), anyRepo, "Document multiprimary setup", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -437,7 +435,7 @@
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
     assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
-    assertThat(m.body()).contains("Change subject: Document multimaster setup\n");
+    assertThat(m.body()).contains("Change subject: Document multiprimary setup\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
     sender.clear();
 
@@ -463,7 +461,7 @@
     // push a change to watched project
     requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
-        cloneProject(new Project.NameKey(watchedProject), admin);
+        cloneProject(Project.nameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
             .create(admin.newIdent(), watchedRepo, "ignored change", "a", "a1")
@@ -496,7 +494,7 @@
     // push a private change to watched project -> should not trigger email notification
     requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
-        cloneProject(new Project.NameKey(watchedProject), admin);
+        cloneProject(Project.nameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
             .create(admin.newIdent(), watchedRepo, "private change", "a", "a1")
@@ -514,12 +512,14 @@
     // create group that can view all private changes
     GroupInfo groupThatCanViewPrivateChanges =
         gApi.groups().create("groupThatCanViewPrivateChanges").get();
-    grant(
-        new Project.NameKey(watchedProject),
-        "refs/*",
-        Permission.VIEW_PRIVATE_CHANGES,
-        false,
-        new AccountGroup.UUID(groupThatCanViewPrivateChanges.id));
+    projectOperations
+        .project(Project.nameKey(watchedProject))
+        .forUpdate()
+        .add(
+            allow(Permission.VIEW_PRIVATE_CHANGES)
+                .ref("refs/*")
+                .group(AccountGroup.uuid(groupThatCanViewPrivateChanges.id)))
+        .update();
 
     // watch project as user that can't view private changes
     requestScopeOperations.setApiUser(user.id());
@@ -536,7 +536,7 @@
     // userThatCanViewPrivateChanges, but not for user
     requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
-        cloneProject(new Project.NameKey(watchedProject), admin);
+        cloneProject(Project.nameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
             .create(admin.newIdent(), watchedRepo, "TRIGGER", "a", "a1")
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
index da3a257..11d39b4 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
@@ -15,21 +15,24 @@
 package com.google.gerrit.acceptance.server.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.entities.RefNames.changeMetaRef;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.UseLocalDisk;
+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.AccountGroup;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.ReflogEntryInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.io.File;
 import java.util.List;
@@ -39,6 +42,7 @@
 
 @UseLocalDisk
 public class ReflogIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -55,7 +59,7 @@
 
       gApi.changes().id(id.get()).topic("foo");
       ReflogEntry last = repo.getReflogReader(changeMetaRef(id)).getLastEntry();
-      assertThat(last).named("last RefLogEntry").isNotNull();
+      assertWithMessage("last RefLogEntry").that(last).isNotNull();
       assertThat(last.getComment()).isEqualTo("restapi.change.PutTopic");
     }
   }
@@ -85,8 +89,8 @@
   @Test
   public void regularUserIsNotAllowedToGetReflog() throws Exception {
     requestScopeOperations.setApiUser(user.id());
-    exception.expect(AuthException.class);
-    gApi.projects().name(project.get()).branch("master").reflog();
+    assertThrows(
+        AuthException.class, () -> gApi.projects().name(project.get()).branch("master").reflog());
   }
 
   @Test
@@ -94,11 +98,11 @@
     GroupApi groupApi = gApi.groups().create(name("get-reflog"));
     groupApi.addMembers("user");
 
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(
-          u.getConfig(), Permission.OWNER, new AccountGroup.UUID(groupApi.get().id), "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(AccountGroup.uuid(groupApi.get().id)))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     gApi.projects().name(project.get()).branch("master").reflog();
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
index adc7807..b3647e7 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
@@ -15,16 +15,17 @@
 package com.google.gerrit.acceptance.server.quota;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.resetToStrict;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.quota.QuotaBackend;
 import com.google.gerrit.server.quota.QuotaEnforcer;
@@ -34,13 +35,12 @@
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import java.util.Collections;
-import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
 
 public class DefaultQuotaBackendIT extends AbstractDaemonTest {
 
-  private static final QuotaEnforcer quotaEnforcer = EasyMock.createStrictMock(QuotaEnforcer.class);
+  private static final QuotaEnforcer quotaEnforcer = mock(QuotaEnforcer.class);
 
   private IdentifiedUser identifiedAdmin;
   @Inject private QuotaBackend quotaBackend;
@@ -60,14 +60,13 @@
   @Before
   public void setUp() {
     identifiedAdmin = identifiedUserFactory.create(admin.id());
-    resetToStrict(quotaEnforcer);
+    clearInvocations(quotaEnforcer);
   }
 
   @Test
   public void requestTokenForUser() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.ok());
     assertThat(quotaBackend.user(identifiedAdmin).requestToken("testGroup"))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
@@ -76,8 +75,7 @@
   public void requestTokenForUserAndAccount() {
     QuotaRequestContext ctx =
         QuotaRequestContext.builder().user(identifiedAdmin).account(user.id()).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.ok());
     assertThat(quotaBackend.user(identifiedAdmin).account(user.id()).requestToken("testGroup"))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
@@ -86,8 +84,7 @@
   public void requestTokenForUserAndProject() {
     QuotaRequestContext ctx =
         QuotaRequestContext.builder().user(identifiedAdmin).project(project).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.ok());
     assertThat(quotaBackend.user(identifiedAdmin).project(project).requestToken("testGroup"))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
@@ -101,8 +98,7 @@
             .change(changeId)
             .project(project)
             .build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.ok());
     assertThat(
             quotaBackend.user(identifiedAdmin).change(changeId, project).requestToken("testGroup"))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
@@ -111,8 +107,7 @@
   @Test
   public void requestTokens() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 123)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 123)).thenReturn(QuotaResponse.ok());
     assertThat(quotaBackend.user(identifiedAdmin).requestTokens("testGroup", 123))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
@@ -120,8 +115,7 @@
   @Test
   public void dryRun() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.dryRun("testGroup", ctx, 123)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.dryRun("testGroup", ctx, 123)).thenReturn(QuotaResponse.ok());
     assertThat(quotaBackend.user(identifiedAdmin).dryRun("testGroup", 123))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
@@ -131,8 +125,7 @@
     QuotaRequestContext ctx =
         QuotaRequestContext.builder().user(identifiedAdmin).account(user.id()).build();
     QuotaResponse r = QuotaResponse.ok(10L);
-    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenReturn(r);
     assertThat(quotaBackend.user(identifiedAdmin).account(user.id()).availableTokens("testGroup"))
         .isEqualTo(singletonAggregation(r));
   }
@@ -142,8 +135,7 @@
     QuotaRequestContext ctx =
         QuotaRequestContext.builder().user(identifiedAdmin).project(project).build();
     QuotaResponse r = QuotaResponse.ok(10L);
-    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenReturn(r);
     assertThat(quotaBackend.user(identifiedAdmin).project(project).availableTokens("testGroup"))
         .isEqualTo(singletonAggregation(r));
   }
@@ -158,8 +150,7 @@
             .project(project)
             .build();
     QuotaResponse r = QuotaResponse.ok(10L);
-    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenReturn(r);
     assertThat(
             quotaBackend
                 .user(identifiedAdmin)
@@ -172,8 +163,7 @@
   public void availableTokens() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
     QuotaResponse r = QuotaResponse.ok(10L);
-    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenReturn(r);
     assertThat(quotaBackend.user(identifiedAdmin).availableTokens("testGroup"))
         .isEqualTo(singletonAggregation(r));
   }
@@ -181,56 +171,51 @@
   @Test
   public void requestTokenError() throws Exception {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1))
-        .andReturn(QuotaResponse.error("failed"));
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1))
+        .thenReturn(QuotaResponse.error("failed"));
 
     QuotaResponse.Aggregated result = quotaBackend.user(identifiedAdmin).requestToken("testGroup");
     assertThat(result).isEqualTo(singletonAggregation(QuotaResponse.error("failed")));
-    exception.expect(QuotaException.class);
-    exception.expectMessage("failed");
-    result.throwOnError();
+    QuotaException thrown = assertThrows(QuotaException.class, () -> result.throwOnError());
+    assertThat(thrown).hasMessageThat().contains("failed");
   }
 
   @Test
   public void availableTokensError() throws Exception {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.availableTokens("testGroup", ctx))
-        .andReturn(QuotaResponse.error("failed"));
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenReturn(QuotaResponse.error("failed"));
     QuotaResponse.Aggregated result =
         quotaBackend.user(identifiedAdmin).availableTokens("testGroup");
     assertThat(result).isEqualTo(singletonAggregation(QuotaResponse.error("failed")));
-    exception.expect(QuotaException.class);
-    exception.expectMessage("failed");
-    result.throwOnError();
+    QuotaException thrown = assertThrows(QuotaException.class, () -> result.throwOnError());
+    assertThat(thrown).hasMessageThat().contains("failed");
   }
 
   @Test
   public void requestTokenPluginThrowsAndRethrows() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andThrow(new NullPointerException());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1)).thenThrow(new NullPointerException());
 
-    exception.expect(NullPointerException.class);
-    quotaBackend.user(identifiedAdmin).requestToken("testGroup");
+    assertThrows(
+        NullPointerException.class,
+        () -> quotaBackend.user(identifiedAdmin).requestToken("testGroup"));
   }
 
   @Test
   public void availableTokensPluginThrowsAndRethrows() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andThrow(new NullPointerException());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenThrow(new NullPointerException());
 
-    exception.expect(NullPointerException.class);
-    quotaBackend.user(identifiedAdmin).availableTokens("testGroup");
+    assertThrows(
+        NullPointerException.class,
+        () -> quotaBackend.user(identifiedAdmin).availableTokens("testGroup"));
   }
 
   private Change.Id retrieveChangeId() throws Exception {
     // use REST API so that repository size quota doesn't have to be stubbed
     ChangeInfo changeInfo =
         gApi.changes().create(new ChangeInput(project.get(), "master", "test")).get();
-    return new Change.Id(changeInfo._number);
+    return Change.id(changeInfo._number);
   }
 
   private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) {
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
index f5f1161..c6b09cc 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.acceptance.server.quota;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.expectLastCall;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.resetToStrict;
-import static org.easymock.EasyMock.verify;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -33,15 +34,12 @@
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import java.util.OptionalLong;
-import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
 
 public class MultipleQuotaPluginsIT extends AbstractDaemonTest {
-  private static final QuotaEnforcer quotaEnforcerA =
-      EasyMock.createStrictMock(QuotaEnforcer.class);
-  private static final QuotaEnforcer quotaEnforcerB =
-      EasyMock.createStrictMock(QuotaEnforcer.class);
+  private static final QuotaEnforcer quotaEnforcerA = mock(QuotaEnforcer.class);
+  private static final QuotaEnforcer quotaEnforcerB = mock(QuotaEnforcer.class);
 
   private IdentifiedUser identifiedAdmin;
   @Inject private QuotaBackend quotaBackend;
@@ -65,93 +63,86 @@
   @Before
   public void setUp() {
     identifiedAdmin = identifiedUserFactory.create(admin.id());
-    resetToStrict(quotaEnforcerA);
-    resetToStrict(quotaEnforcerB);
+    clearInvocations(quotaEnforcerA);
+    clearInvocations(quotaEnforcerB);
   }
 
   @Test
   public void refillsOnError() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcerA.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    expect(quotaEnforcerB.requestTokens("testGroup", ctx, 1))
-        .andReturn(QuotaResponse.error("fail"));
-    quotaEnforcerA.refill("testGroup", ctx, 1);
-    expectLastCall();
-
-    replay(quotaEnforcerA);
-    replay(quotaEnforcerB);
+    when(quotaEnforcerA.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.ok());
+    when(quotaEnforcerB.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.error("fail"));
 
     assertThat(quotaBackend.user(identifiedAdmin).requestToken("testGroup"))
         .isEqualTo(
             QuotaResponse.Aggregated.create(
                 ImmutableList.of(QuotaResponse.ok(), QuotaResponse.error("fail"))));
+
+    verify(quotaEnforcerA).requestTokens("testGroup", ctx, 1);
+    verify(quotaEnforcerB).requestTokens("testGroup", ctx, 1);
+    verify(quotaEnforcerA).refill("testGroup", ctx, 1);
   }
 
   @Test
   public void refillsOnException() {
     NullPointerException exception = new NullPointerException();
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcerA.requestTokens("testGroup", ctx, 1)).andThrow(exception);
-    expect(quotaEnforcerB.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    quotaEnforcerB.refill("testGroup", ctx, 1);
-    expectLastCall();
+    when(quotaEnforcerA.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.ok());
+    when(quotaEnforcerB.requestTokens("testGroup", ctx, 1)).thenThrow(exception);
 
-    replay(quotaEnforcerA);
-    replay(quotaEnforcerB);
+    NullPointerException thrown =
+        assertThrows(
+            NullPointerException.class,
+            () -> quotaBackend.user(identifiedAdmin).requestToken("testGroup"));
+    assertThat(thrown).isEqualTo(exception);
 
-    try {
-      quotaBackend.user(identifiedAdmin).requestToken("testGroup");
-      fail("expected a NullPointerException");
-    } catch (NullPointerException e) {
-      assertThat(exception).isEqualTo(e);
-    }
-
-    verify(quotaEnforcerA);
+    verify(quotaEnforcerA).requestTokens("testGroup", ctx, 1);
+    verify(quotaEnforcerB).requestTokens("testGroup", ctx, 1);
+    verify(quotaEnforcerA).refill("testGroup", ctx, 1);
   }
 
   @Test
   public void doesNotRefillNoOp() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcerA.requestTokens("testGroup", ctx, 1))
-        .andReturn(QuotaResponse.error("fail"));
-    expect(quotaEnforcerB.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.noOp());
-
-    replay(quotaEnforcerA);
-    replay(quotaEnforcerB);
+    when(quotaEnforcerA.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.error("fail"));
+    when(quotaEnforcerB.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.noOp());
 
     assertThat(quotaBackend.user(identifiedAdmin).requestToken("testGroup"))
         .isEqualTo(
             QuotaResponse.Aggregated.create(
                 ImmutableList.of(QuotaResponse.error("fail"), QuotaResponse.noOp())));
+
+    verify(quotaEnforcerA).requestTokens("testGroup", ctx, 1);
+    verify(quotaEnforcerB).requestTokens("testGroup", ctx, 1);
   }
 
   @Test
   public void minimumAvailableTokens() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcerA.availableTokens("testGroup", ctx)).andReturn(QuotaResponse.ok(20L));
-    expect(quotaEnforcerB.availableTokens("testGroup", ctx)).andReturn(QuotaResponse.ok(10L));
-
-    replay(quotaEnforcerA);
-    replay(quotaEnforcerB);
+    when(quotaEnforcerA.availableTokens("testGroup", ctx)).thenReturn(QuotaResponse.ok(20L));
+    when(quotaEnforcerB.availableTokens("testGroup", ctx)).thenReturn(QuotaResponse.ok(10L));
 
     OptionalLong tokens =
         quotaBackend.user(identifiedAdmin).availableTokens("testGroup").availableTokens();
-    assertThat(tokens.isPresent()).isTrue();
+    assertThat(tokens).isPresent();
     assertThat(tokens.getAsLong()).isEqualTo(10L);
+
+    verify(quotaEnforcerA).availableTokens("testGroup", ctx);
+    verify(quotaEnforcerB).availableTokens("testGroup", ctx);
   }
 
   @Test
   public void ignoreNoOpForAvailableTokens() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcerA.availableTokens("testGroup", ctx)).andReturn(QuotaResponse.noOp());
-    expect(quotaEnforcerB.availableTokens("testGroup", ctx)).andReturn(QuotaResponse.ok(20L));
-
-    replay(quotaEnforcerA);
-    replay(quotaEnforcerB);
+    when(quotaEnforcerA.availableTokens("testGroup", ctx)).thenReturn(QuotaResponse.noOp());
+    when(quotaEnforcerB.availableTokens("testGroup", ctx)).thenReturn(QuotaResponse.ok(20L));
 
     OptionalLong tokens =
         quotaBackend.user(identifiedAdmin).availableTokens("testGroup").availableTokens();
-    assertThat(tokens.isPresent()).isTrue();
+    assertThat(tokens).isPresent();
     assertThat(tokens.getAsLong()).isEqualTo(20L);
+
+    verify(quotaEnforcerA).availableTokens("testGroup", ctx);
+    verify(quotaEnforcerB).availableTokens("testGroup", ctx);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java b/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java
index 24c90fc..2692584 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java
@@ -18,12 +18,13 @@
 import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
 import static com.google.gerrit.server.quota.QuotaResponse.ok;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static org.easymock.EasyMock.anyLong;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.resetToStrict;
-import static org.easymock.EasyMock.verify;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.UseLocalDisk;
@@ -33,7 +34,6 @@
 import com.google.gerrit.server.quota.QuotaResponse;
 import com.google.inject.Module;
 import java.util.Collections;
-import org.easymock.EasyMock;
 import org.eclipse.jgit.api.errors.TooLargePackException;
 import org.eclipse.jgit.api.errors.TransportException;
 import org.junit.Before;
@@ -42,9 +42,9 @@
 @UseLocalDisk
 public class RepositorySizeQuotaIT extends AbstractDaemonTest {
   private static final QuotaBackend.WithResource quotaBackendWithResource =
-      EasyMock.createStrictMock(QuotaBackend.WithResource.class);
+      mock(QuotaBackend.WithResource.class);
   private static final QuotaBackend.WithUser quotaBackendWithUser =
-      EasyMock.createStrictMock(QuotaBackend.WithUser.class);
+      mock(QuotaBackend.WithUser.class);
 
   @Override
   public Module createModule() {
@@ -70,56 +70,40 @@
 
   @Before
   public void setUp() {
-    resetToStrict(quotaBackendWithResource);
-    resetToStrict(quotaBackendWithUser);
+    clearInvocations(quotaBackendWithResource);
+    clearInvocations(quotaBackendWithUser);
   }
 
   @Test
   public void pushWithAvailableTokens() throws Exception {
-    expect(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
-        .andReturn(singletonAggregation(ok(277L)))
-        .times(2);
-    expect(quotaBackendWithResource.requestTokens(eq(REPOSITORY_SIZE_GROUP), anyLong()))
-        .andReturn(singletonAggregation(ok()));
-    expect(quotaBackendWithUser.project(project)).andReturn(quotaBackendWithResource).anyTimes();
-    replay(quotaBackendWithResource);
-    replay(quotaBackendWithUser);
+    when(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
+        .thenReturn(singletonAggregation(ok(277L)));
+    when(quotaBackendWithResource.requestTokens(eq(REPOSITORY_SIZE_GROUP), anyLong()))
+        .thenReturn(singletonAggregation(ok()));
+    when(quotaBackendWithUser.project(project)).thenReturn(quotaBackendWithResource);
     pushCommit();
-    verify(quotaBackendWithUser);
-    verify(quotaBackendWithResource);
+    verify(quotaBackendWithResource, times(2)).availableTokens(REPOSITORY_SIZE_GROUP);
   }
 
   @Test
   public void pushWithNotSufficientTokens() throws Exception {
     long availableTokens = 1L;
-    expect(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
-        .andReturn(singletonAggregation(ok(availableTokens)))
-        .anyTimes();
-    expect(quotaBackendWithUser.project(project)).andReturn(quotaBackendWithResource).anyTimes();
-    replay(quotaBackendWithResource);
-    replay(quotaBackendWithUser);
+    when(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
+        .thenReturn(singletonAggregation(ok(availableTokens)));
+    when(quotaBackendWithUser.project(project)).thenReturn(quotaBackendWithResource);
     assertThat(assertThrows(TooLargePackException.class, () -> pushCommit()).getMessage())
         .contains(
             String.format(
                 "Pack exceeds the limit of %d bytes, rejecting the pack", availableTokens));
-    verify(quotaBackendWithUser);
-    verify(quotaBackendWithResource);
   }
 
   @Test
   public void errorGettingAvailableTokens() throws Exception {
     String msg = "quota error";
-    expect(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
-        .andReturn(singletonAggregation(QuotaResponse.error(msg)))
-        .anyTimes();
-    expect(quotaBackendWithUser.project(project)).andReturn(quotaBackendWithResource).anyTimes();
-    replay(quotaBackendWithResource);
-    replay(quotaBackendWithUser);
-
+    when(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
+        .thenReturn(singletonAggregation(QuotaResponse.error(msg)));
+    when(quotaBackendWithUser.project(project)).thenReturn(quotaBackendWithResource);
     assertThrows(TransportException.class, () -> pushCommit());
-
-    verify(quotaBackendWithUser);
-    verify(quotaBackendWithResource);
   }
 
   private void pushCommit() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java b/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java
index dc082fe..c0aab38 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java
@@ -14,30 +14,30 @@
 
 package com.google.gerrit.acceptance.server.quota;
 
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.reset;
-import static org.easymock.EasyMock.verify;
+import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_TOO_MANY_REQUESTS;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.quota.QuotaBackend;
 import com.google.gerrit.server.quota.QuotaResponse;
 import com.google.inject.Module;
 import java.util.Collections;
-import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
 
 public class RestApiQuotaIT extends AbstractDaemonTest {
   private static final QuotaBackend.WithResource quotaBackendWithResource =
-      EasyMock.createStrictMock(QuotaBackend.WithResource.class);
+      mock(QuotaBackend.WithResource.class);
   private static final QuotaBackend.WithUser quotaBackendWithUser =
-      EasyMock.createStrictMock(QuotaBackend.WithUser.class);
+      mock(QuotaBackend.WithUser.class);
 
   @Override
   public Module createModule() {
@@ -63,79 +63,72 @@
 
   @Before
   public void setUp() {
-    reset(quotaBackendWithResource);
-    reset(quotaBackendWithUser);
+    clearInvocations(quotaBackendWithResource);
+    clearInvocations(quotaBackendWithUser);
   }
 
   @Test
   public void changeDetail() throws Exception {
     Change.Id changeId = retrieveChangeId();
-    expect(quotaBackendWithResource.requestToken("/restapi/changes/detail:GET"))
-        .andReturn(singletonAggregation(QuotaResponse.ok()));
-    replay(quotaBackendWithResource);
-    expect(quotaBackendWithUser.change(changeId, project)).andReturn(quotaBackendWithResource);
-    replay(quotaBackendWithUser);
+    when(quotaBackendWithResource.requestToken("/restapi/changes/detail:GET"))
+        .thenReturn(singletonAggregation(QuotaResponse.ok()));
+    when(quotaBackendWithUser.change(changeId, project)).thenReturn(quotaBackendWithResource);
     adminRestSession.get("/changes/" + changeId + "/detail").assertOK();
-    verify(quotaBackendWithUser);
-    verify(quotaBackendWithResource);
+    verify(quotaBackendWithResource).requestToken("/restapi/changes/detail:GET");
+    verify(quotaBackendWithUser).change(changeId, project);
   }
 
   @Test
   public void revisionDetail() throws Exception {
     Change.Id changeId = retrieveChangeId();
-    expect(quotaBackendWithResource.requestToken("/restapi/changes/revisions/actions:GET"))
-        .andReturn(singletonAggregation(QuotaResponse.ok()));
-    replay(quotaBackendWithResource);
-    expect(quotaBackendWithUser.change(changeId, project)).andReturn(quotaBackendWithResource);
-    replay(quotaBackendWithUser);
+    when(quotaBackendWithResource.requestToken("/restapi/changes/revisions/actions:GET"))
+        .thenReturn(singletonAggregation(QuotaResponse.ok()));
+    when(quotaBackendWithUser.change(changeId, project)).thenReturn(quotaBackendWithResource);
     adminRestSession.get("/changes/" + changeId + "/revisions/current/actions").assertOK();
-    verify(quotaBackendWithUser);
-    verify(quotaBackendWithResource);
+    verify(quotaBackendWithResource).requestToken("/restapi/changes/revisions/actions:GET");
+    verify(quotaBackendWithUser).change(changeId, project);
   }
 
   @Test
   public void createChangePost() throws Exception {
-    expect(quotaBackendWithUser.requestToken("/restapi/changes:POST"))
-        .andReturn(singletonAggregation(QuotaResponse.ok()));
-    replay(quotaBackendWithUser);
+    when(quotaBackendWithUser.requestToken("/restapi/changes:POST"))
+        .thenReturn(singletonAggregation(QuotaResponse.ok()));
     ChangeInput changeInput = new ChangeInput(project.get(), "master", "test");
     adminRestSession.post("/changes/", changeInput).assertCreated();
-    verify(quotaBackendWithUser);
+    verify(quotaBackendWithUser).requestToken("/restapi/changes:POST");
   }
 
   @Test
   public void accountDetail() throws Exception {
-    expect(quotaBackendWithResource.requestToken("/restapi/accounts/detail:GET"))
-        .andReturn(singletonAggregation(QuotaResponse.ok()));
-    replay(quotaBackendWithResource);
-    expect(quotaBackendWithUser.account(admin.id())).andReturn(quotaBackendWithResource);
-    replay(quotaBackendWithUser);
+    when(quotaBackendWithResource.requestToken("/restapi/accounts/detail:GET"))
+        .thenReturn(singletonAggregation(QuotaResponse.ok()));
+    when(quotaBackendWithUser.account(admin.id())).thenReturn(quotaBackendWithResource);
     adminRestSession.get("/accounts/self/detail").assertOK();
-    verify(quotaBackendWithUser);
-    verify(quotaBackendWithResource);
+    verify(quotaBackendWithResource).requestToken("/restapi/accounts/detail:GET");
+    verify(quotaBackendWithUser).account(admin.id());
   }
 
   @Test
   public void config() throws Exception {
-    expect(quotaBackendWithUser.requestToken("/restapi/config/version:GET"))
-        .andReturn(singletonAggregation(QuotaResponse.ok()));
-    replay(quotaBackendWithUser);
+    when(quotaBackendWithUser.requestToken("/restapi/config/version:GET"))
+        .thenReturn(singletonAggregation(QuotaResponse.ok()));
     adminRestSession.get("/config/server/version").assertOK();
+    verify(quotaBackendWithUser).requestToken("/restapi/config/version:GET");
   }
 
   @Test
   public void outOfQuotaReturnsError() throws Exception {
-    expect(quotaBackendWithUser.requestToken("/restapi/config/version:GET"))
-        .andReturn(singletonAggregation(QuotaResponse.error("no quota")));
-    replay(quotaBackendWithUser);
-    adminRestSession.get("/config/server/version").assertStatus(429);
+    when(quotaBackendWithUser.requestToken("/restapi/config/version:GET"))
+        .thenReturn(singletonAggregation(QuotaResponse.error("no quota")));
+    adminRestSession.get("/config/server/version").assertStatus(SC_TOO_MANY_REQUESTS);
+    verify(quotaBackendWithUser).requestToken("/restapi/config/version:GET");
   }
 
   private Change.Id retrieveChangeId() throws Exception {
     // use REST API so that repository size quota doesn't have to be stubbed
     ChangeInfo changeInfo =
         gApi.changes().create(new ChangeInput(project.get(), "master", "test")).get();
-    return new Change.Id(changeInfo._number);
+    return Change.id(changeInfo._number);
   }
 
   private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) {
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java b/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
index 83782c9..1c820af 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.server.rules;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -22,11 +23,10 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
-import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.rules.IgnoreSelfApprovalRule;
 import com.google.inject.Inject;
-import java.util.Collection;
 import java.util.Map;
+import java.util.Optional;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Test;
@@ -42,11 +42,10 @@
     PushOneCommit.Result r = createChange();
     approve(r.getChangeId());
 
-    Collection<SubmitRecord> submitRecords =
-        rule.evaluate(r.getChange(), SubmitRuleOptions.defaults());
+    Optional<SubmitRecord> submitRecord = rule.evaluate(r.getChange());
 
-    assertThat(submitRecords).hasSize(1);
-    SubmitRecord result = submitRecords.iterator().next();
+    assertThat(submitRecord).isPresent();
+    SubmitRecord result = submitRecord.get();
     assertThat(result.status).isEqualTo(SubmitRecord.Status.NOT_READY);
     assertThat(result.labels).isNotEmpty();
     assertThat(result.requirements)
@@ -69,9 +68,8 @@
     // Approve as admin
     approve(r.getChangeId());
 
-    Collection<SubmitRecord> submitRecords =
-        rule.evaluate(r.getChange(), SubmitRuleOptions.defaults());
-    assertThat(submitRecords).isEmpty();
+    Optional<SubmitRecord> submitRecord = rule.evaluate(r.getChange());
+    assertThat(submitRecord).isEmpty();
   }
 
   @Test
@@ -81,9 +79,8 @@
     PushOneCommit.Result r = createChange();
     approve(r.getChangeId());
 
-    Collection<SubmitRecord> submitRecords =
-        rule.evaluate(r.getChange(), SubmitRuleOptions.defaults());
-    assertThat(submitRecords).isEmpty();
+    Optional<SubmitRecord> submitRecord = rule.evaluate(r.getChange());
+    assertThat(submitRecord).isEmpty();
   }
 
   private void enableRule(String labelName, boolean newState) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
index fa13be4..92cc396 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
@@ -20,9 +20,9 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.PrologOptions;
 import com.google.gerrit.server.rules.PrologRuleEvaluator;
 import com.google.gerrit.testing.TestChanges;
 import com.google.inject.Inject;
@@ -30,8 +30,8 @@
 import com.googlecode.prolog_cafe.lang.StructureTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
+import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
 public class PrologRuleEvaluatorIT extends AbstractDaemonTest {
@@ -45,9 +45,9 @@
     StructureTerm labels = new StructureTerm("label", verifiedLabel);
 
     List<Term> terms = ImmutableList.of(makeTerm("ok", labels));
-    Collection<SubmitRecord> records = evaluator.resultsToSubmitRecord(null, terms);
+    SubmitRecord record = evaluator.resultsToSubmitRecord(null, terms);
 
-    assertThat(records).hasSize(1);
+    assertThat(record.status).isEqualTo(SubmitRecord.Status.OK);
   }
 
   /**
@@ -112,23 +112,16 @@
     terms.add(makeTerm("not_ready", makeLabels(label3)));
 
     // When
-    List<SubmitRecord> records = evaluator.resultsToSubmitRecord(null, terms);
+    SubmitRecord record = evaluator.resultsToSubmitRecord(null, terms);
 
     // assert that
-    SubmitRecord record1Expected = new SubmitRecord();
-    record1Expected.status = SubmitRecord.Status.OK;
-    record1Expected.labels = new ArrayList<>();
-    record1Expected.labels.add(submitRecordLabel2);
+    SubmitRecord expectedRecord = new SubmitRecord();
+    expectedRecord.status = SubmitRecord.Status.OK;
+    expectedRecord.labels = new ArrayList<>();
+    expectedRecord.labels.add(submitRecordLabel2);
+    expectedRecord.labels.add(submitRecordLabel3);
 
-    SubmitRecord record2Expected = new SubmitRecord();
-    record2Expected.status = SubmitRecord.Status.OK;
-    record2Expected.labels = new ArrayList<>();
-    record2Expected.labels.add(submitRecordLabel3);
-
-    assertThat(records).hasSize(2);
-
-    assertThat(records.get(0)).isEqualTo(record1Expected);
-    assertThat(records.get(1)).isEqualTo(record2Expected);
+    assertThat(record).isEqualTo(expectedRecord);
   }
 
   private static Term makeTerm(String status, StructureTerm labels) {
@@ -149,12 +142,12 @@
   }
 
   private ChangeData makeChangeData() {
-    ChangeData cd = ChangeData.createForTest(project, new Change.Id(1), 1);
+    ChangeData cd = ChangeData.createForTest(project, Change.id(1), 1, ObjectId.zeroId());
     cd.setChange(TestChanges.newChange(project, admin.id()));
     return cd;
   }
 
   private PrologRuleEvaluator makeEvaluator() {
-    return evaluatorFactory.create(makeChangeData(), SubmitRuleOptions.defaults());
+    return evaluatorFactory.create(makeChangeData(), PrologOptions.defaultOptions());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
index 536cbbb..e5432d1 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
@@ -19,8 +19,9 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -40,6 +41,7 @@
   private static final String RULE_TEMPLATE =
       "submit_rule(submit(W)) :- \n" + "%s,\n" + "W = label('OK', ok(user(1000000))).";
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private SubmitRuleEvaluator.Factory evaluatorFactory;
 
   @Before
@@ -84,7 +86,7 @@
   }
 
   private SubmitRecord.Status statusForRule() throws Exception {
-    String oldHead = getRemoteHead().name();
+    String oldHead = projectOperations.project(project).getHead("master").name();
     PushOneCommit.Result result1 =
         pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
diff --git a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
index 9fb7c2d..4fe0df4 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
@@ -20,102 +20,91 @@
 import com.google.common.base.Joiner;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.ChangeIndexedCounter;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.events.ChangeIndexedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import java.util.List;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 @UseSsh
 public abstract class AbstractIndexTests extends AbstractDaemonTest {
-  @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
-
-  private ChangeIndexedCounter changeIndexedCounter;
-  private RegistrationHandle changeIndexedCounterHandle;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   /** @param injector injector */
   public void configureIndex(Injector injector) {}
 
-  @Before
-  public void addChangeIndexedCounter() {
-    changeIndexedCounter = new ChangeIndexedCounter();
-    changeIndexedCounterHandle = changeIndexedListeners.add("gerrit", changeIndexedCounter);
-  }
-
-  @After
-  public void removeChangeIndexedCounter() {
-    if (changeIndexedCounterHandle != null) {
-      changeIndexedCounterHandle.remove();
-    }
-  }
-
   @Test
   @GerritConfig(name = "index.autoReindexIfStale", value = "false")
   public void indexChange() throws Exception {
-    configureIndex(server.getTestInjector());
+    ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedCounter)) {
+      configureIndex(server.getTestInjector());
 
-    PushOneCommit.Result change = createChange("first change", "test1.txt", "test1");
-    String changeId = change.getChangeId();
-    String changeLegacyId = change.getChange().getId().toString();
-    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+      PushOneCommit.Result change = createChange("first change", "test1.txt", "test1");
+      String changeId = change.getChangeId();
+      String changeLegacyId = change.getChange().getId().toString();
+      ChangeInfo changeInfo = gApi.changes().id(changeId).get();
 
-    disableChangeIndexWrites();
-    amendChange(changeId, "second test", "test2.txt", "test2");
+      disableChangeIndexWrites();
+      amendChange(changeId, "second test", "test2.txt", "test2");
 
-    assertChangeQuery(change.getChange(), false);
-    enableChangeIndexWrites();
+      assertChangeQuery(change.getChange(), false);
+      enableChangeIndexWrites();
 
-    changeIndexedCounter.clear();
-    String cmd = Joiner.on(" ").join("gerrit", "index", "changes", changeLegacyId);
-    adminSshSession.exec(cmd);
-    adminSshSession.assertSuccess();
+      changeIndexedCounter.clear();
+      String cmd = Joiner.on(" ").join("gerrit", "index", "changes", changeLegacyId);
+      adminSshSession.exec(cmd);
+      adminSshSession.assertSuccess();
 
-    changeIndexedCounter.assertReindexOf(changeInfo, 1);
+      changeIndexedCounter.assertReindexOf(changeInfo, 1);
 
-    assertChangeQuery(change.getChange(), true);
+      assertChangeQuery(change.getChange(), true);
+    }
   }
 
   @Test
   @GerritConfig(name = "index.autoReindexIfStale", value = "false")
   public void indexProject() throws Exception {
-    configureIndex(server.getTestInjector());
+    ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedCounter)) {
+      configureIndex(server.getTestInjector());
 
-    PushOneCommit.Result change = createChange("first change", "test1.txt", "test1");
-    String changeId = change.getChangeId();
-    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+      PushOneCommit.Result change = createChange("first change", "test1.txt", "test1");
+      String changeId = change.getChangeId();
+      ChangeInfo changeInfo = gApi.changes().id(changeId).get();
 
-    disableChangeIndexWrites();
-    amendChange(changeId, "second test", "test2.txt", "test2");
+      disableChangeIndexWrites();
+      amendChange(changeId, "second test", "test2.txt", "test2");
 
-    assertChangeQuery(change.getChange(), false);
-    enableChangeIndexWrites();
+      assertChangeQuery(change.getChange(), false);
+      enableChangeIndexWrites();
 
-    changeIndexedCounter.clear();
-    String cmd = Joiner.on(" ").join("gerrit", "index", "changes-in-project", project.get());
-    adminSshSession.exec(cmd);
-    adminSshSession.assertSuccess();
-
-    boolean indexing = true;
-    while (indexing) {
-      String out = adminSshSession.exec("gerrit show-queue --wide");
+      changeIndexedCounter.clear();
+      String cmd = Joiner.on(" ").join("gerrit", "index", "changes-in-project", project.get());
+      adminSshSession.exec(cmd);
       adminSshSession.assertSuccess();
-      indexing = out.contains("Index all changes of project " + project.get());
+
+      boolean indexing = true;
+      while (indexing) {
+        String out = adminSshSession.exec("gerrit show-queue --wide");
+        adminSshSession.assertSuccess();
+        indexing = out.contains("Index all changes of project " + project.get());
+      }
+
+      changeIndexedCounter.assertReindexOf(changeInfo, 1);
+
+      assertChangeQuery(change.getChange(), true);
     }
-
-    changeIndexedCounter.assertReindexOf(changeInfo, 1);
-
-    assertChangeQuery(change.getChange(), true);
   }
 
   private void assertChangeQuery(ChangeData change, boolean assertTrue) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
index 0ce05b0..39dbaa7 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.UseSsh;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.project.ProjectState;
 import org.junit.Test;
 
@@ -33,7 +33,7 @@
     adminSshSession.exec(
         "gerrit create-project --branch master --owner " + newGroupName + " " + newProjectName);
     adminSshSession.assertSuccess();
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    ProjectState projectState = projectCache.get(Project.nameKey(newProjectName));
     assertThat(projectState).isNotNull();
   }
 
@@ -46,7 +46,7 @@
     adminSshSession.exec(
         "gerrit create-project --branch master --owner " + wrongGroupName + " " + newProjectName);
     adminSshSession.assertFailure();
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    ProjectState projectState = projectCache.get(Project.nameKey(newProjectName));
     assertThat(projectState).isNull();
   }
 
@@ -62,7 +62,7 @@
             + newProjectName
             + ".git");
     adminSshSession.assertSuccess();
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    ProjectState projectState = projectCache.get(Project.nameKey(newProjectName));
     assertThat(projectState).isNotNull();
     assertThat(projectState.getName()).isEqualTo(newProjectName);
   }
@@ -79,7 +79,7 @@
             + newProjectName
             + "/");
     adminSshSession.assertSuccess();
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    ProjectState projectState = projectCache.get(Project.nameKey(newProjectName));
     assertThat(projectState).isNotNull();
     assertThat(projectState.getName()).isEqualTo(newProjectName);
   }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
new file mode 100644
index 0000000..434071f
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
@@ -0,0 +1,33 @@
+// 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.ssh;
+
+import com.google.gerrit.index.IndexType;
+import com.google.gerrit.testing.ConfigSuite;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Tests for a defaulted custom index configuration. This unknown type is the opposite of {@link
+ * IndexType#getKnownTypes()}.
+ */
+public class CustomIndexIT extends AbstractIndexTests {
+
+  @ConfigSuite.Default
+  public static Config customIndexType() {
+    Config config = new Config();
+    config.setString("index", null, "type", "custom");
+    return config;
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 339e8ae..f35bcb7 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -18,18 +18,15 @@
 import static com.google.gerrit.elasticsearch.ElasticTestUtils.getConfig;
 
 import com.google.gerrit.elasticsearch.ElasticVersion;
+import com.google.gerrit.index.IndexType;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Injector;
 import org.eclipse.jgit.lib.Config;
 
+/** Tests for every supported {@link IndexType#isElasticsearch()} most recent index version. */
 public class ElasticIndexIT extends AbstractIndexTests {
 
   @ConfigSuite.Default
-  public static Config elasticsearchV6() {
-    return getConfig(ElasticVersion.V6_8);
-  }
-
-  @ConfigSuite.Config
   public static Config elasticsearchV7() {
     return getConfig(ElasticVersion.V7_8);
   }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index c23f889d..9dcfa3f 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GarbageCollectionResult;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.GarbageCollectionQueue;
 import com.google.inject.Inject;
diff --git a/javatests/com/google/gerrit/acceptance/ssh/IndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/IndexIT.java
index 370fb72..7062b00 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/IndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/IndexIT.java
@@ -14,4 +14,7 @@
 
 package com.google.gerrit.acceptance.ssh;
 
+import com.google.gerrit.index.IndexType;
+
+/** Tests for the default {@link IndexType#isLucene()} index configuration. */
 public class IndexIT extends AbstractIndexTests {}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java b/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
index e61e2cc..4bf7c19 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.acceptance.AbstractPluginFieldsTest;
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.query.change.OutputStreamQuery;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
diff --git a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
index 0f47a4a..78960bb 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.ssh;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -294,8 +295,8 @@
     // computation while formatting the output, such as labels, reviewers etc.
     merge(r);
     for (ListChangesOption option : ListChangesOption.values()) {
-      assertThat(gApi.changes().query(r.getChangeId()).withOption(option).get())
-          .named("Option: " + option)
+      assertWithMessage("Option: " + option)
+          .that(gApi.changes().query(r.getChangeId()).withOption(option).get())
           .hasSize(1);
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
index 6998a0a..5a31bfd 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.UseSsh;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.testing.ConfigSuite;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index 4e9c4a4..012e998 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Streams;
@@ -29,6 +30,9 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Spliterator;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 import org.junit.Test;
 
 @NoHttpd
@@ -162,6 +166,38 @@
     assertThat(commands).containsExactlyElementsIn(SLAVE_COMMANDS.get("gerrit")).inOrder();
   }
 
+  @Test
+  @Sandboxed
+  public void showConnections() throws Exception {
+    Spliterator<String> connectionsOutput =
+        getOutputLines(adminSshSession.exec("gerrit show-connections"));
+
+    assertThat(findConnectionsInOutput(connectionsOutput, "user")).hasSize(1);
+  }
+
+  @Test
+  @Sandboxed
+  public void cloeConnections() throws Exception {
+    List<String> connectionsOutput =
+        findConnectionsInOutput(
+            getOutputLines(adminSshSession.exec("gerrit show-connections")), "user");
+    String connectionId =
+        Splitter.on(" ")
+            .trimResults()
+            .omitEmptyStrings()
+            .split(connectionsOutput.get(0))
+            .iterator()
+            .next();
+
+    String closeConnectionOutput = adminSshSession.exec("gerrit close-connection " + connectionId);
+    assertThat(closeConnectionOutput).contains(connectionId);
+
+    assertThat(
+            findConnectionsInOutput(
+                getOutputLines(adminSshSession.exec("gerrit show-connections")), "user"))
+        .isEmpty();
+  }
+
   private List<String> parseCommandsFromGerritHelpText(String helpText) {
     List<String> commands = new ArrayList<>();
 
@@ -194,4 +230,16 @@
 
     return commands;
   }
+
+  private List<String> findConnectionsInOutput(Spliterator<String> connectionsOutput, String user) {
+    List<String> connections =
+        StreamSupport.stream(connectionsOutput, false)
+            .filter(s -> s.contains("localhost") && s.contains(user))
+            .collect(Collectors.toList());
+    return connections;
+  }
+
+  private Spliterator<String> getOutputLines(String output) throws Exception {
+    return Splitter.on("\n").trimResults().omitEmptyStrings().split(output).spliterator();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
index 9c1e23d..ae45d90 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
@@ -16,79 +16,94 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.UseSsh;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.project.CreateProjectArgs;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
-import org.junit.After;
-import org.junit.Before;
+import java.util.ArrayList;
+import java.util.List;
 import org.junit.Test;
 
 @UseSsh
 public class SshTraceIT extends AbstractDaemonTest {
-  @Inject private DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
-
-  private TraceValidatingProjectCreationValidationListener projectCreationListener;
-  private RegistrationHandle projectCreationListenerRegistrationHandle;
-
-  @Before
-  public void setup() {
-    projectCreationListener = new TraceValidatingProjectCreationValidationListener();
-    projectCreationListenerRegistrationHandle =
-        projectCreationValidationListeners.add("gerrit", projectCreationListener);
-  }
-
-  @After
-  public void cleanup() {
-    projectCreationListenerRegistrationHandle.remove();
-  }
+  @Inject private ExtensionRegistry extensionRegistry;
 
   @Test
   public void sshCallWithoutTrace() throws Exception {
-    adminSshSession.exec("gerrit create-project new1");
-    adminSshSession.assertSuccess();
-    assertThat(projectCreationListener.traceId).isNull();
-    assertThat(projectCreationListener.foundTraceId).isFalse();
-    assertThat(projectCreationListener.isLoggingForced).isFalse();
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      adminSshSession.exec("gerrit create-project new1");
+      adminSshSession.assertSuccess();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.foundTraceId).isFalse();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+    }
   }
 
   @Test
   public void sshCallWithTrace() throws Exception {
-    adminSshSession.exec("gerrit create-project --trace new2");
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      adminSshSession.exec("gerrit create-project --trace new2");
 
-    // The trace ID is written to stderr.
-    adminSshSession.assertFailure(RequestId.Type.TRACE_ID.name());
+      // The trace ID is written to stderr.
+      adminSshSession.assertFailure(RequestId.Type.TRACE_ID.name());
 
-    assertThat(projectCreationListener.traceId).isNotNull();
-    assertThat(projectCreationListener.foundTraceId).isTrue();
-    assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.traceId).isNotNull();
+      assertThat(projectCreationListener.foundTraceId).isTrue();
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+    }
   }
 
   @Test
   public void sshCallWithTraceAndProvidedTraceId() throws Exception {
-    adminSshSession.exec("gerrit create-project --trace --trace-id issue/123 new3");
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      adminSshSession.exec("gerrit create-project --trace --trace-id issue/123 new3");
 
-    // The trace ID is written to stderr.
-    adminSshSession.assertFailure(RequestId.Type.TRACE_ID.name());
+      // The trace ID is written to stderr.
+      adminSshSession.assertFailure(RequestId.Type.TRACE_ID.name());
 
-    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
-    assertThat(projectCreationListener.foundTraceId).isTrue();
-    assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+      assertThat(projectCreationListener.foundTraceId).isTrue();
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+    }
   }
 
   @Test
   public void sshCallWithTraceIdAndWithoutTraceFails() throws Exception {
-    adminSshSession.exec("gerrit create-project --trace-id issue/123 new3");
+    adminSshSession.exec("gerrit create-project --trace-id issue/123 new4");
     adminSshSession.assertFailure("A trace ID can only be set if --trace was specified.");
   }
 
+  @Test
+  public void performanceLoggingForSshCall() throws Exception {
+    TestPerformanceLogger testPerformanceLogger = new TestPerformanceLogger();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(testPerformanceLogger)) {
+      adminSshSession.exec("gerrit create-project new5");
+      adminSshSession.assertSuccess();
+      assertThat(testPerformanceLogger.logEntries()).isNotEmpty();
+    }
+  }
+
   private static class TraceValidatingProjectCreationValidationListener
       implements ProjectCreationValidationListener {
     String traceId;
@@ -103,4 +118,28 @@
       this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
     }
   }
+
+  private static class TestPerformanceLogger implements PerformanceLogger {
+    private List<PerformanceLogEntry> logEntries = new ArrayList<>();
+
+    @Override
+    public void log(String operation, long durationMs, Metadata metadata) {
+      logEntries.add(PerformanceLogEntry.create(operation, metadata));
+    }
+
+    ImmutableList<PerformanceLogEntry> logEntries() {
+      return ImmutableList.copyOf(logEntries);
+    }
+  }
+
+  @AutoValue
+  abstract static class PerformanceLogEntry {
+    static PerformanceLogEntry create(String operation, Metadata metadata) {
+      return new AutoValue_SshTraceIT_PerformanceLogEntry(operation, metadata);
+    }
+
+    abstract String operation();
+
+    abstract Metadata metadata();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
index e0d6593..96864d9 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
@@ -15,19 +15,20 @@
 package com.google.gerrit.acceptance.testsuite.group;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.truth.Correspondence;
-import com.google.common.truth.Correspondence.BinaryPredicate;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
 import java.util.Objects;
@@ -217,7 +218,7 @@
 
   @Test
   public void notExistingGroupCanBeCheckedForExistence() throws Exception {
-    AccountGroup.UUID notExistingGroupUuid = new AccountGroup.UUID("not-existing-group");
+    AccountGroup.UUID notExistingGroupUuid = AccountGroup.uuid("not-existing-group");
 
     boolean exists = groupOperations.group(notExistingGroupUuid).exists();
 
@@ -226,10 +227,9 @@
 
   @Test
   public void retrievingNotExistingGroupFails() throws Exception {
-    AccountGroup.UUID notExistingGroupUuid = new AccountGroup.UUID("not-existing-group");
-
-    exception.expect(IllegalStateException.class);
-    groupOperations.group(notExistingGroupUuid).get();
+    AccountGroup.UUID notExistingGroupUuid = AccountGroup.uuid("not-existing-group");
+    assertThrows(
+        IllegalStateException.class, () -> groupOperations.group(notExistingGroupUuid).get());
   }
 
   @Test
@@ -270,7 +270,7 @@
 
     AccountGroup.NameKey groupName = groupOperations.group(groupUuid).get().nameKey();
 
-    assertThat(groupName).isEqualTo(new AccountGroup.NameKey("ABC-789-this-name-must-be-unique"));
+    assertThat(groupName).isEqualTo(AccountGroup.nameKey("ABC-789-this-name-must-be-unique"));
   }
 
   @Test
@@ -297,7 +297,7 @@
 
   @Test
   public void ownerGroupUuidOfExistingGroupCanBeRetrieved() throws Exception {
-    AccountGroup.UUID originalOwnerGroupUuid = new AccountGroup.UUID("owner group");
+    AccountGroup.UUID originalOwnerGroupUuid = AccountGroup.uuid("owner group");
     AccountGroup.UUID groupUuid =
         groupOperations.newGroup().ownerGroupUuid(originalOwnerGroupUuid).create();
 
@@ -314,14 +314,16 @@
     TestGroup visibleGroup = groupOperations.group(visibleGroupUuid).get();
     TestGroup invisibleGroup = groupOperations.group(invisibleGroupUuid).get();
 
-    assertThat(visibleGroup.visibleToAll()).named("visibility of visible group").isTrue();
-    assertThat(invisibleGroup.visibleToAll()).named("visibility of invisible group").isFalse();
+    assertWithMessage("visibility of visible group").that(visibleGroup.visibleToAll()).isTrue();
+    assertWithMessage("visibility of invisible group")
+        .that(invisibleGroup.visibleToAll())
+        .isFalse();
   }
 
   @Test
   public void createdOnOfExistingGroupCanBeRetrieved() throws Exception {
     GroupInfo group = gApi.groups().create(createArbitraryGroupInput()).detail();
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID(group.id);
+    AccountGroup.UUID groupUuid = AccountGroup.uuid(group.id);
 
     Timestamp createdOn = groupOperations.group(groupUuid).get().createdOn();
 
@@ -330,9 +332,9 @@
 
   @Test
   public void membersOfExistingGroupCanBeRetrieved() throws Exception {
-    Account.Id memberId1 = new Account.Id(1000);
-    Account.Id memberId2 = new Account.Id(2000);
-    Account.Id memberId3 = new Account.Id(3000);
+    Account.Id memberId1 = Account.id(1000);
+    Account.Id memberId2 = Account.id(2000);
+    Account.Id memberId3 = Account.id(3000);
     AccountGroup.UUID groupUuid =
         groupOperations.newGroup().members(memberId1, memberId2, memberId3).create();
 
@@ -352,9 +354,9 @@
 
   @Test
   public void subgroupsOfExistingGroupCanBeRetrieved() throws Exception {
-    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
-    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
-    AccountGroup.UUID subgroupUuid3 = new AccountGroup.UUID("subgroup 3");
+    AccountGroup.UUID subgroupUuid1 = AccountGroup.uuid("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = AccountGroup.uuid("subgroup 2");
+    AccountGroup.UUID subgroupUuid3 = AccountGroup.uuid("subgroup 3");
     AccountGroup.UUID groupUuid =
         groupOperations.newGroup().subgroups(subgroupUuid1, subgroupUuid2, subgroupUuid3).create();
 
@@ -428,11 +430,11 @@
 
   @Test
   public void ownerGroupUuidCanBeUpdated() throws Exception {
-    AccountGroup.UUID originalOwnerGroupUuid = new AccountGroup.UUID("original owner");
+    AccountGroup.UUID originalOwnerGroupUuid = AccountGroup.uuid("original owner");
     AccountGroup.UUID groupUuid =
         groupOperations.newGroup().ownerGroupUuid(originalOwnerGroupUuid).create();
 
-    AccountGroup.UUID updatedOwnerGroupUuid = new AccountGroup.UUID("updated owner");
+    AccountGroup.UUID updatedOwnerGroupUuid = AccountGroup.uuid("updated owner");
     groupOperations.group(groupUuid).forUpdate().ownerGroupUuid(updatedOwnerGroupUuid).update();
 
     AccountGroup.UUID currentOwnerGroupUuid =
@@ -454,8 +456,8 @@
   public void membersCanBeAdded() throws Exception {
     AccountGroup.UUID groupUuid = groupOperations.newGroup().clearMembers().create();
 
-    Account.Id memberId1 = new Account.Id(1000);
-    Account.Id memberId2 = new Account.Id(2000);
+    Account.Id memberId1 = Account.id(1000);
+    Account.Id memberId2 = Account.id(2000);
     groupOperations.group(groupUuid).forUpdate().addMember(memberId1).addMember(memberId2).update();
 
     ImmutableSet<Account.Id> members = groupOperations.group(groupUuid).get().members();
@@ -464,8 +466,8 @@
 
   @Test
   public void membersCanBeRemoved() throws Exception {
-    Account.Id memberId1 = new Account.Id(1000);
-    Account.Id memberId2 = new Account.Id(2000);
+    Account.Id memberId1 = Account.id(1000);
+    Account.Id memberId2 = Account.id(2000);
     AccountGroup.UUID groupUuid = groupOperations.newGroup().members(memberId1, memberId2).create();
 
     groupOperations.group(groupUuid).forUpdate().removeMember(memberId2).update();
@@ -476,11 +478,11 @@
 
   @Test
   public void memberAdditionAndRemovalCanBeMixed() throws Exception {
-    Account.Id memberId1 = new Account.Id(1000);
-    Account.Id memberId2 = new Account.Id(2000);
+    Account.Id memberId1 = Account.id(1000);
+    Account.Id memberId2 = Account.id(2000);
     AccountGroup.UUID groupUuid = groupOperations.newGroup().members(memberId1, memberId2).create();
 
-    Account.Id memberId3 = new Account.Id(3000);
+    Account.Id memberId3 = Account.id(3000);
     groupOperations
         .group(groupUuid)
         .forUpdate()
@@ -494,8 +496,8 @@
 
   @Test
   public void membersCanBeCleared() throws Exception {
-    Account.Id memberId1 = new Account.Id(1000);
-    Account.Id memberId2 = new Account.Id(2000);
+    Account.Id memberId1 = Account.id(1000);
+    Account.Id memberId2 = Account.id(2000);
     AccountGroup.UUID groupUuid = groupOperations.newGroup().members(memberId1, memberId2).create();
 
     groupOperations.group(groupUuid).forUpdate().clearMembers().update();
@@ -506,11 +508,11 @@
 
   @Test
   public void furtherMembersCanBeAddedAfterClearingAll() throws Exception {
-    Account.Id memberId1 = new Account.Id(1000);
-    Account.Id memberId2 = new Account.Id(2000);
+    Account.Id memberId1 = Account.id(1000);
+    Account.Id memberId2 = Account.id(2000);
     AccountGroup.UUID groupUuid = groupOperations.newGroup().members(memberId1, memberId2).create();
 
-    Account.Id memberId3 = new Account.Id(3000);
+    Account.Id memberId3 = Account.id(3000);
     groupOperations.group(groupUuid).forUpdate().clearMembers().addMember(memberId3).update();
 
     ImmutableSet<Account.Id> members = groupOperations.group(groupUuid).get().members();
@@ -521,8 +523,8 @@
   public void subgroupsCanBeAdded() throws Exception {
     AccountGroup.UUID groupUuid = groupOperations.newGroup().clearSubgroups().create();
 
-    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
-    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    AccountGroup.UUID subgroupUuid1 = AccountGroup.uuid("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = AccountGroup.uuid("subgroup 2");
     groupOperations
         .group(groupUuid)
         .forUpdate()
@@ -536,8 +538,8 @@
 
   @Test
   public void subgroupsCanBeRemoved() throws Exception {
-    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
-    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    AccountGroup.UUID subgroupUuid1 = AccountGroup.uuid("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = AccountGroup.uuid("subgroup 2");
     AccountGroup.UUID groupUuid =
         groupOperations.newGroup().subgroups(subgroupUuid1, subgroupUuid2).create();
 
@@ -549,12 +551,12 @@
 
   @Test
   public void subgroupAdditionAndRemovalCanBeMixed() throws Exception {
-    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
-    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    AccountGroup.UUID subgroupUuid1 = AccountGroup.uuid("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = AccountGroup.uuid("subgroup 2");
     AccountGroup.UUID groupUuid =
         groupOperations.newGroup().subgroups(subgroupUuid1, subgroupUuid2).create();
 
-    AccountGroup.UUID subgroupUuid3 = new AccountGroup.UUID("subgroup 3");
+    AccountGroup.UUID subgroupUuid3 = AccountGroup.uuid("subgroup 3");
     groupOperations
         .group(groupUuid)
         .forUpdate()
@@ -568,8 +570,8 @@
 
   @Test
   public void subgroupsCanBeCleared() throws Exception {
-    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
-    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    AccountGroup.UUID subgroupUuid1 = AccountGroup.uuid("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = AccountGroup.uuid("subgroup 2");
     AccountGroup.UUID groupUuid =
         groupOperations.newGroup().subgroups(subgroupUuid1, subgroupUuid2).create();
 
@@ -581,12 +583,12 @@
 
   @Test
   public void furtherSubgroupsCanBeAddedAfterClearingAll() throws Exception {
-    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
-    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    AccountGroup.UUID subgroupUuid1 = AccountGroup.uuid("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = AccountGroup.uuid("subgroup 2");
     AccountGroup.UUID groupUuid =
         groupOperations.newGroup().subgroups(subgroupUuid1, subgroupUuid2).create();
 
-    AccountGroup.UUID subgroupUuid3 = new AccountGroup.UUID("subgroup 3");
+    AccountGroup.UUID subgroupUuid3 = AccountGroup.uuid("subgroup 3");
     groupOperations
         .group(groupUuid)
         .forUpdate()
@@ -610,37 +612,31 @@
 
   private AccountGroup.UUID createGroupInServer(GroupInput input) throws RestApiException {
     GroupInfo group = gApi.groups().create(input).detail();
-    return new AccountGroup.UUID(group.id);
+    return AccountGroup.uuid(group.id);
   }
 
   private static Correspondence<AccountInfo, Account.Id> getAccountToIdCorrespondence() {
     return Correspondence.from(
-        new BinaryPredicate<AccountInfo, Account.Id>() {
-          @Override
-          public boolean apply(AccountInfo actualAccount, Account.Id expectedId) {
-            Account.Id accountId =
-                Optional.ofNullable(actualAccount)
-                    .map(account -> account._accountId)
-                    .map(Account.Id::new)
-                    .orElse(null);
-            return Objects.equals(accountId, expectedId);
-          }
+        (actualAccount, expectedId) -> {
+          Account.Id accountId =
+              Optional.ofNullable(actualAccount)
+                  .map(account -> account._accountId)
+                  .map(Account::id)
+                  .orElse(null);
+          return Objects.equals(accountId, expectedId);
         },
         "has ID");
   }
 
   private static Correspondence<GroupInfo, AccountGroup.UUID> getGroupToUuidCorrespondence() {
     return Correspondence.from(
-        new BinaryPredicate<GroupInfo, AccountGroup.UUID>() {
-          @Override
-          public boolean apply(GroupInfo actualGroup, AccountGroup.UUID expectedUuid) {
-            AccountGroup.UUID groupUuid =
-                Optional.ofNullable(actualGroup)
-                    .map(group -> group.id)
-                    .map(AccountGroup.UUID::new)
-                    .orElse(null);
-            return Objects.equals(groupUuid, expectedUuid);
-          }
+        (actualGroup, expectedUuid) -> {
+          AccountGroup.UUID groupUuid =
+              Optional.ofNullable(actualGroup)
+                  .map(group -> group.id)
+                  .map(AccountGroup::uuid)
+                  .orElse(null);
+          return Objects.equals(groupUuid, expectedUuid);
         },
         "has UUID");
   }
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
index 3f537c0..2a44571 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -15,14 +15,41 @@
 package com.google.gerrit.acceptance.testsuite.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.QUERY_LIMIT;
+import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
+import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import java.util.List;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.junit.Test;
 
 public class ProjectOperationsImplTest extends AbstractDaemonTest {
@@ -48,9 +75,526 @@
   @Test
   public void emptyCommit() throws Exception {
     Project.NameKey key = projectOperations.newProject().create();
+
     List<BranchInfo> branches = gApi.projects().name(key.get()).branches().get();
     assertThat(branches).isNotEmpty();
     assertThat(branches.stream().map(x -> x.ref).collect(toList()))
         .isEqualTo(ImmutableList.of("HEAD", "refs/meta/config", "refs/heads/master"));
   }
+
+  @Test
+  public void getProjectConfig() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    assertThat(projectOperations.project(key).getProjectConfig().getProject().getDescription())
+        .isEmpty();
+
+    ConfigInput input = new ConfigInput();
+    input.description = "my fancy project";
+    gApi.projects().name(key.get()).config(input);
+
+    assertThat(projectOperations.project(key).getProjectConfig().getProject().getDescription())
+        .isEqualTo("my fancy project");
+  }
+
+  @Test
+  public void mutatingResultOfGetProjectConfigDoesNotMutateGlobalCachedValue() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    ProjectConfig projectConfig = projectOperations.project(key).getProjectConfig();
+    ProjectState cachedProjectState1 = projectCache.checkedGet(key);
+    ProjectConfig cachedProjectConfig1 = cachedProjectState1.getConfig();
+    assertThat(cachedProjectConfig1).isNotSameInstanceAs(projectConfig);
+    assertThat(cachedProjectConfig1.getProject().getDescription()).isEmpty();
+    assertThat(projectConfig.getProject().getDescription()).isEmpty();
+    projectConfig.getProject().setDescription("my fancy project");
+
+    ProjectConfig cachedProjectConfig2 = projectCache.checkedGet(key).getConfig();
+    assertThat(cachedProjectConfig2).isNotSameInstanceAs(projectConfig);
+    assertThat(cachedProjectConfig2.getProject().getDescription()).isEmpty();
+  }
+
+  @Test
+  public void getProjectConfigNoRefsMetaConfig() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    deleteRefsMetaConfig(key);
+
+    ProjectConfig projectConfig = projectOperations.project(key).getProjectConfig();
+    assertThat(projectConfig.getName()).isEqualTo(key);
+    assertThat(projectConfig.getRevision()).isNull();
+  }
+
+  @Test
+  public void getConfig() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).isNotInstanceOf(StoredConfig.class);
+    assertThat(config).text().isEmpty();
+
+    ConfigInput input = new ConfigInput();
+    input.description = "my fancy project";
+    gApi.projects().name(key.get()).config(input);
+
+    config = projectOperations.project(key).getConfig();
+    assertThat(config).isNotInstanceOf(StoredConfig.class);
+    assertThat(config).sections().containsExactly("project");
+    assertThat(config).subsections("project").isEmpty();
+    assertThat(config).sectionValues("project").containsExactly("description", "my fancy project");
+  }
+
+  @Test
+  public void getConfigNoRefsMetaConfig() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    deleteRefsMetaConfig(key);
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).isNotInstanceOf(StoredConfig.class);
+    assertThat(config).isEmpty();
+  }
+
+  @Test
+  public void addAllowPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("abandon", "group global:Registered-Users");
+  }
+
+  @Test
+  public void addDenyPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(deny(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("abandon", "deny group global:Registered-Users");
+  }
+
+  @Test
+  public void addBlockPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(block(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("abandon", "block group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowForcePermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS).force(true))
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("abandon", "+force group global:Registered-Users");
+  }
+
+  @Test
+  public void updateExclusivePermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .setExclusiveGroup(permissionKey(Permission.ABANDON).ref("refs/foo"), true)
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly(
+            "abandon", "group global:Registered-Users",
+            "exclusiveGroupPermissions", "abandon");
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .setExclusiveGroup(permissionKey(Permission.ABANDON).ref("refs/foo"), false)
+        .update();
+
+    config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("abandon", "group global:Registered-Users");
+  }
+
+  @Test
+  public void addMultipleExclusivePermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .setExclusiveGroup(permissionKey(Permission.ABANDON).ref("refs/foo"), true)
+        .setExclusiveGroup(permissionKey(Permission.CREATE).ref("refs/foo"), true)
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsEntry("exclusiveGroupPermissions", "abandon create");
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .setExclusiveGroup(permissionKey(Permission.ABANDON).ref("refs/foo"), false)
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsEntry("exclusiveGroupPermissions", "create");
+  }
+
+  @Test
+  public void addMultiplePermissions() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(PROJECT_OWNERS))
+        .add(allow(Permission.CREATE).ref("refs/foo").group(REGISTERED_USERS))
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly(
+            "abandon", "group global:Project-Owners",
+            "create", "group global:Registered-Users");
+  }
+
+  @Test
+  public void addDuplicatePermissions() throws Exception {
+    TestPermission permission =
+        TestProjectUpdate.allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS).build();
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations.project(key).forUpdate().add(permission).add(permission).update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly(
+            "abandon", "group global:Registered-Users",
+            "abandon", "group global:Registered-Users");
+
+    projectOperations.project(key).forUpdate().add(permission).update();
+    config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly(
+            "abandon", "group global:Registered-Users",
+            "abandon", "group global:Registered-Users",
+            "abandon", "group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowLabelPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("label-Code-Review", "-1..+2 group global:Registered-Users");
+  }
+
+  @Test
+  public void addBlockLabelPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("label-Code-Review", "block -1..+2 group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowExclusiveLabelPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+        .setExclusiveGroup(labelPermissionKey("Code-Review").ref("refs/foo"), true)
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly(
+            "label-Code-Review", "-1..+2 group global:Registered-Users",
+            "exclusiveGroupPermissions", "label-Code-Review");
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .setExclusiveGroup(labelPermissionKey("Code-Review").ref("refs/foo"), false)
+        .update();
+
+    config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("label-Code-Review", "-1..+2 group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowLabelAsPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(
+            allowLabel("Code-Review")
+                .ref("refs/foo")
+                .group(REGISTERED_USERS)
+                .range(-1, 2)
+                .impersonation(true))
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("labelAs-Code-Review", "-1..+2 group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowCapability() throws Exception {
+    Config config = projectOperations.project(allProjects).getConfig();
+    assertThat(config)
+        .sectionValues("capability")
+        .doesNotContainEntry("administrateServer", "group Registered Users");
+
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+        .update();
+
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .sectionValues("capability")
+        .containsEntry("administrateServer", "group Registered Users");
+  }
+
+  @Test
+  public void addAllowCapabilityWithRange() throws Exception {
+    Config config = projectOperations.project(allProjects).getConfig();
+    assertThat(config).sectionValues("capability").doesNotContainKey("queryLimit");
+
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(0, 5000))
+        .update();
+
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .sectionValues("capability")
+        .containsEntry("queryLimit", "+0..+5000 group Registered Users");
+  }
+
+  @Test
+  public void addAllowCapabilityWithDefaultRange() throws Exception {
+    Config config = projectOperations.project(allProjects).getConfig();
+    assertThat(config).sectionValues("capability").doesNotContainKey("queryLimit");
+
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS))
+        .update();
+
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .sectionValues("capability")
+        .containsEntry("queryLimit", "+0..+" + DEFAULT_MAX_QUERY_LIMIT + " group Registered Users");
+  }
+
+  @Test
+  public void removePermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(PROJECT_OWNERS))
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsExactly(
+            "abandon", "group global:Registered-Users",
+            "abandon", "group global:Project-Owners");
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .remove(permissionKey(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("abandon", "group global:Project-Owners");
+  }
+
+  @Test
+  public void removeLabelPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+        .add(allowLabel("Code-Review").ref("refs/foo").group(PROJECT_OWNERS).range(-2, 1))
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsExactly(
+            "label-Code-Review", "-1..+2 group global:Registered-Users",
+            "label-Code-Review", "-2..+1 group global:Project-Owners");
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .remove(labelPermissionKey("Code-Review").ref("refs/foo").group(REGISTERED_USERS))
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("label-Code-Review", "-2..+1 group global:Project-Owners");
+  }
+
+  @Test
+  public void removeCapability() throws Exception {
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+        .add(allowCapability(ADMINISTRATE_SERVER).group(PROJECT_OWNERS))
+        .update();
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .sectionValues("capability")
+        .containsAtLeastEntriesIn(
+            ImmutableListMultimap.of(
+                "administrateServer", "group Registered Users",
+                "administrateServer", "group Project Owners"));
+
+    projectOperations
+        .allProjectsForUpdate()
+        .remove(capabilityKey(ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+        .update();
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .sectionValues("capability")
+        .doesNotContainEntry("administrateServer", "group Registered Users");
+  }
+
+  @Test
+  public void removeOnePermissionForAllGroupsFromOneAccessSection() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(PROJECT_OWNERS))
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .add(allow(Permission.CREATE).ref("refs/foo").group(REGISTERED_USERS))
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsAtLeastEntriesIn(
+            ImmutableListMultimap.of(
+                "abandon", "group global:Project-Owners",
+                "abandon", "group global:Registered-Users",
+                "create", "group global:Registered-Users"));
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .remove(permissionKey(Permission.ABANDON).ref("refs/foo"))
+        .update();
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).subsectionValues("access", "refs/foo").doesNotContainKey("abandon");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsEntry("create", "group global:Registered-Users");
+  }
+
+  @Test
+  public void removeAllAccessSections() {
+    projectOperations.allProjectsForUpdate().removeAllAccessSections().update();
+
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .sectionValues("access")
+        .isEmpty();
+  }
+
+  @Test
+  public void updatingCapabilitiesNotAllowedForNonAllProjects() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    assertThrows(
+        RuntimeException.class,
+        () ->
+            projectOperations
+                .project(key)
+                .forUpdate()
+                .add(allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+                .update());
+    assertThrows(
+        RuntimeException.class,
+        () ->
+            projectOperations
+                .project(key)
+                .forUpdate()
+                .remove(capabilityKey(ADMINISTRATE_SERVER))
+                .update());
+  }
+
+  private void deleteRefsMetaConfig(Project.NameKey key) throws Exception {
+    try (Repository repo = repoManager.openRepository(key);
+        TestRepository<Repository> tr = new TestRepository<>(repo)) {
+      tr.delete(REFS_CONFIG);
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdateTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdateTest.java
new file mode 100644
index 0000000..8fc1677
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdateTest.java
@@ -0,0 +1,202 @@
+// 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.testsuite.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.BATCH_CHANGES_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_BATCH_CHANGES_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.QUERY_LIMIT;
+import static com.google.gerrit.common.data.Permission.ABANDON;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestCapability;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestLabelPermission;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermissionKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import java.util.function.Function;
+import org.junit.Test;
+
+public class TestProjectUpdateTest {
+  private static final AllProjectsName ALL_PROJECTS_NAME = new AllProjectsName("All-Projects");
+
+  @Test
+  public void testCapabilityDisallowsZeroRangeOnCapabilityThatHasNoRange() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS).range(0, 0).build());
+  }
+
+  @Test
+  public void testCapabilityAllowsZeroRangeOnCapabilityThatHasRange() throws Exception {
+    TestCapability c = allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(0, 0).build();
+    assertThat(c.min()).isEqualTo(0);
+    assertThat(c.max()).isEqualTo(0);
+  }
+
+  @Test
+  public void testCapabilityDisallowsInvertedRange() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(1, 0).build());
+  }
+
+  @Test
+  public void testCapabilityDisallowsRangeIfCapabilityDoesNotSupportRange() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS).range(-1, 1).build());
+  }
+
+  @Test
+  public void testCapabilityRangeIsZeroIfCapabilityDoesNotSupportRange() throws Exception {
+    TestCapability c = allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS).build();
+    assertThat(c.min()).isEqualTo(0);
+    assertThat(c.max()).isEqualTo(0);
+  }
+
+  @Test
+  public void testCapabilityUsesDefaultRangeIfUnspecified() throws Exception {
+    TestCapability c = allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).build();
+    assertThat(c.min()).isEqualTo(0);
+    assertThat(c.max()).isEqualTo(DEFAULT_MAX_QUERY_LIMIT);
+
+    c = allowCapability(BATCH_CHANGES_LIMIT).group(REGISTERED_USERS).build();
+    assertThat(c.min()).isEqualTo(0);
+    assertThat(c.max()).isEqualTo(DEFAULT_MAX_BATCH_CHANGES_LIMIT);
+  }
+
+  @Test
+  public void testCapabilityUsesExplicitRangeIfSpecified() throws Exception {
+    TestCapability c = allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(5, 20).build();
+    assertThat(c.min()).isEqualTo(5);
+    assertThat(c.max()).isEqualTo(20);
+  }
+
+  @Test
+  public void testLabelPermissionRequiresValidLabelName() throws Exception {
+    Function<String, TestLabelPermission.Builder> labelBuilder =
+        name -> allowLabel(name).ref("refs/*").group(REGISTERED_USERS).range(-1, 1);
+    assertThat(labelBuilder.apply("Code-Review").build().name()).isEqualTo("Code-Review");
+    assertThrows(RuntimeException.class, () -> labelBuilder.apply("not a label").build());
+    assertThrows(RuntimeException.class, () -> labelBuilder.apply("label-Code-Review").build());
+  }
+
+  @Test
+  public void testLabelPermissionDisallowsZeroRange() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(0, 0).build());
+  }
+
+  @Test
+  public void testLabelPermissionDisallowsInvertedRange() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(1, 0).build());
+  }
+
+  @Test
+  public void testPermissionKeyRequiresValidRefName() throws Exception {
+    Function<String, TestPermissionKey.Builder> keyBuilder =
+        ref -> permissionKey(ABANDON).ref(ref).group(REGISTERED_USERS);
+    assertThat(keyBuilder.apply("refs/*").build().section()).isEqualTo("refs/*");
+    assertThrows(RuntimeException.class, () -> keyBuilder.apply(null).build());
+    assertThrows(RuntimeException.class, () -> keyBuilder.apply("foo").build());
+  }
+
+  @Test
+  public void testLabelPermissionKeyRequiresValidLabelName() throws Exception {
+    Function<String, TestPermissionKey.Builder> keyBuilder =
+        label -> labelPermissionKey(label).ref("refs/*").group(REGISTERED_USERS);
+    assertThat(keyBuilder.apply("Code-Review").build().name()).isEqualTo("label-Code-Review");
+    assertThrows(RuntimeException.class, () -> keyBuilder.apply(null).build());
+    assertThrows(RuntimeException.class, () -> keyBuilder.apply("not a label").build());
+    assertThrows(RuntimeException.class, () -> keyBuilder.apply("label-Code-Review").build());
+  }
+
+  @Test
+  public void testPermissionKeyDisallowsSettingRefOnGlobalCapability() throws Exception {
+    assertThrows(RuntimeException.class, () -> capabilityKey(ADMINISTRATE_SERVER).ref("refs/*"));
+  }
+
+  @Test
+  public void testProjectUpdateDisallowsGroupOnExclusiveGroupPermissionKey() throws Exception {
+    TestPermissionKey.Builder b = permissionKey(ABANDON).ref("refs/*");
+    Function<TestPermissionKey.Builder, TestProjectUpdate.Builder> updateBuilder =
+        kb -> builder().setExclusiveGroup(kb, true);
+
+    assertThat(updateBuilder.apply(b).build().exclusiveGroupPermissions())
+        .containsExactly(b.build(), true);
+
+    b.group(REGISTERED_USERS);
+    assertThrows(RuntimeException.class, () -> updateBuilder.apply(b).build());
+  }
+
+  @Test
+  public void hasCapabilityUpdates() throws Exception {
+    assertThat(builder().build().hasCapabilityUpdates()).isFalse();
+    assertThat(
+            builder()
+                .add(allow(ABANDON).ref("refs/*").group(REGISTERED_USERS))
+                .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(0, 1))
+                .remove(permissionKey(ABANDON).ref("refs/foo"))
+                .remove(labelPermissionKey("Code-Review").ref("refs/foo"))
+                .setExclusiveGroup(permissionKey(ABANDON).ref("refs/bar"), true)
+                .setExclusiveGroup(labelPermissionKey(ABANDON).ref("refs/bar"), true)
+                .build()
+                .hasCapabilityUpdates())
+        .isFalse();
+    assertThat(
+            builder(ALL_PROJECTS_NAME)
+                .add(allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+                .build()
+                .hasCapabilityUpdates())
+        .isTrue();
+    assertThat(
+            builder(ALL_PROJECTS_NAME)
+                .remove(capabilityKey(ADMINISTRATE_SERVER))
+                .build()
+                .hasCapabilityUpdates())
+        .isTrue();
+  }
+
+  @Test
+  public void updatingCapabilitiesNotAllowedForNonAllProjects() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> builder().add(allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS)).update());
+    assertThrows(
+        RuntimeException.class,
+        () -> builder().remove(capabilityKey(ADMINISTRATE_SERVER)).update());
+  }
+
+  private static TestProjectUpdate.Builder builder() {
+    return builder(Project.nameKey("test-project"));
+  }
+
+  private static TestProjectUpdate.Builder builder(Project.NameKey nameKey) {
+    return TestProjectUpdate.builder(nameKey, ALL_PROJECTS_NAME, u -> {});
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
index 5cbed1b..f6421a5 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
@@ -15,8 +15,9 @@
 package com.google.gerrit.acceptance.testsuite.request;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -24,8 +25,8 @@
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.CurrentUser.PropertyKey;
@@ -67,12 +68,9 @@
   @Test
   public void setApiUserToNonExistingUser() throws Exception {
     fastCheckCurrentUser(admin.id());
-    try {
-      requestScopeOperations.setApiUser(new Account.Id(sequences.nextAccountId()));
-      assert_().fail("expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(
+        RuntimeException.class,
+        () -> requestScopeOperations.setApiUser(Account.id(sequences.nextAccountId())));
     checkCurrentUser(admin.id());
   }
 
@@ -98,24 +96,26 @@
 
   private void fastCheckCurrentUser(Account.Id expected) {
     // Check current user quickly, since the full check requires creating changes and is quite slow.
-    assertThat(userProvider.get().isIdentifiedUser())
-        .named("user from provider is an IdentifiedUser")
+    assertWithMessage("user from provider is an IdentifiedUser")
+        .that(userProvider.get().isIdentifiedUser())
         .isTrue();
-    assertThat(userProvider.get().getAccountId()).named("user from provider").isEqualTo(expected);
+    assertWithMessage("user from provider")
+        .that(userProvider.get().getAccountId())
+        .isEqualTo(expected);
   }
 
   private void checkCurrentUser(Account.Id expected) throws Exception {
     // Test all supported ways that an acceptance test might query the active user.
     fastCheckCurrentUser(expected);
-    assertThat(gApi.accounts().self().get()._accountId)
-        .named("user from GerritApi")
+    assertWithMessage("user from GerritApi")
+        .that(gApi.accounts().self().get()._accountId)
         .isEqualTo(expected.get());
     AcceptanceTestRequestScope.Context ctx = atrScope.get();
-    assertThat(ctx.getUser().isIdentifiedUser())
-        .named("user from AcceptanceTestRequestScope.Context is an IdentifiedUser")
+    assertWithMessage("user from AcceptanceTestRequestScope.Context is an IdentifiedUser")
+        .that(ctx.getUser().isIdentifiedUser())
         .isTrue();
-    assertThat(ctx.getUser().getAccountId())
-        .named("user from AcceptanceTestRequestScope.Context")
+    assertWithMessage("user from AcceptanceTestRequestScope.Context")
+        .that(ctx.getUser().getAccountId())
         .isEqualTo(expected);
     checkSshUser(expected);
   }
@@ -131,8 +131,8 @@
     assertThat(gApi.changes().id(changeId).get().owner._accountId).isEqualTo(expected.get());
     String queryResults =
         atrScope.get().getSession().exec("gerrit query owner:self change:" + changeId);
-    assertThat(findDistinct(queryResults, "I[0-9a-f]{40}"))
-        .named("Change-Ids in query results:\n%s", queryResults)
+    assertWithMessage("Change-Ids in query results:\n%s", queryResults)
+        .that(findDistinct(queryResults, "I[0-9a-f]{40}"))
         .containsExactly(changeId);
   }
 
diff --git a/javatests/com/google/gerrit/common/AutoValueTest.java b/javatests/com/google/gerrit/common/AutoValueTest.java
index 89d7bf4..947fe4a 100644
--- a/javatests/com/google/gerrit/common/AutoValueTest.java
+++ b/javatests/com/google/gerrit/common/AutoValueTest.java
@@ -17,10 +17,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.auto.value.AutoValue;
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class AutoValueTest extends GerritBaseTests {
+public class AutoValueTest {
   @AutoValue
   abstract static class Auto {
     static Auto create(String val) {
diff --git a/javatests/com/google/gerrit/common/BUILD b/javatests/com/google/gerrit/common/BUILD
index 18ececd..c7b21a3 100644
--- a/javatests/com/google/gerrit/common/BUILD
+++ b/javatests/com/google/gerrit/common/BUILD
@@ -8,7 +8,6 @@
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/common:version",
         "//java/com/google/gerrit/launcher",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/javatests/com/google/gerrit/common/data/AccessSectionTest.java b/javatests/com/google/gerrit/common/data/AccessSectionTest.java
index faf9d6c..e775cbc 100644
--- a/javatests/com/google/gerrit/common/data/AccessSectionTest.java
+++ b/javatests/com/google/gerrit/common/data/AccessSectionTest.java
@@ -15,16 +15,16 @@
 package com.google.gerrit.common.data;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import org.junit.Before;
 import org.junit.Test;
 
-public class AccessSectionTest extends GerritBaseTests {
+public class AccessSectionTest {
   private static final String REF_PATTERN = "refs/heads/master";
 
   private AccessSection accessSection;
@@ -57,16 +57,17 @@
     Permission submitPermission = new Permission(Permission.SUBMIT);
     accessSection.setPermissions(ImmutableList.of(submitPermission));
     assertThat(accessSection.getPermissions()).containsExactly(submitPermission);
-
-    exception.expect(NullPointerException.class);
-    accessSection.setPermissions(null);
+    assertThrows(NullPointerException.class, () -> accessSection.setPermissions(null));
   }
 
   @Test
   public void cannotSetDuplicatePermissions() {
-    exception.expect(IllegalArgumentException.class);
-    accessSection.setPermissions(
-        ImmutableList.of(new Permission(Permission.ABANDON), new Permission(Permission.ABANDON)));
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            accessSection.setPermissions(
+                ImmutableList.of(
+                    new Permission(Permission.ABANDON), new Permission(Permission.ABANDON))));
   }
 
   @Test
@@ -76,9 +77,11 @@
     Permission abandonPermissionUpperCase =
         new Permission(Permission.ABANDON.toUpperCase(Locale.US));
 
-    exception.expect(IllegalArgumentException.class);
-    accessSection.setPermissions(
-        ImmutableList.of(abandonPermissionLowerCase, abandonPermissionUpperCase));
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            accessSection.setPermissions(
+                ImmutableList.of(abandonPermissionLowerCase, abandonPermissionUpperCase)));
   }
 
   @Test
@@ -92,9 +95,7 @@
     Permission submitPermission = new Permission(Permission.SUBMIT);
     accessSection.setPermissions(ImmutableList.of(submitPermission));
     assertThat(accessSection.getPermission(Permission.SUBMIT)).isEqualTo(submitPermission);
-
-    exception.expect(NullPointerException.class);
-    accessSection.getPermission(null);
+    assertThrows(NullPointerException.class, () -> accessSection.getPermission(null));
   }
 
   @Test
@@ -112,8 +113,7 @@
     assertThat(accessSection.getPermission(Permission.SUBMIT, true))
         .isEqualTo(new Permission(Permission.SUBMIT));
 
-    exception.expect(NullPointerException.class);
-    accessSection.getPermission(null, true);
+    assertThrows(NullPointerException.class, () -> accessSection.getPermission(null, true));
   }
 
   @Test
@@ -130,9 +130,7 @@
     assertThat(accessSection.getPermissions())
         .containsExactly(abandonPermission, rebasePermission, submitPermission)
         .inOrder();
-
-    exception.expect(NullPointerException.class);
-    accessSection.addPermission(null);
+    assertThrows(NullPointerException.class, () -> accessSection.addPermission(null));
   }
 
   @Test
@@ -166,9 +164,7 @@
     assertThat(accessSection.getPermissions())
         .containsExactly(abandonPermission, rebasePermission)
         .inOrder();
-
-    exception.expect(NullPointerException.class);
-    accessSection.remove(null);
+    assertThrows(NullPointerException.class, () -> accessSection.remove(null));
   }
 
   @Test
@@ -187,8 +183,7 @@
         .containsExactly(abandonPermission, rebasePermission)
         .inOrder();
 
-    exception.expect(NullPointerException.class);
-    accessSection.removePermission(null);
+    assertThrows(NullPointerException.class, () -> accessSection.removePermission(null));
   }
 
   @Test
@@ -229,9 +224,7 @@
     assertThat(accessSection1.getPermissions())
         .containsExactly(abandonPermission, rebasePermission, submitPermission)
         .inOrder();
-
-    exception.expect(NullPointerException.class);
-    accessSection.mergeFrom(null);
+    assertThrows(NullPointerException.class, () -> accessSection.mergeFrom(null));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/common/data/BUILD b/javatests/com/google/gerrit/common/data/BUILD
index 776a5e0..f2b7d63 100644
--- a/javatests/com/google/gerrit/common/data/BUILD
+++ b/javatests/com/google/gerrit/common/data/BUILD
@@ -5,7 +5,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib/truth",
diff --git a/javatests/com/google/gerrit/common/data/EncodePathSeparatorTest.java b/javatests/com/google/gerrit/common/data/EncodePathSeparatorTest.java
index 3dd2db3..dcd3c05 100644
--- a/javatests/com/google/gerrit/common/data/EncodePathSeparatorTest.java
+++ b/javatests/com/google/gerrit/common/data/EncodePathSeparatorTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class EncodePathSeparatorTest extends GerritBaseTests {
+public class EncodePathSeparatorTest {
   @Test
   public void defaultBehaviour() {
     assertThat(new GitwebType().replacePathSeparator("a/b")).isEqualTo("a/b");
diff --git a/javatests/com/google/gerrit/common/data/FilenameComparatorTest.java b/javatests/com/google/gerrit/common/data/FilenameComparatorTest.java
index 055f57d..ec71e05 100644
--- a/javatests/com/google/gerrit/common/data/FilenameComparatorTest.java
+++ b/javatests/com/google/gerrit/common/data/FilenameComparatorTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class FilenameComparatorTest extends GerritBaseTests {
+public class FilenameComparatorTest {
   private FilenameComparator comparator = FilenameComparator.INSTANCE;
 
   @Test
diff --git a/javatests/com/google/gerrit/common/data/GroupReferenceTest.java b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
index 8cf486b..25b55c7 100644
--- a/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
+++ b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
@@ -15,17 +15,17 @@
 package com.google.gerrit.common.data;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.AccountGroup.UUID;
 import org.junit.Test;
 
-public class GroupReferenceTest extends GerritBaseTests {
+public class GroupReferenceTest {
   @Test
   public void forGroupDescription() {
     String name = "foo";
-    AccountGroup.UUID uuid = new AccountGroup.UUID("uuid-foo");
+    AccountGroup.UUID uuid = AccountGroup.uuid("uuid-foo");
     GroupReference groupReference =
         GroupReference.forGroup(
             new GroupDescription.Basic() {
@@ -56,7 +56,7 @@
 
   @Test
   public void create() {
-    AccountGroup.UUID uuid = new AccountGroup.UUID("uuid");
+    AccountGroup.UUID uuid = AccountGroup.uuid("uuid");
     String name = "foo";
     GroupReference groupReference = new GroupReference(uuid, name);
     assertThat(groupReference.getUUID()).isEqualTo(uuid);
@@ -68,15 +68,15 @@
     // GroupReferences where the UUID is null are used to represent groups from project.config that
     // cannot be resolved.
     String name = "foo";
-    GroupReference groupReference = new GroupReference(null, name);
+    GroupReference groupReference = new GroupReference(name);
     assertThat(groupReference.getUUID()).isNull();
     assertThat(groupReference.getName()).isEqualTo(name);
   }
 
   @Test
   public void cannotCreateWithoutName() {
-    exception.expect(NullPointerException.class);
-    new GroupReference(new AccountGroup.UUID("uuid"), null);
+    assertThrows(
+        NullPointerException.class, () -> new GroupReference(AccountGroup.uuid("uuid"), null));
   }
 
   @Test
@@ -99,12 +99,12 @@
 
   @Test
   public void getAndSetUuid() {
-    AccountGroup.UUID uuid = new AccountGroup.UUID("uuid-foo");
+    AccountGroup.UUID uuid = AccountGroup.uuid("uuid-foo");
     String name = "foo";
     GroupReference groupReference = new GroupReference(uuid, name);
     assertThat(groupReference.getUUID()).isEqualTo(uuid);
 
-    AccountGroup.UUID uuid2 = new AccountGroup.UUID("uuid-bar");
+    AccountGroup.UUID uuid2 = AccountGroup.uuid("uuid-bar");
     groupReference.setUUID(uuid2);
     assertThat(groupReference.getUUID()).isEqualTo(uuid2);
 
@@ -116,7 +116,7 @@
 
   @Test
   public void getAndSetName() {
-    AccountGroup.UUID uuid = new AccountGroup.UUID("uuid-foo");
+    AccountGroup.UUID uuid = AccountGroup.uuid("uuid-foo");
     String name = "foo";
     GroupReference groupReference = new GroupReference(uuid, name);
     assertThat(groupReference.getName()).isEqualTo(name);
@@ -125,21 +125,20 @@
     groupReference.setName(name2);
     assertThat(groupReference.getName()).isEqualTo(name2);
 
-    exception.expect(NullPointerException.class);
-    groupReference.setName(null);
+    assertThrows(NullPointerException.class, () -> groupReference.setName(null));
   }
 
   @Test
   public void toConfigValue() {
     String name = "foo";
-    GroupReference groupReference = new GroupReference(new AccountGroup.UUID("uuid-foo"), name);
+    GroupReference groupReference = new GroupReference(AccountGroup.uuid("uuid-foo"), name);
     assertThat(groupReference.toConfigValue()).isEqualTo("group " + name);
   }
 
   @Test
   public void testEquals() {
-    AccountGroup.UUID uuid1 = new AccountGroup.UUID("uuid-foo");
-    AccountGroup.UUID uuid2 = new AccountGroup.UUID("uuid-bar");
+    AccountGroup.UUID uuid1 = AccountGroup.uuid("uuid-foo");
+    AccountGroup.UUID uuid2 = AccountGroup.uuid("uuid-bar");
     String name1 = "foo";
     String name2 = "bar";
 
@@ -154,12 +153,11 @@
 
   @Test
   public void testHashcode() {
-    AccountGroup.UUID uuid1 = new AccountGroup.UUID("uuid1");
+    AccountGroup.UUID uuid1 = AccountGroup.uuid("uuid1");
     assertThat(new GroupReference(uuid1, "foo").hashCode())
         .isEqualTo(new GroupReference(uuid1, "bar").hashCode());
 
     // Check that the following calls don't fail with an exception.
-    new GroupReference(null, "bar").hashCode();
-    new GroupReference(new AccountGroup.UUID(null), "bar").hashCode();
+    new GroupReference("bar").hashCode();
   }
 }
diff --git a/javatests/com/google/gerrit/common/data/LabelFunctionTest.java b/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
index a534a9e..6f5232b 100644
--- a/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
+++ b/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
@@ -17,23 +17,22 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.testing.GerritBaseTests;
-import java.sql.Date;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import org.junit.Test;
 
-public class LabelFunctionTest extends GerritBaseTests {
+public class LabelFunctionTest {
   private static final String LABEL_NAME = "Verified";
-  private static final LabelId LABEL_ID = new LabelId(LABEL_NAME);
-  private static final Change.Id CHANGE_ID = new Change.Id(100);
-  private static final PatchSet.Id PS_ID = new PatchSet.Id(CHANGE_ID, 1);
+  private static final LabelId LABEL_ID = LabelId.create(LABEL_NAME);
+  private static final Change.Id CHANGE_ID = Change.id(100);
+  private static final PatchSet.Id PS_ID = PatchSet.id(CHANGE_ID, 1);
   private static final LabelType VERIFIED_LABEL = makeLabel();
   private static final PatchSetApproval APPROVAL_2 = makeApproval(2);
   private static final PatchSetApproval APPROVAL_1 = makeApproval(1);
@@ -82,7 +81,7 @@
     SubmitRecord.Label myLabel = LabelFunction.MAX_NO_BLOCK.check(VERIFIED_LABEL, approvals);
 
     assertThat(myLabel.status).isEqualTo(SubmitRecord.Label.Status.OK);
-    assertThat(myLabel.appliedBy).isEqualTo(APPROVAL_2.getAccountId());
+    assertThat(myLabel.appliedBy).isEqualTo(APPROVAL_2.accountId());
   }
 
   private static LabelType makeLabel() {
@@ -97,14 +96,11 @@
   }
 
   private static PatchSetApproval makeApproval(int value) {
-    Account.Id accountId = new Account.Id(10000 + value);
-    PatchSetApproval.Key key = makeKey(PS_ID, accountId, LABEL_ID);
-    return new PatchSetApproval(key, (short) value, Date.from(Instant.now()));
-  }
-
-  private static PatchSetApproval.Key makeKey(
-      PatchSet.Id psId, Account.Id accountId, LabelId labelId) {
-    return new PatchSetApproval.Key(psId, accountId, labelId);
+    return PatchSetApproval.builder()
+        .key(PatchSetApproval.key(PS_ID, Account.id(10000 + value), LABEL_ID))
+        .value(value)
+        .granted(Date.from(Instant.now()))
+        .build();
   }
 
   private static void checkBlockWorks(LabelFunction function) {
@@ -113,7 +109,7 @@
     SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, approvals);
 
     assertThat(myLabel.status).isEqualTo(SubmitRecord.Label.Status.REJECT);
-    assertThat(myLabel.appliedBy).isEqualTo(APPROVAL_M2.getAccountId());
+    assertThat(myLabel.appliedBy).isEqualTo(APPROVAL_M2.accountId());
   }
 
   private static void checkNothingHappens(LabelFunction function) {
@@ -144,6 +140,6 @@
     SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, approvals);
 
     assertThat(myLabel.status).isEqualTo(SubmitRecord.Label.Status.OK);
-    assertThat(myLabel.appliedBy).isEqualTo(APPROVAL_2.getAccountId());
+    assertThat(myLabel.appliedBy).isEqualTo(APPROVAL_2.accountId());
   }
 }
diff --git a/javatests/com/google/gerrit/common/data/LabelTypeTest.java b/javatests/com/google/gerrit/common/data/LabelTypeTest.java
index db0df2e..6c3befb 100644
--- a/javatests/com/google/gerrit/common/data/LabelTypeTest.java
+++ b/javatests/com/google/gerrit/common/data/LabelTypeTest.java
@@ -17,10 +17,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class LabelTypeTest extends GerritBaseTests {
+public class LabelTypeTest {
   @Test
   public void sortLabelValues() {
     LabelValue v0 = new LabelValue((short) 0, "Zero");
diff --git a/javatests/com/google/gerrit/common/data/ParameterizedStringTest.java b/javatests/com/google/gerrit/common/data/ParameterizedStringTest.java
index b22a511..b646d2b 100644
--- a/javatests/com/google/gerrit/common/data/ParameterizedStringTest.java
+++ b/javatests/com/google/gerrit/common/data/ParameterizedStringTest.java
@@ -17,12 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.HashMap;
 import java.util.Map;
 import org.junit.Test;
 
-public class ParameterizedStringTest extends GerritBaseTests {
+public class ParameterizedStringTest {
   @Test
   public void emptyString() {
     ParameterizedString p = new ParameterizedString("");
diff --git a/javatests/com/google/gerrit/common/data/PermissionRuleTest.java b/javatests/com/google/gerrit/common/data/PermissionRuleTest.java
index f442f39..d815dbc 100644
--- a/javatests/com/google/gerrit/common/data/PermissionRuleTest.java
+++ b/javatests/com/google/gerrit/common/data/PermissionRuleTest.java
@@ -15,20 +15,20 @@
 package com.google.gerrit.common.data;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.AccountGroup;
 import org.junit.Before;
 import org.junit.Test;
 
-public class PermissionRuleTest extends GerritBaseTests {
+public class PermissionRuleTest {
   private GroupReference groupReference;
   private PermissionRule permissionRule;
 
   @Before
   public void setup() {
-    this.groupReference = new GroupReference(new AccountGroup.UUID("uuid"), "group");
+    this.groupReference = new GroupReference(AccountGroup.uuid("uuid"), "group");
     this.permissionRule = new PermissionRule(groupReference);
   }
 
@@ -42,8 +42,7 @@
 
   @Test
   public void cannotSetActionToNull() {
-    exception.expect(NullPointerException.class);
-    permissionRule.setAction(null);
+    assertThrows(NullPointerException.class, () -> permissionRule.setAction(null));
   }
 
   @Test
@@ -131,7 +130,7 @@
 
   @Test
   public void setGroup() {
-    GroupReference groupReference2 = new GroupReference(new AccountGroup.UUID("uuid2"), "group2");
+    GroupReference groupReference2 = new GroupReference(AccountGroup.uuid("uuid2"), "group2");
     assertThat(groupReference2).isNotEqualTo(groupReference);
 
     assertThat(permissionRule.getGroup()).isEqualTo(groupReference);
@@ -142,10 +141,10 @@
 
   @Test
   public void mergeFromAnyBlock() {
-    GroupReference groupReference1 = new GroupReference(new AccountGroup.UUID("uuid1"), "group1");
+    GroupReference groupReference1 = new GroupReference(AccountGroup.uuid("uuid1"), "group1");
     PermissionRule permissionRule1 = new PermissionRule(groupReference1);
 
-    GroupReference groupReference2 = new GroupReference(new AccountGroup.UUID("uuid2"), "group2");
+    GroupReference groupReference2 = new GroupReference(AccountGroup.uuid("uuid2"), "group2");
     PermissionRule permissionRule2 = new PermissionRule(groupReference2);
 
     permissionRule1.mergeFrom(permissionRule2);
@@ -170,10 +169,10 @@
 
   @Test
   public void mergeFromAnyDeny() {
-    GroupReference groupReference1 = new GroupReference(new AccountGroup.UUID("uuid1"), "group1");
+    GroupReference groupReference1 = new GroupReference(AccountGroup.uuid("uuid1"), "group1");
     PermissionRule permissionRule1 = new PermissionRule(groupReference1);
 
-    GroupReference groupReference2 = new GroupReference(new AccountGroup.UUID("uuid2"), "group2");
+    GroupReference groupReference2 = new GroupReference(AccountGroup.uuid("uuid2"), "group2");
     PermissionRule permissionRule2 = new PermissionRule(groupReference2);
 
     permissionRule1.mergeFrom(permissionRule2);
@@ -193,10 +192,10 @@
 
   @Test
   public void mergeFromAnyBatch() {
-    GroupReference groupReference1 = new GroupReference(new AccountGroup.UUID("uuid1"), "group1");
+    GroupReference groupReference1 = new GroupReference(AccountGroup.uuid("uuid1"), "group1");
     PermissionRule permissionRule1 = new PermissionRule(groupReference1);
 
-    GroupReference groupReference2 = new GroupReference(new AccountGroup.UUID("uuid2"), "group2");
+    GroupReference groupReference2 = new GroupReference(AccountGroup.uuid("uuid2"), "group2");
     PermissionRule permissionRule2 = new PermissionRule(groupReference2);
 
     permissionRule1.mergeFrom(permissionRule2);
@@ -216,10 +215,10 @@
 
   @Test
   public void mergeFromAnyForce() {
-    GroupReference groupReference1 = new GroupReference(new AccountGroup.UUID("uuid1"), "group1");
+    GroupReference groupReference1 = new GroupReference(AccountGroup.uuid("uuid1"), "group1");
     PermissionRule permissionRule1 = new PermissionRule(groupReference1);
 
-    GroupReference groupReference2 = new GroupReference(new AccountGroup.UUID("uuid2"), "group2");
+    GroupReference groupReference2 = new GroupReference(AccountGroup.uuid("uuid2"), "group2");
     PermissionRule permissionRule2 = new PermissionRule(groupReference2);
 
     permissionRule1.mergeFrom(permissionRule2);
@@ -239,11 +238,11 @@
 
   @Test
   public void mergeFromMergeRange() {
-    GroupReference groupReference1 = new GroupReference(new AccountGroup.UUID("uuid1"), "group1");
+    GroupReference groupReference1 = new GroupReference(AccountGroup.uuid("uuid1"), "group1");
     PermissionRule permissionRule1 = new PermissionRule(groupReference1);
     permissionRule1.setRange(-1, 2);
 
-    GroupReference groupReference2 = new GroupReference(new AccountGroup.UUID("uuid2"), "group2");
+    GroupReference groupReference2 = new GroupReference(AccountGroup.uuid("uuid2"), "group2");
     PermissionRule permissionRule2 = new PermissionRule(groupReference2);
     permissionRule2.setRange(-2, 1);
 
@@ -256,10 +255,10 @@
 
   @Test
   public void mergeFromGroupNotChanged() {
-    GroupReference groupReference1 = new GroupReference(new AccountGroup.UUID("uuid1"), "group1");
+    GroupReference groupReference1 = new GroupReference(AccountGroup.uuid("uuid1"), "group1");
     PermissionRule permissionRule1 = new PermissionRule(groupReference1);
 
-    GroupReference groupReference2 = new GroupReference(new AccountGroup.UUID("uuid2"), "group2");
+    GroupReference groupReference2 = new GroupReference(AccountGroup.uuid("uuid2"), "group2");
     PermissionRule permissionRule2 = new PermissionRule(groupReference2);
 
     permissionRule1.mergeFrom(permissionRule2);
@@ -348,7 +347,7 @@
 
   @Test
   public void testEquals() {
-    GroupReference groupReference2 = new GroupReference(new AccountGroup.UUID("uuid2"), "group2");
+    GroupReference groupReference2 = new GroupReference(AccountGroup.uuid("uuid2"), "group2");
     PermissionRule permissionRuleOther = new PermissionRule(groupReference2);
     assertThat(permissionRule.equals(permissionRuleOther)).isFalse();
 
diff --git a/javatests/com/google/gerrit/common/data/PermissionTest.java b/javatests/com/google/gerrit/common/data/PermissionTest.java
index 23380e7..1012eff 100644
--- a/javatests/com/google/gerrit/common/data/PermissionTest.java
+++ b/javatests/com/google/gerrit/common/data/PermissionTest.java
@@ -17,14 +17,13 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.AccountGroup;
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 
-public class PermissionTest extends GerritBaseTests {
+public class PermissionTest {
   private static final String PERMISSION_NAME = "foo";
 
   private Permission permission;
@@ -155,14 +154,14 @@
   @Test
   public void setAndGetRules() {
     PermissionRule permissionRule1 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-1"), "group1"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-1"), "group1"));
     PermissionRule permissionRule2 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-2"), "group2"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-2"), "group2"));
     permission.setRules(ImmutableList.of(permissionRule1, permissionRule2));
     assertThat(permission.getRules()).containsExactly(permissionRule1, permissionRule2).inOrder();
 
     PermissionRule permissionRule3 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-3"), "group3"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-3"), "group3"));
     permission.setRules(ImmutableList.of(permissionRule3));
     assertThat(permission.getRules()).containsExactly(permissionRule3);
   }
@@ -170,10 +169,10 @@
   @Test
   public void cannotAddPermissionByModifyingListThatWasProvidedToAccessSection() {
     PermissionRule permissionRule1 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-1"), "group1"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-1"), "group1"));
     PermissionRule permissionRule2 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-2"), "group2"));
-    GroupReference groupReference3 = new GroupReference(new AccountGroup.UUID("uuid-3"), "group3");
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-2"), "group2"));
+    GroupReference groupReference3 = new GroupReference(AccountGroup.uuid("uuid-3"), "group3");
 
     List<PermissionRule> rules = new ArrayList<>();
     rules.add(permissionRule1);
@@ -188,14 +187,14 @@
 
   @Test
   public void getNonExistingRule() {
-    GroupReference groupReference = new GroupReference(new AccountGroup.UUID("uuid-1"), "group1");
+    GroupReference groupReference = new GroupReference(AccountGroup.uuid("uuid-1"), "group1");
     assertThat(permission.getRule(groupReference)).isNull();
     assertThat(permission.getRule(groupReference, false)).isNull();
   }
 
   @Test
   public void getRule() {
-    GroupReference groupReference = new GroupReference(new AccountGroup.UUID("uuid-1"), "group1");
+    GroupReference groupReference = new GroupReference(AccountGroup.uuid("uuid-1"), "group1");
     PermissionRule permissionRule = new PermissionRule(groupReference);
     permission.setRules(ImmutableList.of(permissionRule));
     assertThat(permission.getRule(groupReference)).isEqualTo(permissionRule);
@@ -203,7 +202,7 @@
 
   @Test
   public void createMissingRuleOnGet() {
-    GroupReference groupReference = new GroupReference(new AccountGroup.UUID("uuid-1"), "group1");
+    GroupReference groupReference = new GroupReference(AccountGroup.uuid("uuid-1"), "group1");
     assertThat(permission.getRule(groupReference)).isNull();
 
     assertThat(permission.getRule(groupReference, true))
@@ -213,11 +212,11 @@
   @Test
   public void addRule() {
     PermissionRule permissionRule1 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-1"), "group1"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-1"), "group1"));
     PermissionRule permissionRule2 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-2"), "group2"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-2"), "group2"));
     permission.setRules(ImmutableList.of(permissionRule1, permissionRule2));
-    GroupReference groupReference3 = new GroupReference(new AccountGroup.UUID("uuid-3"), "group3");
+    GroupReference groupReference3 = new GroupReference(AccountGroup.uuid("uuid-3"), "group3");
     assertThat(permission.getRule(groupReference3)).isNull();
 
     PermissionRule permissionRule3 = new PermissionRule(groupReference3);
@@ -231,10 +230,10 @@
   @Test
   public void removeRule() {
     PermissionRule permissionRule1 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-1"), "group1"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-1"), "group1"));
     PermissionRule permissionRule2 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-2"), "group2"));
-    GroupReference groupReference3 = new GroupReference(new AccountGroup.UUID("uuid-3"), "group3");
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-2"), "group2"));
+    GroupReference groupReference3 = new GroupReference(AccountGroup.uuid("uuid-3"), "group3");
     PermissionRule permissionRule3 = new PermissionRule(groupReference3);
 
     permission.setRules(ImmutableList.of(permissionRule1, permissionRule2, permissionRule3));
@@ -248,10 +247,10 @@
   @Test
   public void removeRuleByGroupReference() {
     PermissionRule permissionRule1 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-1"), "group1"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-1"), "group1"));
     PermissionRule permissionRule2 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-2"), "group2"));
-    GroupReference groupReference3 = new GroupReference(new AccountGroup.UUID("uuid-3"), "group3");
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-2"), "group2"));
+    GroupReference groupReference3 = new GroupReference(AccountGroup.uuid("uuid-3"), "group3");
     PermissionRule permissionRule3 = new PermissionRule(groupReference3);
 
     permission.setRules(ImmutableList.of(permissionRule1, permissionRule2, permissionRule3));
@@ -265,9 +264,9 @@
   @Test
   public void clearRules() {
     PermissionRule permissionRule1 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-1"), "group1"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-1"), "group1"));
     PermissionRule permissionRule2 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-2"), "group2"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-2"), "group2"));
 
     permission.setRules(ImmutableList.of(permissionRule1, permissionRule2));
     assertThat(permission.getRules()).isNotEmpty();
@@ -279,11 +278,11 @@
   @Test
   public void mergePermissions() {
     PermissionRule permissionRule1 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-1"), "group1"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-1"), "group1"));
     PermissionRule permissionRule2 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-2"), "group2"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-2"), "group2"));
     PermissionRule permissionRule3 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-3"), "group3"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-3"), "group3"));
 
     Permission permission1 = new Permission("foo");
     permission1.setRules(ImmutableList.of(permissionRule1, permissionRule2));
@@ -300,9 +299,9 @@
   @Test
   public void testEquals() {
     PermissionRule permissionRule1 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-1"), "group1"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-1"), "group1"));
     PermissionRule permissionRule2 =
-        new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-2"), "group2"));
+        new PermissionRule(new GroupReference(AccountGroup.uuid("uuid-2"), "group2"));
 
     permission.setRules(ImmutableList.of(permissionRule1, permissionRule2));
 
diff --git a/javatests/com/google/gerrit/common/data/SubmitRecordTest.java b/javatests/com/google/gerrit/common/data/SubmitRecordTest.java
index 5b9fde7..5386b87 100644
--- a/javatests/com/google/gerrit/common/data/SubmitRecordTest.java
+++ b/javatests/com/google/gerrit/common/data/SubmitRecordTest.java
@@ -16,12 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.ArrayList;
 import java.util.Collection;
 import org.junit.Test;
 
-public class SubmitRecordTest extends GerritBaseTests {
+public class SubmitRecordTest {
   private static final SubmitRecord OK_RECORD;
   private static final SubmitRecord FORCED_RECORD;
   private static final SubmitRecord NOT_READY_RECORD;
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index e2a8483..be35d5a 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -12,13 +12,12 @@
     deps = [
         "//java/com/google/gerrit/elasticsearch",
         "//java/com/google/gerrit/index",
-        "//java/com/google/gerrit/server",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:junit",
         "//lib/guice",
         "//lib/httpcomponents:httpcore",
         "//lib/jackson:jackson-annotations",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/log:api",
         "//lib/testcontainers",
         "//lib/testcontainers:docker-java-api",
@@ -32,7 +31,7 @@
     "//java/com/google/gerrit/elasticsearch",
     "//java/com/google/gerrit/testing:gerrit-test-util",
     "//lib/guice",
-    "//lib/jgit/org.eclipse.jgit:jgit",
+    "//lib:jgit",
 ]
 
 HTTP_TEST_DEPS = [
@@ -51,8 +50,6 @@
 
 SUFFIX = "sTest.java"
 
-ELASTICSEARCH_TESTS_V6 = {i: "ElasticV6Query" + i.capitalize() + SUFFIX for i in TYPES}
-
 ELASTICSEARCH_TESTS_V7 = {i: "ElasticV7Query" + i.capitalize() + SUFFIX for i in TYPES}
 
 ELASTICSEARCH_TAGS = [
@@ -62,14 +59,6 @@
 ]
 
 [junit_tests(
-    name = "elasticsearch_query_%ss_test_V6" % name,
-    size = "large",
-    srcs = [src],
-    tags = ELASTICSEARCH_TAGS,
-    deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name] + HTTP_TEST_DEPS,
-) for name, src in ELASTICSEARCH_TESTS_V6.items()]
-
-[junit_tests(
     name = "elasticsearch_query_%ss_test_V7" % name,
     size = "large",
     srcs = [src],
@@ -89,9 +78,9 @@
         "//java/com/google/gerrit/elasticsearch",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/guice",
         "//lib/httpcomponents:httpcore",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
index 9ce1456..7e044c3 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
@@ -21,17 +21,17 @@
 import static com.google.gerrit.elasticsearch.ElasticConfiguration.KEY_SERVER;
 import static com.google.gerrit.elasticsearch.ElasticConfiguration.KEY_USERNAME;
 import static com.google.gerrit.elasticsearch.ElasticConfiguration.SECTION_ELASTICSEARCH;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.ProvisionException;
 import java.util.Arrays;
 import org.apache.http.HttpHost;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-public class ElasticConfigurationTest extends GerritBaseTests {
+public class ElasticConfigurationTest {
   @Test
   public void singleServerNoOtherConfig() throws Exception {
     Config cfg = newConfig();
@@ -121,9 +121,9 @@
         .containsExactly(hostURIs);
   }
 
-  private void assertProvisionException(Config cfg) throws Exception {
-    exception.expect(ProvisionException.class);
-    exception.expectMessage("No valid Elasticsearch servers configured");
-    new ElasticConfiguration(cfg);
+  private void assertProvisionException(Config cfg) {
+    ProvisionException thrown =
+        assertThrows(ProvisionException.class, () -> new ElasticConfiguration(cfg));
+    assertThat(thrown).hasMessageThat().contains("No valid Elasticsearch servers configured");
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 48295ea..8cd09e6 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -39,16 +39,6 @@
 
   private static String getImageName(ElasticVersion version) {
     switch (version) {
-      case V6_8:
-        return "blacktop/elasticsearch:6.8.13";
-      case V7_0:
-        return "blacktop/elasticsearch:7.0.1";
-      case V7_1:
-        return "blacktop/elasticsearch:7.1.1";
-      case V7_2:
-        return "blacktop/elasticsearch:7.2.1";
-      case V7_3:
-        return "blacktop/elasticsearch:7.3.2";
       case V7_4:
         return "blacktop/elasticsearch:7.4.2";
       case V7_5:
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 0203a22..dcc6880 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.elasticsearch;
 
 import com.google.gerrit.index.IndexDefinition;
-import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
@@ -27,7 +26,7 @@
   public static void configure(Config config, ElasticContainer container, String prefix) {
     String hostname = container.getHttpHost().getHostName();
     int port = container.getHttpHost().getPort();
-    config.setEnum("index", null, "type", IndexType.ELASTICSEARCH);
+    config.setString("index", null, "type", "elasticsearch");
     config.setString("elasticsearch", null, "server", "http://" + hostname + ":" + port);
     config.setString("elasticsearch", null, "prefix", prefix);
     config.setInt("index", null, "maxLimit", 10000);
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
deleted file mode 100644
index d4d321d..0000000
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// 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.
-
-package com.google.gerrit.elasticsearch;
-
-import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.IndexConfig;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-public class ElasticV6QueryAccountsTest extends AbstractQueryAccountsTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return IndexConfig.createForElasticsearch();
-  }
-
-  private static ElasticContainer container;
-
-  @BeforeClass
-  public static void startIndexService() {
-    if (container == null) {
-      // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
-    }
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    Config elasticsearchConfig = new Config(config);
-    InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
-  }
-}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
deleted file mode 100644
index 68c4b71..0000000
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// 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.
-
-package com.google.gerrit.elasticsearch;
-
-import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.IndexConfig;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.protocol.HttpClientContext;
-import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
-import org.apache.http.impl.nio.client.HttpAsyncClients;
-import org.eclipse.jgit.lib.Config;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-public class ElasticV6QueryChangesTest extends AbstractQueryChangesTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return IndexConfig.createForElasticsearch();
-  }
-
-  private static ElasticContainer container;
-  private static CloseableHttpAsyncClient client;
-
-  @BeforeClass
-  public static void startIndexService() {
-    if (container == null) {
-      // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
-      client = HttpAsyncClients.createDefault();
-      client.start();
-    }
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @After
-  public void closeIndex() {
-    // Close the index after each test to prevent exceeding Elasticsearch's
-    // shard limit (see Issue 10120).
-    client.execute(
-        new HttpPost(
-            String.format(
-                "http://%s:%d/%s*/_close",
-                container.getHttpHost().getHostName(),
-                container.getHttpHost().getPort(),
-                getSanitizedMethodName())),
-        HttpClientContext.create(),
-        null);
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    Config elasticsearchConfig = new Config(config);
-    InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
-  }
-}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
deleted file mode 100644
index 99c07f4..0000000
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// 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.
-
-package com.google.gerrit.elasticsearch;
-
-import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.IndexConfig;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-public class ElasticV6QueryGroupsTest extends AbstractQueryGroupsTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return IndexConfig.createForElasticsearch();
-  }
-
-  private static ElasticContainer container;
-
-  @BeforeClass
-  public static void startIndexService() {
-    if (container == null) {
-      // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
-    }
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    Config elasticsearchConfig = new Config(config);
-    InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
-  }
-}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
deleted file mode 100644
index 89c7774..0000000
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// 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.
-
-package com.google.gerrit.elasticsearch;
-
-import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.IndexConfig;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-public class ElasticV6QueryProjectsTest extends AbstractQueryProjectsTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return IndexConfig.createForElasticsearch();
-  }
-
-  private static ElasticContainer container;
-
-  @BeforeClass
-  public static void startIndexService() {
-    if (container == null) {
-      // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
-    }
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    Config elasticsearchConfig = new Config(config);
-    InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
-  }
-}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
index 91d366e..4826490 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -57,7 +57,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = getSanitizedMethodName();
+    String indicesPrefix = testName.getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index f11dc5b..d9a4d2e 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.elasticsearch;
 
+import static java.util.concurrent.TimeUnit.MINUTES;
+
 import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
 import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.GerritTestName;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.gerrit.testing.IndexConfig;
 import com.google.inject.Guice;
@@ -28,6 +31,7 @@
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 
 public class ElasticV7QueryChangesTest extends AbstractQueryChangesTest {
   @ConfigSuite.Default
@@ -55,19 +59,23 @@
     }
   }
 
+  @Rule public final GerritTestName testName = new GerritTestName();
+
   @After
-  public void closeIndex() {
+  public void closeIndex() throws Exception {
     // Close the index after each test to prevent exceeding Elasticsearch's
     // shard limit (see Issue 10120).
-    client.execute(
-        new HttpPost(
-            String.format(
-                "http://%s:%d/%s*/_close",
-                container.getHttpHost().getHostName(),
-                container.getHttpHost().getPort(),
-                getSanitizedMethodName())),
-        HttpClientContext.create(),
-        null);
+    client
+        .execute(
+            new HttpPost(
+                String.format(
+                    "http://%s:%d/%s*/_close",
+                    container.getHttpHost().getHostName(),
+                    container.getHttpHost().getPort(),
+                    testName.getSanitizedMethodName())),
+            HttpClientContext.create(),
+            null)
+        .get(5, MINUTES);
   }
 
   @Override
@@ -80,7 +88,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = getSanitizedMethodName();
+    String indicesPrefix = testName.getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
index 282763a..0fc96f8 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -57,7 +57,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = getSanitizedMethodName();
+    String indicesPrefix = testName.getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
index 4d83cf5..1e56af9 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -57,7 +57,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = getSanitizedMethodName();
+    String indicesPrefix = testName.getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index 0a7a422..508dc84 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -15,28 +15,13 @@
 package com.google.gerrit.elasticsearch;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class ElasticVersionTest extends GerritBaseTests {
+public class ElasticVersionTest {
   @Test
   public void supportedVersion() throws Exception {
-    assertThat(ElasticVersion.forVersion("6.8.0")).isEqualTo(ElasticVersion.V6_8);
-    assertThat(ElasticVersion.forVersion("6.8.1")).isEqualTo(ElasticVersion.V6_8);
-
-    assertThat(ElasticVersion.forVersion("7.0.0")).isEqualTo(ElasticVersion.V7_0);
-    assertThat(ElasticVersion.forVersion("7.0.1")).isEqualTo(ElasticVersion.V7_0);
-
-    assertThat(ElasticVersion.forVersion("7.1.0")).isEqualTo(ElasticVersion.V7_1);
-    assertThat(ElasticVersion.forVersion("7.1.1")).isEqualTo(ElasticVersion.V7_1);
-
-    assertThat(ElasticVersion.forVersion("7.2.0")).isEqualTo(ElasticVersion.V7_2);
-    assertThat(ElasticVersion.forVersion("7.2.1")).isEqualTo(ElasticVersion.V7_2);
-
-    assertThat(ElasticVersion.forVersion("7.3.0")).isEqualTo(ElasticVersion.V7_3);
-    assertThat(ElasticVersion.forVersion("7.3.1")).isEqualTo(ElasticVersion.V7_3);
-
     assertThat(ElasticVersion.forVersion("7.4.0")).isEqualTo(ElasticVersion.V7_4);
     assertThat(ElasticVersion.forVersion("7.4.1")).isEqualTo(ElasticVersion.V7_4);
 
@@ -55,51 +40,13 @@
 
   @Test
   public void unsupportedVersion() throws Exception {
-    exception.expect(ElasticVersion.UnsupportedVersion.class);
-    exception.expectMessage(
-        "Unsupported version: [4.0.0]. Supported versions: " + ElasticVersion.supportedVersions());
-    ElasticVersion.forVersion("4.0.0");
-  }
-
-  @Test
-  public void atLeastMinorVersion() throws Exception {
-    assertThat(ElasticVersion.V6_8.isAtLeastMinorVersion(ElasticVersion.V6_8)).isTrue();
-    assertThat(ElasticVersion.V7_0.isAtLeastMinorVersion(ElasticVersion.V6_8)).isFalse();
-    assertThat(ElasticVersion.V7_1.isAtLeastMinorVersion(ElasticVersion.V6_8)).isFalse();
-    assertThat(ElasticVersion.V7_2.isAtLeastMinorVersion(ElasticVersion.V6_8)).isFalse();
-    assertThat(ElasticVersion.V7_3.isAtLeastMinorVersion(ElasticVersion.V6_8)).isFalse();
-    assertThat(ElasticVersion.V7_4.isAtLeastMinorVersion(ElasticVersion.V6_8)).isFalse();
-    assertThat(ElasticVersion.V7_5.isAtLeastMinorVersion(ElasticVersion.V6_8)).isFalse();
-    assertThat(ElasticVersion.V7_6.isAtLeastMinorVersion(ElasticVersion.V6_8)).isFalse();
-    assertThat(ElasticVersion.V7_7.isAtLeastMinorVersion(ElasticVersion.V6_8)).isFalse();
-    assertThat(ElasticVersion.V7_8.isAtLeastMinorVersion(ElasticVersion.V6_8)).isFalse();
-  }
-
-  @Test
-  public void version6OrLater() throws Exception {
-    assertThat(ElasticVersion.V6_8.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_0.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_1.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_2.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_3.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_4.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_5.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_6.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_7.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_8.isV6OrLater()).isTrue();
-  }
-
-  @Test
-  public void version7OrLater() throws Exception {
-    assertThat(ElasticVersion.V6_8.isV7OrLater()).isFalse();
-    assertThat(ElasticVersion.V7_0.isV7OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_1.isV7OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_2.isV7OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_3.isV7OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_4.isV7OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_5.isV7OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_6.isV7OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_7.isV7OrLater()).isTrue();
-    assertThat(ElasticVersion.V7_8.isV7OrLater()).isTrue();
+    ElasticVersion.UnsupportedVersion thrown =
+        assertThrows(
+            ElasticVersion.UnsupportedVersion.class, () -> ElasticVersion.forVersion("4.0.0"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "Unsupported version: [4.0.0]. Supported versions: "
+                + ElasticVersion.supportedVersions());
   }
 }
diff --git a/javatests/com/google/gerrit/reviewdb/client/AccountGroupTest.java b/javatests/com/google/gerrit/entities/AccountGroupTest.java
similarity index 68%
rename from javatests/com/google/gerrit/reviewdb/client/AccountGroupTest.java
rename to javatests/com/google/gerrit/entities/AccountGroupTest.java
index 18a55bf..a9d5188 100644
--- a/javatests/com/google/gerrit/reviewdb/client/AccountGroupTest.java
+++ b/javatests/com/google/gerrit/entities/AccountGroupTest.java
@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.AccountGroup.UUID.fromRef;
-import static com.google.gerrit.reviewdb.client.AccountGroup.UUID.fromRefPart;
+import static com.google.gerrit.entities.AccountGroup.UUID.fromRef;
+import static com.google.gerrit.entities.AccountGroup.UUID.fromRefPart;
+import static com.google.gerrit.entities.AccountGroup.uuid;
 
 import java.sql.Timestamp;
 import java.time.Instant;
@@ -70,7 +71,29 @@
     assertThat(fromRefPart("ab/" + TEST_UUID)).isNull();
   }
 
-  private AccountGroup.UUID uuid(String uuid) {
-    return new AccountGroup.UUID(uuid);
+  @Test
+  public void uuidToString() {
+    assertThat(uuid("foo").toString()).isEqualTo("foo");
+    assertThat(uuid("foo bar").toString()).isEqualTo("foo+bar");
+    assertThat(uuid("foo:bar").toString()).isEqualTo("foo%3Abar");
+  }
+
+  @Test
+  public void parseUuid() {
+    assertThat(AccountGroup.UUID.parse("foo")).isEqualTo(uuid("foo"));
+    assertThat(AccountGroup.UUID.parse("foo+bar")).isEqualTo(uuid("foo bar"));
+    assertThat(AccountGroup.UUID.parse("foo%3Abar")).isEqualTo(uuid("foo:bar"));
+  }
+
+  @Test
+  public void idToString() {
+    assertThat(AccountGroup.id(123).toString()).isEqualTo("123");
+  }
+
+  @Test
+  public void nameKeyToString() {
+    assertThat(AccountGroup.nameKey("foo").toString()).isEqualTo("foo");
+    assertThat(AccountGroup.nameKey("foo bar").toString()).isEqualTo("foo+bar");
+    assertThat(AccountGroup.nameKey("foo:bar").toString()).isEqualTo("foo%3Abar");
   }
 }
diff --git a/javatests/com/google/gerrit/reviewdb/client/AccountTest.java b/javatests/com/google/gerrit/entities/AccountTest.java
similarity index 90%
rename from javatests/com/google/gerrit/reviewdb/client/AccountTest.java
rename to javatests/com/google/gerrit/entities/AccountTest.java
index 11a562f..e8909432 100644
--- a/javatests/com/google/gerrit/reviewdb/client/AccountTest.java
+++ b/javatests/com/google/gerrit/entities/AccountTest.java
@@ -12,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.Account.Id.fromRef;
-import static com.google.gerrit.reviewdb.client.Account.Id.fromRefPart;
-import static com.google.gerrit.reviewdb.client.Account.Id.fromRefSuffix;
+import static com.google.gerrit.entities.Account.Id.fromRef;
+import static com.google.gerrit.entities.Account.Id.fromRefPart;
+import static com.google.gerrit.entities.Account.Id.fromRefSuffix;
+import static com.google.gerrit.entities.Account.id;
 
 import org.junit.Test;
 
@@ -90,8 +91,4 @@
     assertThat(fromRefSuffix("12/34")).isEqualTo(id(34));
     assertThat(fromRefSuffix("ab/cd")).isNull();
   }
-
-  private Account.Id id(int n) {
-    return new Account.Id(n);
-  }
 }
diff --git a/javatests/com/google/gerrit/entities/BUILD b/javatests/com/google/gerrit/entities/BUILD
new file mode 100644
index 0000000..b24781a
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/BUILD
@@ -0,0 +1,13 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "entities_tests",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:guava",
+        "//lib:jgit",
+        "//lib/truth",
+    ],
+)
diff --git a/javatests/com/google/gerrit/entities/BranchTest.java b/javatests/com/google/gerrit/entities/BranchTest.java
new file mode 100644
index 0000000..0483ebc
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/BranchTest.java
@@ -0,0 +1,39 @@
+// 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.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class BranchTest {
+  @Test
+  public void canonicalizeNameDuringConstruction() {
+    assertThat(BranchNameKey.create(new Project.NameKey("foo"), "bar").branch())
+        .isEqualTo("refs/heads/bar");
+    assertThat(BranchNameKey.create(new Project.NameKey("foo"), "refs/heads/bar").branch())
+        .isEqualTo("refs/heads/bar");
+  }
+
+  @Test
+  public void idToString() {
+    assertThat(BranchNameKey.create(new Project.NameKey("foo"), "bar").toString())
+        .isEqualTo("foo,refs/heads/bar");
+    assertThat(BranchNameKey.create(new Project.NameKey("foo bar"), "bar baz").toString())
+        .isEqualTo("foo+bar,refs/heads/bar+baz");
+    assertThat(BranchNameKey.create(new Project.NameKey("foo^bar"), "bar^baz").toString())
+        .isEqualTo("foo%5Ebar,refs/heads/bar%5Ebaz");
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/client/ChangeTest.java b/javatests/com/google/gerrit/entities/ChangeTest.java
similarity index 86%
rename from javatests/com/google/gerrit/reviewdb/client/ChangeTest.java
rename to javatests/com/google/gerrit/entities/ChangeTest.java
index 6d1d0a6..c75ad5a 100644
--- a/javatests/com/google/gerrit/reviewdb/client/ChangeTest.java
+++ b/javatests/com/google/gerrit/entities/ChangeTest.java
@@ -12,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
+import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
 public class ChangeTest {
@@ -122,8 +123,8 @@
 
   @Test
   public void toRefPrefix() {
-    assertThat(new Change.Id(1).toRefPrefix()).isEqualTo("refs/changes/01/1/");
-    assertThat(new Change.Id(1234).toRefPrefix()).isEqualTo("refs/changes/34/1234/");
+    assertThat(Change.id(1).toRefPrefix()).isEqualTo("refs/changes/01/1/");
+    assertThat(Change.id(1234).toRefPrefix()).isEqualTo("refs/changes/34/1234/");
   }
 
   @Test
@@ -147,8 +148,20 @@
     assertNotRefPart("1/1");
   }
 
+  @Test
+  public void idToString() {
+    assertThat(Change.id(3).toString()).isEqualTo("3");
+  }
+
+  @Test
+  public void keyToString() {
+    String key = "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    assertThat(ObjectId.isId(key.substring(1))).isTrue();
+    assertThat(Change.key(key).toString()).isEqualTo(key);
+  }
+
   private static void assertRef(int changeId, String refName) {
-    assertThat(Change.Id.fromRef(refName)).isEqualTo(new Change.Id(changeId));
+    assertThat(Change.Id.fromRef(refName)).isEqualTo(Change.id(changeId));
   }
 
   private static void assertNotRef(String refName) {
@@ -156,7 +169,7 @@
   }
 
   private static void assertAllUsersRef(int changeId, String refName) {
-    assertThat(Change.Id.fromAllUsersRef(refName)).isEqualTo(new Change.Id(changeId));
+    assertThat(Change.Id.fromAllUsersRef(refName)).isEqualTo(Change.id(changeId));
   }
 
   private static void assertNotAllUsersRef(String refName) {
@@ -164,7 +177,7 @@
   }
 
   private static void assertRefPart(int changeId, String refName) {
-    assertEquals(new Change.Id(changeId), Change.Id.fromRefPart(refName));
+    assertEquals(Change.id(changeId), Change.Id.fromRefPart(refName));
   }
 
   private static void assertNotRefPart(String refName) {
diff --git a/javatests/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java b/javatests/com/google/gerrit/entities/PatchSetApprovalTest.java
similarity index 72%
rename from javatests/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java
rename to javatests/com/google/gerrit/entities/PatchSetApprovalTest.java
index 5e42ce0..81aa3b8 100644
--- a/javatests/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java
+++ b/javatests/com/google/gerrit/entities/PatchSetApprovalTest.java
@@ -12,27 +12,26 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.HashMap;
 import java.util.Map;
 import org.junit.Test;
 
-public class PatchSetApprovalTest extends GerritBaseTests {
+public class PatchSetApprovalTest {
   @Test
   public void keyEquality() {
     PatchSetApproval.Key k1 =
-        new PatchSetApproval.Key(
-            new PatchSet.Id(new Change.Id(1), 2), new Account.Id(3), new LabelId("My-Label"));
+        PatchSetApproval.key(
+            PatchSet.id(Change.id(1), 2), Account.id(3), LabelId.create("My-Label"));
     PatchSetApproval.Key k2 =
-        new PatchSetApproval.Key(
-            new PatchSet.Id(new Change.Id(1), 2), new Account.Id(3), new LabelId("My-Label"));
+        PatchSetApproval.key(
+            PatchSet.id(Change.id(1), 2), Account.id(3), LabelId.create("My-Label"));
     PatchSetApproval.Key k3 =
-        new PatchSetApproval.Key(
-            new PatchSet.Id(new Change.Id(1), 2), new Account.Id(3), new LabelId("Other-Label"));
+        PatchSetApproval.key(
+            PatchSet.id(Change.id(1), 2), Account.id(3), LabelId.create("Other-Label"));
 
     assertThat(k2).isEqualTo(k1);
     assertThat(k3).isNotEqualTo(k1);
diff --git a/javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java b/javatests/com/google/gerrit/entities/PatchSetTest.java
similarity index 64%
rename from javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java
rename to javatests/com/google/gerrit/entities/PatchSetTest.java
index 51a405f..61e1b4c 100644
--- a/javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java
+++ b/javatests/com/google/gerrit/entities/PatchSetTest.java
@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.PatchSet.joinGroups;
-import static com.google.gerrit.reviewdb.client.PatchSet.splitGroups;
+import static com.google.gerrit.entities.PatchSet.joinGroups;
+import static com.google.gerrit.entities.PatchSet.splitGroups;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import org.junit.Test;
@@ -64,37 +65,67 @@
 
   @Test
   public void testSplitGroups() {
+    assertRuntimeException(() -> splitGroups(null));
     assertThat(splitGroups("")).containsExactly("");
     assertThat(splitGroups("abcd")).containsExactly("abcd");
     assertThat(splitGroups("ab,cd")).containsExactly("ab", "cd").inOrder();
+    assertThat(splitGroups("ab , cd")).containsExactly("ab ", " cd").inOrder();
     assertThat(splitGroups("ab,")).containsExactly("ab", "").inOrder();
     assertThat(splitGroups(",cd")).containsExactly("", "cd").inOrder();
   }
 
   @Test
   public void testJoinGroups() {
+    assertRuntimeException(() -> joinGroups(null));
+    assertRuntimeException(() -> joinGroups(ImmutableList.of("a,", "b")));
     assertThat(joinGroups(ImmutableList.of(""))).isEqualTo("");
     assertThat(joinGroups(ImmutableList.of("abcd"))).isEqualTo("abcd");
     assertThat(joinGroups(ImmutableList.of("ab", "cd"))).isEqualTo("ab,cd");
+    assertThat(joinGroups(ImmutableList.of("ab ", " cd"))).isEqualTo("ab , cd");
     assertThat(joinGroups(ImmutableList.of("ab", ""))).isEqualTo("ab,");
     assertThat(joinGroups(ImmutableList.of("", "cd"))).isEqualTo(",cd");
   }
 
   @Test
   public void toRefName() {
-    assertThat(new PatchSet.Id(new Change.Id(1), 23).toRefName()).isEqualTo("refs/changes/01/1/23");
-    assertThat(new PatchSet.Id(new Change.Id(1234), 5).toRefName())
-        .isEqualTo("refs/changes/34/1234/5");
+    assertThat(PatchSet.id(Change.id(1), 23).toRefName()).isEqualTo("refs/changes/01/1/23");
+    assertThat(PatchSet.id(Change.id(1234), 5).toRefName()).isEqualTo("refs/changes/34/1234/5");
+  }
+
+  @Test
+  public void parseId() {
+    assertThat(PatchSet.Id.parse("1,2")).isEqualTo(PatchSet.id(Change.id(1), 2));
+    assertThat(PatchSet.Id.parse("01,02")).isEqualTo(PatchSet.id(Change.id(1), 2));
+    assertInvalidId(null);
+    assertInvalidId("");
+    assertInvalidId("1");
+    assertInvalidId("1,foo.txt");
+    assertInvalidId("foo.txt,1");
+
+    String hexComma = "%" + String.format("%02x", (int) ',');
+    assertInvalidId("1" + hexComma + "2");
+  }
+
+  @Test
+  public void idToString() {
+    assertThat(PatchSet.id(Change.id(2), 3).toString()).isEqualTo("2,3");
   }
 
   private static void assertRef(int changeId, int psId, String refName) {
     assertThat(PatchSet.isChangeRef(refName)).isTrue();
-    assertThat(PatchSet.Id.fromRef(refName))
-        .isEqualTo(new PatchSet.Id(new Change.Id(changeId), psId));
+    assertThat(PatchSet.Id.fromRef(refName)).isEqualTo(PatchSet.id(Change.id(changeId), psId));
   }
 
   private static void assertNotRef(String refName) {
     assertThat(PatchSet.isChangeRef(refName)).isFalse();
     assertThat(PatchSet.Id.fromRef(refName)).isNull();
   }
+
+  private static void assertInvalidId(String str) {
+    assertRuntimeException(() -> PatchSet.Id.parse(str));
+  }
+
+  private static void assertRuntimeException(Runnable runnable) {
+    assertThrows(RuntimeException.class, () -> runnable.run());
+  }
 }
diff --git a/javatests/com/google/gerrit/entities/PatchTest.java b/javatests/com/google/gerrit/entities/PatchTest.java
new file mode 100644
index 0000000..9f906a9
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/PatchTest.java
@@ -0,0 +1,54 @@
+// 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.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import org.junit.Test;
+
+public class PatchTest {
+  @Test
+  public void isMagic() {
+    assertThat(Patch.isMagic("/COMMIT_MSG")).isTrue();
+    assertThat(Patch.isMagic("/MERGE_LIST")).isTrue();
+
+    assertThat(Patch.isMagic("/COMMIT_MSG/")).isFalse();
+    assertThat(Patch.isMagic("COMMIT_MSG")).isFalse();
+    assertThat(Patch.isMagic("/commit_msg")).isFalse();
+  }
+
+  @Test
+  public void parseKey() {
+    assertThat(Patch.Key.parse("1,2,foo.txt"))
+        .isEqualTo(Patch.key(PatchSet.id(Change.id(1), 2), "foo.txt"));
+    assertThat(Patch.Key.parse("01,02,foo.txt"))
+        .isEqualTo(Patch.key(PatchSet.id(Change.id(1), 2), "foo.txt"));
+    assertInvalidKey(null);
+    assertInvalidKey("");
+    assertInvalidKey("1,2");
+    assertInvalidKey("1, 2, foo.txt");
+    assertInvalidKey("1,foo.txt");
+    assertInvalidKey("1,foo.txt,2");
+    assertInvalidKey("foo.txt,1,2");
+
+    String hexComma = "%" + String.format("%02x", (int) ',');
+    assertInvalidKey("1" + hexComma + "2" + hexComma + "foo.txt");
+  }
+
+  private static void assertInvalidKey(String str) {
+    assertThrows(RuntimeException.class, () -> Patch.Key.parse(str));
+  }
+}
diff --git a/javatests/com/google/gerrit/entities/ProjectTest.java b/javatests/com/google/gerrit/entities/ProjectTest.java
new file mode 100644
index 0000000..341b54f
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/ProjectTest.java
@@ -0,0 +1,39 @@
+// 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.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class ProjectTest {
+  @Test
+  public void parseId() {
+    assertThat(Project.NameKey.parse("foo")).isEqualTo(new Project.NameKey("foo"));
+    assertThat(Project.NameKey.parse("foo%20bar")).isEqualTo(new Project.NameKey("foo bar"));
+    assertThat(Project.NameKey.parse("foo+bar")).isEqualTo(new Project.NameKey("foo bar"));
+    assertThat(Project.NameKey.parse("foo%2fbar")).isEqualTo(new Project.NameKey("foo/bar"));
+    assertThat(Project.NameKey.parse("foo%2Fbar")).isEqualTo(new Project.NameKey("foo/bar"));
+  }
+
+  @Test
+  public void idToString() {
+    assertThat(Project.nameKey("foo").toString()).isEqualTo("foo");
+    assertThat(Project.nameKey("foo bar").toString()).isEqualTo("foo+bar");
+    assertThat(Project.nameKey("foo/bar").toString()).isEqualTo("foo/bar");
+    assertThat(Project.nameKey("foo^bar").toString()).isEqualTo("foo%5Ebar");
+    assertThat(Project.nameKey("foo%bar").toString()).isEqualTo("foo%25bar");
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/client/RefNamesTest.java b/javatests/com/google/gerrit/entities/RefNamesTest.java
similarity index 89%
rename from javatests/com/google/gerrit/reviewdb/client/RefNamesTest.java
rename to javatests/com/google/gerrit/entities/RefNamesTest.java
index fa6a722..1990292 100644
--- a/javatests/com/google/gerrit/reviewdb/client/RefNamesTest.java
+++ b/javatests/com/google/gerrit/entities/RefNamesTest.java
@@ -12,29 +12,25 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.entities;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.RefNames.parseAfterShardedRefPart;
-import static com.google.gerrit.reviewdb.client.RefNames.parseRefSuffix;
-import static com.google.gerrit.reviewdb.client.RefNames.parseShardedRefPart;
-import static com.google.gerrit.reviewdb.client.RefNames.parseShardedUuidFromRefPart;
-import static com.google.gerrit.reviewdb.client.RefNames.skipShardedRefPart;
+import static com.google.gerrit.entities.RefNames.parseAfterShardedRefPart;
+import static com.google.gerrit.entities.RefNames.parseRefSuffix;
+import static com.google.gerrit.entities.RefNames.parseShardedRefPart;
+import static com.google.gerrit.entities.RefNames.parseShardedUuidFromRefPart;
+import static com.google.gerrit.entities.RefNames.skipShardedRefPart;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
 public class RefNamesTest {
   private static final String TEST_GROUP_UUID = "ccab3195282a8ce4f5014efa391e82d10f884c64";
   private static final String TEST_SHARDED_GROUP_UUID =
       TEST_GROUP_UUID.substring(0, 2) + "/" + TEST_GROUP_UUID;
-
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
-  private final Account.Id accountId = new Account.Id(1011123);
-  private final Change.Id changeId = new Change.Id(67473);
-  private final PatchSet.Id psId = new PatchSet.Id(changeId, 42);
+  private final Account.Id accountId = Account.id(1011123);
+  private final Change.Id changeId = Change.id(67473);
+  private final PatchSet.Id psId = PatchSet.id(changeId, 42);
 
   @Test
   public void fullName() throws Exception {
@@ -54,34 +50,35 @@
     String robotCommentsRef = RefNames.robotCommentsRef(changeId);
     assertThat(robotCommentsRef).isEqualTo("refs/changes/73/67473/robot-comments");
     assertThat(RefNames.isNoteDbMetaRef(robotCommentsRef)).isTrue();
+
+    String changeRefPrefix = RefNames.changeRefPrefix(changeId);
+    assertThat(changeRefPrefix).isEqualTo("refs/changes/73/67473/");
   }
 
   @Test
   public void refForGroupIsSharded() throws Exception {
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID("ABCDEFG");
+    AccountGroup.UUID groupUuid = AccountGroup.uuid("ABCDEFG");
     String groupRef = RefNames.refsGroups(groupUuid);
     assertThat(groupRef).isEqualTo("refs/groups/AB/ABCDEFG");
   }
 
   @Test
   public void refForGroupWithUuidLessThanTwoCharsIsRejected() throws Exception {
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID("A");
-    expectedException.expect(IllegalArgumentException.class);
-    RefNames.refsGroups(groupUuid);
+    AccountGroup.UUID groupUuid = AccountGroup.uuid("A");
+    assertThrows(IllegalArgumentException.class, () -> RefNames.refsGroups(groupUuid));
   }
 
   @Test
   public void refForDeletedGroupIsSharded() throws Exception {
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID("ABCDEFG");
+    AccountGroup.UUID groupUuid = AccountGroup.uuid("ABCDEFG");
     String groupRef = RefNames.refsDeletedGroups(groupUuid);
     assertThat(groupRef).isEqualTo("refs/deleted-groups/AB/ABCDEFG");
   }
 
   @Test
   public void refForDeletedGroupWithUuidLessThanTwoCharsIsRejected() throws Exception {
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID("A");
-    expectedException.expect(IllegalArgumentException.class);
-    RefNames.refsDeletedGroups(groupUuid);
+    AccountGroup.UUID groupUuid = AccountGroup.uuid("A");
+    assertThrows(IllegalArgumentException.class, () -> RefNames.refsDeletedGroups(groupUuid));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/reviewdb/converter/AccountIdProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/AccountIdProtoConverterTest.java
similarity index 86%
rename from javatests/com/google/gerrit/reviewdb/converter/AccountIdProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/AccountIdProtoConverterTest.java
index 123a973..0e4fbc8 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/AccountIdProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/AccountIdProtoConverterTest.java
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.protobuf.Parser;
 import org.junit.Test;
 
@@ -30,7 +30,7 @@
 
   @Test
   public void allValuesConvertedToProto() {
-    Account.Id accountId = new Account.Id(24);
+    Account.Id accountId = Account.id(24);
 
     Entities.Account_Id proto = accountIdProtoConverter.toProto(accountId);
 
@@ -40,7 +40,7 @@
 
   @Test
   public void allValuesConvertedToProtoAndBackAgain() {
-    Account.Id accountId = new Account.Id(34832);
+    Account.Id accountId = Account.id(34832);
 
     Account.Id convertedAccountId =
         accountIdProtoConverter.fromProto(accountIdProtoConverter.toProto(accountId));
@@ -61,7 +61,8 @@
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
-  public void fieldsExistAsExpected() {
-    assertThatSerializedClass(Account.Id.class).hasFields(ImmutableMap.of("id", int.class));
+  public void methodsExistAsExpected() {
+    assertThatSerializedClass(Account.Id.class)
+        .hasAutoValueMethods(ImmutableMap.of("id", int.class));
   }
 }
diff --git a/javatests/com/google/gerrit/reviewdb/converter/BUILD b/javatests/com/google/gerrit/entities/converter/BUILD
similarity index 79%
rename from javatests/com/google/gerrit/reviewdb/converter/BUILD
rename to javatests/com/google/gerrit/entities/converter/BUILD
index 9cc941c..6ca9871 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/BUILD
+++ b/javatests/com/google/gerrit/entities/converter/BUILD
@@ -4,10 +4,12 @@
     name = "proto_converter_tests",
     srcs = glob(["*.java"]),
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/proto/testing",
-        "//java/com/google/gerrit/reviewdb:server",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:protobuf",
+        "//lib/guice",
         "//lib/truth",
         "//lib/truth:truth-proto-extension",
         "//proto:entities_java_proto",
diff --git a/javatests/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/BranchNameKeyProtoConverterTest.java
similarity index 73%
rename from javatests/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/BranchNameKeyProtoConverterTest.java
index 412641f..0a73db8 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/BranchNameKeyProtoConverterTest.java
@@ -12,17 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.protobuf.Parser;
 import java.lang.reflect.Type;
 import org.junit.Test;
@@ -33,23 +33,23 @@
 
   @Test
   public void allValuesConvertedToProto() {
-    Branch.NameKey nameKey = new Branch.NameKey(new Project.NameKey("project-13"), "branch-72");
+    BranchNameKey nameKey = BranchNameKey.create(Project.nameKey("project-13"), "branch-72");
 
     Entities.Branch_NameKey proto = branchNameKeyProtoConverter.toProto(nameKey);
 
     Entities.Branch_NameKey expectedProto =
         Entities.Branch_NameKey.newBuilder()
-            .setProjectName(Entities.Project_NameKey.newBuilder().setName("project-13"))
-            .setBranchName("refs/heads/branch-72")
+            .setProject(Entities.Project_NameKey.newBuilder().setName("project-13"))
+            .setBranch("refs/heads/branch-72")
             .build();
     assertThat(proto).isEqualTo(expectedProto);
   }
 
   @Test
   public void allValuesConvertedToProtoAndBackAgain() {
-    Branch.NameKey nameKey = new Branch.NameKey(new Project.NameKey("project-52"), "branch 14");
+    BranchNameKey nameKey = BranchNameKey.create(Project.nameKey("project-52"), "branch 14");
 
-    Branch.NameKey convertedNameKey =
+    BranchNameKey convertedNameKey =
         branchNameKeyProtoConverter.fromProto(branchNameKeyProtoConverter.toProto(nameKey));
 
     assertThat(convertedNameKey).isEqualTo(nameKey);
@@ -59,8 +59,8 @@
   public void protoCanBeParsedFromBytes() throws Exception {
     Entities.Branch_NameKey proto =
         Entities.Branch_NameKey.newBuilder()
-            .setProjectName(Entities.Project_NameKey.newBuilder().setName("project 1"))
-            .setBranchName("branch 36")
+            .setProject(Entities.Project_NameKey.newBuilder().setName("project 1"))
+            .setBranch("branch 36")
             .build();
     byte[] bytes = proto.toByteArray();
 
@@ -72,12 +72,12 @@
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
-  public void fieldsExistAsExpected() {
-    assertThatSerializedClass(Branch.NameKey.class)
-        .hasFields(
+  public void methodsExistAsExpected() {
+    assertThatSerializedClass(BranchNameKey.class)
+        .hasAutoValueMethods(
             ImmutableMap.<String, Type>builder()
-                .put("projectName", Project.NameKey.class)
-                .put("branchName", String.class)
+                .put("project", Project.NameKey.class)
+                .put("branch", String.class)
                 .build());
   }
 }
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeIdProtoConverterTest.java
similarity index 86%
rename from javatests/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/ChangeIdProtoConverterTest.java
index d5ebb51..12f3f33 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeIdProtoConverterTest.java
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.protobuf.Parser;
 import org.junit.Test;
 
@@ -30,7 +30,7 @@
 
   @Test
   public void allValuesConvertedToProto() {
-    Change.Id changeId = new Change.Id(94);
+    Change.Id changeId = Change.id(94);
 
     Entities.Change_Id proto = changeIdProtoConverter.toProto(changeId);
 
@@ -40,7 +40,7 @@
 
   @Test
   public void allValuesConvertedToProtoAndBackAgain() {
-    Change.Id changeId = new Change.Id(2903482);
+    Change.Id changeId = Change.id(2903482);
 
     Change.Id convertedChangeId =
         changeIdProtoConverter.fromProto(changeIdProtoConverter.toProto(changeId));
@@ -61,7 +61,8 @@
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
-  public void fieldsExistAsExpected() {
-    assertThatSerializedClass(Change.Id.class).hasFields(ImmutableMap.of("id", int.class));
+  public void methodsExistAsExpected() {
+    assertThatSerializedClass(Change.Id.class)
+        .hasAutoValueMethods(ImmutableMap.of("id", int.class));
   }
 }
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeKeyProtoConverterTest.java
similarity index 86%
rename from javatests/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/ChangeKeyProtoConverterTest.java
index d948706..e9080b3 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeKeyProtoConverterTest.java
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.protobuf.Parser;
 import org.junit.Test;
 
@@ -30,7 +30,7 @@
 
   @Test
   public void allValuesConvertedToProto() {
-    Change.Key changeKey = new Change.Key("change-1");
+    Change.Key changeKey = Change.key("change-1");
 
     Entities.Change_Key proto = changeKeyProtoConverter.toProto(changeKey);
 
@@ -40,7 +40,7 @@
 
   @Test
   public void allValuesConvertedToProtoAndBackAgain() {
-    Change.Key changeKey = new Change.Key("change-52");
+    Change.Key changeKey = Change.key("change-52");
 
     Change.Key convertedChangeKey =
         changeKeyProtoConverter.fromProto(changeKeyProtoConverter.toProto(changeKey));
@@ -61,7 +61,8 @@
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
-  public void fieldsExistAsExpected() {
-    assertThatSerializedClass(Change.Key.class).hasFields(ImmutableMap.of("id", String.class));
+  public void methodsExistAsExpected() {
+    assertThatSerializedClass(Change.Key.class)
+        .hasAutoValueMethods(ImmutableMap.of("key", String.class));
   }
 }
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeMessageKeyProtoConverterTest.java
similarity index 87%
rename from javatests/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/ChangeMessageKeyProtoConverterTest.java
index c8bb2ed..72ce896 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeMessageKeyProtoConverterTest.java
@@ -12,17 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.protobuf.Parser;
 import java.lang.reflect.Type;
 import org.junit.Test;
@@ -33,7 +33,7 @@
 
   @Test
   public void allValuesConvertedToProto() {
-    ChangeMessage.Key messageKey = new ChangeMessage.Key(new Change.Id(704), "aabbcc");
+    ChangeMessage.Key messageKey = ChangeMessage.key(Change.id(704), "aabbcc");
 
     Entities.ChangeMessage_Key proto = messageKeyProtoConverter.toProto(messageKey);
 
@@ -47,7 +47,7 @@
 
   @Test
   public void allValuesConvertedToProtoAndBackAgain() {
-    ChangeMessage.Key messageKey = new ChangeMessage.Key(new Change.Id(704), "aabbcc");
+    ChangeMessage.Key messageKey = ChangeMessage.key(Change.id(704), "aabbcc");
 
     ChangeMessage.Key convertedMessageKey =
         messageKeyProtoConverter.fromProto(messageKeyProtoConverter.toProto(messageKey));
@@ -72,9 +72,9 @@
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
-  public void fieldsExistAsExpected() {
+  public void methodsExistAsExpected() {
     assertThatSerializedClass(ChangeMessage.Key.class)
-        .hasFields(
+        .hasAutoValueMethods(
             ImmutableMap.<String, Type>builder()
                 .put("changeId", Change.Id.class)
                 .put("uuid", String.class)
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
similarity index 82%
rename from javatests/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
index 65bdfbb..933ffb4 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
@@ -12,19 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.protobuf.Parser;
 import java.lang.reflect.Type;
 import java.sql.Timestamp;
@@ -38,13 +38,13 @@
   public void allValuesConvertedToProto() {
     ChangeMessage changeMessage =
         new ChangeMessage(
-            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
-            new Account.Id(63),
+            ChangeMessage.key(Change.id(543), "change-message-21"),
+            Account.id(63),
             new Timestamp(9876543),
-            new PatchSet.Id(new Change.Id(34), 13));
+            PatchSet.id(Change.id(34), 13));
     changeMessage.setMessage("This is a change message.");
     changeMessage.setTag("An arbitrary tag.");
-    changeMessage.setRealAuthor(new Account.Id(10003));
+    changeMessage.setRealAuthor(Account.id(10003));
 
     Entities.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
 
@@ -60,7 +60,7 @@
             .setPatchset(
                 Entities.PatchSet_Id.newBuilder()
                     .setChangeId(Entities.Change_Id.newBuilder().setId(34))
-                    .setPatchSetId(13))
+                    .setId(13))
             .setTag("An arbitrary tag.")
             .setRealAuthor(Entities.Account_Id.newBuilder().setId(10003))
             .build();
@@ -71,10 +71,10 @@
   public void mainValuesConvertedToProto() {
     ChangeMessage changeMessage =
         new ChangeMessage(
-            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
-            new Account.Id(63),
+            ChangeMessage.key(Change.id(543), "change-message-21"),
+            Account.id(63),
             new Timestamp(9876543),
-            new PatchSet.Id(new Change.Id(34), 13));
+            PatchSet.id(Change.id(34), 13));
 
     Entities.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
 
@@ -89,7 +89,7 @@
             .setPatchset(
                 Entities.PatchSet_Id.newBuilder()
                     .setChangeId(Entities.Change_Id.newBuilder().setId(34))
-                    .setPatchSetId(13))
+                    .setId(13))
             .build();
     assertThat(proto).isEqualTo(expectedProto);
   }
@@ -99,10 +99,7 @@
   public void realAuthorIsNotAutomaticallySetToAuthorWhenConvertedToProto() {
     ChangeMessage changeMessage =
         new ChangeMessage(
-            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
-            new Account.Id(63),
-            null,
-            null);
+            ChangeMessage.key(Change.id(543), "change-message-21"), Account.id(63), null, null);
 
     Entities.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
 
@@ -122,8 +119,7 @@
     // writtenOn may not be null according to the column definition but it's optional for the
     // protobuf definition. -> assume as optional and hence test null
     ChangeMessage changeMessage =
-        new ChangeMessage(
-            new ChangeMessage.Key(new Change.Id(543), "change-message-21"), null, null, null);
+        new ChangeMessage(ChangeMessage.key(Change.id(543), "change-message-21"), null, null, null);
 
     Entities.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
 
@@ -141,13 +137,13 @@
   public void allValuesConvertedToProtoAndBackAgain() {
     ChangeMessage changeMessage =
         new ChangeMessage(
-            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
-            new Account.Id(63),
+            ChangeMessage.key(Change.id(543), "change-message-21"),
+            Account.id(63),
             new Timestamp(9876543),
-            new PatchSet.Id(new Change.Id(34), 13));
+            PatchSet.id(Change.id(34), 13));
     changeMessage.setMessage("This is a change message.");
     changeMessage.setTag("An arbitrary tag.");
-    changeMessage.setRealAuthor(new Account.Id(10003));
+    changeMessage.setRealAuthor(Account.id(10003));
 
     ChangeMessage convertedChangeMessage =
         changeMessageProtoConverter.fromProto(changeMessageProtoConverter.toProto(changeMessage));
@@ -158,10 +154,10 @@
   public void mainValuesConvertedToProtoAndBackAgain() {
     ChangeMessage changeMessage =
         new ChangeMessage(
-            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
-            new Account.Id(63),
+            ChangeMessage.key(Change.id(543), "change-message-21"),
+            Account.id(63),
             new Timestamp(9876543),
-            new PatchSet.Id(new Change.Id(34), 13));
+            PatchSet.id(Change.id(34), 13));
 
     ChangeMessage convertedChangeMessage =
         changeMessageProtoConverter.fromProto(changeMessageProtoConverter.toProto(changeMessage));
@@ -171,8 +167,7 @@
   @Test
   public void mandatoryValuesConvertedToProtoAndBackAgain() {
     ChangeMessage changeMessage =
-        new ChangeMessage(
-            new ChangeMessage.Key(new Change.Id(543), "change-message-21"), null, null, null);
+        new ChangeMessage(ChangeMessage.key(Change.id(543), "change-message-21"), null, null, null);
 
     ChangeMessage convertedChangeMessage =
         changeMessageProtoConverter.fromProto(changeMessageProtoConverter.toProto(changeMessage));
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
similarity index 81%
rename from javatests/com/google/gerrit/reviewdb/converter/ChangeProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
index 61bf105..72e4a7a 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/ChangeProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
@@ -12,20 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.protobuf.Parser;
 import java.lang.reflect.Type;
 import java.sql.Timestamp;
@@ -38,22 +38,22 @@
   public void allValuesConvertedToProto() {
     Change change =
         new Change(
-            new Change.Key("change 1"),
-            new Change.Id(14),
-            new Account.Id(35),
-            new Branch.NameKey(new Project.NameKey("project 67"), "branch 74"),
+            Change.key("change 1"),
+            Change.id(14),
+            Account.id(35),
+            BranchNameKey.create(Project.nameKey("project 67"), "branch 74"),
             new Timestamp(987654L));
     change.setLastUpdatedOn(new Timestamp(1234567L));
     change.setStatus(Change.Status.MERGED);
     change.setCurrentPatchSet(
-        new PatchSet.Id(new Change.Id(14), 23), "subject XYZ", "original subject ABC");
+        PatchSet.id(Change.id(14), 23), "subject XYZ", "original subject ABC");
     change.setTopic("my topic");
     change.setSubmissionId("submission ID 234");
-    change.setAssignee(new Account.Id(100001));
+    change.setAssignee(Account.id(100001));
     change.setPrivate(true);
     change.setWorkInProgress(true);
     change.setReviewStarted(true);
-    change.setRevertOf(new Change.Id(180));
+    change.setRevertOf(Change.id(180));
 
     Entities.Change proto = changeProtoConverter.toProto(change);
 
@@ -67,8 +67,8 @@
             .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
             .setDest(
                 Entities.Branch_NameKey.newBuilder()
-                    .setProjectName(Entities.Project_NameKey.newBuilder().setName("project 67"))
-                    .setBranchName("refs/heads/branch 74"))
+                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
+                    .setBranch("refs/heads/branch 74"))
             .setStatus(Change.STATUS_MERGED)
             .setCurrentPatchSetId(23)
             .setSubject("subject XYZ")
@@ -88,10 +88,10 @@
   public void mandatoryValuesConvertedToProto() {
     Change change =
         new Change(
-            new Change.Key("change 1"),
-            new Change.Id(14),
-            new Account.Id(35),
-            new Branch.NameKey(new Project.NameKey("project 67"), "branch-74"),
+            Change.key("change 1"),
+            Change.id(14),
+            Account.id(35),
+            BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
             new Timestamp(987654L));
 
     Entities.Change proto = changeProtoConverter.toProto(change);
@@ -106,8 +106,8 @@
             .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
             .setDest(
                 Entities.Branch_NameKey.newBuilder()
-                    .setProjectName(Entities.Project_NameKey.newBuilder().setName("project 67"))
-                    .setBranchName("refs/heads/branch-74"))
+                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
+                    .setBranch("refs/heads/branch-74"))
             // Default values which can't be unset.
             .setCurrentPatchSetId(0)
             .setRowVersion(0)
@@ -124,13 +124,13 @@
   public void currentPatchSetIsAlwaysSetWhenConvertedToProto() {
     Change change =
         new Change(
-            new Change.Key("change 1"),
-            new Change.Id(14),
-            new Account.Id(35),
-            new Branch.NameKey(new Project.NameKey("project 67"), "branch-74"),
+            Change.key("change 1"),
+            Change.id(14),
+            Account.id(35),
+            BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
             new Timestamp(987654L));
     // O as ID actually means that no current patch set is present.
-    change.setCurrentPatchSet(new PatchSet.Id(new Change.Id(14), 0), null, null);
+    change.setCurrentPatchSet(PatchSet.id(Change.id(14), 0), null, null);
 
     Entities.Change proto = changeProtoConverter.toProto(change);
 
@@ -144,8 +144,8 @@
             .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
             .setDest(
                 Entities.Branch_NameKey.newBuilder()
-                    .setProjectName(Entities.Project_NameKey.newBuilder().setName("project 67"))
-                    .setBranchName("refs/heads/branch-74"))
+                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
+                    .setBranch("refs/heads/branch-74"))
             .setCurrentPatchSetId(0)
             // Default values which can't be unset.
             .setRowVersion(0)
@@ -162,12 +162,12 @@
   public void originalSubjectIsNotAutomaticallySetToSubjectWhenConvertedToProto() {
     Change change =
         new Change(
-            new Change.Key("change 1"),
-            new Change.Id(14),
-            new Account.Id(35),
-            new Branch.NameKey(new Project.NameKey("project 67"), "branch-74"),
+            Change.key("change 1"),
+            Change.id(14),
+            Account.id(35),
+            BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
             new Timestamp(987654L));
-    change.setCurrentPatchSet(new PatchSet.Id(new Change.Id(14), 23), "subject ABC", null);
+    change.setCurrentPatchSet(PatchSet.id(Change.id(14), 23), "subject ABC", null);
 
     Entities.Change proto = changeProtoConverter.toProto(change);
 
@@ -181,8 +181,8 @@
             .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
             .setDest(
                 Entities.Branch_NameKey.newBuilder()
-                    .setProjectName(Entities.Project_NameKey.newBuilder().setName("project 67"))
-                    .setBranchName("refs/heads/branch-74"))
+                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
+                    .setBranch("refs/heads/branch-74"))
             .setCurrentPatchSetId(23)
             .setSubject("subject ABC")
             // Default values which can't be unset.
@@ -199,22 +199,22 @@
   public void allValuesConvertedToProtoAndBackAgain() {
     Change change =
         new Change(
-            new Change.Key("change 1"),
-            new Change.Id(14),
-            new Account.Id(35),
-            new Branch.NameKey(new Project.NameKey("project 67"), "branch-74"),
+            Change.key("change 1"),
+            Change.id(14),
+            Account.id(35),
+            BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
             new Timestamp(987654L));
     change.setLastUpdatedOn(new Timestamp(1234567L));
     change.setStatus(Change.Status.MERGED);
     change.setCurrentPatchSet(
-        new PatchSet.Id(new Change.Id(14), 23), "subject XYZ", "original subject ABC");
+        PatchSet.id(Change.id(14), 23), "subject XYZ", "original subject ABC");
     change.setTopic("my topic");
     change.setSubmissionId("submission ID 234");
-    change.setAssignee(new Account.Id(100001));
+    change.setAssignee(Account.id(100001));
     change.setPrivate(true);
     change.setWorkInProgress(true);
     change.setReviewStarted(true);
-    change.setRevertOf(new Change.Id(180));
+    change.setRevertOf(Change.id(180));
 
     Change convertedChange = changeProtoConverter.fromProto(changeProtoConverter.toProto(change));
     assertEqualChange(convertedChange, change);
@@ -224,10 +224,10 @@
   public void mandatoryValuesConvertedToProtoAndBackAgain() {
     Change change =
         new Change(
-            new Change.Key("change 1"),
-            new Change.Id(14),
-            new Account.Id(35),
-            new Branch.NameKey(new Project.NameKey("project 67"), "branch-74"),
+            Change.key("change 1"),
+            Change.id(14),
+            Account.id(35),
+            BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
             new Timestamp(987654L));
 
     Change convertedChange = changeProtoConverter.fromProto(changeProtoConverter.toProto(change));
@@ -269,8 +269,8 @@
             .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
             .setDest(
                 Entities.Branch_NameKey.newBuilder()
-                    .setProjectName(Entities.Project_NameKey.newBuilder().setName("project 67"))
-                    .setBranchName("branch 74"))
+                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
+                    .setBranch("branch 74"))
             .build();
     Change change = changeProtoConverter.fromProto(proto);
 
@@ -289,8 +289,8 @@
             .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
             .setDest(
                 Entities.Branch_NameKey.newBuilder()
-                    .setProjectName(Entities.Project_NameKey.newBuilder().setName("project 67"))
-                    .setBranchName("branch 74"))
+                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
+                    .setBranch("branch 74"))
             .setStatus(Change.STATUS_MERGED)
             .setCurrentPatchSetId(23)
             .setSubject("subject XYZ")
@@ -323,7 +323,7 @@
                 .put("createdOn", Timestamp.class)
                 .put("lastUpdatedOn", Timestamp.class)
                 .put("owner", Account.Id.class)
-                .put("dest", Branch.NameKey.class)
+                .put("dest", BranchNameKey.class)
                 .put("status", char.class)
                 .put("currentPatchSetId", int.class)
                 .put("subject", String.class)
diff --git a/javatests/com/google/gerrit/reviewdb/converter/LabelIdProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/LabelIdProtoConverterTest.java
similarity index 86%
rename from javatests/com/google/gerrit/reviewdb/converter/LabelIdProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/LabelIdProtoConverterTest.java
index 41e0f3f..88b9fb6 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/LabelIdProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/LabelIdProtoConverterTest.java
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.protobuf.Parser;
 import org.junit.Test;
 
@@ -30,7 +30,7 @@
 
   @Test
   public void allValuesConvertedToProto() {
-    LabelId labelId = new LabelId("Label ID 42");
+    LabelId labelId = LabelId.create("Label ID 42");
 
     Entities.LabelId proto = labelIdProtoConverter.toProto(labelId);
 
@@ -40,7 +40,7 @@
 
   @Test
   public void allValuesConvertedToProtoAndBackAgain() {
-    LabelId labelId = new LabelId("label-5");
+    LabelId labelId = LabelId.create("label-5");
 
     LabelId convertedLabelId =
         labelIdProtoConverter.fromProto(labelIdProtoConverter.toProto(labelId));
@@ -61,7 +61,8 @@
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
-  public void fieldsExistAsExpected() {
-    assertThatSerializedClass(LabelId.class).hasFields(ImmutableMap.of("id", String.class));
+  public void methodsExistAsExpected() {
+    assertThatSerializedClass(LabelId.class)
+        .hasAutoValueMethods(ImmutableMap.of("id", String.class));
   }
 }
diff --git a/javatests/com/google/gerrit/entities/converter/ObjectIdProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ObjectIdProtoConverterTest.java
new file mode 100644
index 0000000..8408b69
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/converter/ObjectIdProtoConverterTest.java
@@ -0,0 +1,75 @@
+// 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.entities.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.protobuf.Parser;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class ObjectIdProtoConverterTest {
+  private final ObjectIdProtoConverter objectIdProtoConverter = ObjectIdProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    ObjectId objectId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    Entities.ObjectId proto = objectIdProtoConverter.toProto(objectId);
+
+    Entities.ObjectId expectedProto =
+        Entities.ObjectId.newBuilder().setName("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    ObjectId objectId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    ObjectId convertedObjectId =
+        objectIdProtoConverter.fromProto(objectIdProtoConverter.toProto(objectId));
+
+    assertThat(convertedObjectId).isEqualTo(objectId);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.ObjectId proto =
+        Entities.ObjectId.newBuilder().setName("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.ObjectId> parser = objectIdProtoConverter.getParser();
+    Entities.ObjectId parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(ObjectId.class)
+        .hasFields(
+            ImmutableMap.of(
+                "w1", int.class,
+                "w2", int.class,
+                "w3", int.class,
+                "w4", int.class,
+                "w5", int.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalKeyProtoConverterTest.java
similarity index 76%
rename from javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/PatchSetApprovalKeyProtoConverterTest.java
index d1ed419..11aac0d 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalKeyProtoConverterTest.java
@@ -12,20 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.protobuf.Parser;
 import java.lang.reflect.Type;
 import org.junit.Test;
@@ -37,8 +37,8 @@
   @Test
   public void allValuesConvertedToProto() {
     PatchSetApproval.Key key =
-        new PatchSetApproval.Key(
-            new PatchSet.Id(new Change.Id(42), 14), new Account.Id(100013), new LabelId("label-8"));
+        PatchSetApproval.key(
+            PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8"));
 
     Entities.PatchSetApproval_Key proto = protoConverter.toProto(key);
 
@@ -47,9 +47,9 @@
             .setPatchSetId(
                 Entities.PatchSet_Id.newBuilder()
                     .setChangeId(Entities.Change_Id.newBuilder().setId(42))
-                    .setPatchSetId(14))
+                    .setId(14))
             .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
-            .setCategoryId(Entities.LabelId.newBuilder().setId("label-8"))
+            .setLabelId(Entities.LabelId.newBuilder().setId("label-8"))
             .build();
     assertThat(proto).isEqualTo(expectedProto);
   }
@@ -57,8 +57,8 @@
   @Test
   public void allValuesConvertedToProtoAndBackAgain() {
     PatchSetApproval.Key key =
-        new PatchSetApproval.Key(
-            new PatchSet.Id(new Change.Id(42), 14), new Account.Id(100013), new LabelId("label-8"));
+        PatchSetApproval.key(
+            PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8"));
 
     PatchSetApproval.Key convertedKey = protoConverter.fromProto(protoConverter.toProto(key));
 
@@ -72,9 +72,9 @@
             .setPatchSetId(
                 Entities.PatchSet_Id.newBuilder()
                     .setChangeId(Entities.Change_Id.newBuilder().setId(42))
-                    .setPatchSetId(14))
+                    .setId(14))
             .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
-            .setCategoryId(Entities.LabelId.newBuilder().setId("label-8"))
+            .setLabelId(Entities.LabelId.newBuilder().setId("label-8"))
             .build();
     byte[] bytes = proto.toByteArray();
 
@@ -86,13 +86,13 @@
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
-  public void fieldsExistAsExpected() {
+  public void methodsExistAsExpected() {
     assertThatSerializedClass(PatchSetApproval.Key.class)
-        .hasFields(
+        .hasAutoValueMethods(
             ImmutableMap.<String, Type>builder()
                 .put("patchSetId", PatchSet.Id.class)
                 .put("accountId", Account.Id.class)
-                .put("categoryId", LabelId.class)
+                .put("labelId", LabelId.class)
                 .build());
   }
 }
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
similarity index 68%
rename from javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
index 80b2cc2..bca5eea 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
@@ -12,24 +12,26 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.inject.TypeLiteral;
 import com.google.protobuf.Parser;
 import java.lang.reflect.Type;
 import java.sql.Timestamp;
 import java.util.Date;
+import java.util.Optional;
 import org.junit.Test;
 
 public class PatchSetApprovalProtoConverterTest {
@@ -39,16 +41,16 @@
   @Test
   public void allValuesConvertedToProto() {
     PatchSetApproval patchSetApproval =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(
-                new PatchSet.Id(new Change.Id(42), 14),
-                new Account.Id(100013),
-                new LabelId("label-8")),
-            (short) 456,
-            new Date(987654L));
-    patchSetApproval.setTag("tag-21");
-    patchSetApproval.setRealAccountId(new Account.Id(612));
-    patchSetApproval.setPostSubmit(true);
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
+            .value(456)
+            .granted(new Date(987654L))
+            .tag("tag-21")
+            .realAccountId(Account.id(612))
+            .postSubmit(true)
+            .build();
 
     Entities.PatchSetApproval proto = protoConverter.toProto(patchSetApproval);
 
@@ -59,9 +61,9 @@
                     .setPatchSetId(
                         Entities.PatchSet_Id.newBuilder()
                             .setChangeId(Entities.Change_Id.newBuilder().setId(42))
-                            .setPatchSetId(14))
+                            .setId(14))
                     .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
-                    .setCategoryId(Entities.LabelId.newBuilder().setId("label-8")))
+                    .setLabelId(Entities.LabelId.newBuilder().setId("label-8")))
             .setValue(456)
             .setGranted(987654L)
             .setTag("tag-21")
@@ -74,13 +76,13 @@
   @Test
   public void mandatoryValuesConvertedToProto() {
     PatchSetApproval patchSetApproval =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(
-                new PatchSet.Id(new Change.Id(42), 14),
-                new Account.Id(100013),
-                new LabelId("label-8")),
-            (short) 456,
-            new Date(987654L));
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
+            .value(456)
+            .granted(new Date(987654L))
+            .build();
 
     Entities.PatchSetApproval proto = protoConverter.toProto(patchSetApproval);
 
@@ -91,9 +93,9 @@
                     .setPatchSetId(
                         Entities.PatchSet_Id.newBuilder()
                             .setChangeId(Entities.Change_Id.newBuilder().setId(42))
-                            .setPatchSetId(14))
+                            .setId(14))
                     .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
-                    .setCategoryId(Entities.LabelId.newBuilder().setId("label-8")))
+                    .setLabelId(Entities.LabelId.newBuilder().setId("label-8")))
             .setValue(456)
             .setGranted(987654L)
             // This value can't be unset when our entity class is given.
@@ -105,16 +107,16 @@
   @Test
   public void allValuesConvertedToProtoAndBackAgain() {
     PatchSetApproval patchSetApproval =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(
-                new PatchSet.Id(new Change.Id(42), 14),
-                new Account.Id(100013),
-                new LabelId("label-8")),
-            (short) 456,
-            new Date(987654L));
-    patchSetApproval.setTag("tag-21");
-    patchSetApproval.setRealAccountId(new Account.Id(612));
-    patchSetApproval.setPostSubmit(true);
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
+            .value(456)
+            .granted(new Date(987654L))
+            .tag("tag-21")
+            .realAccountId(Account.id(612))
+            .postSubmit(true)
+            .build();
 
     PatchSetApproval convertedPatchSetApproval =
         protoConverter.fromProto(protoConverter.toProto(patchSetApproval));
@@ -124,13 +126,13 @@
   @Test
   public void mandatoryValuesConvertedToProtoAndBackAgain() {
     PatchSetApproval patchSetApproval =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(
-                new PatchSet.Id(new Change.Id(42), 14),
-                new Account.Id(100013),
-                new LabelId("label-8")),
-            (short) 456,
-            new Date(987654L));
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
+            .value(456)
+            .granted(new Date(987654L))
+            .build();
 
     PatchSetApproval convertedPatchSetApproval =
         protoConverter.fromProto(protoConverter.toProto(patchSetApproval));
@@ -148,19 +150,19 @@
                     .setPatchSetId(
                         Entities.PatchSet_Id.newBuilder()
                             .setChangeId(Entities.Change_Id.newBuilder().setId(42))
-                            .setPatchSetId(14))
+                            .setId(14))
                     .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
-                    .setCategoryId(Entities.LabelId.newBuilder().setId("label-8")))
+                    .setLabelId(Entities.LabelId.newBuilder().setId("label-8")))
             .build();
     PatchSetApproval patchSetApproval = protoConverter.fromProto(proto);
 
-    assertThat(patchSetApproval.getPatchSetId()).isEqualTo(new PatchSet.Id(new Change.Id(42), 14));
-    assertThat(patchSetApproval.getAccountId()).isEqualTo(new Account.Id(100013));
-    assertThat(patchSetApproval.getLabelId()).isEqualTo(new LabelId("label-8"));
+    assertThat(patchSetApproval.patchSetId()).isEqualTo(PatchSet.id(Change.id(42), 14));
+    assertThat(patchSetApproval.accountId()).isEqualTo(Account.id(100013));
+    assertThat(patchSetApproval.labelId()).isEqualTo(LabelId.create("label-8"));
     // Default values for unset protobuf fields which can't be unset in the entity object.
-    assertThat(patchSetApproval.getValue()).isEqualTo(0);
-    assertThat(patchSetApproval.getGranted()).isEqualTo(new Timestamp(0));
-    assertThat(patchSetApproval.isPostSubmit()).isEqualTo(false);
+    assertThat(patchSetApproval.value()).isEqualTo(0);
+    assertThat(patchSetApproval.granted()).isEqualTo(new Timestamp(0));
+    assertThat(patchSetApproval.postSubmit()).isEqualTo(false);
   }
 
   @Test
@@ -172,9 +174,9 @@
                     .setPatchSetId(
                         Entities.PatchSet_Id.newBuilder()
                             .setChangeId(Entities.Change_Id.newBuilder().setId(42))
-                            .setPatchSetId(14))
+                            .setId(14))
                     .setAccountId(Entities.Account_Id.newBuilder().setId(100013))
-                    .setCategoryId(Entities.LabelId.newBuilder().setId("label-8")))
+                    .setLabelId(Entities.LabelId.newBuilder().setId("label-8")))
             .setValue(456)
             .setGranted(987654L)
             .build();
@@ -190,14 +192,15 @@
   @Test
   public void fieldsExistAsExpected() {
     assertThatSerializedClass(PatchSetApproval.class)
-        .hasFields(
+        .hasAutoValueMethods(
             ImmutableMap.<String, Type>builder()
                 .put("key", PatchSetApproval.Key.class)
                 .put("value", short.class)
                 .put("granted", Timestamp.class)
-                .put("tag", String.class)
+                .put("tag", new TypeLiteral<Optional<String>>() {}.getType())
                 .put("realAccountId", Account.Id.class)
                 .put("postSubmit", boolean.class)
+                .put("toBuilder", PatchSetApproval.Builder.class)
                 .build());
   }
 }
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetIdProtoConverterTest.java
similarity index 85%
rename from javatests/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/PatchSetIdProtoConverterTest.java
index 1598ef2..530b431 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetIdProtoConverterTest.java
@@ -12,17 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.protobuf.Parser;
 import java.lang.reflect.Type;
 import org.junit.Test;
@@ -33,21 +33,21 @@
 
   @Test
   public void allValuesConvertedToProto() {
-    PatchSet.Id patchSetId = new PatchSet.Id(new Change.Id(103), 73);
+    PatchSet.Id patchSetId = PatchSet.id(Change.id(103), 73);
 
     Entities.PatchSet_Id proto = patchSetIdProtoConverter.toProto(patchSetId);
 
     Entities.PatchSet_Id expectedProto =
         Entities.PatchSet_Id.newBuilder()
             .setChangeId(Entities.Change_Id.newBuilder().setId(103))
-            .setPatchSetId(73)
+            .setId(73)
             .build();
     assertThat(proto).isEqualTo(expectedProto);
   }
 
   @Test
   public void allValuesConvertedToProtoAndBackAgain() {
-    PatchSet.Id patchSetId = new PatchSet.Id(new Change.Id(20), 13);
+    PatchSet.Id patchSetId = PatchSet.id(Change.id(20), 13);
 
     PatchSet.Id convertedPatchSetId =
         patchSetIdProtoConverter.fromProto(patchSetIdProtoConverter.toProto(patchSetId));
@@ -60,7 +60,7 @@
     Entities.PatchSet_Id proto =
         Entities.PatchSet_Id.newBuilder()
             .setChangeId(Entities.Change_Id.newBuilder().setId(103))
-            .setPatchSetId(73)
+            .setId(73)
             .build();
     byte[] bytes = proto.toByteArray();
 
@@ -72,12 +72,12 @@
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
-  public void fieldsExistAsExpected() {
+  public void methodsExistAsExpected() {
     assertThatSerializedClass(PatchSet.Id.class)
-        .hasFields(
+        .hasAutoValueMethods(
             ImmutableMap.<String, Type>builder()
                 .put("changeId", Change.Id.class)
-                .put("patchSetId", int.class)
+                .put("id", int.class)
                 .build());
   }
 }
diff --git a/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
new file mode 100644
index 0000000..2519e75
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
@@ -0,0 +1,183 @@
+// 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.
+
+package com.google.gerrit.entities.converter;
+
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.truth.Truth;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.inject.TypeLiteral;
+import com.google.protobuf.Parser;
+import java.lang.reflect.Type;
+import java.sql.Timestamp;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class PatchSetProtoConverterTest {
+  private final PatchSetProtoConverter patchSetProtoConverter = PatchSetProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    PatchSet patchSet =
+        PatchSet.builder()
+            .id(PatchSet.id(Change.id(103), 73))
+            .commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
+            .uploader(Account.id(452))
+            .createdOn(new Timestamp(930349320L))
+            .groups(ImmutableList.of("group1", " group2"))
+            .pushCertificate("my push certificate")
+            .description("This is a patch set description.")
+            .build();
+
+    Entities.PatchSet proto = patchSetProtoConverter.toProto(patchSet);
+
+    Entities.PatchSet expectedProto =
+        Entities.PatchSet.newBuilder()
+            .setId(
+                Entities.PatchSet_Id.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(103))
+                    .setId(73))
+            .setCommitId(
+                Entities.ObjectId.newBuilder().setName("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
+            .setUploaderAccountId(Entities.Account_Id.newBuilder().setId(452))
+            .setCreatedOn(930349320L)
+            .setGroups("group1, group2")
+            .setPushCertificate("my push certificate")
+            .setDescription("This is a patch set description.")
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void mandatoryValuesConvertedToProto() {
+    PatchSet patchSet =
+        PatchSet.builder()
+            .id(PatchSet.id(Change.id(103), 73))
+            .commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
+            .uploader(Account.id(452))
+            .createdOn(new Timestamp(930349320L))
+            .build();
+
+    Entities.PatchSet proto = patchSetProtoConverter.toProto(patchSet);
+
+    Entities.PatchSet expectedProto =
+        Entities.PatchSet.newBuilder()
+            .setId(
+                Entities.PatchSet_Id.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(103))
+                    .setId(73))
+            .setCommitId(
+                Entities.ObjectId.newBuilder().setName("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
+            .setUploaderAccountId(Entities.Account_Id.newBuilder().setId(452))
+            .setCreatedOn(930349320L)
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    PatchSet patchSet =
+        PatchSet.builder()
+            .id(PatchSet.id(Change.id(103), 73))
+            .commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
+            .uploader(Account.id(452))
+            .createdOn(new Timestamp(930349320L))
+            .groups(ImmutableList.of("group1", " group2"))
+            .pushCertificate("my push certificate")
+            .description("This is a patch set description.")
+            .build();
+
+    PatchSet convertedPatchSet =
+        patchSetProtoConverter.fromProto(patchSetProtoConverter.toProto(patchSet));
+    Truth.assertThat(convertedPatchSet).isEqualTo(patchSet);
+  }
+
+  @Test
+  public void mandatoryValuesConvertedToProtoAndBackAgain() {
+    PatchSet patchSet =
+        PatchSet.builder()
+            .id(PatchSet.id(Change.id(103), 73))
+            .commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
+            .uploader(Account.id(452))
+            .createdOn(new Timestamp(930349320L))
+            .build();
+
+    PatchSet convertedPatchSet =
+        patchSetProtoConverter.fromProto(patchSetProtoConverter.toProto(patchSet));
+    Truth.assertThat(convertedPatchSet).isEqualTo(patchSet);
+  }
+
+  @Test
+  public void previouslyOptionalValuesMayBeMissingFromProto() {
+    Entities.PatchSet proto =
+        Entities.PatchSet.newBuilder()
+            .setId(
+                Entities.PatchSet_Id.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(103))
+                    .setId(73))
+            .build();
+
+    PatchSet convertedPatchSet = patchSetProtoConverter.fromProto(proto);
+    Truth.assertThat(convertedPatchSet)
+        .isEqualTo(
+            PatchSet.builder()
+                .id(PatchSet.id(Change.id(103), 73))
+                .commitId(ObjectId.fromString("0000000000000000000000000000000000000000"))
+                .uploader(Account.id(0))
+                .createdOn(new Timestamp(0))
+                .build());
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Entities.PatchSet proto =
+        Entities.PatchSet.newBuilder()
+            .setId(
+                Entities.PatchSet_Id.newBuilder()
+                    .setChangeId(Entities.Change_Id.newBuilder().setId(103))
+                    .setId(73))
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Entities.PatchSet> parser = patchSetProtoConverter.getParser();
+    Entities.PatchSet parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(PatchSet.class)
+        .hasAutoValueMethods(
+            ImmutableMap.<String, Type>builder()
+                .put("id", PatchSet.Id.class)
+                .put("commitId", ObjectId.class)
+                .put("uploader", Account.Id.class)
+                .put("createdOn", Timestamp.class)
+                .put("groups", new TypeLiteral<ImmutableList<String>>() {}.getType())
+                .put("pushCertificate", new TypeLiteral<Optional<String>>() {}.getType())
+                .put("description", new TypeLiteral<Optional<String>>() {}.getType())
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java
similarity index 91%
rename from javatests/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverterTest.java
rename to javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java
index 2ad6107..2f693e6 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.converter;
+package com.google.gerrit.entities.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.protobuf.Parser;
 import org.junit.Test;
 
@@ -31,7 +31,7 @@
 
   @Test
   public void allValuesConvertedToProto() {
-    Project.NameKey nameKey = new Project.NameKey("project-72");
+    Project.NameKey nameKey = Project.nameKey("project-72");
 
     Entities.Project_NameKey proto = projectNameKeyProtoConverter.toProto(nameKey);
 
@@ -42,7 +42,7 @@
 
   @Test
   public void allValuesConvertedToProtoAndBackAgain() {
-    Project.NameKey nameKey = new Project.NameKey("project-52");
+    Project.NameKey nameKey = Project.nameKey("project-52");
 
     Project.NameKey convertedNameKey =
         projectNameKeyProtoConverter.fromProto(projectNameKeyProtoConverter.toProto(nameKey));
diff --git a/javatests/com/google/gerrit/extensions/BUILD b/javatests/com/google/gerrit/extensions/BUILD
index 94e433c..2202a11 100644
--- a/javatests/com/google/gerrit/extensions/BUILD
+++ b/javatests/com/google/gerrit/extensions/BUILD
@@ -7,7 +7,6 @@
     deps = [
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/extensions/common/testing:common-test-util",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib/guice",
         "//lib/truth",
diff --git a/javatests/com/google/gerrit/extensions/api/lfs/LfsDefinitionsTest.java b/javatests/com/google/gerrit/extensions/api/lfs/LfsDefinitionsTest.java
index 86dce04..0be10ee 100644
--- a/javatests/com/google/gerrit/extensions/api/lfs/LfsDefinitionsTest.java
+++ b/javatests/com/google/gerrit/extensions/api/lfs/LfsDefinitionsTest.java
@@ -16,12 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.junit.Test;
 
-public class LfsDefinitionsTest extends GerritBaseTests {
+public class LfsDefinitionsTest {
   private static final String[] URL_PREFIXES = new String[] {"/", "/a/", "/p/", "/a/p/"};
 
   @Test
diff --git a/javatests/com/google/gerrit/extensions/client/ListOptionTest.java b/javatests/com/google/gerrit/extensions/client/ListOptionTest.java
index 4bb9107..5e8c7b6 100644
--- a/javatests/com/google/gerrit/extensions/client/ListOptionTest.java
+++ b/javatests/com/google/gerrit/extensions/client/ListOptionTest.java
@@ -21,11 +21,10 @@
 import static com.google.gerrit.extensions.client.ListOptionTest.MyOption.FOO;
 
 import com.google.common.math.IntMath;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.EnumSet;
 import org.junit.Test;
 
-public class ListOptionTest extends GerritBaseTests {
+public class ListOptionTest {
   enum MyOption implements ListOption {
     FOO(0),
     BAR(1),
diff --git a/javatests/com/google/gerrit/extensions/client/RangeTest.java b/javatests/com/google/gerrit/extensions/client/RangeTest.java
index 2c713b5..b8938aa 100644
--- a/javatests/com/google/gerrit/extensions/client/RangeTest.java
+++ b/javatests/com/google/gerrit/extensions/client/RangeTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.gerrit.extensions.common.testing.RangeSubject.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class RangeTest extends GerritBaseTests {
+public class RangeTest {
 
   @Test
   public void rangeOverMultipleLinesWithSmallerEndCharacterIsValid() {
diff --git a/javatests/com/google/gerrit/extensions/conditions/BUILD b/javatests/com/google/gerrit/extensions/conditions/BUILD
index 7ad2ad3..e2d5951 100644
--- a/javatests/com/google/gerrit/extensions/conditions/BUILD
+++ b/javatests/com/google/gerrit/extensions/conditions/BUILD
@@ -5,7 +5,6 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/extensions:lib",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/extensions/conditions/BooleanConditionTest.java b/javatests/com/google/gerrit/extensions/conditions/BooleanConditionTest.java
index 81cb719..f9f1fa85 100644
--- a/javatests/com/google/gerrit/extensions/conditions/BooleanConditionTest.java
+++ b/javatests/com/google/gerrit/extensions/conditions/BooleanConditionTest.java
@@ -20,10 +20,9 @@
 import static com.google.gerrit.extensions.conditions.BooleanCondition.valueOf;
 import static org.junit.Assert.assertEquals;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class BooleanConditionTest extends GerritBaseTests {
+public class BooleanConditionTest {
 
   private static final BooleanCondition NO_TRIVIAL_EVALUATION =
       new BooleanCondition() {
diff --git a/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java b/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
index d950224..0542c35 100644
--- a/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
+++ b/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
@@ -17,14 +17,13 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.util.stream.Collectors.toSet;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 import java.util.Iterator;
 import org.junit.Test;
 
-public class DynamicSetTest extends GerritBaseTests {
+public class DynamicSetTest {
   // In tests for {@link DynamicSet#contains(Object)}, be sure to avoid
   // {@code assertThat(ds).contains(...) @} and
   // {@code assertThat(ds).DoesNotContains(...) @} as (since
diff --git a/javatests/com/google/gerrit/git/BUILD b/javatests/com/google/gerrit/git/BUILD
index d57d73f..c2c9cce 100644
--- a/javatests/com/google/gerrit/git/BUILD
+++ b/javatests/com/google/gerrit/git/BUILD
@@ -10,11 +10,10 @@
     tags = ["no_windows"],
     deps = [
         "//java/com/google/gerrit/git",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib:junit",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/truth",
     ],
 )
@@ -30,7 +29,8 @@
         "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/git/ObjectIdsTest.java b/javatests/com/google/gerrit/git/ObjectIdsTest.java
new file mode 100644
index 0000000..b254d6f
--- /dev/null
+++ b/javatests/com/google/gerrit/git/ObjectIdsTest.java
@@ -0,0 +1,156 @@
+// 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.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
+
+import java.util.function.Function;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.junit.Test;
+
+public class ObjectIdsTest {
+  private static final ObjectId ID =
+      ObjectId.fromString("0000000000100000000000000000000000000000");
+  private static final ObjectId AMBIGUOUS_BLOB_ID =
+      ObjectId.fromString("0000000000b36b6aa7ea4b75318ed078f55505c3");
+  private static final ObjectId AMBIGUOUS_TREE_ID =
+      ObjectId.fromString("0000000000cdcf04beb2fab69e65622616294984");
+
+  @Test
+  public void abbreviateNameDefaultLength() throws Exception {
+    assertRuntimeException(() -> abbreviateName(null));
+    assertThat(abbreviateName(ID)).isEqualTo("0000000");
+    assertThat(abbreviateName(AMBIGUOUS_BLOB_ID)).isEqualTo(abbreviateName(ID));
+    assertThat(abbreviateName(AMBIGUOUS_TREE_ID)).isEqualTo(abbreviateName(ID));
+  }
+
+  @Test
+  public void abbreviateNameCustomLength() throws Exception {
+    assertRuntimeException(() -> abbreviateName(null, 1));
+    assertRuntimeException(() -> abbreviateName(ID, -1));
+    assertRuntimeException(() -> abbreviateName(ID, 0));
+    assertRuntimeException(() -> abbreviateName(ID, 41));
+    assertThat(abbreviateName(ID, 5)).isEqualTo("00000");
+    assertThat(abbreviateName(ID, 40)).isEqualTo(ID.name());
+  }
+
+  @Test
+  public void abbreviateNameDefaultLengthWithReader() throws Exception {
+    assertRuntimeException(() -> abbreviateName(ID, null));
+
+    ObjectReader reader = newReaderWithAmbiguousIds();
+    assertThat(abbreviateName(ID, reader)).isEqualTo("00000000001");
+  }
+
+  @Test
+  public void abbreviateNameCustomLengthWithReader() throws Exception {
+    ObjectReader reader = newReaderWithAmbiguousIds();
+    assertRuntimeException(() -> abbreviateName(ID, -1, reader));
+    assertRuntimeException(() -> abbreviateName(ID, 0, reader));
+    assertRuntimeException(() -> abbreviateName(ID, 41, reader));
+    assertRuntimeException(() -> abbreviateName(ID, 5, null));
+
+    String shortest = "00000000001";
+    assertThat(abbreviateName(ID, 1, reader)).isEqualTo(shortest);
+    assertThat(abbreviateName(ID, 7, reader)).isEqualTo(shortest);
+    assertThat(abbreviateName(ID, shortest.length(), reader)).isEqualTo(shortest);
+    assertThat(abbreviateName(ID, shortest.length() + 1, reader)).isEqualTo("000000000010");
+  }
+
+  @Test
+  public void copyOrNull() throws Exception {
+    testCopy(ObjectIds::copyOrNull);
+    assertThat(ObjectIds.copyOrNull(null)).isNull();
+  }
+
+  @Test
+  public void copyOrZero() throws Exception {
+    testCopy(ObjectIds::copyOrZero);
+    assertThat(ObjectIds.copyOrZero(null)).isEqualTo(ObjectId.zeroId());
+  }
+
+  private void testCopy(Function<AnyObjectId, ObjectId> copyFunc) {
+    MyObjectId myId = new MyObjectId(ID);
+    assertThat(myId).isEqualTo(ID);
+
+    ObjectId copy = copyFunc.apply(myId);
+    assertThat(copy).isEqualTo(myId);
+    assertThat(copy).isNotSameInstanceAs(myId);
+    assertThat(copy.getClass()).isEqualTo(ObjectId.class);
+  }
+
+  @Test
+  public void matchesAbbreviation() throws Exception {
+    assertThat(ObjectIds.matchesAbbreviation(null, "")).isFalse();
+    assertThat(ObjectIds.matchesAbbreviation(null, "0")).isFalse();
+    assertThat(ObjectIds.matchesAbbreviation(null, "00000")).isFalse();
+    assertThat(ObjectIds.matchesAbbreviation(null, "not a SHA-1")).isFalse();
+    assertThat(ObjectIds.matchesAbbreviation(null, ID.name())).isFalse();
+
+    assertThat(ObjectIds.matchesAbbreviation(ID, "")).isTrue();
+    for (int i = 1; i <= OBJECT_ID_STRING_LENGTH; i++) {
+      String prefix = ID.name().substring(0, i);
+      assertWithMessage("match %s against %s", ID.name(), prefix)
+          .that(ObjectIds.matchesAbbreviation(ID, prefix))
+          .isTrue();
+    }
+
+    assertThat(ObjectIds.matchesAbbreviation(ID, "1")).isFalse();
+    assertThat(ObjectIds.matchesAbbreviation(ID, "x")).isFalse();
+    assertThat(ObjectIds.matchesAbbreviation(ID, "not a SHA-1")).isFalse();
+    assertThat(ObjectIds.matchesAbbreviation(ID, AMBIGUOUS_BLOB_ID.name())).isFalse();
+  }
+
+  @FunctionalInterface
+  private interface Func {
+    void call() throws Exception;
+  }
+
+  private static void assertRuntimeException(Func func) throws Exception {
+    assertThrows(RuntimeException.class, () -> func.call());
+  }
+
+  private static ObjectReader newReaderWithAmbiguousIds() throws Exception {
+    // Recipe for creating ambiguous IDs courtesy of git core:
+    // https://github.com/git/git/blob/df799f5d99ac51d4fc791d546de3f936088582fc/t/t1512-rev-parse-disambiguation.sh
+    try (TestRepository<Repository> tr =
+        new TestRepository<>(new InMemoryRepository(new DfsRepositoryDescription("repo")))) {
+      String blobData = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\nb1rwzyc3\n";
+      RevBlob blob = tr.blob(blobData);
+      assertThat(blob.name()).isEqualTo(AMBIGUOUS_BLOB_ID.name());
+      assertThat(tr.tree(tr.file("a0blgqsjc", blob)).name()).isEqualTo(AMBIGUOUS_TREE_ID.name());
+      return tr.getRevWalk().getObjectReader();
+    }
+  }
+
+  private static class MyObjectId extends ObjectId {
+    private static final long serialVersionUID = 1L;
+
+    MyObjectId(AnyObjectId src) {
+      super(src);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java b/javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java
index 59961ff..60b90f3 100644
--- a/javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java
+++ b/javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java
@@ -19,7 +19,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.MoreFiles;
 import com.google.common.io.RecursiveDeleteOption;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@@ -36,7 +35,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class RefUpdateUtilRepoTest extends GerritBaseTests {
+public class RefUpdateUtilRepoTest {
   public enum RepoSetup {
     LOCAL_DISK {
       @Override
diff --git a/javatests/com/google/gerrit/git/RefUpdateUtilTest.java b/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
index 429583a..1d021f7 100644
--- a/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
+++ b/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.IOException;
 import java.util.function.Consumer;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@@ -33,7 +32,7 @@
 import org.junit.runners.JUnit4;
 
 @RunWith(JUnit4.class)
-public class RefUpdateUtilTest extends GerritBaseTests {
+public class RefUpdateUtilTest {
   private static final Consumer<ReceiveCommand> OK = c -> c.setResult(ReceiveCommand.Result.OK);
   private static final Consumer<ReceiveCommand> LOCK_FAILURE =
       c -> c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
@@ -81,23 +80,18 @@
 
   @SafeVarargs
   private static void assertIoException(Consumer<ReceiveCommand>... resultSetters) {
-    try {
-      RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
-      assert_().fail("expected IOException");
-    } catch (IOException e) {
-      assertThat(e).isNotInstanceOf(LockFailureException.class);
-    }
+    IOException thrown =
+        assertThrows(
+            IOException.class, () -> RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters)));
+    assertThat(thrown).isNotInstanceOf(LockFailureException.class);
   }
 
   @SafeVarargs
   private static void assertLockFailureException(Consumer<ReceiveCommand>... resultSetters)
       throws Exception {
-    try {
-      RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
-      assert_().fail("expected LockFailureException");
-    } catch (LockFailureException e) {
-      // Expected.
-    }
+    assertThrows(
+        LockFailureException.class,
+        () -> RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters)));
   }
 
   @SafeVarargs
diff --git a/javatests/com/google/gerrit/git/testing/BUILD b/javatests/com/google/gerrit/git/testing/BUILD
index 1309185..56e9ec2 100644
--- a/javatests/com/google/gerrit/git/testing/BUILD
+++ b/javatests/com/google/gerrit/git/testing/BUILD
@@ -5,7 +5,6 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/git/testing",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/git/testing/PushResultSubjectTest.java b/javatests/com/google/gerrit/git/testing/PushResultSubjectTest.java
index 5ab52d4..3bf815b 100644
--- a/javatests/com/google/gerrit/git/testing/PushResultSubjectTest.java
+++ b/javatests/com/google/gerrit/git/testing/PushResultSubjectTest.java
@@ -18,10 +18,9 @@
 import static com.google.gerrit.git.testing.PushResultSubject.parseProcessed;
 import static com.google.gerrit.git.testing.PushResultSubject.trimMessages;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class PushResultSubjectTest extends GerritBaseTests {
+public class PushResultSubjectTest {
   @Test
   public void testTrimMessages() {
     assertThat(trimMessages(null)).isNull();
diff --git a/javatests/com/google/gerrit/gpg/BUILD b/javatests/com/google/gerrit/gpg/BUILD
index f73208d..f459b1a 100644
--- a/javatests/com/google/gerrit/gpg/BUILD
+++ b/javatests/com/google/gerrit/gpg/BUILD
@@ -5,16 +5,19 @@
     srcs = glob(["**/*.java"]),
     tags = ["no_windows"],
     visibility = ["//visibility:public"],
+    runtime_deps = ["//java/com/google/gerrit/lucene"],
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/gpg",
         "//java/com/google/gerrit/gpg/testing:gpg-test-util",
         "//java/com/google/gerrit/lifecycle",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib/bouncycastle:bcpg",
         "//lib/bouncycastle:bcpg-neverlink",
         "//lib/bouncycastle:bcprov",
@@ -22,8 +25,6 @@
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index 220361e..45b3419 100644
--- a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -29,10 +29,10 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterators;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
 import com.google.gerrit.gpg.testing.TestKey;
 import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountManager;
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
@@ -64,7 +63,7 @@
 import org.junit.Test;
 
 /** Unit tests for {@link GerritPublicKeyChecker}. */
-public class GerritPublicKeyCheckerTest extends GerritBaseTests {
+public class GerritPublicKeyCheckerTest {
   @Inject @ServerInitiated private Provider<AccountsUpdate> accountsUpdateProvider;
 
   @Inject private AccountManager accountManager;
@@ -199,7 +198,7 @@
         .update(
             "Delete External IDs",
             user.getAccountId(),
-            (a, u) -> u.deleteExternalIds(a.getExternalIds()));
+            (a, u) -> u.deleteExternalIds(a.externalIds()));
     reloadUser();
 
     TestKey key = validKeyWithSecondUserId();
diff --git a/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java b/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
index 145b9cf..7703fb0 100644
--- a/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
@@ -38,7 +38,6 @@
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.gpg.testing.TestKey;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -60,7 +59,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class PublicKeyCheckerTest extends GerritBaseTests {
+public class PublicKeyCheckerTest {
   private InMemoryRepository repo;
   private PublicKeyStore store;
 
diff --git a/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java b/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
index be65752..3727d38 100644
--- a/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
+++ b/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -29,7 +29,6 @@
 
 import com.google.common.collect.Iterators;
 import com.google.gerrit.gpg.testing.TestKey;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
@@ -51,7 +50,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class PublicKeyStoreTest extends GerritBaseTests {
+public class PublicKeyStoreTest {
   private TestRepository<?> tr;
   private PublicKeyStore store;
 
diff --git a/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java b/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java
index 67bf050..266f868 100644
--- a/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java
@@ -23,7 +23,6 @@
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.gpg.testing.TestKey;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.InputStreamReader;
@@ -54,7 +53,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class PushCertificateCheckerTest extends GerritBaseTests {
+public class PushCertificateCheckerTest {
   private InMemoryRepository repo;
   private PublicKeyStore store;
   private SignedPushConfig signedPushConfig;
diff --git a/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java b/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
index e2c58d8..4932248 100644
--- a/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
+++ b/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
@@ -14,14 +14,16 @@
 
 package com.google.gerrit.httpd;
 
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.capture;
-import static org.easymock.EasyMock.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
 import com.google.gerrit.server.plugins.Plugin;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
 import com.google.inject.Key;
@@ -30,13 +32,12 @@
 import javax.servlet.FilterConfig;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.easymock.Capture;
-import org.easymock.EasyMockSupport;
-import org.easymock.IMocksControl;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 
-public class AllRequestFilterFilterProxyTest extends GerritBaseTests {
+public class AllRequestFilterFilterProxyTest {
   /**
    * Set of filters for FilterProxy
    *
@@ -82,16 +83,11 @@
 
   @Test
   public void noFilters() throws Exception {
-    EasyMockSupport ems = new EasyMockSupport();
-
-    FilterConfig config = ems.createMock(FilterConfig.class);
+    FilterConfig config = mock(FilterConfig.class);
     HttpServletRequest req = new FakeHttpServletRequest();
     HttpServletResponse res = new FakeHttpServletResponse();
 
-    FilterChain chain = ems.createMock(FilterChain.class);
-    chain.doFilter(req, res);
-
-    ems.replayAll();
+    FilterChain chain = mock(FilterChain.class);
 
     AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
 
@@ -99,25 +95,18 @@
     filterProxy.doFilter(req, res, chain);
     filterProxy.destroy();
 
-    ems.verifyAll();
+    verify(chain).doFilter(req, res);
   }
 
   @Test
   public void singleFilterNoBubbling() throws Exception {
-    EasyMockSupport ems = new EasyMockSupport();
-
-    FilterConfig config = ems.createMock("config", FilterConfig.class);
+    FilterConfig config = mock(FilterConfig.class);
     HttpServletRequest req = new FakeHttpServletRequest();
     HttpServletResponse res = new FakeHttpServletResponse();
 
-    FilterChain chain = ems.createMock("chain", FilterChain.class);
+    FilterChain chain = mock(FilterChain.class);
 
-    AllRequestFilter filter = ems.createStrictMock("filter", AllRequestFilter.class);
-    filter.init(config);
-    filter.doFilter(eq(req), eq(res), anyObject(FilterChain.class));
-    filter.destroy();
-
-    ems.replayAll();
+    AllRequestFilter filter = mock(AllRequestFilter.class);
 
     AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
     addFilter(filter);
@@ -126,63 +115,52 @@
     filterProxy.doFilter(req, res, chain);
     filterProxy.destroy();
 
-    ems.verifyAll();
+    InOrder inorder = inOrder(filter);
+    inorder.verify(filter).init(config);
+    inorder.verify(filter).doFilter(eq(req), eq(res), any(FilterChain.class));
+    inorder.verify(filter).destroy();
   }
 
   @Test
   public void singleFilterBubbling() throws Exception {
-    EasyMockSupport ems = new EasyMockSupport();
-
-    FilterConfig config = ems.createMock(FilterConfig.class);
+    FilterConfig config = mock(FilterConfig.class);
     HttpServletRequest req = new FakeHttpServletRequest();
     HttpServletResponse res = new FakeHttpServletResponse();
 
-    IMocksControl mockControl = ems.createStrictControl();
-    FilterChain chain = mockControl.createMock(FilterChain.class);
+    FilterChain chain = mock(FilterChain.class);
 
-    Capture<FilterChain> capturedChain = new Capture<>();
+    ArgumentCaptor<FilterChain> capturedChain = ArgumentCaptor.forClass(FilterChain.class);
 
-    AllRequestFilter filter = mockControl.createMock(AllRequestFilter.class);
-    filter.init(config);
-    filter.doFilter(eq(req), eq(res), capture(capturedChain));
-    chain.doFilter(req, res);
-    filter.destroy();
-
-    ems.replayAll();
+    AllRequestFilter filter = mock(AllRequestFilter.class);
 
     AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
     addFilter(filter);
 
+    InOrder inorder = inOrder(filter, chain);
+
     filterProxy.init(config);
     filterProxy.doFilter(req, res, chain);
-    capturedChain.getValue().doFilter(req, res);
-    filterProxy.destroy();
 
-    ems.verifyAll();
+    inorder.verify(filter).init(config);
+    inorder.verify(filter).doFilter(eq(req), eq(res), capturedChain.capture());
+    capturedChain.getValue().doFilter(req, res);
+    inorder.verify(chain).doFilter(req, res);
+
+    filterProxy.destroy();
+    inorder.verify(filter).destroy();
   }
 
   @Test
   public void twoFiltersNoBubbling() throws Exception {
-    EasyMockSupport ems = new EasyMockSupport();
-
-    FilterConfig config = ems.createMock(FilterConfig.class);
+    FilterConfig config = mock(FilterConfig.class);
     HttpServletRequest req = new FakeHttpServletRequest();
     HttpServletResponse res = new FakeHttpServletResponse();
 
-    IMocksControl mockControl = ems.createStrictControl();
-    FilterChain chain = mockControl.createMock(FilterChain.class);
+    FilterChain chain = mock(FilterChain.class);
 
-    AllRequestFilter filterA = mockControl.createMock(AllRequestFilter.class);
+    AllRequestFilter filterA = mock(AllRequestFilter.class);
 
-    AllRequestFilter filterB = mockControl.createMock(AllRequestFilter.class);
-    filterA.init(config);
-    filterB.init(config);
-    filterA.doFilter(eq(req), eq(res), anyObject(FilterChain.class));
-    filterA.destroy();
-    filterB.destroy();
-
-    ems.replayAll();
-
+    AllRequestFilter filterB = mock(AllRequestFilter.class);
     AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
     addFilter(filterA);
     addFilter(filterB);
@@ -191,35 +169,27 @@
     filterProxy.doFilter(req, res, chain);
     filterProxy.destroy();
 
-    ems.verifyAll();
+    InOrder inorder = inOrder(filterA, filterB);
+    inorder.verify(filterA).init(config);
+    inorder.verify(filterB).init(config);
+    inorder.verify(filterA).doFilter(eq(req), eq(res), any(FilterChain.class));
+    inorder.verify(filterA).destroy();
+    inorder.verify(filterB).destroy();
   }
 
   @Test
   public void twoFiltersBubbling() throws Exception {
-    EasyMockSupport ems = new EasyMockSupport();
-
-    FilterConfig config = ems.createMock(FilterConfig.class);
+    FilterConfig config = mock(FilterConfig.class);
     HttpServletRequest req = new FakeHttpServletRequest();
     HttpServletResponse res = new FakeHttpServletResponse();
 
-    IMocksControl mockControl = ems.createStrictControl();
-    FilterChain chain = mockControl.createMock(FilterChain.class);
+    FilterChain chain = mock(FilterChain.class);
 
-    Capture<FilterChain> capturedChainA = new Capture<>();
-    Capture<FilterChain> capturedChainB = new Capture<>();
+    ArgumentCaptor<FilterChain> capturedChainA = ArgumentCaptor.forClass(FilterChain.class);
+    ArgumentCaptor<FilterChain> capturedChainB = ArgumentCaptor.forClass(FilterChain.class);
 
-    AllRequestFilter filterA = mockControl.createMock(AllRequestFilter.class);
-    AllRequestFilter filterB = mockControl.createMock(AllRequestFilter.class);
-
-    filterA.init(config);
-    filterB.init(config);
-    filterA.doFilter(eq(req), eq(res), capture(capturedChainA));
-    filterB.doFilter(eq(req), eq(res), capture(capturedChainB));
-    chain.doFilter(req, res);
-    filterA.destroy();
-    filterB.destroy();
-
-    ems.replayAll();
+    AllRequestFilter filterA = mock(AllRequestFilter.class);
+    AllRequestFilter filterB = mock(AllRequestFilter.class);
 
     AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
     addFilter(filterA);
@@ -227,70 +197,69 @@
 
     filterProxy.init(config);
     filterProxy.doFilter(req, res, chain);
-    capturedChainA.getValue().doFilter(req, res);
-    capturedChainB.getValue().doFilter(req, res);
-    filterProxy.destroy();
 
-    ems.verifyAll();
+    InOrder inorder = inOrder(filterA, filterB, chain);
+
+    inorder.verify(filterA).init(config);
+    inorder.verify(filterB).init(config);
+    inorder.verify(filterA).doFilter(eq(req), eq(res), capturedChainA.capture());
+    capturedChainA.getValue().doFilter(req, res);
+    inorder.verify(filterB).doFilter(eq(req), eq(res), capturedChainB.capture());
+    capturedChainB.getValue().doFilter(req, res);
+    inorder.verify(chain).doFilter(req, res);
+
+    filterProxy.destroy();
+    inorder.verify(filterA).destroy();
+    inorder.verify(filterB).destroy();
   }
 
   @Test
   public void postponedLoading() throws Exception {
-    EasyMockSupport ems = new EasyMockSupport();
-
-    FilterConfig config = ems.createMock(FilterConfig.class);
+    FilterConfig config = mock(FilterConfig.class);
     HttpServletRequest req1 = new FakeHttpServletRequest();
     HttpServletRequest req2 = new FakeHttpServletRequest();
     HttpServletResponse res1 = new FakeHttpServletResponse();
     HttpServletResponse res2 = new FakeHttpServletResponse();
 
-    IMocksControl mockControl = ems.createStrictControl();
-    FilterChain chain = mockControl.createMock("chain", FilterChain.class);
+    FilterChain chain = mock(FilterChain.class);
 
-    Capture<FilterChain> capturedChainA1 = new Capture<>();
-    Capture<FilterChain> capturedChainA2 = new Capture<>();
-    Capture<FilterChain> capturedChainB = new Capture<>();
+    ArgumentCaptor<FilterChain> capturedChainA1 = ArgumentCaptor.forClass(FilterChain.class);
+    ArgumentCaptor<FilterChain> capturedChainA2 = ArgumentCaptor.forClass(FilterChain.class);
+    ArgumentCaptor<FilterChain> capturedChainB = ArgumentCaptor.forClass(FilterChain.class);
 
-    AllRequestFilter filterA = mockControl.createMock("filterA", AllRequestFilter.class);
-    AllRequestFilter filterB = mockControl.createMock("filterB", AllRequestFilter.class);
+    AllRequestFilter filterA = mock(AllRequestFilter.class);
+    AllRequestFilter filterB = mock(AllRequestFilter.class);
 
-    filterA.init(config);
-    filterA.doFilter(eq(req1), eq(res1), capture(capturedChainA1));
-    chain.doFilter(req1, res1);
-
-    filterA.doFilter(eq(req2), eq(res2), capture(capturedChainA2));
-    filterB.init(config); // <-- This is crucial part. filterB got loaded
-    // after filterProxy's init finished. Nonetheless filterB gets initialized.
-    filterB.doFilter(eq(req2), eq(res2), capture(capturedChainB));
-    chain.doFilter(req2, res2);
-
-    filterA.destroy();
-    filterB.destroy();
-
-    ems.replayAll();
+    InOrder inorder = inOrder(filterA, filterB, chain);
 
     AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
     addFilter(filterA);
 
     filterProxy.init(config);
     filterProxy.doFilter(req1, res1, chain);
+    inorder.verify(filterA).init(config);
+    inorder.verify(filterA).doFilter(eq(req1), eq(res1), capturedChainA1.capture());
     capturedChainA1.getValue().doFilter(req1, res1);
+    inorder.verify(chain).doFilter(req1, res1);
 
     addFilter(filterB); // <-- Adds filter after filterProxy's init got called.
     filterProxy.doFilter(req2, res2, chain);
+    // after filterProxy's init finished. Nonetheless filterB gets initialized.
+    inorder.verify(filterA).doFilter(eq(req2), eq(res2), capturedChainA2.capture());
     capturedChainA2.getValue().doFilter(req2, res2);
+    inorder.verify(filterB).init(config); // <-- This is crucial part. filterB got loaded
+    inorder.verify(filterB).doFilter(eq(req2), eq(res2), capturedChainB.capture());
     capturedChainB.getValue().doFilter(req2, res2);
+    inorder.verify(chain).doFilter(req2, res2);
 
     filterProxy.destroy();
-
-    ems.verifyAll();
+    inorder.verify(filterA).destroy();
+    inorder.verify(filterB).destroy();
   }
 
   @Test
   public void dynamicUnloading() throws Exception {
-    EasyMockSupport ems = new EasyMockSupport();
-
-    FilterConfig config = ems.createMock(FilterConfig.class);
+    FilterConfig config = mock(FilterConfig.class);
     HttpServletRequest req1 = new FakeHttpServletRequest();
     HttpServletRequest req2 = new FakeHttpServletRequest();
     HttpServletRequest req3 = new FakeHttpServletRequest();
@@ -298,64 +267,62 @@
     HttpServletResponse res2 = new FakeHttpServletResponse();
     HttpServletResponse res3 = new FakeHttpServletResponse();
 
-    Plugin plugin = ems.createMock(Plugin.class);
+    Plugin plugin = mock(Plugin.class);
 
-    IMocksControl mockControl = ems.createStrictControl();
-    FilterChain chain = mockControl.createMock("chain", FilterChain.class);
+    FilterChain chain = mock(FilterChain.class);
 
-    Capture<FilterChain> capturedChainA1 = new Capture<>();
-    Capture<FilterChain> capturedChainB1 = new Capture<>();
-    Capture<FilterChain> capturedChainB2 = new Capture<>();
+    ArgumentCaptor<FilterChain> capturedChainA1 = ArgumentCaptor.forClass(FilterChain.class);
+    ArgumentCaptor<FilterChain> capturedChainB1 = ArgumentCaptor.forClass(FilterChain.class);
+    ArgumentCaptor<FilterChain> capturedChainB2 = ArgumentCaptor.forClass(FilterChain.class);
 
-    AllRequestFilter filterA = mockControl.createMock("filterA", AllRequestFilter.class);
-    AllRequestFilter filterB = mockControl.createMock("filterB", AllRequestFilter.class);
-
-    filterA.init(config);
-    filterB.init(config);
-
-    filterA.doFilter(eq(req1), eq(res1), capture(capturedChainA1));
-    filterB.doFilter(eq(req1), eq(res1), capture(capturedChainB1));
-    chain.doFilter(req1, res1);
-
-    filterA.destroy(); // Cleaning up of filterA after it got unloaded
-
-    filterB.doFilter(eq(req2), eq(res2), capture(capturedChainB2));
-    chain.doFilter(req2, res2);
-
-    filterB.destroy(); // Cleaning up of filterA after it got unloaded
-
-    chain.doFilter(req3, res3);
-
-    ems.replayAll();
+    AllRequestFilter filterA = mock(AllRequestFilter.class);
+    AllRequestFilter filterB = mock(AllRequestFilter.class);
 
     AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
     ReloadableRegistrationHandle<AllRequestFilter> handleFilterA = addFilter(filterA);
     ReloadableRegistrationHandle<AllRequestFilter> handleFilterB = addFilter(filterB);
 
+    InOrder inorder = inOrder(filterA, filterB, chain);
+
     filterProxy.init(config);
 
+    inorder.verify(filterA).init(config);
+    inorder.verify(filterB).init(config);
+
     // Request #1 with filterA and filterB
     filterProxy.doFilter(req1, res1, chain);
+    inorder.verify(filterA).doFilter(eq(req1), eq(res1), capturedChainA1.capture());
     capturedChainA1.getValue().doFilter(req1, res1);
+    inorder.verify(filterB).doFilter(eq(req1), eq(res1), capturedChainB1.capture());
     capturedChainB1.getValue().doFilter(req1, res1);
+    inorder.verify(chain).doFilter(req1, res1);
 
     // Unloading filterA
     handleFilterA.remove();
     filterProxy.onStopPlugin(plugin);
 
-    // Request #1 only with filterB
+    inorder.verify(filterA).destroy(); // Cleaning up of filterA after it got unloaded
+
+    // Request #2 only with filterB
     filterProxy.doFilter(req2, res2, chain);
-    capturedChainA1.getValue().doFilter(req2, res2);
+
+    inorder.verify(filterB).doFilter(eq(req2), eq(res2), capturedChainB2.capture());
+    inorder.verify(filterA, never()).doFilter(eq(req2), eq(res2), any(FilterChain.class));
+    capturedChainB2.getValue().doFilter(req2, res2);
+    inorder.verify(chain).doFilter(req2, res2);
 
     // Unloading filterB
     handleFilterB.remove();
     filterProxy.onStopPlugin(plugin);
 
-    // Request #1 with no additional filters
+    inorder.verify(filterB).destroy(); // Cleaning up of filterA after it got unloaded
+
+    // Request #3 with no additional filters
     filterProxy.doFilter(req3, res3, chain);
+    inorder.verify(chain).doFilter(req3, res3);
+    inorder.verify(filterA, never()).doFilter(eq(req2), eq(res2), any(FilterChain.class));
+    inorder.verify(filterB, never()).doFilter(eq(req2), eq(res2), any(FilterChain.class));
 
     filterProxy.destroy();
-
-    ems.verifyAll();
   }
 }
diff --git a/javatests/com/google/gerrit/httpd/AllowRenderInFrameFilterTest.java b/javatests/com/google/gerrit/httpd/AllowRenderInFrameFilterTest.java
index 0d6d482..2679829 100644
--- a/javatests/com/google/gerrit/httpd/AllowRenderInFrameFilterTest.java
+++ b/javatests/com/google/gerrit/httpd/AllowRenderInFrameFilterTest.java
@@ -17,53 +17,41 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.httpd.AllowRenderInFrameFilter.X_FRAME_OPTIONS_HEADER_NAME;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static org.easymock.EasyMock.expectLastCall;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import com.google.gerrit.httpd.AllowRenderInFrameFilter.XFrameOption;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.IOException;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.easymock.EasyMockSupport;
 import org.eclipse.jgit.lib.Config;
-import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
 
-public class AllowRenderInFrameFilterTest extends GerritBaseTests {
+@RunWith(MockitoJUnitRunner.class)
+public class AllowRenderInFrameFilterTest {
 
-  Config cfg;
-  ServletRequest request;
-  HttpServletResponse response;
-  FilterChain filterChain;
-
-  EasyMockSupport ems = new EasyMockSupport();
-
-  @Before
-  public void setup() throws IOException, ServletException {
-    cfg = new Config();
-    request = ems.createMock(ServletRequest.class);
-    response = ems.createMock(HttpServletResponse.class);
-    filterChain = ems.createMock(FilterChain.class);
-    ems.resetAll();
-    // we want to make sure that doFilter is always called
-    filterChain.doFilter(request, response);
-  }
+  private Config cfg = new Config();
+  @Mock ServletRequest request;
+  @Mock HttpServletResponse response;
+  @Mock FilterChain filterChain;
 
   @Test
   public void shouldDenyInFrameRenderingWhenCanRenderInFrameIsFalse()
       throws IOException, ServletException {
     cfg.setBoolean("gerrit", null, "canLoadInIFrame", false);
 
-    response.addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
-    expectLastCall().times(1);
-    ems.replayAll();
-
     AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
     objectUnderTest.doFilter(request, response, filterChain);
 
-    ems.verifyAll();
+    verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
   }
 
   @Test
@@ -72,14 +60,10 @@
     cfg.setBoolean("gerrit", null, "canLoadInIFrame", false);
     cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.SAMEORIGIN);
 
-    response.addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
-    expectLastCall().times(1);
-    ems.replayAll();
-
     AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
     objectUnderTest.doFilter(request, response, filterChain);
 
-    ems.verifyAll();
+    verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
   }
 
   @Test
@@ -88,14 +72,10 @@
     cfg.setBoolean("gerrit", null, "canLoadInIFrame", false);
     cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.ALLOW);
 
-    response.addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
-    expectLastCall().times(1);
-    ems.replayAll();
-
     AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
     objectUnderTest.doFilter(request, response, filterChain);
 
-    ems.verifyAll();
+    verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
   }
 
   @Test
@@ -103,14 +83,10 @@
       throws IOException, ServletException {
     cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
 
-    response.addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
-    expectLastCall().times(1);
-    ems.replayAll();
-
     AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
     objectUnderTest.doFilter(request, response, filterChain);
 
-    ems.verifyAll();
+    verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
   }
 
   @Test
@@ -118,12 +94,11 @@
       throws IOException, ServletException {
     cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
     cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.ALLOW);
-    ems.replayAll();
 
     AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
     objectUnderTest.doFilter(request, response, filterChain);
 
-    ems.verifyAll();
+    verify(response, never()).addHeader(eq(X_FRAME_OPTIONS_HEADER_NAME), anyString());
   }
 
   @Test
@@ -132,14 +107,10 @@
     cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
     cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.SAMEORIGIN);
 
-    response.addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
-    expectLastCall().times(1);
-    ems.replayAll();
-
     AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
     objectUnderTest.doFilter(request, response, filterChain);
 
-    ems.verifyAll();
+    verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
   }
 
   @Test
@@ -147,14 +118,10 @@
     cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
     cfg.setString("gerrit", null, "xframeOption", "sameOrigin");
 
-    response.addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
-    expectLastCall().times(1);
-    ems.replayAll();
-
     AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
     objectUnderTest.doFilter(request, response, filterChain);
 
-    ems.verifyAll();
+    verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/httpd/BUILD b/javatests/com/google/gerrit/httpd/BUILD
index f2c8ded..75f005e 100644
--- a/javatests/com/google/gerrit/httpd/BUILD
+++ b/javatests/com/google/gerrit/httpd/BUILD
@@ -4,23 +4,24 @@
     name = "httpd_tests",
     srcs = glob(["**/*.java"]),
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//java/com/google/gerrit/util/http",
         "//javatests/com/google/gerrit/util/http/testutil",
         "//lib:gson",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib:jimfs",
         "//lib:junit",
         "//lib:servlet-api-without-neverlink",
         "//lib:soy",
-        "//lib/easymock",
         "//lib/guice",
         "//lib/guice:guice-servlet",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib/mockito",
         "//lib/truth",
+        "//lib/truth:truth-java8-extension",
     ],
 )
diff --git a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
new file mode 100644
index 0000000..2f0fafa
--- /dev/null
+++ b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
@@ -0,0 +1,293 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Optional;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ProjectBasicAuthFilterTest {
+  private static final Base64.Encoder B64_ENC = Base64.getEncoder();
+  private static final Account.Id AUTH_ACCOUNT_ID = Account.id(1000);
+  private static final String AUTH_USER = "johndoe";
+  private static final String AUTH_USER_B64 =
+      B64_ENC.encodeToString(AUTH_USER.getBytes(StandardCharsets.UTF_8));
+  private static final String AUTH_PASSWORD = "jd123";
+  private static final String GERRIT_COOKIE_KEY = "GerritAccount";
+  private static final String AUTH_COOKIE_VALUE = "gerritcookie";
+  private static final ExternalId AUTH_USER_PASSWORD_EXTERNAL_ID =
+      ExternalId.createWithPassword(
+          ExternalId.Key.create(ExternalId.SCHEME_USERNAME, AUTH_USER),
+          AUTH_ACCOUNT_ID,
+          null,
+          AUTH_PASSWORD);
+
+  @Mock private DynamicItem<WebSession> webSessionItem;
+
+  @Mock private AccountCache accountCache;
+
+  @Mock private AccountState accountState;
+
+  @Mock private Account account;
+
+  @Mock private AccountManager accountManager;
+
+  @Mock private AuthConfig authConfig;
+
+  @Mock private FilterChain chain;
+
+  @Captor private ArgumentCaptor<HttpServletResponse> filterResponseCaptor;
+
+  @Mock private IdentifiedUser.RequestFactory userRequestFactory;
+
+  @Mock private WebSessionManager webSessionManager;
+
+  private WebSession webSession;
+  private FakeHttpServletRequest req;
+  private HttpServletResponse res;
+  private AuthResult authSuccessful;
+
+  @Before
+  public void setUp() throws Exception {
+    req = new FakeHttpServletRequest("gerrit.example.com", 80, "", "");
+    res = new FakeHttpServletResponse();
+
+    authSuccessful =
+        new AuthResult(AUTH_ACCOUNT_ID, ExternalId.Key.create("username", AUTH_USER), false);
+    doReturn(Optional.of(accountState)).when(accountCache).getByUsername(AUTH_USER);
+    doReturn(Optional.of(accountState)).when(accountCache).get(AUTH_ACCOUNT_ID);
+    doReturn(account).when(accountState).account();
+    doReturn(true).when(account).isActive();
+    doReturn(authSuccessful).when(accountManager).authenticate(any());
+
+    doReturn(new WebSessionManager.Key(AUTH_COOKIE_VALUE)).when(webSessionManager).createKey(any());
+    WebSessionManager.Val webSessionValue =
+        new WebSessionManager.Val(AUTH_ACCOUNT_ID, 0L, false, null, 0L, "", "");
+    doReturn(webSessionValue)
+        .when(webSessionManager)
+        .createVal(any(), any(), eq(false), any(), any(), any());
+  }
+
+  @Test
+  public void shouldAllowAnonymousRequest() throws Exception {
+    initMockedWebSession();
+    res.setStatus(HttpServletResponse.SC_OK);
+
+    ProjectBasicAuthFilter basicAuthFilter =
+        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+
+    basicAuthFilter.doFilter(req, res, chain);
+
+    verify(chain).doFilter(eq(req), filterResponseCaptor.capture());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+  }
+
+  @Test
+  public void shouldRequestAuthenticationForBasicAuthRequest() throws Exception {
+    initMockedWebSession();
+    req.addHeader("Authorization", "Basic " + AUTH_USER_B64);
+    res.setStatus(HttpServletResponse.SC_OK);
+
+    ProjectBasicAuthFilter basicAuthFilter =
+        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+
+    basicAuthFilter.doFilter(req, res, chain);
+
+    verify(chain, never()).doFilter(any(), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
+    assertThat(res.getHeader("WWW-Authenticate")).contains("Basic realm=");
+  }
+
+  @Test
+  public void shouldAuthenticateSucessfullyAgainstRealmAndReturnCookie() throws Exception {
+    initWebSessionWithoutCookie();
+    requestBasicAuth(req);
+    res.setStatus(HttpServletResponse.SC_OK);
+
+    doReturn(true).when(account).isActive();
+    doReturn(GitBasicAuthPolicy.LDAP).when(authConfig).getGitBasicAuthPolicy();
+
+    ProjectBasicAuthFilter basicAuthFilter =
+        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+
+    basicAuthFilter.doFilter(req, res, chain);
+
+    verify(accountManager).authenticate(any());
+
+    verify(chain).doFilter(eq(req), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+    assertThat(res.getHeader("Set-Cookie")).contains(GERRIT_COOKIE_KEY);
+  }
+
+  @Test
+  public void shouldValidateUserPasswordAndNotReturnCookie() throws Exception {
+    initWebSessionWithoutCookie();
+    requestBasicAuth(req);
+    initMockedUsernamePasswordExternalId();
+    doReturn(GitBasicAuthPolicy.HTTP).when(authConfig).getGitBasicAuthPolicy();
+    res.setStatus(HttpServletResponse.SC_OK);
+
+    ProjectBasicAuthFilter basicAuthFilter =
+        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+
+    basicAuthFilter.doFilter(req, res, chain);
+
+    verify(accountManager, never()).authenticate(any());
+
+    verify(chain).doFilter(eq(req), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+    assertThat(res.getHeader("Set-Cookie")).isNull();
+  }
+
+  @Test
+  public void shouldNotReauthenticateForGitPostRequest() throws Exception {
+    req.setPathInfo("/a/project.git/git-upload-pack");
+    req.setMethod("POST");
+    req.addHeader("Content-Type", "application/x-git-upload-pack-request");
+    doFilterForRequestWhenAlreadySignedIn();
+
+    verify(accountManager, never()).authenticate(any());
+    verify(chain).doFilter(eq(req), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+  }
+
+  @Test
+  public void shouldReauthenticateForRegularRequestEvenIfAlreadySignedIn() throws Exception {
+    doReturn(GitBasicAuthPolicy.LDAP).when(authConfig).getGitBasicAuthPolicy();
+    doFilterForRequestWhenAlreadySignedIn();
+
+    verify(accountManager).authenticate(any());
+    verify(chain).doFilter(eq(req), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+  }
+
+  @Test
+  public void shouldReauthenticateEvenIfHasExistingCookie() throws Exception {
+    initWebSessionWithCookie("GerritAccount=" + AUTH_COOKIE_VALUE);
+    res.setStatus(HttpServletResponse.SC_OK);
+    requestBasicAuth(req);
+    doReturn(GitBasicAuthPolicy.LDAP).when(authConfig).getGitBasicAuthPolicy();
+
+    ProjectBasicAuthFilter basicAuthFilter =
+        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+
+    basicAuthFilter.doFilter(req, res, chain);
+
+    verify(accountManager).authenticate(any());
+    verify(chain).doFilter(eq(req), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+  }
+
+  @Test
+  public void shouldFailedAuthenticationAgainstRealm() throws Exception {
+    initMockedWebSession();
+    requestBasicAuth(req);
+
+    doReturn(true).when(account).isActive();
+    doThrow(new AccountException("Authentication error")).when(accountManager).authenticate(any());
+    doReturn(GitBasicAuthPolicy.LDAP).when(authConfig).getGitBasicAuthPolicy();
+
+    ProjectBasicAuthFilter basicAuthFilter =
+        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+
+    basicAuthFilter.doFilter(req, res, chain);
+
+    verify(accountManager).authenticate(any());
+
+    verify(chain, never()).doFilter(any(), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
+  }
+
+  private void doFilterForRequestWhenAlreadySignedIn()
+      throws IOException, ServletException, AccountException {
+    initMockedWebSession();
+    doReturn(true).when(account).isActive();
+    doReturn(true).when(webSession).isSignedIn();
+    doReturn(authSuccessful).when(accountManager).authenticate(any());
+    requestBasicAuth(req);
+    res.setStatus(HttpServletResponse.SC_OK);
+
+    ProjectBasicAuthFilter basicAuthFilter =
+        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+
+    basicAuthFilter.doFilter(req, res, chain);
+  }
+
+  private void initWebSessionWithCookie(String cookie) {
+    req.addHeader("Cookie", cookie);
+    initWebSessionWithoutCookie();
+  }
+
+  private void initWebSessionWithoutCookie() {
+    webSession =
+        new CacheBasedWebSession(
+            req, res, webSessionManager, authConfig, null, userRequestFactory, accountCache) {};
+    doReturn(webSession).when(webSessionItem).get();
+  }
+
+  private void initMockedWebSession() {
+    webSession = mock(WebSession.class);
+    doReturn(webSession).when(webSessionItem).get();
+  }
+
+  private void initMockedUsernamePasswordExternalId() {
+    doReturn(ImmutableSet.builder().add(AUTH_USER_PASSWORD_EXTERNAL_ID).build())
+        .when(accountState)
+        .externalIds();
+  }
+
+  private void requestBasicAuth(FakeHttpServletRequest fakeReq) {
+    fakeReq.addHeader(
+        "Authorization",
+        "Basic "
+            + B64_ENC.encodeToString(
+                (AUTH_USER + ":" + AUTH_PASSWORD).getBytes(StandardCharsets.UTF_8)));
+  }
+}
diff --git a/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java b/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java
index e19085d..f012ee3 100644
--- a/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java
@@ -17,10 +17,9 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.httpd.RemoteUserUtil.extractUsername;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class RemoteUserUtilTest extends GerritBaseTests {
+public class RemoteUserUtilTest {
   @Test
   public void testExtractUsername() {
     assertThat(extractUsername(null)).isNull();
diff --git a/javatests/com/google/gerrit/httpd/plugins/ContextMapperTest.java b/javatests/com/google/gerrit/httpd/plugins/ContextMapperTest.java
index 2de3788..684a241 100644
--- a/javatests/com/google/gerrit/httpd/plugins/ContextMapperTest.java
+++ b/javatests/com/google/gerrit/httpd/plugins/ContextMapperTest.java
@@ -16,12 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 import javax.servlet.http.HttpServletRequest;
 import org.junit.Test;
 
-public class ContextMapperTest extends GerritBaseTests {
+public class ContextMapperTest {
 
   private static final String CONTEXT = "/context";
   private static final String PLUGIN_NAME = "my-plugin";
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
new file mode 100644
index 0000000..15c66eb
--- /dev/null
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -0,0 +1,77 @@
+// 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.httpd.raw;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.httpd.raw.IndexHtmlUtil.staticTemplateData;
+
+import com.google.template.soy.data.SanitizedContent;
+import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
+import java.util.HashMap;
+import org.junit.Test;
+
+public class IndexHtmlUtilTest {
+
+  @Test
+  public void noPathAndNoCDN() throws Exception {
+    assertThat(
+            staticTemplateData(
+                "http://example.com/", null, null, new HashMap<>(), IndexHtmlUtilTest::ordain))
+        .containsExactly("canonicalPath", "", "staticResourcePath", ordain(""));
+  }
+
+  @Test
+  public void pathAndNoCDN() throws Exception {
+    assertThat(
+            staticTemplateData(
+                "http://example.com/gerrit/",
+                null,
+                null,
+                new HashMap<>(),
+                IndexHtmlUtilTest::ordain))
+        .containsExactly("canonicalPath", "/gerrit", "staticResourcePath", ordain("/gerrit"));
+  }
+
+  @Test
+  public void noPathAndCDN() throws Exception {
+    assertThat(
+            staticTemplateData(
+                "http://example.com/",
+                "http://my-cdn.com/foo/bar/",
+                null,
+                new HashMap<>(),
+                IndexHtmlUtilTest::ordain))
+        .containsExactly(
+            "canonicalPath", "", "staticResourcePath", ordain("http://my-cdn.com/foo/bar/"));
+  }
+
+  @Test
+  public void pathAndCDN() throws Exception {
+    assertThat(
+            staticTemplateData(
+                "http://example.com/gerrit",
+                "http://my-cdn.com/foo/bar/",
+                null,
+                new HashMap<>(),
+                IndexHtmlUtilTest::ordain))
+        .containsExactly(
+            "canonicalPath", "/gerrit", "staticResourcePath", ordain("http://my-cdn.com/foo/bar/"));
+  }
+
+  private static SanitizedContent ordain(String s) {
+    return UnsafeSanitizedContentOrdainer.ordainAsSafe(
+        s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
+  }
+}
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
index 340f690..77ab58b 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
@@ -15,66 +15,52 @@
 package com.google.gerrit.httpd.raw;
 
 import static com.google.common.truth.Truth.assertThat;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
-import com.google.gerrit.testing.GerritBaseTests;
-import java.net.URISyntaxException;
-import java.util.Map;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.accounts.Accounts;
+import com.google.gerrit.extensions.api.config.Config;
+import com.google.gerrit.extensions.api.config.Server;
+import com.google.gerrit.extensions.common.ServerInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
 import org.junit.Test;
 
-public class IndexServletTest extends GerritBaseTests {
-  static class TestIndexServlet extends IndexServlet {
-    private static final long serialVersionUID = 1L;
-
-    TestIndexServlet(String canonicalURL, String cdnPath, String faviconPath)
-        throws URISyntaxException {
-      super(canonicalURL, cdnPath, faviconPath);
-    }
-
-    String getIndexSource() {
-      return new String(indexSource, UTF_8);
-    }
-  }
+public class IndexServletTest {
 
   @Test
-  public void noPathAndNoCDN() throws URISyntaxException {
-    Map<String, Object> data = IndexServlet.getTemplateData("http://example.com/", null, null);
-    assertThat(data.get("canonicalPath")).isEqualTo("");
-    assertThat(data.get("staticResourcePath").toString()).isEqualTo("");
-  }
+  public void renderTemplate() throws Exception {
+    Accounts accountsApi = mock(Accounts.class);
+    when(accountsApi.self()).thenThrow(new AuthException("user needs to be authenticated"));
 
-  @Test
-  public void pathAndNoCDN() throws URISyntaxException {
-    Map<String, Object> data =
-        IndexServlet.getTemplateData("http://example.com/gerrit/", null, null);
-    assertThat(data.get("canonicalPath")).isEqualTo("/gerrit");
-    assertThat(data.get("staticResourcePath").toString()).isEqualTo("/gerrit");
-  }
+    Server serverApi = mock(Server.class);
+    when(serverApi.getVersion()).thenReturn("123");
+    when(serverApi.topMenus()).thenReturn(ImmutableList.of());
+    ServerInfo serverInfo = new ServerInfo();
+    serverInfo.defaultTheme = "my-default-theme";
+    when(serverApi.getInfo()).thenReturn(serverInfo);
 
-  @Test
-  public void noPathAndCDN() throws URISyntaxException {
-    Map<String, Object> data =
-        IndexServlet.getTemplateData("http://example.com/", "http://my-cdn.com/foo/bar/", null);
-    assertThat(data.get("canonicalPath")).isEqualTo("");
-    assertThat(data.get("staticResourcePath").toString()).isEqualTo("http://my-cdn.com/foo/bar/");
-  }
+    Config configApi = mock(Config.class);
+    when(configApi.server()).thenReturn(serverApi);
 
-  @Test
-  public void pathAndCDN() throws URISyntaxException {
-    Map<String, Object> data =
-        IndexServlet.getTemplateData(
-            "http://example.com/gerrit", "http://my-cdn.com/foo/bar/", null);
-    assertThat(data.get("canonicalPath")).isEqualTo("/gerrit");
-    assertThat(data.get("staticResourcePath").toString()).isEqualTo("http://my-cdn.com/foo/bar/");
-  }
+    GerritApi gerritApi = mock(GerritApi.class);
+    when(gerritApi.accounts()).thenReturn(accountsApi);
+    when(gerritApi.config()).thenReturn(configApi);
 
-  @Test
-  public void renderTemplate() throws URISyntaxException {
     String testCanonicalUrl = "foo-url";
     String testCdnPath = "bar-cdn";
     String testFaviconURL = "zaz-url";
-    TestIndexServlet servlet = new TestIndexServlet(testCanonicalUrl, testCdnPath, testFaviconURL);
-    String output = servlet.getIndexSource();
+    IndexServlet servlet =
+        new IndexServlet(testCanonicalUrl, testCdnPath, testFaviconURL, gerritApi);
+
+    FakeHttpServletResponse response = new FakeHttpServletResponse();
+
+    servlet.doGet(new FakeHttpServletRequest(), response);
+
+    String output = response.getActualBodyString();
     assertThat(output).contains("<!DOCTYPE html>");
     assertThat(output).contains("window.CANONICAL_PATH = '" + testCanonicalUrl);
     assertThat(output).contains("<link rel=\"preload\" href=\"" + testCdnPath);
@@ -84,5 +70,12 @@
                 + testCanonicalUrl
                 + "/"
                 + testFaviconURL);
+    assertThat(output)
+        .contains(
+            "window.INITIAL_DATA = JSON.parse("
+                + "'\\x7b\\x22\\/config\\/server\\/version\\x22: \\x22123\\x22, "
+                + "\\x22\\/config\\/server\\/info\\x22: \\x7b\\x22default_theme\\x22:"
+                + "\\x22my-default-theme\\x22\\x7d, \\x22\\/config\\/server\\/top-menus\\x22: "
+                + "\\x5b\\x5d\\x7d');</script>");
   }
 }
diff --git a/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java b/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java
index cfcc1d0..dd594d6 100644
--- a/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.raw;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
@@ -28,7 +29,6 @@
 import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Jimfs;
 import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
 import java.io.ByteArrayInputStream;
@@ -45,7 +45,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class ResourceServletTest extends GerritBaseTests {
+public class ResourceServletTest {
   private static Cache<Path, Resource> newCache(int size) {
     return CacheBuilder.newBuilder().maximumSize(size).recordStats().build();
   }
@@ -336,8 +336,8 @@
   }
 
   private static void assertCacheHits(Cache<?, ?> cache, int hits, int misses) {
-    assertThat(cache.stats().hitCount()).named("hits").isEqualTo(hits);
-    assertThat(cache.stats().missCount()).named("misses").isEqualTo(misses);
+    assertWithMessage("hits").that(cache.stats().hitCount()).isEqualTo(hits);
+    assertWithMessage("misses").that(cache.stats().missCount()).isEqualTo(misses);
   }
 
   private static void assertCacheable(FakeHttpServletResponse res, boolean revalidate) {
diff --git a/javatests/com/google/gerrit/httpd/restapi/HttpLogRedactTest.java b/javatests/com/google/gerrit/httpd/restapi/HttpLogRedactTest.java
index fa3eaea..fb1ebd9 100644
--- a/javatests/com/google/gerrit/httpd/restapi/HttpLogRedactTest.java
+++ b/javatests/com/google/gerrit/httpd/restapi/HttpLogRedactTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class HttpLogRedactTest extends GerritBaseTests {
+public class HttpLogRedactTest {
   @Test
   public void redactAuth() {
     assertThat(LogRedactUtil.redactQueryString("query=status:open")).isEqualTo("query=status:open");
diff --git a/javatests/com/google/gerrit/httpd/restapi/ParameterParserTest.java b/javatests/com/google/gerrit/httpd/restapi/ParameterParserTest.java
index 30d318b..a550ac7 100644
--- a/javatests/com/google/gerrit/httpd/restapi/ParameterParserTest.java
+++ b/javatests/com/google/gerrit/httpd/restapi/ParameterParserTest.java
@@ -15,21 +15,20 @@
 package com.google.gerrit.httpd.restapi;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.httpd.restapi.ParameterParser.QueryParams;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
 import org.junit.Test;
 
-public class ParameterParserTest extends GerritBaseTests {
+public class ParameterParserTest {
   @Test
   public void convertFormToJson() throws BadRequestException {
     JsonObject obj =
@@ -110,35 +109,26 @@
   public void rejectDuplicateMethod() {
     FakeHttpServletRequest req = new FakeHttpServletRequest();
     req.setQueryString("$m=PUT&$m=DELETE");
-    try {
-      ParameterParser.getQueryParams(req);
-      fail("expected BadRequestException");
-    } catch (BadRequestException bad) {
-      assertThat(bad).hasMessageThat().isEqualTo("duplicate $m");
-    }
+    BadRequestException bad =
+        assertThrows(BadRequestException.class, () -> ParameterParser.getQueryParams(req));
+    assertThat(bad).hasMessageThat().isEqualTo("duplicate $m");
   }
 
   @Test
   public void rejectDuplicateContentType() {
     FakeHttpServletRequest req = new FakeHttpServletRequest();
     req.setQueryString("$ct=json&$ct=string");
-    try {
-      ParameterParser.getQueryParams(req);
-      fail("expected BadRequestException");
-    } catch (BadRequestException bad) {
-      assertThat(bad).hasMessageThat().isEqualTo("duplicate $ct");
-    }
+    BadRequestException bad =
+        assertThrows(BadRequestException.class, () -> ParameterParser.getQueryParams(req));
+    assertThat(bad).hasMessageThat().isEqualTo("duplicate $ct");
   }
 
   @Test
   public void rejectInvalidMethod() {
     FakeHttpServletRequest req = new FakeHttpServletRequest();
     req.setQueryString("$m=CONNECT");
-    try {
-      ParameterParser.getQueryParams(req);
-      fail("expected BadRequestException");
-    } catch (BadRequestException bad) {
-      assertThat(bad).hasMessageThat().isEqualTo("invalid $m");
-    }
+    BadRequestException bad =
+        assertThrows(BadRequestException.class, () -> ParameterParser.getQueryParams(req));
+    assertThat(bad).hasMessageThat().isEqualTo("invalid $m");
   }
 }
diff --git a/javatests/com/google/gerrit/index/BUILD b/javatests/com/google/gerrit/index/BUILD
index a1f60de..861e768 100644
--- a/javatests/com/google/gerrit/index/BUILD
+++ b/javatests/com/google/gerrit/index/BUILD
@@ -10,11 +10,12 @@
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/query/testing",
+        "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:junit",
         "//lib/antlr:java-runtime",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/index/SchemaUtilTest.java b/javatests/com/google/gerrit/index/SchemaUtilTest.java
index d6b8421..698e00a 100644
--- a/javatests/com/google/gerrit/index/SchemaUtilTest.java
+++ b/javatests/com/google/gerrit/index/SchemaUtilTest.java
@@ -18,13 +18,13 @@
 import static com.google.gerrit.index.SchemaUtil.getNameParts;
 import static com.google.gerrit.index.SchemaUtil.getPersonParts;
 import static com.google.gerrit.index.SchemaUtil.schema;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Map;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Test;
 
-public class SchemaUtilTest extends GerritBaseTests {
+public class SchemaUtilTest {
   static class TestSchemas {
     static final Schema<String> V1 = schema();
     static final Schema<String> V2 = schema();
@@ -43,9 +43,9 @@
     assertThat(all.get(1)).isEqualTo(TestSchemas.V1);
     assertThat(all.get(2)).isEqualTo(TestSchemas.V2);
     assertThat(all.get(4)).isEqualTo(TestSchemas.V4);
-
-    exception.expect(IllegalArgumentException.class);
-    SchemaUtil.schemasFromClass(TestSchemas.class, Object.class);
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> SchemaUtil.schemasFromClass(TestSchemas.class, Object.class));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/index/query/AndPredicateTest.java b/javatests/com/google/gerrit/index/query/AndPredicateTest.java
index 21098b3..16828dd 100644
--- a/javatests/com/google/gerrit/index/query/AndPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/AndPredicateTest.java
@@ -16,12 +16,12 @@
 
 import static com.google.common.collect.ImmutableList.of;
 import static com.google.gerrit.index.query.Predicate.and;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import java.util.List;
 import org.junit.Test;
@@ -43,28 +43,13 @@
     final TestPredicate b = f("author", "bob");
     final Predicate<String> n = and(a, b);
 
-    try {
-      n.getChildren().clear();
-      fail("Expected UnsupportedOperationException");
-    } catch (UnsupportedOperationException e) {
-      // Expected
-    }
+    assertThrows(UnsupportedOperationException.class, () -> n.getChildren().clear());
     assertChildren("clear", n, of(a, b));
 
-    try {
-      n.getChildren().remove(0);
-      fail("Expected UnsupportedOperationException");
-    } catch (UnsupportedOperationException e) {
-      // Expected
-    }
+    assertThrows(UnsupportedOperationException.class, () -> n.getChildren().remove(0));
     assertChildren("remove(0)", n, of(a, b));
 
-    try {
-      n.getChildren().iterator().remove();
-      fail("Expected UnsupportedOperationException");
-    } catch (UnsupportedOperationException e) {
-      // Expected
-    }
+    assertThrows(UnsupportedOperationException.class, () -> n.getChildren().iterator().remove());
     assertChildren("iterator().remove()", n, of(a, b));
   }
 
diff --git a/javatests/com/google/gerrit/index/query/FieldPredicateTest.java b/javatests/com/google/gerrit/index/query/FieldPredicateTest.java
index 805f31c..2d2c99e 100644
--- a/javatests/com/google/gerrit/index/query/FieldPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/FieldPredicateTest.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.index.query;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
@@ -61,8 +63,9 @@
     assertSame(f, f.copy(Collections.emptyList()));
     assertSame(f, f.copy(f.getChildren()));
 
-    exception.expect(IllegalArgumentException.class);
-    exception.expectMessage("Expected 0 children");
-    f.copy(Collections.singleton(f("owner", "bob")));
+    IllegalArgumentException thrown =
+        assertThrows(
+            IllegalArgumentException.class, () -> f.copy(Collections.singleton(f("owner", "bob"))));
+    assertThat(thrown).hasMessageThat().contains("Expected 0 children");
   }
 }
diff --git a/javatests/com/google/gerrit/index/query/LazyDataSourceTest.java b/javatests/com/google/gerrit/index/query/LazyDataSourceTest.java
new file mode 100644
index 0000000..7064f64
--- /dev/null
+++ b/javatests/com/google/gerrit/index/query/LazyDataSourceTest.java
@@ -0,0 +1,106 @@
+// 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.index.query;
+
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.OrSource;
+import java.util.Collection;
+import java.util.Iterator;
+import org.junit.Test;
+
+/**
+ * Tests that boolean data sources are lazy in that they don't call {@link ResultSet#toList()} or
+ * {@link ResultSet#toList()}. This is necessary because it allows Gerrit to send multiple queries
+ * to the index in parallel, have the results come in asynchronously and wait for them only when we
+ * call aforementioned methods on the {@link ResultSet}.
+ */
+public class LazyDataSourceTest {
+
+  /** Helper to avoid a mock which would be hard to create because of the type inference. */
+  static class LazyPredicate extends Predicate<ChangeData> implements ChangeDataSource {
+    @Override
+    public int getCardinality() {
+      return 1;
+    }
+
+    @Override
+    public ResultSet<ChangeData> read() {
+      return new FailingResultSet<>();
+    }
+
+    @Override
+    public ResultSet<FieldBundle> readRaw() {
+      return new FailingResultSet<>();
+    }
+
+    @Override
+    public Predicate<ChangeData> copy(Collection<? extends Predicate<ChangeData>> children) {
+      throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public int hashCode() {
+      throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public boolean hasChange() {
+      throw new UnsupportedOperationException("not implemented");
+    }
+  }
+
+  /** Implementation that throws {@link AssertionError} when accessing results. */
+  static class FailingResultSet<T> implements ResultSet<T> {
+    @Override
+    public Iterator<T> iterator() {
+      throw new AssertionError(
+          "called iterator() on the result set, but shouldn't have because the data source must be lazy");
+    }
+
+    @Override
+    public ImmutableList<T> toList() {
+      throw new AssertionError(
+          "called toList() on the result set, but shouldn't have because the data source must be lazy");
+    }
+
+    @Override
+    public void close() {
+      // No-op
+    }
+  }
+
+  @Test
+  public void andSourceIsLazy() {
+    AndSource<ChangeData> and = new AndSource<>(ImmutableList.of(new LazyPredicate()));
+    ResultSet<ChangeData> resultSet = and.read();
+    assertThrows(AssertionError.class, () -> resultSet.toList());
+  }
+
+  @Test
+  public void orSourceIsLazy() {
+    OrSource or = new OrSource(ImmutableList.of(new LazyPredicate()));
+    ResultSet<ChangeData> resultSet = or.read();
+    assertThrows(AssertionError.class, () -> resultSet.toList());
+  }
+}
diff --git a/javatests/com/google/gerrit/index/query/NotPredicateTest.java b/javatests/com/google/gerrit/index/query/NotPredicateTest.java
index d10d2df..3d1839d 100644
--- a/javatests/com/google/gerrit/index/query/NotPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/NotPredicateTest.java
@@ -16,12 +16,12 @@
 
 import static com.google.gerrit.index.query.Predicate.and;
 import static com.google.gerrit.index.query.Predicate.not;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import java.util.Collections;
 import java.util.List;
@@ -50,26 +50,14 @@
     final TestPredicate p = f("author", "bob");
     final Predicate<String> n = not(p);
 
-    try {
-      n.getChildren().clear();
-      fail("Expected UnsupportedOperationException");
-    } catch (UnsupportedOperationException e) {
-      assertOnlyChild("clear", p, n);
-    }
+    assertThrows(UnsupportedOperationException.class, () -> n.getChildren().clear());
+    assertOnlyChild("clear", p, n);
 
-    try {
-      n.getChildren().remove(0);
-      fail("Expected UnsupportedOperationException");
-    } catch (UnsupportedOperationException e) {
-      assertOnlyChild("remove(0)", p, n);
-    }
+    assertThrows(UnsupportedOperationException.class, () -> n.getChildren().remove(0));
+    assertOnlyChild("remove(0)", p, n);
 
-    try {
-      n.getChildren().iterator().remove();
-      fail("Expected UnsupportedOperationException");
-    } catch (UnsupportedOperationException e) {
-      assertOnlyChild("remove()", p, n);
-    }
+    assertThrows(UnsupportedOperationException.class, () -> n.getChildren().iterator().remove());
+    assertOnlyChild("remove()", p, n);
   }
 
   private static void assertOnlyChild(String o, Predicate<String> c, Predicate<String> p) {
@@ -112,18 +100,11 @@
     assertNotSame(n, n.copy(sb));
     assertEquals(sb, n.copy(sb).getChildren());
 
-    try {
-      n.copy(Collections.emptyList());
-      fail("Expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      assertEquals("Expected exactly one child", e.getMessage());
-    }
+    IllegalArgumentException e =
+        assertThrows(IllegalArgumentException.class, () -> n.copy(Collections.emptyList()));
+    assertEquals("Expected exactly one child", e.getMessage());
 
-    try {
-      n.copy(and(a, b).getChildren());
-      fail("Expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      assertEquals("Expected exactly one child", e.getMessage());
-    }
+    e = assertThrows(IllegalArgumentException.class, () -> n.copy(and(a, b).getChildren()));
+    assertEquals("Expected exactly one child", e.getMessage());
   }
 }
diff --git a/javatests/com/google/gerrit/index/query/OrPredicateTest.java b/javatests/com/google/gerrit/index/query/OrPredicateTest.java
index 255a3f8..1cbcb75 100644
--- a/javatests/com/google/gerrit/index/query/OrPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/OrPredicateTest.java
@@ -16,12 +16,12 @@
 
 import static com.google.common.collect.ImmutableList.of;
 import static com.google.gerrit.index.query.Predicate.or;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import java.util.List;
 import org.junit.Test;
@@ -43,28 +43,13 @@
     final TestPredicate b = f("author", "bob");
     final Predicate<String> n = or(a, b);
 
-    try {
-      n.getChildren().clear();
-      fail("Expected UnsupportedOperationException");
-    } catch (UnsupportedOperationException e) {
-      // Expected
-    }
+    assertThrows(UnsupportedOperationException.class, () -> n.getChildren().clear());
     assertChildren("clear", n, of(a, b));
 
-    try {
-      n.getChildren().remove(0);
-      fail("Expected UnsupportedOperationException");
-    } catch (UnsupportedOperationException e) {
-      // Expected
-    }
+    assertThrows(UnsupportedOperationException.class, () -> n.getChildren().remove(0));
     assertChildren("remove(0)", n, of(a, b));
 
-    try {
-      n.getChildren().iterator().remove();
-      fail("Expected UnsupportedOperationException");
-    } catch (UnsupportedOperationException e) {
-      // Expected
-    }
+    assertThrows(UnsupportedOperationException.class, () -> n.getChildren().iterator().remove());
     assertChildren("iterator().remove()", n, of(a, b));
   }
 
diff --git a/javatests/com/google/gerrit/index/query/PredicateTest.java b/javatests/com/google/gerrit/index/query/PredicateTest.java
index 2295a60..3ec7f13 100644
--- a/javatests/com/google/gerrit/index/query/PredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/PredicateTest.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.index.query;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Ignore;
 
 @Ignore
-public abstract class PredicateTest extends GerritBaseTests {
+public abstract class PredicateTest {
   protected static final class TestPredicate extends OperatorPredicate<String> {
     protected TestPredicate(String name, String value) {
       super(name, value);
diff --git a/javatests/com/google/gerrit/index/query/QueryBuilderTest.java b/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
index 6a397dc..f653759 100644
--- a/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
+++ b/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
@@ -17,12 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.truth.ThrowableSubject;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Collection;
 import java.util.Objects;
 import org.junit.Test;
 
-public class QueryBuilderTest extends GerritBaseTests {
+public class QueryBuilderTest {
   private static class TestPredicate extends Predicate<Object> {
     private final String field;
     private final String value;
diff --git a/javatests/com/google/gerrit/index/query/QueryParserTest.java b/javatests/com/google/gerrit/index/query/QueryParserTest.java
index b4dd1ee..776a2c4 100644
--- a/javatests/com/google/gerrit/index/query/QueryParserTest.java
+++ b/javatests/com/google/gerrit/index/query/QueryParserTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.index.query;
 
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.index.query.QueryParser.AND;
 import static com.google.gerrit.index.query.QueryParser.COLON;
 import static com.google.gerrit.index.query.QueryParser.DEFAULT_FIELD;
@@ -22,12 +21,12 @@
 import static com.google.gerrit.index.query.QueryParser.SINGLE_WORD;
 import static com.google.gerrit.index.query.QueryParser.parse;
 import static com.google.gerrit.index.query.testing.TreeSubject.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.antlr.runtime.tree.Tree;
 import org.junit.Test;
 
-public class QueryParserTest extends GerritBaseTests {
+public class QueryParserTest {
   @Test
   public void fieldNameAndValue() throws Exception {
     Tree r = parse("project:tools/gerrit");
@@ -206,11 +205,6 @@
   }
 
   private static void assertParseFails(String query) {
-    try {
-      parse(query);
-      assert_().fail("expected parse to fail: %s", query);
-    } catch (QueryParseException e) {
-      // Expected.
-    }
+    assertThrows(QueryParseException.class, () -> parse(query));
   }
 }
diff --git a/javatests/com/google/gerrit/integration/git/BUILD b/javatests/com/google/gerrit/integration/git/BUILD
index 4f1be09..28755af 100644
--- a/javatests/com/google/gerrit/integration/git/BUILD
+++ b/javatests/com/google/gerrit/integration/git/BUILD
@@ -1,6 +1,12 @@
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 acceptance_tests(
+    srcs = ["GitProtocolV2IT.java"],
+    group = "protocol-v2",
+    labels = ["git-protocol-v2"],
+)
+
+acceptance_tests(
     srcs = ["UploadArchiveIT.java"],
     group = "upload-archive",
     labels = ["git-upload-archive"],
diff --git a/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java b/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java
new file mode 100644
index 0000000..8577c16
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java
@@ -0,0 +1,382 @@
+// 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.integration.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.GitClientVersion;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.inject.Inject;
+import java.io.File;
+import java.net.InetSocketAddress;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+@UseSsh
+public class GitProtocolV2IT extends StandaloneSiteTest {
+  private static final String ADMIN_PASSWORD = "secret";
+  private final String[] SSH_KEYGEN_CMD =
+      new String[] {"ssh-keygen", "-t", "rsa", "-q", "-P", "", "-f"};
+  private final String[] GIT_LS_REMOTE =
+      new String[] {"git", "-c", "protocol.version=2", "ls-remote", "-o", "trace=12345"};
+  private final String[] GIT_CLONE_MIRROR =
+      new String[] {"git", "-c", "protocol.version=2", "clone", "--mirror"};
+  private final String[] GIT_FETCH = new String[] {"git", "-c", "protocol.version=2", "fetch"};
+  private final String[] GIT_INIT = new String[] {"git", "init"};
+  private final String GIT_SSH_COMMAND =
+      "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i";
+
+  @Inject private GerritApi gApi;
+  @Inject private AccountCreator accountCreator;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private @TestSshServerAddress InetSocketAddress sshAddress;
+  @Inject private @GerritServerConfig Config config;
+  @Inject private AllProjectsName allProjectsName;
+
+  @BeforeClass
+  public static void assertGitClientVersion() throws Exception {
+    // Minimum required git-core version that supports wire protocol v2 is 2.18.0
+    GitClientVersion requiredGitVersion = new GitClientVersion(2, 18, 0);
+    GitClientVersion actualGitVersion =
+        new GitClientVersion(execute(ImmutableList.of("git", "version"), new File("/")));
+    // If git client version cannot be updated, consider to skip this tests. Due to
+    // an existing issue in bazel, JUnit assumption violation feature cannot be used.
+    assertThat(actualGitVersion).isAtLeast(requiredGitVersion);
+  }
+
+  @Test
+  public void testGitWireProtocolV2WithSsh() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+
+      // Create project
+      Project.NameKey project = Project.nameKey("foo");
+      gApi.projects().create(project.get());
+
+      // Set up project permission
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .add(deny(Permission.READ).ref("refs/*").group(SystemGroupBackend.ANONYMOUS_USERS))
+          .add(
+              allow(Permission.READ)
+                  .ref("refs/heads/master")
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
+
+      // Set protocol.version=2 in target repository
+      execute(
+          ImmutableList.of("git", "config", "protocol.version", "2"),
+          sitePaths.site_path.resolve("git").resolve(project.get() + Constants.DOT_GIT).toFile());
+
+      // Retrieve HTTP url
+      String url = config.getString("gerrit", null, "canonicalweburl");
+      String urlDestinationTemplate =
+          url.substring(0, 7)
+              + "%s:secret@"
+              + url.substring(7, url.length())
+              + "/a/"
+              + project.get();
+
+      // Retrieve SSH host and port
+      String sshDestinationTemplate =
+          "ssh://%s@" + sshAddress.getHostName() + ":" + sshAddress.getPort() + "/" + project.get();
+
+      // Admin user was already created by the base class
+      setUpUserAuthentication(admin.username());
+
+      // Create non-admin user
+      TestAccount user = accountCreator.user();
+      setUpUserAuthentication(user.username());
+
+      // Prepare data for new change on master branch
+      ChangeInput in = new ChangeInput(project.get(), "master", "Test public change");
+      in.newBranch = true;
+
+      // Create new change and retrieve SHA1 for the created patch set
+      String commit =
+          gApi.changes()
+              .id(gApi.changes().create(in).info().changeId)
+              .current()
+              .commit(false)
+              .commit;
+
+      // Prepare new change on secret branch
+      in = new ChangeInput(project.get(), ADMIN_PASSWORD, "Test secret change");
+      in.newBranch = true;
+
+      // Create new change and retrieve SHA1 for the created patch set
+      String secretCommit =
+          gApi.changes()
+              .id(gApi.changes().create(in).info().changeId)
+              .current()
+              .commit(false)
+              .commit;
+
+      // Read refs from target repository using git wire protocol v2 over HTTP for admin user
+      String out =
+          execute(
+              ImmutableList.<String>builder()
+                  .add(GIT_LS_REMOTE)
+                  .add(String.format(urlDestinationTemplate, admin.username()))
+                  .build(),
+              ImmutableMap.of("GIT_TRACE_PACKET", "1"));
+
+      assertGitProtocolV2Refs(commit, out);
+      assertThat(out).contains(secretCommit);
+
+      // Read refs from target repository using git wire protocol v2 over SSH for admin user
+      out =
+          execute(
+              ImmutableList.<String>builder()
+                  .add(GIT_LS_REMOTE)
+                  .add(String.format(sshDestinationTemplate, admin.username()))
+                  .build(),
+              ImmutableMap.of(
+                  "GIT_SSH_COMMAND",
+                  GIT_SSH_COMMAND
+                      + sitePaths.data_dir.resolve(String.format("id_rsa_%s", admin.username())),
+                  "GIT_TRACE_PACKET",
+                  "1"));
+
+      assertGitProtocolV2Refs(commit, out);
+      assertThat(out).contains(secretCommit);
+
+      // Read refs from target repository using git wire protocol v2 over HTTP for non-admin user
+      out =
+          execute(
+              ImmutableList.<String>builder()
+                  .add(GIT_LS_REMOTE)
+                  .add(String.format(urlDestinationTemplate, user.username()))
+                  .build(),
+              ImmutableMap.of("GIT_TRACE_PACKET", "1"));
+
+      assertGitProtocolV2Refs(commit, out);
+      assertThat(out).doesNotContain(secretCommit);
+
+      // Read refs from target repository using git wire protocol v2 over SSH for non-admin user
+      out =
+          execute(
+              ImmutableList.<String>builder()
+                  .add(GIT_LS_REMOTE)
+                  .add(String.format(sshDestinationTemplate, user.username()))
+                  .build(),
+              ImmutableMap.of(
+                  "GIT_SSH_COMMAND",
+                  GIT_SSH_COMMAND
+                      + sitePaths.data_dir.resolve(String.format("id_rsa_%s", user.username())),
+                  "GIT_TRACE_PACKET",
+                  "1"));
+
+      assertGitProtocolV2Refs(commit, out);
+      assertThat(out).doesNotContain(secretCommit);
+    }
+  }
+
+  @Test
+  public void testGitWireProtocolV2HidesRefMetaConfig() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+      String url = config.getString("gerrit", null, "canonicalweburl");
+
+      // Create project
+      Project.NameKey allRefsVisibleProject = Project.nameKey("all-refs-visible");
+      gApi.projects().create(allRefsVisibleProject.get());
+
+      // Set protocol.version=2 in target repository
+      execute(
+          ImmutableList.of("git", "config", "protocol.version", "2"),
+          sitePaths
+              .site_path
+              .resolve("git")
+              .resolve(allRefsVisibleProject.get() + Constants.DOT_GIT)
+              .toFile());
+
+      // Set up project permission to allow reading all refs
+      projectOperations
+          .project(allRefsVisibleProject)
+          .forUpdate()
+          .add(allow(Permission.READ).ref("refs/heads/*").group(SystemGroupBackend.ANONYMOUS_USERS))
+          .add(
+              allow(Permission.READ)
+                  .ref("refs/changes/*")
+                  .group(SystemGroupBackend.ANONYMOUS_USERS))
+          .update();
+
+      // Create new change and retrieve refs for the created patch set
+      ChangeInput visibleChangeIn =
+          new ChangeInput(allRefsVisibleProject.get(), "master", "Test public change");
+      visibleChangeIn.newBranch = true;
+      int visibleChangeNumber = gApi.changes().create(visibleChangeIn).info()._number;
+      Change.Id changeId = Change.id(visibleChangeNumber);
+      String visibleChangeNumberRef = RefNames.patchSetRef(PatchSet.id(changeId, 1));
+      String visibleChangeNumberMetaRef = RefNames.changeMetaRef(changeId);
+
+      // Read refs from target repository using git wire protocol v2 over HTTP anonymously
+      String outAnonymousLsRemote =
+          execute(
+              ImmutableList.<String>builder()
+                  .add(GIT_CLONE_MIRROR)
+                  .add(url + "/" + allRefsVisibleProject.get())
+                  .build(),
+              ImmutableMap.of("GIT_TRACE_PACKET", "1"));
+
+      assertThat(outAnonymousLsRemote).contains("git< version 2");
+      assertThat(outAnonymousLsRemote).doesNotContain(RefNames.REFS_CONFIG);
+      assertThat(outAnonymousLsRemote).contains(visibleChangeNumberRef);
+      assertThat(outAnonymousLsRemote).contains(visibleChangeNumberMetaRef);
+    }
+  }
+
+  @Test
+  public void testGitWireProtocolV2FetchIndividualRef() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+
+      // Setup admin password
+      gApi.accounts().id(admin.username()).setHttpPassword(ADMIN_PASSWORD);
+
+      // Get authenticated Git/HTTP URL
+      String urlWithCredentials =
+          config
+              .getString("gerrit", null, "canonicalweburl")
+              .replace("http://", "http://" + admin.username() + ":" + ADMIN_PASSWORD + "@");
+
+      // Create project
+      Project.NameKey privateProject = Project.nameKey("private-project");
+      gApi.projects().create(privateProject.get());
+
+      // Set protocol.version=2 in target repository
+      execute(
+          ImmutableList.of("git", "config", "protocol.version", "2"),
+          sitePaths
+              .site_path
+              .resolve("git")
+              .resolve(privateProject.get() + Constants.DOT_GIT)
+              .toFile());
+
+      // Disallow general read permissions for anonymous users
+      projectOperations
+          .project(allProjectsName)
+          .forUpdate()
+          .add(deny(Permission.READ).ref("refs/*").group(SystemGroupBackend.ANONYMOUS_USERS))
+          .add(
+              allow(Permission.READ)
+                  .ref("refs/heads/master")
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
+
+      // Set up project permission to allow registered users fetching changes/*
+      projectOperations
+          .project(privateProject)
+          .forUpdate()
+          .add(
+              allow(Permission.READ)
+                  .ref("refs/changes/*")
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
+
+      // Create new change and retrieve refs for the created patch set
+      ChangeInput visibleChangeIn =
+          new ChangeInput(privateProject.get(), "master", "Test private change");
+      visibleChangeIn.newBranch = true;
+      int visibleChangeNumber = gApi.changes().create(visibleChangeIn).info()._number;
+      Change.Id changeId = Change.id(visibleChangeNumber);
+      String visibleChangeNumberRef = RefNames.patchSetRef(PatchSet.id(changeId, 1));
+
+      // Fetch a single ref using git wire protocol v2 over HTTP with authentication
+      execute(GIT_INIT);
+
+      String outFetchRef =
+          execute(
+              ImmutableList.<String>builder()
+                  .add(GIT_FETCH)
+                  .add(urlWithCredentials + "/" + privateProject.get())
+                  .add(visibleChangeNumberRef)
+                  .build(),
+              ImmutableMap.of("GIT_TRACE_PACKET", "1"));
+
+      assertThat(outFetchRef).contains("git< version 2");
+      assertThat(outFetchRef).contains(visibleChangeNumberRef);
+    }
+  }
+
+  private void setUpUserAuthentication(String username) throws Exception {
+    // Assign HTTP password to user
+    gApi.accounts().id(username).setHttpPassword(ADMIN_PASSWORD);
+
+    // Generate private/public key for user
+    execute(
+        ImmutableList.<String>builder()
+            .add(SSH_KEYGEN_CMD)
+            .add(String.format("id_rsa_%s", username))
+            .build());
+
+    // Read the content of generated public key and add it for the user in Gerrit
+    gApi.accounts()
+        .id(username)
+        .addSshKey(
+            new String(
+                java.nio.file.Files.readAllBytes(
+                    sitePaths.data_dir.resolve(String.format("id_rsa_%s.pub", username))),
+                UTF_8));
+  }
+
+  private static void assertGitProtocolV2Refs(String commit, String out) {
+    assertThat(out).contains("git< version 2");
+    assertThat(out).contains("refs/changes/01/1/1");
+    assertThat(out).contains("refs/changes/01/1/meta");
+    assertThat(out).contains(commit);
+  }
+
+  private String execute(String... cmds) throws Exception {
+    return execute(ImmutableList.<String>builder().add(cmds).build());
+  }
+
+  private String execute(ImmutableList<String> cmd) throws Exception {
+    return execute(cmd, sitePaths.data_dir.toFile(), ImmutableMap.of());
+  }
+
+  private String execute(ImmutableList<String> cmd, ImmutableMap<String, String> env)
+      throws Exception {
+    return execute(cmd, sitePaths.data_dir.toFile(), env);
+  }
+
+  private static String execute(ImmutableList<String> cmd, File dir) throws Exception {
+    return execute(cmd, dir, ImmutableMap.of());
+  }
+}
diff --git a/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java b/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java
index 18a78b0..2968111 100644
--- a/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java
+++ b/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java
@@ -27,11 +27,11 @@
 import com.google.gerrit.acceptance.StandaloneSiteTest;
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import java.io.BufferedInputStream;
 import java.io.IOException;
@@ -133,7 +133,7 @@
 
   private void setUpTestHarness(ServerContext ctx) throws RestApiException, Exception {
     ctx.getInjector().injectMembers(this);
-    project = new Project.NameKey("upload-archive-project-test");
+    project = Project.nameKey("upload-archive-project-test");
     gApi.projects().create(project.get());
     setUpAuthentication();
     sshDestination =
diff --git a/javatests/com/google/gerrit/integration/ssh/BUILD b/javatests/com/google/gerrit/integration/ssh/BUILD
new file mode 100644
index 0000000..72f0785
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/ssh/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = ["NoShellIT.java"],
+    group = "no-shell",
+    labels = ["ssh-no-shell"],
+)
diff --git a/javatests/com/google/gerrit/integration/ssh/NoShellIT.java b/javatests/com/google/gerrit/integration/ssh/NoShellIT.java
new file mode 100644
index 0000000..2bbbf1a
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/ssh/NoShellIT.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.integration.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import org.junit.Test;
+
+@NoHttpd
+@UseSsh
+public class NoShellIT extends StandaloneSiteTest {
+  private static final String[] SSH_KEYGEN_CMD =
+      new String[] {"ssh-keygen", "-t", "rsa", "-q", "-P", "", "-f"};
+
+  @Inject private GerritApi gApi;
+  @Inject private @TestSshServerAddress InetSocketAddress sshAddress;
+
+  private String identityPath;
+
+  @Test(timeout = 60000)
+  public void verifyCommandsIsClosed() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      setUpTestHarness(ctx);
+
+      IOException thrown = assertThrows(IOException.class, () -> execute(cmd()));
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains("Hi Administrator, you have successfully connected over SSH.");
+    }
+  }
+
+  private void setUpTestHarness(ServerContext ctx) throws Exception {
+    ctx.getInjector().injectMembers(this);
+    setUpAuthentication();
+    identityPath = sitePaths.data_dir.resolve(String.format("id_rsa_%s", "admin")).toString();
+  }
+
+  private void setUpAuthentication() throws Exception {
+    execute(
+        ImmutableList.<String>builder()
+            .add(SSH_KEYGEN_CMD)
+            .add(String.format("id_rsa_%s", "admin"))
+            .build());
+    gApi.accounts()
+        .id("admin")
+        .addSshKey(
+            new String(
+                java.nio.file.Files.readAllBytes(
+                    sitePaths.data_dir.resolve(String.format("id_rsa_%s.pub", "admin"))),
+                UTF_8));
+  }
+
+  private ImmutableList<String> cmd() {
+    return ImmutableList.<String>builder()
+        .add("ssh")
+        .add("-tt")
+        .add("-o")
+        .add("StrictHostKeyChecking=no")
+        .add("-o")
+        .add("UserKnownHostsFile=/dev/null")
+        .add("-p")
+        .add(String.valueOf(sshAddress.getPort()))
+        .add("admin@" + sshAddress.getHostName())
+        .add("-i")
+        .add(identityPath)
+        .build();
+  }
+
+  private String execute(ImmutableList<String> cmd) throws Exception {
+    return execute(cmd, sitePaths.data_dir.toFile(), ImmutableMap.of());
+  }
+}
diff --git a/javatests/com/google/gerrit/json/BUILD b/javatests/com/google/gerrit/json/BUILD
index 4894cdb..575f575 100644
--- a/javatests/com/google/gerrit/json/BUILD
+++ b/javatests/com/google/gerrit/json/BUILD
@@ -5,6 +5,9 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/json",
+        "//java/com/google/gerrit/server/util/time",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:gson",
         "//lib:guava",
         "//lib/truth",
     ],
diff --git a/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java b/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java
index a8488a9..05a9cfb 100644
--- a/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java
+++ b/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.json;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.json.JavaSqlTimestampHelper.parseTimestamp;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import java.text.SimpleDateFormat;
 import java.util.TimeZone;
@@ -73,12 +73,7 @@
   }
 
   private static void assertInvalid(String input) {
-    try {
-      parseTimestamp(input);
-      assert_().fail("Expected IllegalArgumentException for: " + input);
-    } catch (IllegalArgumentException e) {
-      // Expected;
-    }
+    assertThrows(IllegalArgumentException.class, () -> parseTimestamp(input));
   }
 
   private String reformat(String input) {
diff --git a/javatests/com/google/gerrit/json/JsonEnumMappingTest.java b/javatests/com/google/gerrit/json/JsonEnumMappingTest.java
new file mode 100644
index 0000000..dd710f9
--- /dev/null
+++ b/javatests/com/google/gerrit/json/JsonEnumMappingTest.java
@@ -0,0 +1,80 @@
+// 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.json;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import org.junit.Test;
+
+public class JsonEnumMappingTest {
+
+  // Use the regular, pre-configured Gson object we use throughout the Gerrit server to ensure that
+  // the EnumTypeAdapterFactory is properly set up.
+  private final Gson gson = OutputFormat.JSON.newGson();
+
+  @Test
+  public void nullCanBeWrittenAndParsedBack() {
+    String resultingJson = gson.toJson(null, TestEnum.class);
+    TestEnum value = gson.fromJson(resultingJson, TestEnum.class);
+    assertThat(value).isNull();
+  }
+
+  @Test
+  public void enumValueCanBeWrittenAndParsedBack() {
+    String resultingJson = gson.toJson(TestEnum.ONE, TestEnum.class);
+    TestEnum value = gson.fromJson(resultingJson, TestEnum.class);
+    assertThat(value).isEqualTo(TestEnum.ONE);
+  }
+
+  @Test
+  public void enumValueCanBeParsed() {
+    TestData data = gson.fromJson("{\"value\":\"ONE\"}", TestData.class);
+    assertThat(data.value).isEqualTo(TestEnum.ONE);
+  }
+
+  @Test
+  public void mixedCaseEnumValueIsTreatedAsUnset() {
+    TestData data = gson.fromJson("{\"value\":\"oNe\"}", TestData.class);
+    assertThat(data.value).isNull();
+  }
+
+  @Test
+  public void lowerCaseEnumValueIsTreatedAsUnset() {
+    TestData data = gson.fromJson("{\"value\":\"one\"}", TestData.class);
+    assertThat(data.value).isNull();
+  }
+
+  @Test
+  public void notExistingEnumValueIsTreatedAsUnset() {
+    TestData data = gson.fromJson("{\"value\":\"FOUR\"}", TestData.class);
+    assertThat(data.value).isNull();
+  }
+
+  @Test
+  public void emptyEnumValueIsTreatedAsUnset() {
+    TestData data = gson.fromJson("{\"value\":\"\"}", TestData.class);
+    assertThat(data.value).isNull();
+  }
+
+  private static class TestData {
+    TestEnum value;
+  }
+
+  private enum TestEnum {
+    ONE,
+    TWO
+  }
+}
diff --git a/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java b/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java
new file mode 100644
index 0000000..2699c3b
--- /dev/null
+++ b/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java
@@ -0,0 +1,33 @@
+// 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.json;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gson.JsonPrimitive;
+import java.sql.Timestamp;
+import org.junit.Test;
+
+public class SqlTimestampDeserializerTest {
+
+  private final SqlTimestampDeserializer deserializer = new SqlTimestampDeserializer();
+
+  @Test
+  public void emptyStringIsDeserializedToMagicTimestamp() {
+    Timestamp timestamp = deserializer.deserialize(new JsonPrimitive(""), Timestamp.class, null);
+    assertThat(timestamp).isEqualTo(TimeUtil.never());
+  }
+}
diff --git a/javatests/com/google/gerrit/mail/AbstractParserTest.java b/javatests/com/google/gerrit/mail/AbstractParserTest.java
index bcff6a7..3c84420 100644
--- a/javatests/com/google/gerrit/mail/AbstractParserTest.java
+++ b/javatests/com/google/gerrit/mail/AbstractParserTest.java
@@ -16,9 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Comment;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.ArrayList;
@@ -26,7 +25,7 @@
 import org.junit.Ignore;
 
 @Ignore
-public class AbstractParserTest extends GerritBaseTests {
+public class AbstractParserTest {
   protected static final String CHANGE_URL =
       "https://gerrit-review.googlesource.com/c/project/+/123";
 
@@ -56,7 +55,7 @@
     Comment c =
         new Comment(
             new Comment.Key(uuid, file, 1),
-            new Account.Id(0),
+            Account.id(0),
             new Timestamp(0L),
             (short) 0,
             message,
@@ -70,7 +69,7 @@
     Comment c =
         new Comment(
             new Comment.Key(uuid, file, 1),
-            new Account.Id(0),
+            Account.id(0),
             new Timestamp(0L),
             (short) 0,
             message,
diff --git a/javatests/com/google/gerrit/mail/AddressTest.java b/javatests/com/google/gerrit/mail/AddressTest.java
index 53ff1fe..da26123 100644
--- a/javatests/com/google/gerrit/mail/AddressTest.java
+++ b/javatests/com/google/gerrit/mail/AddressTest.java
@@ -15,12 +15,11 @@
 package com.google.gerrit.mail;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class AddressTest extends GerritBaseTests {
+public class AddressTest {
   @Test
   public void parse_NameEmail1() {
     final Address a = Address.parse("A U Thor <author@example.com>");
@@ -97,12 +96,9 @@
   }
 
   private void assertInvalid(String in) {
-    try {
-      Address.parse(in);
-      fail("Expected IllegalArgumentException for " + in);
-    } catch (IllegalArgumentException e) {
-      assertThat(e.getMessage()).isEqualTo("Invalid email address: " + in);
-    }
+    IllegalArgumentException thrown =
+        assertThrows(IllegalArgumentException.class, () -> Address.parse(in));
+    assertThat(thrown).hasMessageThat().isEqualTo("Invalid email address: " + in);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/mail/BUILD b/javatests/com/google/gerrit/mail/BUILD
index b1c9712..bd2c478 100644
--- a/javatests/com/google/gerrit/mail/BUILD
+++ b/javatests/com/google/gerrit/mail/BUILD
@@ -8,15 +8,15 @@
     ),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/mail",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:gson",
         "//lib:guava-retrying",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib/commons:codec",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
     ],
diff --git a/javatests/com/google/gerrit/mail/HtmlParserTest.java b/javatests/com/google/gerrit/mail/HtmlParserTest.java
index d630bd6..345cb05 100644
--- a/javatests/com/google/gerrit/mail/HtmlParserTest.java
+++ b/javatests/com/google/gerrit/mail/HtmlParserTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.entities.Comment;
 import java.util.List;
 import org.junit.Ignore;
 import org.junit.Test;
diff --git a/javatests/com/google/gerrit/mail/MailHeaderParserTest.java b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
index cdc8d7a..2d2c2ea 100644
--- a/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
+++ b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
@@ -16,14 +16,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
 import org.junit.Test;
 
-public class MailHeaderParserTest extends GerritBaseTests {
+public class MailHeaderParserTest {
   @Test
   public void parseMetadataFromHeader() {
     // This tests if the metadata parser is able to parse metadata from the
diff --git a/javatests/com/google/gerrit/mail/ParserUtilTest.java b/javatests/com/google/gerrit/mail/ParserUtilTest.java
index ed40a57..47a5367 100644
--- a/javatests/com/google/gerrit/mail/ParserUtilTest.java
+++ b/javatests/com/google/gerrit/mail/ParserUtilTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class ParserUtilTest extends GerritBaseTests {
+public class ParserUtilTest {
   @Test
   public void trimQuotationLineOnMessageWithoutQuoatationLine() throws Exception {
     assertThat(ParserUtil.trimQuotation("One line")).isEqualTo("One line");
diff --git a/javatests/com/google/gerrit/mail/RawMailParserTest.java b/javatests/com/google/gerrit/mail/RawMailParserTest.java
index 9049704..0ab2811 100644
--- a/javatests/com/google/gerrit/mail/RawMailParserTest.java
+++ b/javatests/com/google/gerrit/mail/RawMailParserTest.java
@@ -23,10 +23,9 @@
 import com.google.gerrit.mail.data.QuotedPrintableHeaderMessage;
 import com.google.gerrit.mail.data.RawMailMessage;
 import com.google.gerrit.mail.data.SimpleTextMessage;
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class RawMailParserTest extends GerritBaseTests {
+public class RawMailParserTest {
   @Test
   public void parseEmail() throws Exception {
     RawMailMessage[] messages =
diff --git a/javatests/com/google/gerrit/mail/TextParserTest.java b/javatests/com/google/gerrit/mail/TextParserTest.java
index a5c2152..00d5b41 100644
--- a/javatests/com/google/gerrit/mail/TextParserTest.java
+++ b/javatests/com/google/gerrit/mail/TextParserTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.entities.Comment;
 import java.util.List;
 import org.junit.Test;
 
diff --git a/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java b/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
index a8f5b94..c4737e6 100644
--- a/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
+++ b/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
@@ -39,7 +39,7 @@
           + "when I try to load this change:\n"
           + "\n"
           + "  Error in GET /changes/90018/detail?O=10004\n"
-          + "  com.google.gwtorm.OrmException: java.lang.NullPointerException\n"
+          + "  com.google.gerrit.exceptions.StorageException: java.lang.NullPointerException\n"
           + "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:303)\n"
           + "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:285)\n"
           + "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:263)\n"
diff --git a/javatests/com/google/gerrit/metrics/dropwizard/BUILD b/javatests/com/google/gerrit/metrics/dropwizard/BUILD
index 63d4452..98d12b2 100644
--- a/javatests/com/google/gerrit/metrics/dropwizard/BUILD
+++ b/javatests/com/google/gerrit/metrics/dropwizard/BUILD
@@ -7,7 +7,6 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/metrics/dropwizard",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java b/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java
index d6bcb62..9b21bf6 100644
--- a/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java
+++ b/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class DropWizardMetricMakerTest extends GerritBaseTests {
+public class DropWizardMetricMakerTest {
   DropWizardMetricMaker metrics =
       new DropWizardMetricMaker(null /* MetricRegistry unused in tests */);
 
diff --git a/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java b/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
index 0a5dabf..33919e7 100644
--- a/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
+++ b/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.metrics.proc;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.codahale.metrics.Counter;
 import com.codahale.metrics.Gauge;
@@ -30,7 +32,6 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -39,7 +40,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class ProcMetricModuleTest extends GerritBaseTests {
+public class ProcMetricModuleTest {
   @Inject MetricMaker metrics;
 
   @Inject MetricRegistry registry;
@@ -79,7 +80,9 @@
   public void counter1() {
     Counter1<String> cntr =
         metrics.newCounter(
-            "test/count", new Description("simple test").setCumulative(), Field.ofString("action"));
+            "test/count",
+            new Description("simple test").setCumulative(),
+            Field.ofString("action", Field.ignoreMetadata()).build());
 
     Counter total = get("test/count_total", Counter.class);
     assertThat(total.getCount()).isEqualTo(0);
@@ -104,7 +107,7 @@
             new Description("simple test")
                 .setCumulative()
                 .setFieldOrdering(FieldOrdering.PREFIX_FIELDS_BASENAME),
-            Field.ofString("action"));
+            Field.ofString("action", Field.ignoreMetadata()).build());
 
     Counter total = get("test/count_total", Counter.class);
     assertThat(total.getCount()).isEqualTo(0);
@@ -147,14 +150,16 @@
 
   @Test
   public void invalidName1() {
-    exception.expect(IllegalArgumentException.class);
-    metrics.newCounter("invalid name", new Description("fail"));
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> metrics.newCounter("invalid name", new Description("fail")));
   }
 
   @Test
   public void invalidName2() {
-    exception.expect(IllegalArgumentException.class);
-    metrics.newCounter("invalid/ name", new Description("fail"));
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> metrics.newCounter("invalid/ name", new Description("fail")));
   }
 
   @SuppressWarnings({"unchecked", "cast"})
@@ -164,8 +169,8 @@
 
   private <M extends Metric> M get(String name, Class<M> type) {
     Metric m = registry.getMetrics().get(name);
-    assertThat(m).named(name).isNotNull();
-    assertThat(m).named(name).isInstanceOf(type);
+    assertWithMessage(name).that(m).isNotNull();
+    assertWithMessage(name).that(m).isInstanceOf(type);
 
     @SuppressWarnings("unchecked")
     M result = (M) m;
diff --git a/javatests/com/google/gerrit/pgm/BUILD b/javatests/com/google/gerrit/pgm/BUILD
index e82b884..1445707 100644
--- a/javatests/com/google/gerrit/pgm/BUILD
+++ b/javatests/com/google/gerrit/pgm/BUILD
@@ -5,19 +5,17 @@
     name = "pgm_tests",
     srcs = glob(["**/*.java"]),
     deps = [
-        "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/pgm",
         "//java/com/google/gerrit/pgm/init",
         "//java/com/google/gerrit/pgm/init/api",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/securestore/testing",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib:junit",
-        "//lib/easymock",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib/mockito",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java b/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java
index a34007e..e34b578 100644
--- a/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java
+++ b/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java
@@ -15,8 +15,6 @@
 package com.google.gerrit.pgm.init.api;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.easymock.EasyMock.createStrictMock;
-import static org.easymock.EasyMock.replay;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.config.SitePaths;
@@ -37,12 +35,18 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
 
+@RunWith(MockitoJUnitRunner.class)
 public class AllProjectsConfigTest {
   private static final String ALL_PROJECTS = "All-The-Projects";
 
   @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
 
+  @Mock ConsoleUI ui;
+
   private SitePaths sitePaths;
   private AllProjectsConfig allProjectsConfig;
   private File allProjectsRepoFile;
@@ -70,8 +74,6 @@
 
     InMemorySecureStore secureStore = new InMemorySecureStore();
     InitFlags flags = new InitFlags(sitePaths, secureStore, ImmutableList.of(), false);
-    ConsoleUI ui = createStrictMock(ConsoleUI.class);
-    replay(ui);
     Section.Factory sections =
         (name, subsection) -> new Section(flags, sitePaths, secureStore, ui, name, subsection);
     allProjectsConfig =
diff --git a/javatests/com/google/gerrit/proto/ProtosTest.java b/javatests/com/google/gerrit/proto/ProtosTest.java
index 29e8fe0..550bcc5 100644
--- a/javatests/com/google/gerrit/proto/ProtosTest.java
+++ b/javatests/com/google/gerrit/proto/ProtosTest.java
@@ -14,17 +14,16 @@
 
 package com.google.gerrit.proto;
 
-import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.protobuf.ByteString;
 import java.util.Arrays;
 import org.junit.Test;
 
-public class ProtosTest extends GerritBaseTests {
+public class ProtosTest {
   @Test
   public void parseUncheckedByteArrayWrongProtoType() {
     ChangeNotesKeyProto proto =
@@ -34,23 +33,17 @@
             .setId(ByteString.copyFromUtf8("foo"))
             .build();
     byte[] bytes = Protos.toByteArray(proto);
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes));
   }
 
   @Test
   public void parseUncheckedByteArrayInvalidData() {
     byte[] bytes = new byte[] {0x00};
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes));
   }
 
   @Test
@@ -74,23 +67,17 @@
             .setId(ByteString.copyFromUtf8("foo"))
             .build();
     byte[] bytes = Protos.toByteArray(proto);
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes, 0, bytes.length);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes, 0, bytes.length));
   }
 
   @Test
   public void parseUncheckedSegmentOfByteArrayInvalidData() {
     byte[] bytes = new byte[] {0x00};
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes, 0, bytes.length);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes, 0, bytes.length));
   }
 
   @Test
@@ -123,23 +110,17 @@
             .setId(ByteString.copyFromUtf8("foo"))
             .build();
     ByteString byteString = Protos.toByteString(proto);
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString));
   }
 
   @Test
   public void parseUncheckedByteStringInvalidData() {
     ByteString byteString = ByteString.copyFrom(new byte[] {0x00});
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/reviewdb/client/BUILD b/javatests/com/google/gerrit/reviewdb/client/BUILD
deleted file mode 100644
index bc993d5..0000000
--- a/javatests/com/google/gerrit/reviewdb/client/BUILD
+++ /dev/null
@@ -1,13 +0,0 @@
-load("//tools/bzl:junit.bzl", "junit_tests")
-
-junit_tests(
-    name = "client_tests",
-    srcs = glob(["*.java"]),
-    deps = [
-        "//java/com/google/gerrit/reviewdb:server",
-        "//java/com/google/gerrit/server/project/testing:project-test-util",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//lib:guava",
-        "//lib/truth",
-    ],
-)
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetProtoConverterTest.java
deleted file mode 100644
index b8d2b1e..0000000
--- a/javatests/com/google/gerrit/reviewdb/converter/PatchSetProtoConverterTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-// 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.
-
-package com.google.gerrit.reviewdb.converter;
-
-import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.truth.Truth;
-import com.google.gerrit.proto.Entities;
-import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.protobuf.Parser;
-import java.lang.reflect.Type;
-import java.sql.Timestamp;
-import org.junit.Test;
-
-public class PatchSetProtoConverterTest {
-  private final PatchSetProtoConverter patchSetProtoConverter = PatchSetProtoConverter.INSTANCE;
-
-  @Test
-  public void allValuesConvertedToProto() {
-    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
-    patchSet.setRevision(new RevId("aabbccddeeff"));
-    patchSet.setUploader(new Account.Id(452));
-    patchSet.setCreatedOn(new Timestamp(930349320L));
-    patchSet.setGroups(ImmutableList.of("group1, group2"));
-    patchSet.setPushCertificate("my push certificate");
-    patchSet.setDescription("This is a patch set description.");
-
-    Entities.PatchSet proto = patchSetProtoConverter.toProto(patchSet);
-
-    Entities.PatchSet expectedProto =
-        Entities.PatchSet.newBuilder()
-            .setId(
-                Entities.PatchSet_Id.newBuilder()
-                    .setChangeId(Entities.Change_Id.newBuilder().setId(103))
-                    .setPatchSetId(73))
-            .setRevision(Entities.RevId.newBuilder().setId("aabbccddeeff"))
-            .setUploaderAccountId(Entities.Account_Id.newBuilder().setId(452))
-            .setCreatedOn(930349320L)
-            .setGroups("group1, group2")
-            .setPushCertificate("my push certificate")
-            .setDescription("This is a patch set description.")
-            .build();
-    assertThat(proto).isEqualTo(expectedProto);
-  }
-
-  @Test
-  public void mandatoryValuesConvertedToProto() {
-    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
-
-    Entities.PatchSet proto = patchSetProtoConverter.toProto(patchSet);
-
-    Entities.PatchSet expectedProto =
-        Entities.PatchSet.newBuilder()
-            .setId(
-                Entities.PatchSet_Id.newBuilder()
-                    .setChangeId(Entities.Change_Id.newBuilder().setId(103))
-                    .setPatchSetId(73))
-            .build();
-    assertThat(proto).isEqualTo(expectedProto);
-  }
-
-  @Test
-  public void allValuesConvertedToProtoAndBackAgain() {
-    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
-    patchSet.setRevision(new RevId("aabbccddeeff"));
-    patchSet.setUploader(new Account.Id(452));
-    patchSet.setCreatedOn(new Timestamp(930349320L));
-    patchSet.setGroups(ImmutableList.of("group1, group2"));
-    patchSet.setPushCertificate("my push certificate");
-    patchSet.setDescription("This is a patch set description.");
-
-    PatchSet convertedPatchSet =
-        patchSetProtoConverter.fromProto(patchSetProtoConverter.toProto(patchSet));
-    Truth.assertThat(convertedPatchSet).isEqualTo(patchSet);
-  }
-
-  @Test
-  public void mandatoryValuesConvertedToProtoAndBackAgain() {
-    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
-
-    PatchSet convertedPatchSet =
-        patchSetProtoConverter.fromProto(patchSetProtoConverter.toProto(patchSet));
-    Truth.assertThat(convertedPatchSet).isEqualTo(patchSet);
-  }
-
-  @Test
-  public void protoCanBeParsedFromBytes() throws Exception {
-    Entities.PatchSet proto =
-        Entities.PatchSet.newBuilder()
-            .setId(
-                Entities.PatchSet_Id.newBuilder()
-                    .setChangeId(Entities.Change_Id.newBuilder().setId(103))
-                    .setPatchSetId(73))
-            .build();
-    byte[] bytes = proto.toByteArray();
-
-    Parser<Entities.PatchSet> parser = patchSetProtoConverter.getParser();
-    Entities.PatchSet parsedProto = parser.parseFrom(bytes);
-
-    assertThat(parsedProto).isEqualTo(proto);
-  }
-
-  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
-  @Test
-  public void fieldsExistAsExpected() {
-    assertThatSerializedClass(PatchSet.class)
-        .hasFields(
-            ImmutableMap.<String, Type>builder()
-                .put("id", PatchSet.Id.class)
-                .put("revision", RevId.class)
-                .put("uploader", Account.Id.class)
-                .put("createdOn", Timestamp.class)
-                .put("groups", String.class)
-                .put("pushCertificate", String.class)
-                .put("description", String.class)
-                .build());
-  }
-}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/RevIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/RevIdProtoConverterTest.java
deleted file mode 100644
index 2c354be..0000000
--- a/javatests/com/google/gerrit/reviewdb/converter/RevIdProtoConverterTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// 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.
-
-package com.google.gerrit.reviewdb.converter;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.proto.Entities;
-import com.google.gerrit.proto.testing.SerializedClassSubject;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.protobuf.Parser;
-import org.junit.Test;
-
-public class RevIdProtoConverterTest {
-  private final RevIdProtoConverter revIdProtoConverter = RevIdProtoConverter.INSTANCE;
-
-  @Test
-  public void allValuesConvertedToProto() {
-    RevId revId = new RevId("9903402f303249e");
-
-    Entities.RevId proto = revIdProtoConverter.toProto(revId);
-
-    Entities.RevId expectedProto = Entities.RevId.newBuilder().setId("9903402f303249e").build();
-    assertThat(proto).isEqualTo(expectedProto);
-  }
-
-  @Test
-  public void allValuesConvertedToProtoAndBackAgain() {
-    RevId revId = new RevId("ff3934a320bb");
-
-    RevId convertedRevId = revIdProtoConverter.fromProto(revIdProtoConverter.toProto(revId));
-
-    assertThat(convertedRevId).isEqualTo(revId);
-  }
-
-  @Test
-  public void protoCanBeParsedFromBytes() throws Exception {
-    Entities.RevId proto = Entities.RevId.newBuilder().setId("9903402f303249e").build();
-    byte[] bytes = proto.toByteArray();
-
-    Parser<Entities.RevId> parser = revIdProtoConverter.getParser();
-    Entities.RevId parsedProto = parser.parseFrom(bytes);
-
-    assertThat(parsedProto).isEqualTo(proto);
-  }
-
-  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
-  @Test
-  public void fieldsExistAsExpected() {
-    assertThatSerializedClass(RevId.class).hasFields(ImmutableMap.of("id", String.class));
-  }
-}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 7b8f602..585a272 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -30,30 +30,31 @@
     tags = ["no_windows"],
     visibility = ["//visibility:public"],
     runtime_deps = [
+        "//java/com/google/gerrit/lucene",
         "//lib/bouncycastle:bcprov",
         "//prolog:gerrit-prolog-common",
     ],
     deps = [
         ":custom-truth-subjects",
+        "//java/com/google/gerrit/acceptance/testsuite/project",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/extensions/common/testing:common-test-util",
         "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/jgit",
+        "//java/com/google/gerrit/json",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
-        "//java/com/google/gerrit/proto",
         "//java/com/google/gerrit/proto/testing",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/account/externalids/testing",
         "//java/com/google/gerrit/server/cache/serialize",
         "//java/com/google/gerrit/server/cache/testing",
-        "//java/com/google/gerrit/server/group/testing",
         "//java/com/google/gerrit/server/ioutil",
         "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/project/testing:project-test-util",
@@ -62,19 +63,21 @@
         "//java/com/google/gerrit/server/schema/testing",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/sshd",
+        "//java/com/google/gerrit/testing:assertable-executor",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//java/com/google/gerrit/truth",
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib:protobuf",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/commons:codec",
         "//lib/flogger:api",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib/mockito",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
         "//lib/truth:truth-proto-extension",
diff --git a/javatests/com/google/gerrit/server/ChangeUtilTest.java b/javatests/com/google/gerrit/server/ChangeUtilTest.java
index 5cb474d..5f73d2c 100644
--- a/javatests/com/google/gerrit/server/ChangeUtilTest.java
+++ b/javatests/com/google/gerrit/server/ChangeUtilTest.java
@@ -16,11 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.regex.Pattern;
 import org.junit.Test;
 
-public class ChangeUtilTest extends GerritBaseTests {
+public class ChangeUtilTest {
   @Test
   public void changeMessageUuid() throws Exception {
     Pattern pat = Pattern.compile("^[0-9a-f]{8}_[0-9a-f]{8}$");
diff --git a/javatests/com/google/gerrit/server/IdentifiedUserTest.java b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
index 485de49..1672ce1 100644
--- a/javatests/com/google/gerrit/server/IdentifiedUserTest.java
+++ b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
@@ -17,7 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.FakeRealm;
 import com.google.gerrit.server.account.GroupBackend;
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.FakeAccountCache;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
@@ -45,7 +44,7 @@
 import org.junit.runner.RunWith;
 
 @RunWith(ConfigSuite.class)
-public class IdentifiedUserTest extends GerritBaseTests {
+public class IdentifiedUserTest {
   @ConfigSuite.Parameter public Config config;
 
   private IdentifiedUser identifiedUser;
@@ -99,8 +98,11 @@
     Injector injector = Guice.createInjector(mod);
     injector.injectMembers(this);
 
-    Account account = new Account(new Account.Id(1), TimeUtil.nowTs());
-    Account.Id ownerId = account.getId();
+    Account account =
+        Account.builder(Account.id(1), TimeUtil.nowTs())
+            .setMetaId("1234567812345678123456781234567812345678")
+            .build();
+    Account.Id ownerId = account.id();
 
     identifiedUser = identifiedUserFactory.create(ownerId);
 
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
index c37a945..5f14d28 100644
--- a/javatests/com/google/gerrit/server/account/AccountResolverTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -17,26 +17,24 @@
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.AccountResolver.Result;
 import com.google.gerrit.server.account.AccountResolver.Searcher;
 import com.google.gerrit.server.account.AccountResolver.StringSearcher;
 import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
-import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Arrays;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Stream;
 import org.junit.Test;
 
-public class AccountResolverTest extends GerritBaseTests {
+public class AccountResolverTest {
   private static class TestSearcher extends StringSearcher {
     private final String pattern;
     private final boolean shortCircuit;
@@ -86,7 +84,7 @@
     @Override
     public String toString() {
       return accounts.stream()
-          .map(a -> a.getAccount().getId().toString())
+          .map(a -> a.account().id().toString())
           .collect(joining(",", pattern + "(", ")"));
     }
   }
@@ -158,7 +156,7 @@
     // Searchers always short-circuit when finding a non-empty result list, and this one didn't
     // filter out inactive results, so the second searcher never ran.
     assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1));
-    assertThat(getOnlyElement(result.asList()).getAccount().isActive()).isFalse();
+    assertThat(getOnlyElement(result.asList()).account().isActive()).isFalse();
     assertThat(filteredInactiveIds(result)).isEmpty();
   }
 
@@ -175,7 +173,7 @@
     // and this one didn't filter out inactive results,
     // so the second searcher never ran.
     assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1));
-    assertThat(getOnlyElement(result.asList()).getAccount().isActive()).isFalse();
+    assertThat(getOnlyElement(result.asList()).account().isActive()).isFalse();
     assertThat(filteredInactiveIds(result)).isEmpty();
   }
 
@@ -242,15 +240,14 @@
 
   @Test
   public void asUniqueWithNoResults() throws Exception {
-    try {
-      String input = "foo";
-      ImmutableList<Searcher<?>> searchers = ImmutableList.of();
-      Supplier<Predicate<AccountState>> visibilitySupplier = allVisible();
-      search(input, searchers, visibilitySupplier).asUnique();
-      assert_().fail("Expected UnresolvableAccountException");
-    } catch (UnresolvableAccountException e) {
-      assertThat(e).hasMessageThat().isEqualTo("Account 'foo' not found");
-    }
+    String input = "foo";
+    ImmutableList<Searcher<?>> searchers = ImmutableList.of();
+    Supplier<Predicate<AccountState>> visibilitySupplier = allVisible();
+    UnresolvableAccountException thrown =
+        assertThrows(
+            UnresolvableAccountException.class,
+            () -> search(input, searchers, visibilitySupplier).asUnique());
+    assertThat(thrown).hasMessageThat().isEqualTo("Account 'foo' not found");
   }
 
   @Test
@@ -258,22 +255,22 @@
     AccountState account = newAccount(1);
     ImmutableList<Searcher<?>> searchers =
         ImmutableList.of(new TestSearcher("foo", false, account));
-    assertThat(search("foo", searchers, allVisible()).asUnique().getAccount().getId())
-        .isEqualTo(account.getAccount().getId());
+    assertThat(search("foo", searchers, allVisible()).asUnique().account().id())
+        .isEqualTo(account.account().id());
   }
 
   @Test
   public void asUniqueWithMultipleResults() throws Exception {
     ImmutableList<Searcher<?>> searchers =
         ImmutableList.of(new TestSearcher("foo", false, newAccount(1), newAccount(2)));
-    try {
-      search("foo", searchers, allVisible()).asUnique();
-      assert_().fail("Expected UnresolvableAccountException");
-    } catch (UnresolvableAccountException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo("Account 'foo' is ambiguous:\n1: Anonymous Name (1)\n2: Anonymous Name (2)");
-    }
+    UnresolvableAccountException thrown =
+        assertThrows(
+            UnresolvableAccountException.class,
+            () -> search("foo", searchers, allVisible()).asUnique());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            "Account 'foo' is ambiguous (at most 3 shown):\n1: Anonymous Name (1)\n2: Anonymous Name (2)");
   }
 
   @Test
@@ -315,7 +312,8 @@
                 .new Result(
                     "foo", ImmutableList.of(newAccount(3), newAccount(1)), ImmutableList.of())))
         .hasMessageThat()
-        .isEqualTo("Account 'foo' is ambiguous:\n1: Anonymous Name (1)\n3: Anonymous Name (3)");
+        .isEqualTo(
+            "Account 'foo' is ambiguous (at most 3 shown):\n1: Anonymous Name (1)\n3: Anonymous Name (3)");
   }
 
   @Test
@@ -359,17 +357,19 @@
 
   private AccountState newAccount(int id) {
     return AccountState.forAccount(
-        new AllUsersName("All-Users"), new Account(new Account.Id(id), TimeUtil.nowTs()));
+        Account.builder(Account.id(id), TimeUtil.nowTs())
+            .setMetaId("1234567812345678123456781234567812345678")
+            .build());
   }
 
   private AccountState newInactiveAccount(int id) {
-    Account a = new Account(new Account.Id(id), TimeUtil.nowTs());
+    Account.Builder a = Account.builder(Account.id(id), TimeUtil.nowTs());
     a.setActive(false);
-    return AccountState.forAccount(new AllUsersName("All-Users"), a);
+    return AccountState.forAccount(a.build());
   }
 
   private static ImmutableSet<Account.Id> ids(int... ids) {
-    return Arrays.stream(ids).mapToObj(Account.Id::new).collect(toImmutableSet());
+    return Arrays.stream(ids).mapToObj(Account::id).collect(toImmutableSet());
   }
 
   private static Supplier<Predicate<AccountState>> allVisible() {
@@ -377,18 +377,16 @@
   }
 
   private Predicate<AccountState> activityPrediate() {
-    return (AccountState accountState) -> accountState.getAccount().isActive();
+    return (AccountState accountState) -> accountState.account().isActive();
   }
 
   private static Supplier<Predicate<AccountState>> only(int... ids) {
     ImmutableSet<Account.Id> idSet =
-        Arrays.stream(ids).mapToObj(Account.Id::new).collect(toImmutableSet());
-    return () -> a -> idSet.contains(a.getAccount().getId());
+        Arrays.stream(ids).mapToObj(Account::id).collect(toImmutableSet());
+    return () -> a -> idSet.contains(a.account().id());
   }
 
   private static ImmutableSet<Account.Id> filteredInactiveIds(Result result) {
-    return result.filteredInactive().stream()
-        .map(a -> a.getAccount().getId())
-        .collect(toImmutableSet());
+    return result.filteredInactive().stream().map(a -> a.account().id()).collect(toImmutableSet());
   }
 }
diff --git a/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java b/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
index 51a34f5..1381c75 100644
--- a/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
+++ b/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
@@ -16,14 +16,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Account;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import org.junit.Test;
 
-public class AuthorizedKeysTest extends GerritBaseTests {
+public class AuthorizedKeysTest {
   private static final String KEY1 =
       "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCgug5VyMXQGnem2H1KVC4/HcRcD4zzBqS"
           + "uJBRWVonSSoz3RoAZ7bWXCVVGwchtXwUURD689wFYdiPecOrWOUgeeyRq754YWRhU+W28"
@@ -55,7 +54,7 @@
           + "zRuEL5e/QOu9yGq9xkWApCmg6edpWAHG+Bx4AldU78MiZvzoB7gMMdxc9RmZ1gYj/DjxV"
           + "w== john.doe@example.com";
 
-  private final Account.Id accountId = new Account.Id(1);
+  private final Account.Id accountId = Account.id(1);
 
   @Test
   public void test() throws Exception {
@@ -151,7 +150,7 @@
 
   private static void assertParse(
       StringBuilder authorizedKeys, List<Optional<AccountSshKey>> expectedKeys) {
-    Account.Id accountId = new Account.Id(1);
+    Account.Id accountId = Account.id(1);
     List<Optional<AccountSshKey>> parsedKeys =
         AuthorizedKeys.parse(accountId, authorizedKeys.toString());
     assertThat(parsedKeys).containsExactlyElementsIn(expectedKeys);
@@ -171,7 +170,7 @@
    * @return the expected line for this key in the authorized_keys file
    */
   private static String addKey(List<Optional<AccountSshKey>> keys, String pub) {
-    AccountSshKey key = AccountSshKey.create(new Account.Id(1), keys.size() + 1, pub);
+    AccountSshKey key = AccountSshKey.create(Account.id(1), keys.size() + 1, pub);
     keys.add(Optional.of(key));
     return key.sshPublicKey() + "\n";
   }
@@ -182,7 +181,7 @@
    * @return the expected line for this key in the authorized_keys file
    */
   private static String addInvalidKey(List<Optional<AccountSshKey>> keys, String pub) {
-    AccountSshKey key = AccountSshKey.createInvalid(new Account.Id(1), keys.size() + 1, pub);
+    AccountSshKey key = AccountSshKey.createInvalid(Account.id(1), keys.size() + 1, pub);
     keys.add(Optional.of(key));
     return AuthorizedKeys.INVALID_KEY_COMMENT_PREFIX + key.sshPublicKey() + "\n";
   }
diff --git a/javatests/com/google/gerrit/server/account/DestinationListTest.java b/javatests/com/google/gerrit/server/account/DestinationListTest.java
index e51b041..4188f39 100644
--- a/javatests/com/google/gerrit/server/account/DestinationListTest.java
+++ b/javatests/com/google/gerrit/server/account/DestinationListTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.ValidationError;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -27,7 +26,7 @@
 import java.util.Set;
 import org.junit.Test;
 
-public class DestinationListTest extends GerritBaseTests {
+public class DestinationListTest {
   public static final String R_FOO = "refs/heads/foo";
   public static final String R_BAR = "refs/heads/bar";
 
@@ -55,11 +54,11 @@
   public static final String LABEL = "label";
   public static final String LABEL2 = "another";
 
-  public static final Branch.NameKey B_FOO = dest(P_MY, R_FOO);
-  public static final Branch.NameKey B_BAR = dest(P_SLASH, R_BAR);
-  public static final Branch.NameKey B_COMPLEX = dest(P_COMPLEX, R_FOO);
+  public static final BranchNameKey B_FOO = dest(P_MY, R_FOO);
+  public static final BranchNameKey B_BAR = dest(P_SLASH, R_BAR);
+  public static final BranchNameKey B_COMPLEX = dest(P_COMPLEX, R_FOO);
 
-  public static final Set<Branch.NameKey> D_SIMPLE = new HashSet<>();
+  public static final Set<BranchNameKey> D_SIMPLE = new HashSet<>();
 
   static {
     D_SIMPLE.clear();
@@ -67,15 +66,15 @@
     D_SIMPLE.add(B_BAR);
   }
 
-  private static Branch.NameKey dest(String project, String ref) {
-    return new Branch.NameKey(new Project.NameKey(project), ref);
+  private static BranchNameKey dest(String project, String ref) {
+    return BranchNameKey.create(Project.nameKey(project), ref);
   }
 
   @Test
   public void testParseSimple() throws Exception {
     DestinationList dl = new DestinationList();
     dl.parseLabel(LABEL, F_SIMPLE, null);
-    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    Set<BranchNameKey> branches = dl.getDestinations(LABEL);
     assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
   }
 
@@ -83,7 +82,7 @@
   public void testParseWHeader() throws Exception {
     DestinationList dl = new DestinationList();
     dl.parseLabel(LABEL, HEADER + F_SIMPLE, null);
-    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    Set<BranchNameKey> branches = dl.getDestinations(LABEL);
     assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
   }
 
@@ -91,7 +90,7 @@
   public void testParseWComments() throws Exception {
     DestinationList dl = new DestinationList();
     dl.parseLabel(LABEL, C1 + F_SIMPLE + C2, null);
-    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    Set<BranchNameKey> branches = dl.getDestinations(LABEL);
     assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
   }
 
@@ -99,7 +98,7 @@
   public void testParseFooComment() throws Exception {
     DestinationList dl = new DestinationList();
     dl.parseLabel(LABEL, "#" + L_FOO + L_BAR, null);
-    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    Set<BranchNameKey> branches = dl.getDestinations(LABEL);
     assertThat(branches).doesNotContain(B_FOO);
     assertThat(branches).contains(B_BAR);
   }
@@ -108,7 +107,7 @@
   public void testParsePaddedFronts() throws Exception {
     DestinationList dl = new DestinationList();
     dl.parseLabel(LABEL, F_PAD_F, null);
-    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    Set<BranchNameKey> branches = dl.getDestinations(LABEL);
     assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
   }
 
@@ -116,7 +115,7 @@
   public void testParsePaddedEnds() throws Exception {
     DestinationList dl = new DestinationList();
     dl.parseLabel(LABEL, F_PAD_E, null);
-    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    Set<BranchNameKey> branches = dl.getDestinations(LABEL);
     assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
   }
 
@@ -124,7 +123,7 @@
   public void testParseComplex() throws Exception {
     DestinationList dl = new DestinationList();
     dl.parseLabel(LABEL, L_COMPLEX, null);
-    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    Set<BranchNameKey> branches = dl.getDestinations(LABEL);
     assertThat(branches).contains(B_COMPLEX);
   }
 
@@ -140,7 +139,7 @@
   public void testParse2Labels() throws Exception {
     DestinationList dl = new DestinationList();
     dl.parseLabel(LABEL, F_SIMPLE, null);
-    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    Set<BranchNameKey> branches = dl.getDestinations(LABEL);
     assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
 
     dl.parseLabel(LABEL2, L_COMPLEX, null);
diff --git a/javatests/com/google/gerrit/server/account/GroupUUIDTest.java b/javatests/com/google/gerrit/server/account/GroupUUIDTest.java
index 70887e6..a155d7f 100644
--- a/javatests/com/google/gerrit/server/account/GroupUUIDTest.java
+++ b/javatests/com/google/gerrit/server/account/GroupUUIDTest.java
@@ -16,12 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.AccountGroup;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Test;
 
-public class GroupUUIDTest extends GerritBaseTests {
+public class GroupUUIDTest {
   @Test
   public void createdUuidsForSameInputShouldBeDifferent() {
     String groupName = "Users";
diff --git a/javatests/com/google/gerrit/server/account/HashedPasswordTest.java b/javatests/com/google/gerrit/server/account/HashedPasswordTest.java
index 9a0c9cb9..3443720 100644
--- a/javatests/com/google/gerrit/server/account/HashedPasswordTest.java
+++ b/javatests/com/google/gerrit/server/account/HashedPasswordTest.java
@@ -15,13 +15,12 @@
 package com.google.gerrit.server.account;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.common.base.Strings;
-import com.google.gerrit.testing.GerritBaseTests;
-import org.apache.commons.codec.DecoderException;
+import com.google.gerrit.server.account.HashedPassword.DecoderException;
 import org.junit.Test;
 
-public class HashedPasswordTest extends GerritBaseTests {
+public class HashedPasswordTest {
 
   @Test
   public void encodeOneLine() throws Exception {
@@ -41,17 +40,9 @@
     assertThat(roundtrip.checkPassword("not the password")).isFalse();
   }
 
-  @Test(expected = DecoderException.class)
-  public void invalidDecode() throws Exception {
-    HashedPassword.decode("invalid");
-  }
-
   @Test
-  public void lengthLimit() throws Exception {
-    String password = Strings.repeat("1", 72);
-
-    // make sure it fits in varchar(255).
-    assertThat(HashedPassword.fromPassword(password).encode().length()).isLessThan(255);
+  public void invalidDecode() throws Exception {
+    assertThrows(DecoderException.class, () -> HashedPassword.decode("invalid"));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/account/PreferencesTest.java b/javatests/com/google/gerrit/server/account/PreferencesTest.java
new file mode 100644
index 0000000..9866481
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/PreferencesTest.java
@@ -0,0 +1,50 @@
+// 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.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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.json.OutputFormat;
+import com.google.gson.Gson;
+import org.junit.Test;
+
+public class PreferencesTest {
+
+  private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
+
+  @Test
+  public void generalPreferencesRoundTrip() {
+    GeneralPreferencesInfo original = GeneralPreferencesInfo.defaults();
+    assertThat(GSON.toJson(original))
+        .isEqualTo(GSON.toJson(Preferences.General.fromInfo(original).toInfo()));
+  }
+
+  @Test
+  public void diffPreferencesRoundTrip() {
+    DiffPreferencesInfo original = DiffPreferencesInfo.defaults();
+    assertThat(GSON.toJson(original))
+        .isEqualTo(GSON.toJson(Preferences.Diff.fromInfo(original).toInfo()));
+  }
+
+  @Test
+  public void editPreferencesRoundTrip() {
+    EditPreferencesInfo original = EditPreferencesInfo.defaults();
+    assertThat(GSON.toJson(original))
+        .isEqualTo(GSON.toJson(Preferences.Edit.fromInfo(original).toInfo()));
+  }
+}
diff --git a/javatests/com/google/gerrit/server/account/QueryListTest.java b/javatests/com/google/gerrit/server/account/QueryListTest.java
index a0876e1..7d491c9 100644
--- a/javatests/com/google/gerrit/server/account/QueryListTest.java
+++ b/javatests/com/google/gerrit/server/account/QueryListTest.java
@@ -17,12 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.server.git.ValidationError;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
 
-public class QueryListTest extends GerritBaseTests {
+public class QueryListTest {
   public static final String Q_P = "project:foo";
   public static final String Q_B = "branch:bar";
   public static final String Q_COMPLEX = "branch:bar AND peers:'is:open\t'";
diff --git a/javatests/com/google/gerrit/server/account/StoredPreferencesTest.java b/javatests/com/google/gerrit/server/account/StoredPreferencesTest.java
new file mode 100644
index 0000000..c39e496
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/StoredPreferencesTest.java
@@ -0,0 +1,66 @@
+// 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.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.server.git.ValidationError;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+/** Tests for parsing user preferences from Git. */
+public class StoredPreferencesTest {
+
+  enum Unknown {
+    STATE
+  }
+
+  @Test
+  public void ignoreUnknownAccountPreferencesWhenParsing() {
+    ValidationError.Sink errorSink = Mockito.mock(ValidationError.Sink.class);
+    StoredPreferences preferences =
+        new StoredPreferences(Account.id(1), configWithUnknownEntries(), new Config(), errorSink);
+    GeneralPreferencesInfo parsedPreferences = preferences.getGeneralPreferences();
+
+    assertThat(parsedPreferences).isNotNull();
+    assertThat(parsedPreferences.expandInlineDiffs).isTrue();
+    verifyNoMoreInteractions(errorSink);
+  }
+
+  @Test
+  public void ignoreUnknownDefaultAccountPreferencesWhenParsing() {
+    ValidationError.Sink errorSink = Mockito.mock(ValidationError.Sink.class);
+    StoredPreferences preferences =
+        new StoredPreferences(Account.id(1), new Config(), configWithUnknownEntries(), errorSink);
+    GeneralPreferencesInfo parsedPreferences = preferences.getGeneralPreferences();
+
+    assertThat(parsedPreferences).isNotNull();
+    assertThat(parsedPreferences.expandInlineDiffs).isTrue();
+    verifyNoMoreInteractions(errorSink);
+  }
+
+  private static Config configWithUnknownEntries() {
+    Config cfg = new Config();
+    cfg.setBoolean("general", null, "expandInlineDiffs", true);
+    cfg.setBoolean("general", null, "unknown", true);
+    cfg.setEnum("general", null, "unknownenum", Unknown.STATE);
+    cfg.setString("general", null, "unknownstring", "bla");
+    return cfg;
+  }
+}
diff --git a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
index 334c627..1e3063e 100644
--- a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
+++ b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
@@ -17,34 +17,32 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.createNiceMock;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.getCurrentArguments;
-import static org.easymock.EasyMock.not;
-import static org.easymock.EasyMock.replay;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.AccountGroup.UUID;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
-public class UniversalGroupBackendTest extends GerritBaseTests {
-  private static final AccountGroup.UUID OTHER_UUID = new AccountGroup.UUID("other");
+public class UniversalGroupBackendTest {
+  private static final AccountGroup.UUID OTHER_UUID = AccountGroup.uuid("other");
 
   private UniversalGroupBackend backend;
   private IdentifiedUser user;
@@ -53,8 +51,7 @@
 
   @Before
   public void setup() {
-    user = createNiceMock(IdentifiedUser.class);
-    replay(user);
+    user = mock(IdentifiedUser.class);
     backends = new DynamicSet<>();
     backends.add("gerrit", new SystemGroupBackend(new Config()));
     backend =
@@ -102,25 +99,26 @@
 
   @Test
   public void otherMemberships() {
-    final AccountGroup.UUID handled = new AccountGroup.UUID("handled");
-    final AccountGroup.UUID notHandled = new AccountGroup.UUID("not handled");
-    final IdentifiedUser member = createNiceMock(IdentifiedUser.class);
-    final IdentifiedUser notMember = createNiceMock(IdentifiedUser.class);
+    final AccountGroup.UUID handled = AccountGroup.uuid("handled");
+    final AccountGroup.UUID notHandled = AccountGroup.uuid("not handled");
+    final IdentifiedUser member = mock(IdentifiedUser.class);
+    final IdentifiedUser notMember = mock(IdentifiedUser.class);
 
-    GroupBackend backend = createMock(GroupBackend.class);
-    expect(backend.handles(handled)).andStubReturn(true);
-    expect(backend.handles(not(eq(handled)))).andStubReturn(false);
-    expect(backend.membershipsOf(anyObject(IdentifiedUser.class)))
-        .andStubAnswer(
-            () -> {
-              Object[] args = getCurrentArguments();
-              GroupMembership membership = createMock(GroupMembership.class);
-              expect(membership.contains(eq(handled))).andStubReturn(args[0] == member);
-              expect(membership.contains(not(eq(notHandled)))).andStubReturn(false);
-              replay(membership);
-              return membership;
+    GroupBackend backend = mock(GroupBackend.class);
+    when(backend.handles(eq(handled))).thenReturn(true);
+    when(backend.handles(not(eq(handled)))).thenReturn(false);
+    when(backend.membershipsOf(any(IdentifiedUser.class)))
+        .thenAnswer(
+            new Answer<GroupMembership>() {
+              @Override
+              public GroupMembership answer(InvocationOnMock invocation) {
+                GroupMembership membership = mock(GroupMembership.class);
+                when(membership.contains(eq(handled)))
+                    .thenReturn(invocation.getArguments()[0] == member);
+                when(membership.contains(eq(notHandled))).thenReturn(false);
+                return membership;
+              }
             });
-    replay(member, notMember, backend);
 
     backends = new DynamicSet<>();
     backends.add("gerrit", backend);
diff --git a/javatests/com/google/gerrit/server/account/WatchConfigTest.java b/javatests/com/google/gerrit/server/account/WatchConfigTest.java
index 2ac7be7..95dbbde 100644
--- a/javatests/com/google/gerrit/server/account/WatchConfigTest.java
+++ b/javatests/com/google/gerrit/server/account/WatchConfigTest.java
@@ -15,10 +15,11 @@
 package com.google.gerrit.server.account;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+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;
@@ -54,12 +55,12 @@
             + "  notify = [NEW_PATCHSETS]\n"
             + "  notify = * [NEW_PATCHSETS, ALL_COMMENTS]\n");
     Map<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches =
-        ProjectWatches.parse(new Account.Id(1000000), cfg, this);
+        ProjectWatches.parse(Account.id(1000000), cfg, this);
 
     assertThat(validationErrors).isEmpty();
 
-    Project.NameKey myProject = new Project.NameKey("myProject");
-    Project.NameKey otherProject = new Project.NameKey("otherProject");
+    Project.NameKey myProject = Project.nameKey("myProject");
+    Project.NameKey otherProject = Project.nameKey("otherProject");
     Map<ProjectWatchKey, Set<NotifyType>> expectedProjectWatches = new HashMap<>();
     expectedProjectWatches.put(
         ProjectWatchKey.create(myProject, null),
@@ -87,7 +88,7 @@
             + "[project \"otherProject\"]\n"
             + "  notify = [NEW_PATCHSETS]\n");
 
-    ProjectWatches.parse(new Account.Id(1000000), cfg, this);
+    ProjectWatches.parse(Account.id(1000000), cfg, this);
     assertThat(validationErrors).hasSize(1);
     assertThat(validationErrors.get(0).getMessage())
         .isEqualTo(
@@ -170,14 +171,14 @@
   private void assertParseNotifyValueFails(String notifyValue) {
     assertThat(validationErrors).isEmpty();
     parseNotifyValue(notifyValue);
-    assertThat(validationErrors)
-        .named("expected validation error for notifyValue: " + notifyValue)
+    assertWithMessage("expected validation error for notifyValue: " + notifyValue)
+        .that(validationErrors)
         .isNotEmpty();
     validationErrors.clear();
   }
 
   private NotifyValue parseNotifyValue(String notifyValue) {
-    return NotifyValue.parse(new Account.Id(1000000), "project", notifyValue, this);
+    return NotifyValue.parse(Account.id(1000000), "project", notifyValue, this);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java b/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
index d757f71..45dacd9 100644
--- a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
@@ -21,17 +21,16 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSetMultimap;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.AllExternalIds.Serializer;
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto;
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto.ExternalIdProto;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.TypeLiteral;
 import java.util.Arrays;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class AllExternalIdsTest extends GerritBaseTests {
+public class AllExternalIdsTest {
   @Test
   public void serializeEmptyExternalIds() throws Exception {
     assertRoundTrip(allExternalIds(), AllExternalIdsProto.getDefaultInstance());
@@ -39,8 +38,8 @@
 
   @Test
   public void serializeMultipleExternalIds() throws Exception {
-    Account.Id accountId1 = new Account.Id(1001);
-    Account.Id accountId2 = new Account.Id(1002);
+    Account.Id accountId1 = Account.id(1001);
+    Account.Id accountId2 = Account.id(1002);
     assertRoundTrip(
         allExternalIds(
             ExternalId.create("scheme1", "id1", accountId1),
@@ -62,7 +61,7 @@
   @Test
   public void serializeExternalIdWithEmail() throws Exception {
     assertRoundTrip(
-        allExternalIds(ExternalId.createEmail(new Account.Id(1001), "foo@example.com")),
+        allExternalIds(ExternalId.createEmail(Account.id(1001), "foo@example.com")),
         AllExternalIdsProto.newBuilder()
             .addExternalId(
                 ExternalIdProto.newBuilder()
@@ -76,7 +75,7 @@
   public void serializeExternalIdWithPassword() throws Exception {
     assertRoundTrip(
         allExternalIds(
-            ExternalId.create("scheme", "id", new Account.Id(1001), null, "hashed password")),
+            ExternalId.create("scheme", "id", Account.id(1001), null, "hashed password")),
         AllExternalIdsProto.newBuilder()
             .addExternalId(
                 ExternalIdProto.newBuilder()
@@ -91,7 +90,7 @@
     assertRoundTrip(
         allExternalIds(
             ExternalId.create(
-                ExternalId.create("scheme", "id", new Account.Id(1001)),
+                ExternalId.create("scheme", "id", Account.id(1001)),
                 ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))),
         AllExternalIdsProto.newBuilder()
             .addExternalId(
diff --git a/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java b/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
new file mode 100644
index 0000000..054b1aa
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
@@ -0,0 +1,307 @@
+// 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.account.externalids;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.server.account.externalids.testing.ExternalIdTestUtil;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import com.google.inject.util.Providers;
+import java.io.IOException;
+import java.util.function.Consumer;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ExternalIDCacheLoaderTest {
+  private static AllUsersName ALL_USERS = new AllUsersName(AllUsersNameProvider.DEFAULT);
+
+  @Mock Cache<ObjectId, AllExternalIds> externalIdCache;
+
+  private ExternalIdCacheLoader loader;
+  private GitRepositoryManager repoManager = new InMemoryRepositoryManager();
+  private ExternalIdReader externalIdReader;
+  private ExternalIdReader externalIdReaderSpy;
+
+  @Before
+  public void setUp() throws Exception {
+    repoManager.createRepository(ALL_USERS).close();
+    externalIdReader = new ExternalIdReader(repoManager, ALL_USERS, new DisabledMetricMaker());
+    externalIdReaderSpy = Mockito.spy(externalIdReader);
+    loader = createLoader(true);
+  }
+
+  @Test
+  public void worksOnSingleCommit() throws Exception {
+    ObjectId firstState = insertExternalId(1, 1);
+    assertThat(loader.load(firstState)).isEqualTo(allFromGit(firstState));
+    verify(externalIdReaderSpy, times(1)).all(firstState);
+  }
+
+  @Test
+  public void reloadsSingleUpdateUsingPartialReload() throws Exception {
+    ObjectId firstState = insertExternalId(1, 1);
+    ObjectId head = insertExternalId(2, 2);
+
+    when(externalIdCache.getIfPresent(firstState)).thenReturn(allFromGit(firstState));
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verifyZeroInteractions(externalIdReaderSpy);
+  }
+
+  @Test
+  public void reloadsMultipleUpdatesUsingPartialReload() throws Exception {
+    ObjectId firstState = insertExternalId(1, 1);
+    insertExternalId(2, 2);
+    insertExternalId(3, 3);
+    ObjectId head = insertExternalId(4, 4);
+
+    when(externalIdCache.getIfPresent(firstState)).thenReturn(allFromGit(firstState));
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verifyZeroInteractions(externalIdReaderSpy);
+  }
+
+  @Test
+  public void reloadsAllExternalIdsWhenNoOldStateIsCached() throws Exception {
+    ObjectId firstState = insertExternalId(1, 1);
+    ObjectId head = insertExternalId(2, 2);
+
+    when(externalIdCache.getIfPresent(firstState)).thenReturn(null);
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verify(externalIdReaderSpy, times(1)).all(head);
+  }
+
+  @Test
+  public void partialReloadingDisabledAlwaysTriggersFullReload() throws Exception {
+    loader = createLoader(false);
+    insertExternalId(1, 1);
+    ObjectId head = insertExternalId(2, 2);
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verify(externalIdReaderSpy, times(1)).all(head);
+  }
+
+  @Test
+  public void fallsBackToFullReloadOnManyUpdatesOnBranch() throws Exception {
+    insertExternalId(1, 1);
+    ObjectId head = null;
+    for (int i = 2; i < 20; i++) {
+      head = insertExternalId(i, i);
+    }
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verify(externalIdReaderSpy, times(1)).all(head);
+  }
+
+  @Test
+  public void doesFullReloadWhenNoCacheStateIsFound() throws Exception {
+    ObjectId head = insertExternalId(1, 1);
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verify(externalIdReaderSpy, times(1)).all(head);
+  }
+
+  @Test
+  public void handlesDeletionInPartialReload() throws Exception {
+    ObjectId firstState = insertExternalId(1, 1);
+    ObjectId head = deleteExternalId(1, 1);
+    assertThat(allFromGit(head).byAccount().size()).isEqualTo(0);
+
+    when(externalIdCache.getIfPresent(firstState)).thenReturn(allFromGit(firstState));
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verifyZeroInteractions(externalIdReaderSpy);
+  }
+
+  @Test
+  public void handlesModifyInPartialReload() throws Exception {
+    ObjectId firstState = insertExternalId(1, 1);
+    ObjectId head =
+        modifyExternalId(
+            externalId(1, 1),
+            ExternalId.create("fooschema", "bar1", Account.id(1), "foo@bar.com", "password"));
+    assertThat(allFromGit(head).byAccount().size()).isEqualTo(1);
+
+    when(externalIdCache.getIfPresent(firstState)).thenReturn(allFromGit(firstState));
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verifyZeroInteractions(externalIdReaderSpy);
+  }
+
+  @Test
+  public void ignoresInvalidExternalId() throws Exception {
+    ObjectId firstState = insertExternalId(1, 1);
+    ObjectId head;
+    try (Repository repo = repoManager.openRepository(ALL_USERS);
+        RevWalk rw = new RevWalk(repo)) {
+      ExternalIdTestUtil.insertExternalIdWithKeyThatDoesntMatchNoteId(
+          repo, rw, new PersonIdent("foo", "foo@bar.com"), Account.id(2), "test");
+      head = repo.exactRef(RefNames.REFS_EXTERNAL_IDS).getObjectId();
+    }
+
+    when(externalIdCache.getIfPresent(firstState)).thenReturn(allFromGit(firstState));
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verifyZeroInteractions(externalIdReaderSpy);
+  }
+
+  @Test
+  public void handlesTreePrefixesInDifferentialReload() throws Exception {
+    // Create more than 256 notes (NoteMap's current sharding limit) and check that we really have
+    // created a situation where NoteNames are sharded.
+    ObjectId oldState = inserExternalIds(257);
+    assertAllFilesHaveSlashesInPath();
+    ObjectId head = insertExternalId(500, 500);
+
+    when(externalIdCache.getIfPresent(oldState)).thenReturn(allFromGit(oldState));
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verifyZeroInteractions(externalIdReaderSpy);
+  }
+
+  @Test
+  public void handlesReshard() throws Exception {
+    // Create 256 notes (NoteMap's current sharding limit) and check that we are not yet sharding
+    ObjectId oldState = inserExternalIds(256);
+    assertNoFilesHaveSlashesInPath();
+    // Create one more external ID and then have the Loader compute the new state
+    ObjectId head = insertExternalId(500, 500);
+    assertAllFilesHaveSlashesInPath(); // NoteMap resharded
+
+    when(externalIdCache.getIfPresent(oldState)).thenReturn(allFromGit(oldState));
+
+    assertThat(loader.load(head)).isEqualTo(allFromGit(head));
+    verifyZeroInteractions(externalIdReaderSpy);
+  }
+
+  private ExternalIdCacheLoader createLoader(boolean allowPartial) {
+    Config cfg = new Config();
+    cfg.setBoolean("cache", "external_ids_map", "enablePartialReloads", allowPartial);
+    return new ExternalIdCacheLoader(
+        repoManager,
+        ALL_USERS,
+        externalIdReaderSpy,
+        Providers.of(externalIdCache),
+        new DisabledMetricMaker(),
+        cfg);
+  }
+
+  private AllExternalIds allFromGit(ObjectId revision) throws Exception {
+    return AllExternalIds.create(externalIdReader.all(revision));
+  }
+
+  private ObjectId inserExternalIds(int numberOfIdsToInsert) throws Exception {
+    ObjectId oldState = null;
+    // Create more than 256 notes (NoteMap's current sharding limit) and check that we really have
+    // created a situation where NoteNames are sharded.
+    for (int i = 0; i < numberOfIdsToInsert; i++) {
+      oldState = insertExternalId(i, i);
+    }
+    return oldState;
+  }
+
+  private ObjectId insertExternalId(int key, int accountId) throws Exception {
+    return performExternalIdUpdate(
+        u -> {
+          try {
+            u.insert(externalId(key, accountId));
+          } catch (IOException e) {
+            throw new RuntimeException(e);
+          }
+        });
+  }
+
+  private ObjectId modifyExternalId(ExternalId oldId, ExternalId newId) throws Exception {
+    return performExternalIdUpdate(
+        u -> {
+          try {
+            u.replace(oldId, newId);
+          } catch (IOException e) {
+            throw new RuntimeException(e);
+          }
+        });
+  }
+
+  private ObjectId deleteExternalId(int key, int accountId) throws Exception {
+    return performExternalIdUpdate(u -> u.delete(externalId(key, accountId)));
+  }
+
+  private ExternalId externalId(int key, int accountId) {
+    return ExternalId.create("fooschema", "bar" + key, Account.id(accountId));
+  }
+
+  private ObjectId performExternalIdUpdate(Consumer<ExternalIdNotes> update) throws Exception {
+    try (Repository repo = repoManager.openRepository(ALL_USERS)) {
+      PersonIdent updater = new PersonIdent("Foo bar", "foo@bar.com");
+      ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(ALL_USERS, repo);
+      update.accept(extIdNotes);
+      try (MetaDataUpdate metaDataUpdate =
+          new MetaDataUpdate(GitReferenceUpdated.DISABLED, null, repo)) {
+        metaDataUpdate.getCommitBuilder().setAuthor(updater);
+        metaDataUpdate.getCommitBuilder().setCommitter(updater);
+        return extIdNotes.commit(metaDataUpdate).getId();
+      }
+    }
+  }
+
+  private void assertAllFilesHaveSlashesInPath() throws Exception {
+    assertThat(allFilesInExternalIdRef().stream().allMatch(f -> f.contains("/"))).isTrue();
+  }
+
+  private void assertNoFilesHaveSlashesInPath() throws Exception {
+    assertThat(allFilesInExternalIdRef().stream().noneMatch(f -> f.contains("/"))).isTrue();
+  }
+
+  private ImmutableList<String> allFilesInExternalIdRef() throws Exception {
+    try (Repository repo = repoManager.openRepository(ALL_USERS);
+        TreeWalk treeWalk = new TreeWalk(repo);
+        RevWalk rw = new RevWalk(repo)) {
+      treeWalk.reset(
+          rw.parseCommit(repo.exactRef(RefNames.REFS_EXTERNAL_IDS).getObjectId()).getTree());
+      treeWalk.setRecursive(true);
+      ImmutableList.Builder<String> allPaths = ImmutableList.builder();
+      while (treeWalk.next()) {
+        allPaths.add(treeWalk.getPathString());
+      }
+      return allPaths.build();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java b/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java
index 13de3e7..ba40d8c 100644
--- a/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java
+++ b/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java
@@ -24,8 +24,8 @@
 import static com.google.gerrit.server.auth.ldap.LdapModule.USERNAME_CACHE;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.testing.InMemoryModule;
@@ -64,7 +64,7 @@
   }
 
   private ExternalId id(String scheme, String id) {
-    return ExternalId.create(scheme, id, new Account.Id(1000));
+    return ExternalId.create(scheme, id, Account.id(1000));
   }
 
   private boolean accountBelongsToRealm(ExternalId... ids) {
diff --git a/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java
index 7a0661a..dc62a61 100644
--- a/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java
@@ -19,7 +19,7 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.inject.Guice;
@@ -39,7 +39,7 @@
   }
 
   private ExternalId id(String scheme, String id) {
-    return ExternalId.create(scheme, id, new Account.Id(1000));
+    return ExternalId.create(scheme, id, Account.id(1000));
   }
 
   private boolean accountBelongsToRealm(ExternalId... ids) {
diff --git a/javatests/com/google/gerrit/server/cache/BUILD b/javatests/com/google/gerrit/server/cache/BUILD
index 0c3bf3e..346c578 100644
--- a/javatests/com/google/gerrit/server/cache/BUILD
+++ b/javatests/com/google/gerrit/server/cache/BUILD
@@ -7,10 +7,10 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//javatests/com/google/gerrit/util/http/testutil",
+        "//lib:jgit",
         "//lib:junit",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
-        "@jgit-lib//jar",
         "@servlet-api//jar",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
index 2e7d8eb..35a28c0 100644
--- a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.cache.PerThreadCache.Key;
 import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow;
 import com.google.gerrit.server.git.RefCache;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 import java.io.IOException;
 import java.util.Optional;
@@ -32,7 +31,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class PerThreadCacheTest extends GerritBaseTests {
+public class PerThreadCacheTest {
   @Test
   public void key_respectsClass() {
     assertThat(PerThreadCache.Key.create(String.class))
@@ -96,9 +95,9 @@
   @Test
   public void doubleInstantiationFails() {
     try (PerThreadCache ignored = PerThreadCache.create(null)) {
-      exception.expect(IllegalStateException.class);
-      exception.expectMessage("called create() twice on the same request");
-      PerThreadCache.create(null);
+      IllegalStateException thrown =
+          assertThrows(IllegalStateException.class, () -> PerThreadCache.create(null));
+      assertThat(thrown).hasMessageThat().contains("called create() twice on the same request");
     }
   }
 
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 147aeeb..69c2799 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.cache.h2;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
@@ -66,22 +67,22 @@
                   return "bar";
                 }))
         .isEqualTo("bar");
-    assertThat(called.get()).named("Callable was called").isTrue();
-    assertThat(impl.getIfPresent("foo")).named("in-memory value").isEqualTo("bar");
+    assertWithMessage("Callable was called").that(called.get()).isTrue();
+    assertWithMessage("in-memory value").that(impl.getIfPresent("foo")).isEqualTo("bar");
     mem.invalidate("foo");
-    assertThat(impl.getIfPresent("foo")).named("persistent value").isEqualTo("bar");
+    assertWithMessage("persistent value").that(impl.getIfPresent("foo")).isEqualTo("bar");
 
     called.set(false);
-    assertThat(
+    assertWithMessage("cached value")
+        .that(
             impl.get(
                 "foo",
                 () -> {
                   called.set(true);
                   return "baz";
                 }))
-        .named("cached value")
         .isEqualTo("bar");
-    assertThat(called.get()).named("Callable was called").isFalse();
+    assertWithMessage("Callable was called").that(called.get()).isFalse();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/cache/serialize/BUILD b/javatests/com/google/gerrit/server/cache/serialize/BUILD
index a0d5ea6..ce5f273 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/BUILD
+++ b/javatests/com/google/gerrit/server/cache/serialize/BUILD
@@ -4,16 +4,16 @@
     name = "tests",
     srcs = glob(["*.java"]),
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server/cache/serialize",
         "//java/com/google/gerrit/server/cache/testing",
         "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//java/com/google/gwtorm",
         "//lib:guava",
+        "//lib:jgit",
         "//lib:junit",
         "//lib:protobuf",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
         "//lib/truth:truth-proto-extension",
         "//proto:cache_java_proto",
diff --git a/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
index c634a78..ebd7d55 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
@@ -15,14 +15,12 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.gerrit.testing.GerritBaseTests;
-import com.google.protobuf.TextFormat;
 import org.junit.Test;
 
-public class BooleanCacheSerializerTest extends GerritBaseTests {
+public class BooleanCacheSerializerTest {
   @Test
   public void serialize() throws Exception {
     assertThat(BooleanCacheSerializer.INSTANCE.serialize(true))
@@ -53,11 +51,6 @@
   }
 
   private static void assertDeserializeFails(byte[] in) {
-    try {
-      BooleanCacheSerializer.INSTANCE.deserialize(in);
-      assert_().fail("expected deserialization to fail for \"%s\"", TextFormat.escapeBytes(in));
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(RuntimeException.class, () -> BooleanCacheSerializer.INSTANCE.deserialize(in));
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/CacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/CacheSerializerTest.java
new file mode 100644
index 0000000..819189f
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/CacheSerializerTest.java
@@ -0,0 +1,50 @@
+// 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.
+
+package com.google.gerrit.server.cache.serialize;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Converter;
+import org.junit.Test;
+
+public class CacheSerializerTest {
+  @AutoValue
+  abstract static class MyAutoValue {
+    static MyAutoValue create(int val) {
+      return new AutoValue_CacheSerializerTest_MyAutoValue(val);
+    }
+
+    abstract int val();
+  }
+
+  private static final CacheSerializer<MyAutoValue> SERIALIZER =
+      CacheSerializer.convert(
+          IntegerCacheSerializer.INSTANCE, Converter.from(MyAutoValue::val, MyAutoValue::create));
+
+  @Test
+  public void serialize() throws Exception {
+    MyAutoValue v = MyAutoValue.create(1234);
+    byte[] serialized = SERIALIZER.serialize(v);
+    assertThat(serialized).isEqualTo(new byte[] {-46, 9});
+    assertThat(SERIALIZER.deserialize(serialized).val()).isEqualTo(1234);
+  }
+
+  @Test
+  public void deserializeNullFails() throws Exception {
+    assertThrows(RuntimeException.class, () -> SERIALIZER.deserialize(null));
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
index c6efc21..7bfcc59 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
@@ -15,13 +15,12 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class EnumCacheSerializerTest extends GerritBaseTests {
+public class EnumCacheSerializerTest {
   @Test
   public void serialize() throws Exception {
     assertRoundTrip(MyEnum.FOO);
@@ -50,11 +49,6 @@
 
   private static void assertDeserializeFails(byte[] in) {
     CacheSerializer<MyEnum> s = new EnumCacheSerializer<>(MyEnum.class);
-    try {
-      s.deserialize(in);
-      assert_().fail("expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(RuntimeException.class, () -> s.deserialize(in));
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java
deleted file mode 100644
index 56dd6ad..0000000
--- a/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.cache.serialize;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
-
-import com.google.gerrit.testing.GerritBaseTests;
-import com.google.gwtorm.client.IntKey;
-import com.google.gwtorm.client.Key;
-import org.junit.Test;
-
-public class IntKeyCacheSerializerTest extends GerritBaseTests {
-
-  private static class MyIntKey extends IntKey<Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    private int val;
-
-    MyIntKey(int val) {
-      this.val = val;
-    }
-
-    @Override
-    public int get() {
-      return val;
-    }
-
-    @Override
-    protected void set(int newValue) {
-      this.val = newValue;
-    }
-  }
-
-  private static final IntKeyCacheSerializer<MyIntKey> SERIALIZER =
-      new IntKeyCacheSerializer<>(MyIntKey::new);
-
-  @Test
-  public void serialize() throws Exception {
-    MyIntKey k = new MyIntKey(1234);
-    byte[] serialized = SERIALIZER.serialize(k);
-    assertThat(serialized).isEqualTo(new byte[] {-46, 9});
-    assertThat(SERIALIZER.deserialize(serialized).get()).isEqualTo(1234);
-  }
-
-  @Test
-  public void deserializeNullFails() throws Exception {
-    try {
-      SERIALIZER.deserialize(null);
-      assert_().fail("expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
index 1d54010..40ff0ac 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
@@ -14,16 +14,15 @@
 
 package com.google.gerrit.server.cache.serialize;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Bytes;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.protobuf.TextFormat;
 import org.junit.Test;
 
-public class IntegerCacheSerializerTest extends GerritBaseTests {
+public class IntegerCacheSerializerTest {
   @Test
   public void serialize() throws Exception {
     for (int i :
@@ -49,17 +48,12 @@
   private static void assertRoundTrip(int i) throws Exception {
     byte[] serialized = IntegerCacheSerializer.INSTANCE.serialize(i);
     int result = IntegerCacheSerializer.INSTANCE.deserialize(serialized);
-    assertThat(result)
-        .named("round-trip of %s via \"%s\"", i, TextFormat.escapeBytes(serialized))
+    assertWithMessage("round-trip of %s via \"%s\"", i, TextFormat.escapeBytes(serialized))
+        .that(result)
         .isEqualTo(i);
   }
 
   private static void assertDeserializeFails(byte[] in) {
-    try {
-      IntegerCacheSerializer.INSTANCE.deserialize(in);
-      assert_().fail("expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(RuntimeException.class, () -> IntegerCacheSerializer.INSTANCE.deserialize(in));
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java
index 9fcb8a4..effc801 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java
@@ -17,11 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.auto.value.AutoValue;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Project;
 import java.io.Serializable;
 import org.junit.Test;
 
-public class JavaCacheSerializerTest extends GerritBaseTests {
+public class JavaCacheSerializerTest {
   @Test
   public void builtInTypes() throws Exception {
     assertRoundTrip("foo");
@@ -34,6 +34,11 @@
     assertRoundTrip(new AutoValue_JavaCacheSerializerTest_MyType(123, "four five six"));
   }
 
+  @Test
+  public void gerritEntities() throws Exception {
+    assertRoundTrip(Project.nameKey("foo"));
+  }
+
   @AutoValue
   abstract static class MyType implements Serializable {
     private static final long serialVersionUID = 1L;
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java
index 257be54..7d6647a 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java
@@ -15,14 +15,13 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteArray;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class ObjectIdCacheSerializerTest extends GerritBaseTests {
+public class ObjectIdCacheSerializerTest {
   @Test
   public void serialize() {
     ObjectId id = ObjectId.fromString("aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
@@ -47,11 +46,7 @@
   }
 
   private void assertDeserializeFails(byte[] bytes) {
-    try {
-      ObjectIdCacheSerializer.INSTANCE.deserialize(bytes);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class, () -> ObjectIdCacheSerializer.INSTANCE.deserialize(bytes));
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
index c5ea2ea..f6d6c8a 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
@@ -15,15 +15,14 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.protobuf.ByteString;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class ObjectIdConverterTest extends GerritBaseTests {
+public class ObjectIdConverterTest {
   @Test
   public void objectIdFromByteString() {
     ObjectIdConverter idConverter = ObjectIdConverter.create();
@@ -43,12 +42,9 @@
 
   @Test
   public void objectIdFromByteStringWrongSize() {
-    try {
-      ObjectIdConverter.create().fromByteString(ByteString.copyFromUtf8("foo"));
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> ObjectIdConverter.create().fromByteString(ByteString.copyFromUtf8("foo")));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java
index 845da9b..04d2f73 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java
@@ -17,10 +17,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.proto.testing.Test.SerializableProto;
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class ProtobufSerializerTest extends GerritBaseTests {
+public class ProtobufSerializerTest {
   @Test
   public void requiredAndOptionalTypes() {
     assertRoundTrip(SerializableProto.newBuilder().setId(123));
diff --git a/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
index ff0cf9a..dc22805 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
@@ -15,14 +15,13 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.StandardCharsets;
 import org.junit.Test;
 
-public class StringCacheSerializerTest extends GerritBaseTests {
+public class StringCacheSerializerTest {
   @Test
   public void serialize() {
     assertThat(StringCacheSerializer.INSTANCE.serialize("")).isEmpty();
@@ -35,12 +34,11 @@
   @Test
   public void serializeInvalidChar() {
     // Can't use UTF-8 for the test, since it can encode all Unicode code points.
-    try {
-      StringCacheSerializer.serialize(StandardCharsets.US_ASCII, "\u1234");
-      assert_().fail("expected IllegalStateException");
-    } catch (IllegalStateException expected) {
-      assertThat(expected).hasCauseThat().isInstanceOf(CharacterCodingException.class);
-    }
+    IllegalStateException thrown =
+        assertThrows(
+            IllegalStateException.class,
+            () -> StringCacheSerializer.serialize(StandardCharsets.US_ASCII, "\u1234"));
+    assertThat(thrown).hasCauseThat().isInstanceOf(CharacterCodingException.class);
   }
 
   @Test
@@ -56,11 +54,10 @@
 
   @Test
   public void deserializeInvalidChar() {
-    try {
-      StringCacheSerializer.INSTANCE.deserialize(new byte[] {(byte) 0xff});
-      assert_().fail("expected IllegalStateException");
-    } catch (IllegalStateException expected) {
-      assertThat(expected).hasCauseThat().isInstanceOf(CharacterCodingException.class);
-    }
+    IllegalStateException thrown =
+        assertThrows(
+            IllegalStateException.class,
+            () -> StringCacheSerializer.INSTANCE.deserialize(new byte[] {(byte) 0xff}));
+    assertThat(thrown).hasCauseThat().isInstanceOf(CharacterCodingException.class);
   }
 }
diff --git a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
index fffb1da..20813f6 100644
--- a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
@@ -24,11 +24,10 @@
 import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import com.google.gerrit.server.change.ChangeKindCacheImpl.Key;
-import com.google.gerrit.testing.GerritBaseTests;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class ChangeKindCacheImplTest extends GerritBaseTests {
+public class ChangeKindCacheImplTest {
   @Test
   public void keySerializer() throws Exception {
     ChangeKindCacheImpl.Key key =
diff --git a/javatests/com/google/gerrit/server/change/HashtagsTest.java b/javatests/com/google/gerrit/server/change/HashtagsTest.java
index 49d2952..780ac71 100644
--- a/javatests/com/google/gerrit/server/change/HashtagsTest.java
+++ b/javatests/com/google/gerrit/server/change/HashtagsTest.java
@@ -17,10 +17,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Sets;
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class HashtagsTest extends GerritBaseTests {
+public class HashtagsTest {
   @Test
   public void emptyCommitMessage() throws Exception {
     assertThat(HashtagsUtil.extractTags("")).isEmpty();
diff --git a/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java b/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
index 0cfe483..b69a894 100644
--- a/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
+++ b/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
@@ -15,19 +15,22 @@
 package com.google.gerrit.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_TAGS;
+import static com.google.gerrit.entities.RefNames.REFS_TAGS;
 
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.common.truth.Correspondence;
+import com.google.gerrit.truth.NullAwareCorrespondence;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
 import org.junit.Before;
 import org.junit.Test;
 
-public class IncludedInResolverTest extends GerritBaseTests {
+public class IncludedInResolverTest {
   // Branch names
   private static final String BRANCH_MASTER = "master";
   private static final String BRANCH_1_0 = "rel-1.0";
@@ -113,8 +116,12 @@
     IncludedInResolver.Result detail = resolve(commit_v2_5);
 
     // Check that only tags and branches which refer the tip are returned
-    assertThat(detail.tags()).containsExactly(TAG_2_5, TAG_2_5_ANNOTATED, TAG_2_5_ANNOTATED_TWICE);
-    assertThat(detail.branches()).containsExactly(BRANCH_2_5);
+    assertThat(detail.tags())
+        .comparingElementsUsing(hasShortName())
+        .containsExactly(TAG_2_5, TAG_2_5_ANNOTATED, TAG_2_5_ANNOTATED_TWICE);
+    assertThat(detail.branches())
+        .comparingElementsUsing(hasShortName())
+        .containsExactly(BRANCH_2_5);
   }
 
   @Test
@@ -124,6 +131,7 @@
 
     // Check whether all tags and branches are returned
     assertThat(detail.tags())
+        .comparingElementsUsing(hasShortName())
         .containsExactly(
             TAG_1_0,
             TAG_1_0_1,
@@ -134,6 +142,7 @@
             TAG_2_5_ANNOTATED,
             TAG_2_5_ANNOTATED_TWICE);
     assertThat(detail.branches())
+        .comparingElementsUsing(hasShortName())
         .containsExactly(BRANCH_MASTER, BRANCH_1_0, BRANCH_1_3, BRANCH_2_0, BRANCH_2_5);
   }
 
@@ -144,8 +153,11 @@
 
     // Check whether all succeeding tags and branches are returned
     assertThat(detail.tags())
+        .comparingElementsUsing(hasShortName())
         .containsExactly(TAG_1_3, TAG_2_5, TAG_2_5_ANNOTATED, TAG_2_5_ANNOTATED_TWICE);
-    assertThat(detail.branches()).containsExactly(BRANCH_1_3, BRANCH_2_5);
+    assertThat(detail.branches())
+        .comparingElementsUsing(hasShortName())
+        .containsExactly(BRANCH_1_3, BRANCH_2_5);
   }
 
   private IncludedInResolver.Result resolve(RevCommit commit) throws Exception {
@@ -155,4 +167,9 @@
   private RevTag tag(String name, RevObject dest) throws Exception {
     return tr.update(REFS_TAGS + name, tr.tag(name, dest));
   }
+
+  private static Correspondence<Ref, String> hasShortName() {
+    return NullAwareCorrespondence.transforming(
+        ref -> Repository.shortenRefName(ref.getName()), "has short name");
+  }
 }
diff --git a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
index 6e02d61..c259e60 100644
--- a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
+++ b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
@@ -14,24 +14,25 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.common.data.Permission.forLabel;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.allow;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
@@ -45,7 +46,6 @@
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
@@ -57,7 +57,7 @@
 import org.junit.Test;
 
 /** Unit tests for {@link LabelNormalizer}. */
-public class LabelNormalizerTest extends GerritBaseTests {
+public class LabelNormalizerTest {
   @Inject private AccountManager accountManager;
   @Inject private AllProjectsName allProjects;
   @Inject private GitRepositoryManager repoManager;
@@ -70,6 +70,7 @@
   @Inject private ChangeNotes.Factory changeNotesFactory;
   @Inject private ProjectConfig.Factory projectConfigFactory;
   @Inject private GerritApi gApi;
+  @Inject private ProjectOperations projectOperations;
 
   private LifecycleManager lifecycle;
   private Account.Id userId;
@@ -103,7 +104,7 @@
       }
     }
     LabelType lt =
-        category("Verified", value(1, "Verified"), value(0, "No score"), value(-1, "Fails"));
+        label("Verified", value(1, "Verified"), value(0, "No score"), value(-1, "Fails"));
     pc.getLabelSections().put(lt.getName(), lt);
     save(pc);
   }
@@ -115,7 +116,7 @@
     input.newBranch = true;
     input.subject = "Test change";
     ChangeInfo info = gApi.changes().create(input).get();
-    notes = changeNotesFactory.createChecked(allProjects, new Change.Id(info._number));
+    notes = changeNotesFactory.createChecked(allProjects, Change.id(info._number));
     change = notes.getChange();
   }
 
@@ -129,10 +130,11 @@
 
   @Test
   public void noNormalizeByPermission() throws Exception {
-    ProjectConfig pc = loadAllProjects();
-    allow(pc, forLabel("Code-Review"), -1, 1, REGISTERED_USERS, "refs/heads/*");
-    allow(pc, forLabel("Verified"), -1, 1, REGISTERED_USERS, "refs/heads/*");
-    save(pc);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
+        .add(allowLabel("Verified").ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
+        .update();
 
     PatchSetApproval cr = psa(userId, "Code-Review", 2);
     PatchSetApproval v = psa(userId, "Verified", 1);
@@ -141,10 +143,11 @@
 
   @Test
   public void normalizeByType() throws Exception {
-    ProjectConfig pc = loadAllProjects();
-    allow(pc, forLabel("Code-Review"), -5, 5, REGISTERED_USERS, "refs/heads/*");
-    allow(pc, forLabel("Verified"), -5, 5, REGISTERED_USERS, "refs/heads/*");
-    save(pc);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-5, 5))
+        .add(allowLabel("Verified").ref("refs/heads/*").group(REGISTERED_USERS).range(-5, 5))
+        .update();
 
     PatchSetApproval cr = psa(userId, "Code-Review", 5);
     PatchSetApproval v = psa(userId, "Verified", 5);
@@ -162,9 +165,10 @@
 
   @Test
   public void explicitZeroVoteOnNonEmptyRangeIsPresent() throws Exception {
-    ProjectConfig pc = loadAllProjects();
-    allow(pc, forLabel("Code-Review"), -1, 1, REGISTERED_USERS, "refs/heads/*");
-    save(pc);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
+        .update();
 
     PatchSetApproval cr = psa(userId, "Code-Review", 0);
     PatchSetApproval v = psa(userId, "Verified", 0);
@@ -187,16 +191,15 @@
   }
 
   private PatchSetApproval psa(Account.Id accountId, String label, int value) {
-    return new PatchSetApproval(
-        new PatchSetApproval.Key(change.currentPatchSetId(), accountId, new LabelId(label)),
-        (short) value,
-        TimeUtil.nowTs());
+    return PatchSetApproval.builder()
+        .key(PatchSetApproval.key(change.currentPatchSetId(), accountId, LabelId.create(label)))
+        .value(value)
+        .granted(TimeUtil.nowTs())
+        .build();
   }
 
   private PatchSetApproval copy(PatchSetApproval src, int newValue) {
-    PatchSetApproval result = new PatchSetApproval(src.getKey().getParentKey(), src);
-    result.setValue((short) newValue);
-    return result;
+    return src.toBuilder().value(newValue).build();
   }
 
   private static List<PatchSetApproval> list(PatchSetApproval... psas) {
diff --git a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
index 46ddbc2..19c8998 100644
--- a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
@@ -23,11 +23,10 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.gerrit.server.cache.proto.Cache.MergeabilityKeyProto;
-import com.google.gerrit.testing.GerritBaseTests;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class MergeabilityCacheImplTest extends GerritBaseTests {
+public class MergeabilityCacheImplTest {
   @Test
   public void keySerializer() throws Exception {
     MergeabilityCacheImpl.EntryKey key =
diff --git a/javatests/com/google/gerrit/server/change/WalkSorterTest.java b/javatests/com/google/gerrit/server/change/WalkSorterTest.java
index 189dfbc..2c4c98f 100644
--- a/javatests/com/google/gerrit/server/change/WalkSorterTest.java
+++ b/javatests/com/google/gerrit/server/change/WalkSorterTest.java
@@ -19,14 +19,12 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.change.WalkSorter.PatchSetData;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
 import com.google.gerrit.testing.TestChanges;
@@ -39,13 +37,13 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class WalkSorterTest extends GerritBaseTests {
+public class WalkSorterTest {
   private Account.Id userId;
   private InMemoryRepositoryManager repoManager;
 
   @Before
   public void setUp() {
-    userId = new Account.Id(1);
+    userId = Account.id(1);
     repoManager = new InMemoryRepositoryManager();
   }
 
@@ -280,7 +278,7 @@
 
     // If we restrict to PS1 of each change, the sorter uses that commit.
     sorter.includePatchSets(
-        ImmutableSet.of(new PatchSet.Id(cd1.getId(), 1), new PatchSet.Id(cd2.getId(), 1)));
+        ImmutableSet.of(PatchSet.id(cd1.getId(), 1), PatchSet.id(cd2.getId(), 1)));
     assertSorted(
         sorter, changes, ImmutableList.of(patchSetData(cd2, 1, c2_1), patchSetData(cd1, 1, c1_1)));
   }
@@ -297,8 +295,7 @@
 
     List<ChangeData> changes = ImmutableList.of(cd1, cd2);
     WalkSorter sorter =
-        new WalkSorter(repoManager)
-            .includePatchSets(ImmutableSet.of(cd1.currentPatchSet().getId()));
+        new WalkSorter(repoManager).includePatchSets(ImmutableSet.of(cd1.currentPatchSet().id()));
 
     assertSorted(sorter, changes, ImmutableList.of(patchSetData(cd1, c1)));
   }
@@ -335,17 +332,15 @@
   private ChangeData newChange(TestRepository<Repo> tr, ObjectId id) throws Exception {
     Project.NameKey project = tr.getRepository().getDescription().getProject();
     Change c = TestChanges.newChange(project, userId);
-    ChangeData cd = ChangeData.createForTest(project, c.getId(), 1);
+    ChangeData cd = ChangeData.createForTest(project, c.getId(), 1, id);
     cd.setChange(c);
-    cd.currentPatchSet().setRevision(new RevId(id.name()));
     cd.setPatchSets(ImmutableList.of(cd.currentPatchSet()));
     return cd;
   }
 
   private PatchSet addPatchSet(ChangeData cd, ObjectId id) throws Exception {
     TestChanges.incrementPatchSet(cd.change());
-    PatchSet ps = new PatchSet(cd.change().currentPatchSetId());
-    ps.setRevision(new RevId(id.name()));
+    PatchSet ps = TestChanges.newPatchSet(cd.change().currentPatchSetId(), id.name(), userId);
     List<PatchSet> patchSets = new ArrayList<>(cd.patchSets());
     patchSets.add(ps);
     cd.setPatchSets(patchSets);
@@ -353,7 +348,7 @@
   }
 
   private TestRepository<Repo> newRepo(String name) throws Exception {
-    return new TestRepository<>(repoManager.createRepository(new Project.NameKey(name)));
+    return new TestRepository<>(repoManager.createRepository(Project.nameKey(name)));
   }
 
   private static PatchSetData patchSetData(ChangeData cd, RevCommit commit) throws Exception {
@@ -362,7 +357,7 @@
 
   private static PatchSetData patchSetData(ChangeData cd, int psId, RevCommit commit)
       throws Exception {
-    return PatchSetData.create(cd, cd.patchSet(new PatchSet.Id(cd.getId(), psId)), commit);
+    return PatchSetData.create(cd, cd.patchSet(PatchSet.id(cd.getId(), psId)), commit);
   }
 
   private static void assertSorted(
diff --git a/javatests/com/google/gerrit/server/config/AllProjectsNameTest.java b/javatests/com/google/gerrit/server/config/AllProjectsNameTest.java
new file mode 100644
index 0000000..64a5f83
--- /dev/null
+++ b/javatests/com/google/gerrit/server/config/AllProjectsNameTest.java
@@ -0,0 +1,40 @@
+// 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.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.Project;
+import org.junit.Test;
+
+public class AllProjectsNameTest {
+  @Test
+  public void equalToProjectNameKey() {
+    String name = "a-project";
+    AllProjectsName allProjectsName = new AllProjectsName(name);
+    Project.NameKey projectName = Project.nameKey(name);
+    assertThat(allProjectsName.get()).isEqualTo(projectName.get());
+    assertThat(allProjectsName).isEqualTo(projectName);
+  }
+
+  @Test
+  public void equalToAllUsersName() {
+    String name = "a-project";
+    AllProjectsName allProjectsName = new AllProjectsName(name);
+    AllUsersName allUsersName = new AllUsersName(name);
+    assertThat(allProjectsName.get()).isEqualTo(allUsersName.get());
+    assertThat(allProjectsName).isEqualTo(allUsersName);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/config/AllUsersNameTest.java b/javatests/com/google/gerrit/server/config/AllUsersNameTest.java
new file mode 100644
index 0000000..d46b7a6
--- /dev/null
+++ b/javatests/com/google/gerrit/server/config/AllUsersNameTest.java
@@ -0,0 +1,40 @@
+// 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.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.Project;
+import org.junit.Test;
+
+public class AllUsersNameTest {
+  @Test
+  public void equalToProjectNameKey() {
+    String name = "a-project";
+    AllUsersName allUsersName = new AllUsersName(name);
+    Project.NameKey projectName = Project.nameKey(name);
+    assertThat(allUsersName.get()).isEqualTo(projectName.get());
+    assertThat(allUsersName).isEqualTo(projectName);
+  }
+
+  @Test
+  public void equalToAllProjectsName() {
+    String name = "a-project";
+    AllUsersName allUsersName = new AllUsersName(name);
+    AllProjectsName allProjectsName = new AllProjectsName(name);
+    assertThat(allUsersName.get()).isEqualTo(allProjectsName.get());
+    assertThat(allUsersName).isEqualTo(allProjectsName);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
index b1378ad..035878c 100644
--- a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -15,20 +15,20 @@
 package com.google.gerrit.server.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-public class ConfigUtilTest extends GerritBaseTests {
+public class ConfigUtilTest {
   private static final String SECT = "foo";
   private static final String SUB = "bar";
 
@@ -85,17 +85,17 @@
     Config cfg = new Config();
     ConfigUtil.storeSection(cfg, SECT, SUB, in, d);
 
-    assertThat(cfg.getString(SECT, SUB, "CONSTANT")).isNull();
-    assertThat(cfg.getString(SECT, SUB, "missing")).isNull();
-    assertThat(cfg.getBoolean(SECT, SUB, "b", false)).isEqualTo(in.b);
-    assertThat(cfg.getBoolean(SECT, SUB, "bb", false)).isEqualTo(in.bb);
-    assertThat(cfg.getInt(SECT, SUB, "i", 0)).isEqualTo(0);
-    assertThat(cfg.getInt(SECT, SUB, "ii", 0)).isEqualTo(in.ii);
-    assertThat(cfg.getLong(SECT, SUB, "l", 0L)).isEqualTo(0L);
-    assertThat(cfg.getLong(SECT, SUB, "ll", 0L)).isEqualTo(in.ll);
-    assertThat(cfg.getString(SECT, SUB, "s")).isEqualTo(in.s);
-    assertThat(cfg.getString(SECT, SUB, "sd")).isNull();
-    assertThat(cfg.getString(SECT, SUB, "nd")).isNull();
+    assertThat(cfg).stringValue(SECT, SUB, "CONSTANT").isNull();
+    assertThat(cfg).stringValue(SECT, SUB, "missing").isNull();
+    assertThat(cfg).booleanValue(SECT, SUB, "b", false).isEqualTo(in.b);
+    assertThat(cfg).booleanValue(SECT, SUB, "bb", false).isEqualTo(in.bb);
+    assertThat(cfg).intValue(SECT, SUB, "i", 0).isEqualTo(0);
+    assertThat(cfg).intValue(SECT, SUB, "ii", 0).isEqualTo(in.ii);
+    assertThat(cfg).longValue(SECT, SUB, "l", 0L).isEqualTo(0L);
+    assertThat(cfg).longValue(SECT, SUB, "ll", 0L).isEqualTo(in.ll);
+    assertThat(cfg).stringValue(SECT, SUB, "s").isEqualTo(in.s);
+    assertThat(cfg).stringValue(SECT, SUB, "sd").isNull();
+    assertThat(cfg).stringValue(SECT, SUB, "nd").isNull();
 
     SectionInfo out = new SectionInfo();
     ConfigUtil.loadSection(cfg, SECT, SUB, out, d, null);
diff --git a/javatests/com/google/gerrit/server/config/GitwebConfigTest.java b/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
index bf7e4fd..cb6de34 100644
--- a/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class GitwebConfigTest extends GerritBaseTests {
+public class GitwebConfigTest {
   private static final String VALID_CHARACTERS = "*()";
   private static final String SOME_INVALID_CHARACTERS = "09AZaz$-_.+!',";
 
diff --git a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
index 30fabdc..708e247 100644
--- a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
+++ b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -18,16 +18,15 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.restapi.config.ListCapabilities;
 import com.google.gerrit.server.restapi.config.ListCapabilities.CapabilityInfo;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -36,7 +35,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class ListCapabilitiesTest extends GerritBaseTests {
+public class ListCapabilitiesTest {
   private Injector injector;
 
   @Before
@@ -74,7 +73,7 @@
   @Test
   public void list() throws Exception {
     Map<String, CapabilityInfo> m =
-        injector.getInstance(ListCapabilities.class).apply(new ConfigResource());
+        injector.getInstance(ListCapabilities.class).apply(new ConfigResource()).value();
     for (String id : GlobalCapability.getAllNames()) {
       assertThat(m).containsKey(id);
       assertThat(m.get(id).id).isEqualTo(id);
diff --git a/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java b/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
index 2a473f4..d7aae6a0 100644
--- a/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
@@ -18,9 +18,8 @@
 import static com.google.common.truth.Truth8.assertThat;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
@@ -28,7 +27,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class RepositoryConfigTest extends GerritBaseTests {
+public class RepositoryConfigTest {
 
   private Config cfg;
   private RepositoryConfig repoCfg;
@@ -42,35 +41,35 @@
   @Test
   public void defaultSubmitTypeWhenNotConfigured() {
     // Check expected value explicitly rather than depending on constant.
-    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(Project.nameKey("someProject")))
         .isEqualTo(SubmitType.INHERIT);
   }
 
   @Test
   public void defaultSubmitTypeForStarFilter() {
     configureDefaultSubmitType("*", SubmitType.CHERRY_PICK);
-    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(Project.nameKey("someProject")))
         .isEqualTo(SubmitType.CHERRY_PICK);
 
     configureDefaultSubmitType("*", SubmitType.FAST_FORWARD_ONLY);
-    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(Project.nameKey("someProject")))
         .isEqualTo(SubmitType.FAST_FORWARD_ONLY);
 
     configureDefaultSubmitType("*", SubmitType.REBASE_IF_NECESSARY);
-    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(Project.nameKey("someProject")))
         .isEqualTo(SubmitType.REBASE_IF_NECESSARY);
 
     configureDefaultSubmitType("*", SubmitType.REBASE_ALWAYS);
-    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(Project.nameKey("someProject")))
         .isEqualTo(SubmitType.REBASE_ALWAYS);
   }
 
   @Test
   public void defaultSubmitTypeForSpecificFilter() {
     configureDefaultSubmitType("someProject", SubmitType.CHERRY_PICK);
-    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someOtherProject")))
+    assertThat(repoCfg.getDefaultSubmitType(Project.nameKey("someOtherProject")))
         .isEqualTo(RepositoryConfig.DEFAULT_SUBMIT_TYPE);
-    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(Project.nameKey("someProject")))
         .isEqualTo(SubmitType.CHERRY_PICK);
   }
 
@@ -80,13 +79,13 @@
     configureDefaultSubmitType("somePath/*", SubmitType.CHERRY_PICK);
     configureDefaultSubmitType("*", SubmitType.MERGE_ALWAYS);
 
-    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(Project.nameKey("someProject")))
         .isEqualTo(SubmitType.MERGE_ALWAYS);
 
-    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("somePath/someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(Project.nameKey("somePath/someProject")))
         .isEqualTo(SubmitType.CHERRY_PICK);
 
-    assertThat(repoCfg.getDefaultSubmitType(new Project.NameKey("somePath/somePath/someProject")))
+    assertThat(repoCfg.getDefaultSubmitType(Project.nameKey("somePath/somePath/someProject")))
         .isEqualTo(SubmitType.REBASE_IF_NECESSARY);
   }
 
@@ -100,14 +99,14 @@
 
   @Test
   public void ownerGroupsWhenNotConfigured() {
-    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("someProject"))).isEmpty();
+    assertThat(repoCfg.getOwnerGroups(Project.nameKey("someProject"))).isEmpty();
   }
 
   @Test
   public void ownerGroupsForStarFilter() {
     ImmutableList<String> ownerGroups = ImmutableList.of("group1", "group2");
     configureOwnerGroups("*", ownerGroups);
-    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("someProject")))
+    assertThat(repoCfg.getOwnerGroups(Project.nameKey("someProject")))
         .containsExactlyElementsIn(ownerGroups);
   }
 
@@ -115,8 +114,8 @@
   public void ownerGroupsForSpecificFilter() {
     ImmutableList<String> ownerGroups = ImmutableList.of("group1", "group2");
     configureOwnerGroups("someProject", ownerGroups);
-    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("someOtherProject"))).isEmpty();
-    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("someProject")))
+    assertThat(repoCfg.getOwnerGroups(Project.nameKey("someOtherProject"))).isEmpty();
+    assertThat(repoCfg.getOwnerGroups(Project.nameKey("someProject")))
         .containsExactlyElementsIn(ownerGroups);
   }
 
@@ -130,13 +129,13 @@
     configureOwnerGroups("somePath/*", ownerGroups2);
     configureOwnerGroups("somePath/somePath/*", ownerGroups3);
 
-    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("someProject")))
+    assertThat(repoCfg.getOwnerGroups(Project.nameKey("someProject")))
         .containsExactlyElementsIn(ownerGroups1);
 
-    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("somePath/someProject")))
+    assertThat(repoCfg.getOwnerGroups(Project.nameKey("somePath/someProject")))
         .containsExactlyElementsIn(ownerGroups2);
 
-    assertThat(repoCfg.getOwnerGroups(new Project.NameKey("somePath/somePath/someProject")))
+    assertThat(repoCfg.getOwnerGroups(Project.nameKey("somePath/somePath/someProject")))
         .containsExactlyElementsIn(ownerGroups3);
   }
 
@@ -150,24 +149,22 @@
 
   @Test
   public void basePathWhenNotConfigured() {
-    assertThat(repoCfg.getBasePath(new Project.NameKey("someProject"))).isNull();
+    assertThat(repoCfg.getBasePath(Project.nameKey("someProject"))).isNull();
   }
 
   @Test
   public void basePathForStarFilter() {
     String basePath = "/someAbsolutePath/someDirectory";
     configureBasePath("*", basePath);
-    assertThat(repoCfg.getBasePath(new Project.NameKey("someProject")).toString())
-        .isEqualTo(basePath);
+    assertThat(repoCfg.getBasePath(Project.nameKey("someProject")).toString()).isEqualTo(basePath);
   }
 
   @Test
   public void basePathForSpecificFilter() {
     String basePath = "/someAbsolutePath/someDirectory";
     configureBasePath("someProject", basePath);
-    assertThat(repoCfg.getBasePath(new Project.NameKey("someOtherProject"))).isNull();
-    assertThat(repoCfg.getBasePath(new Project.NameKey("someProject")).toString())
-        .isEqualTo(basePath);
+    assertThat(repoCfg.getBasePath(Project.nameKey("someOtherProject"))).isNull();
+    assertThat(repoCfg.getBasePath(Project.nameKey("someProject")).toString()).isEqualTo(basePath);
   }
 
   @Test
@@ -182,14 +179,12 @@
     configureBasePath("project/*", basePath3);
     configureBasePath("*", basePath4);
 
-    assertThat(repoCfg.getBasePath(new Project.NameKey("project1")).toString())
-        .isEqualTo(basePath1);
-    assertThat(repoCfg.getBasePath(new Project.NameKey("project/project/someProject")).toString())
+    assertThat(repoCfg.getBasePath(Project.nameKey("project1")).toString()).isEqualTo(basePath1);
+    assertThat(repoCfg.getBasePath(Project.nameKey("project/project/someProject")).toString())
         .isEqualTo(basePath2);
-    assertThat(repoCfg.getBasePath(new Project.NameKey("project/someProject")).toString())
+    assertThat(repoCfg.getBasePath(Project.nameKey("project/someProject")).toString())
         .isEqualTo(basePath3);
-    assertThat(repoCfg.getBasePath(new Project.NameKey("someProject")).toString())
-        .isEqualTo(basePath4);
+    assertThat(repoCfg.getBasePath(Project.nameKey("someProject")).toString()).isEqualTo(basePath4);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
index 6926052..55f0374 100644
--- a/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
@@ -22,7 +22,6 @@
 import static java.util.concurrent.TimeUnit.MINUTES;
 
 import com.google.gerrit.server.config.ScheduleConfig.Schedule;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
@@ -32,7 +31,7 @@
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-public class ScheduleConfigTest extends GerritBaseTests {
+public class ScheduleConfigTest {
 
   // Friday June 13, 2014 10:00 UTC
   private static final ZonedDateTime NOW =
diff --git a/javatests/com/google/gerrit/server/config/SitePathsTest.java b/javatests/com/google/gerrit/server/config/SitePathsTest.java
index b4cde14..1e5f41d 100644
--- a/javatests/com/google/gerrit/server/config/SitePathsTest.java
+++ b/javatests/com/google/gerrit/server/config/SitePathsTest.java
@@ -16,9 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.server.ioutil.HostPlatform;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.NotDirectoryException;
@@ -26,7 +26,7 @@
 import java.nio.file.Paths;
 import org.junit.Test;
 
-public class SitePathsTest extends GerritBaseTests {
+public class SitePathsTest {
   @Test
   public void create_NotExisting() throws IOException {
     final Path root = random();
@@ -72,8 +72,8 @@
     final Path root = random();
     try {
       Files.createFile(root);
-      exception.expect(NotDirectoryException.class);
-      new SitePaths(root);
+      assertThrows(NotDirectoryException.class, () -> new SitePaths(root));
+
     } finally {
       Files.delete(root);
     }
diff --git a/javatests/com/google/gerrit/server/edit/ChangeEditTest.java b/javatests/com/google/gerrit/server/edit/ChangeEditTest.java
index 4c0b5a1..2bad815 100644
--- a/javatests/com/google/gerrit/server/edit/ChangeEditTest.java
+++ b/javatests/com/google/gerrit/server/edit/ChangeEditTest.java
@@ -16,19 +16,18 @@
 
 import static org.junit.Assert.assertEquals;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.RefNames;
 import org.junit.Test;
 
-public class ChangeEditTest extends GerritBaseTests {
+public class ChangeEditTest {
   @Test
   public void changeEditRef() throws Exception {
-    Account.Id accountId = new Account.Id(1000042);
-    Change.Id changeId = new Change.Id(56414);
-    PatchSet.Id psId = new PatchSet.Id(changeId, 50);
+    Account.Id accountId = Account.id(1000042);
+    Change.Id changeId = Change.id(56414);
+    PatchSet.Id psId = PatchSet.id(changeId, 50);
     String refName = RefNames.refsEdit(accountId, changeId, psId);
     assertEquals("refs/users/42/1000042/edit-56414/50", refName);
   }
diff --git a/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java b/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
index 265b24e..a618c9e 100644
--- a/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
+++ b/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
@@ -25,31 +25,38 @@
 import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
 
-public class ChangeFileContentModificationSubject
-    extends Subject<ChangeFileContentModificationSubject, ChangeFileContentModification> {
+public class ChangeFileContentModificationSubject extends Subject {
 
   public static ChangeFileContentModificationSubject assertThat(
       ChangeFileContentModification modification) {
-    return assertAbout(ChangeFileContentModificationSubject::new).that(modification);
+    return assertAbout(modifications()).that(modification);
   }
 
+  public static Factory<ChangeFileContentModificationSubject, ChangeFileContentModification>
+      modifications() {
+    return ChangeFileContentModificationSubject::new;
+  }
+
+  private final ChangeFileContentModification modification;
+
   private ChangeFileContentModificationSubject(
       FailureMetadata failureMetadata, ChangeFileContentModification modification) {
     super(failureMetadata, modification);
+    this.modification = modification;
   }
 
   public StringSubject filePath() {
     isNotNull();
-    return check("filePath()").that(actual().getFilePath());
+    return check("getFilePath()").that(modification.getFilePath());
   }
 
   public StringSubject newContent() throws IOException {
     isNotNull();
-    RawInput newContent = actual().getNewContent();
-    check("newContent()").that(newContent).isNotNull();
+    RawInput newContent = modification.getNewContent();
+    check("getNewContent()").that(newContent).isNotNull();
     String contentString =
         CharStreams.toString(
             new InputStreamReader(newContent.getInputStream(), StandardCharsets.UTF_8));
-    return check("newContent()").that(contentString);
+    return check("getNewContent()").that(contentString);
   }
 }
diff --git a/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java b/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
index bd9d4df..d5b70bb 100644
--- a/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
+++ b/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.truth.ListSubject;
 import java.util.List;
 
-public class TreeModificationSubject extends Subject<TreeModificationSubject, TreeModification> {
+public class TreeModificationSubject extends Subject {
 
   public static TreeModificationSubject assertThat(TreeModification treeModification) {
     return assertAbout(treeModifications()).that(treeModification);
@@ -33,19 +33,21 @@
 
   public static ListSubject<TreeModificationSubject, TreeModification> assertThatList(
       List<TreeModification> treeModifications) {
-    return assertAbout(ListSubject.elements())
-        .thatCustom(treeModifications, treeModifications())
-        .named("treeModifications");
+    return ListSubject.assertThat(treeModifications, treeModifications());
   }
 
+  private final TreeModification treeModification;
+
   private TreeModificationSubject(
       FailureMetadata failureMetadata, TreeModification treeModification) {
     super(failureMetadata, treeModification);
+    this.treeModification = treeModification;
   }
 
   public ChangeFileContentModificationSubject asChangeFileContentModification() {
     isInstanceOf(ChangeFileContentModification.class);
-    return ChangeFileContentModificationSubject.assertThat(
-        (ChangeFileContentModification) actual());
+    return check("asChangeFileContentModification()")
+        .about(ChangeFileContentModificationSubject.modifications())
+        .that((ChangeFileContentModification) treeModification);
   }
 }
diff --git a/javatests/com/google/gerrit/server/events/BUILD b/javatests/com/google/gerrit/server/events/BUILD
new file mode 100644
index 0000000..eed83c8
--- /dev/null
+++ b/javatests/com/google/gerrit/server/events/BUILD
@@ -0,0 +1,15 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "events_tests",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/util/time",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:gson",
+        "//lib:guava",
+        "//lib/truth",
+    ],
+)
diff --git a/javatests/com/google/gerrit/server/events/EventDeserializerTest.java b/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
index eac8d0d..e0223e4 100644
--- a/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
+++ b/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
@@ -18,38 +18,32 @@
 
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.RefUpdateAttribute;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
+import java.sql.Timestamp;
 import org.junit.Test;
 
-public class EventDeserializerTest extends GerritBaseTests {
+public class EventDeserializerTest {
+  private final Gson gson = new EventGsonProvider().get();
 
   @Test
   public void refUpdatedEvent() {
-    RefUpdatedEvent refUpdatedEvent = new RefUpdatedEvent();
-
+    RefUpdatedEvent orig = new RefUpdatedEvent();
     RefUpdateAttribute refUpdatedAttribute = new RefUpdateAttribute();
     refUpdatedAttribute.refName = "refs/heads/master";
-    refUpdatedEvent.refUpdate = createSupplier(refUpdatedAttribute);
+    orig.refUpdate = createSupplier(refUpdatedAttribute);
 
     AccountAttribute accountAttribute = new AccountAttribute();
     accountAttribute.email = "some.user@domain.com";
-    refUpdatedEvent.submitter = createSupplier(accountAttribute);
+    orig.submitter = createSupplier(accountAttribute);
 
-    Gson gsonSerializer =
-        new GsonBuilder().registerTypeAdapter(Supplier.class, new SupplierSerializer()).create();
-    String serializedEvent = gsonSerializer.toJson(refUpdatedEvent);
-
-    Gson gsonDeserializer =
-        new GsonBuilder()
-            .registerTypeAdapter(Event.class, new EventDeserializer())
-            .registerTypeAdapter(Supplier.class, new SupplierDeserializer())
-            .create();
-
-    RefUpdatedEvent e = (RefUpdatedEvent) gsonDeserializer.fromJson(serializedEvent, Event.class);
+    RefUpdatedEvent e = roundTrip(orig);
 
     assertThat(e).isNotNull();
     assertThat(e.refUpdate).isInstanceOf(Supplier.class);
@@ -58,7 +52,271 @@
     assertThat(e.submitter.get().email).isEqualTo(accountAttribute.email);
   }
 
+  @Test
+  public void patchSetCreatedEvent() {
+    Change change = newChange();
+    PatchSetCreatedEvent orig = new PatchSetCreatedEvent(change);
+    orig.change = asChangeAttribute(change);
+    orig.uploader = newAccount("uploader");
+
+    PatchSetCreatedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+    assertSameAccount(e.uploader, orig.uploader);
+  }
+
+  @Test
+  public void assigneeChangedEvent() {
+    Change change = newChange();
+    AssigneeChangedEvent orig = new AssigneeChangedEvent(change);
+    orig.change = asChangeAttribute(change);
+    orig.changer = newAccount("changer");
+    orig.oldAssignee = newAccount("oldAssignee");
+
+    AssigneeChangedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+    assertSameAccount(e.changer, orig.changer);
+    assertSameAccount(e.oldAssignee, orig.oldAssignee);
+  }
+
+  @Test
+  public void changeDeletedEvent() {
+    Change change = newChange();
+    ChangeDeletedEvent orig = new ChangeDeletedEvent(change);
+    orig.change = asChangeAttribute(change);
+    orig.deleter = newAccount("deleter");
+
+    ChangeDeletedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+    assertSameAccount(e.deleter, orig.deleter);
+  }
+
+  @Test
+  public void hashtagsChangedEvent() {
+    Change change = newChange();
+    HashtagsChangedEvent orig = new HashtagsChangedEvent(change);
+    orig.change = asChangeAttribute(change);
+    orig.editor = newAccount("editor");
+    orig.added = new String[] {"added"};
+    orig.removed = new String[] {"removed"};
+    orig.hashtags = new String[] {"hashtags"};
+
+    HashtagsChangedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+    assertSameAccount(e.editor, orig.editor);
+    assertThat(e.added).isEqualTo(orig.added);
+    assertThat(e.removed).isEqualTo(orig.removed);
+    assertThat(e.hashtags).isEqualTo(orig.hashtags);
+  }
+
+  @Test
+  public void changeAbandonedEvent() {
+    Change change = newChange();
+    ChangeAbandonedEvent orig = new ChangeAbandonedEvent(change);
+    orig.change = asChangeAttribute(change);
+    orig.abandoner = newAccount("abandoner");
+    orig.reason = "some reason";
+
+    ChangeAbandonedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+    assertSameAccount(e.abandoner, orig.abandoner);
+    assertThat(e.reason).isEqualTo(orig.reason);
+  }
+
+  @Test
+  public void changeMergedEvent() {
+    Change change = newChange();
+    ChangeMergedEvent orig = new ChangeMergedEvent(change);
+    orig.change = asChangeAttribute(change);
+
+    ChangeMergedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+  }
+
+  @Test
+  public void changeRestoredEvent() {
+    Change change = newChange();
+    ChangeRestoredEvent orig = new ChangeRestoredEvent(change);
+    orig.change = asChangeAttribute(change);
+
+    ChangeRestoredEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+  }
+
+  @Test
+  public void commentAddedEvent() {
+    Change change = newChange();
+    CommentAddedEvent orig = new CommentAddedEvent(change);
+    orig.change = asChangeAttribute(change);
+
+    CommentAddedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+  }
+
+  @Test
+  public void privateStateChangedEvent() {
+    Change change = newChange();
+    PrivateStateChangedEvent orig = new PrivateStateChangedEvent(change);
+    orig.change = asChangeAttribute(change);
+
+    PrivateStateChangedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+  }
+
+  @Test
+  public void reviewerAddedEvent() {
+    Change change = newChange();
+    ReviewerAddedEvent orig = new ReviewerAddedEvent(change);
+    orig.change = asChangeAttribute(change);
+
+    ReviewerAddedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+  }
+
+  @Test
+  public void reviewerDeletedEvent() {
+    Change change = newChange();
+    ReviewerDeletedEvent orig = new ReviewerDeletedEvent(change);
+    orig.change = asChangeAttribute(change);
+
+    ReviewerDeletedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+  }
+
+  @Test
+  public void voteDeletedEvent() {
+    Change change = newChange();
+    VoteDeletedEvent orig = new VoteDeletedEvent(change);
+    orig.change = asChangeAttribute(change);
+
+    VoteDeletedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+  }
+
+  @Test
+  public void workinProgressStateChangedEvent() {
+    Change change = newChange();
+    WorkInProgressStateChangedEvent orig = new WorkInProgressStateChangedEvent(change);
+    orig.change = asChangeAttribute(change);
+
+    WorkInProgressStateChangedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+  }
+
+  @Test
+  public void topicChangedEvent() {
+    Change change = newChange();
+    TopicChangedEvent orig = new TopicChangedEvent(change);
+    orig.change = asChangeAttribute(change);
+
+    TopicChangedEvent e = roundTrip(orig);
+
+    assertThat(e).isNotNull();
+    assertSameChangeEvent(e, orig);
+  }
+
   private <T> Supplier<T> createSupplier(T value) {
     return Suppliers.memoize(() -> value);
   }
+
+  private Change newChange() {
+    Change change =
+        new Change(
+            Change.key("Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
+            Change.id(1000),
+            Account.id(1000),
+            BranchNameKey.create(Project.nameKey("myproject"), "mybranch"),
+            new Timestamp(System.currentTimeMillis()));
+    return change;
+  }
+
+  private Supplier<AccountAttribute> newAccount(String name) {
+    AccountAttribute account = new AccountAttribute();
+    account.name = name;
+    account.email = name + "@somewhere.com";
+    account.username = name;
+    return Suppliers.ofInstance(account);
+  }
+
+  private void assertSameChangeEvent(ChangeEvent current, ChangeEvent expected) {
+    assertThat(current.changeKey.get()).isEqualTo(expected.changeKey.get());
+    assertThat(current.refName).isEqualTo(expected.refName);
+    assertThat(current.project).isEqualTo(expected.project);
+    assertSameChange(current.change, expected.change);
+  }
+
+  private void assertSameChange(
+      Supplier<ChangeAttribute> currentSupplier, Supplier<ChangeAttribute> expectedSupplier) {
+    ChangeAttribute current = currentSupplier.get();
+    ChangeAttribute expected = expectedSupplier.get();
+    assertThat(current.project).isEqualTo(expected.project);
+    assertThat(current.branch).isEqualTo(expected.branch);
+    assertThat(current.topic).isEqualTo(expected.topic);
+    assertThat(current.id).isEqualTo(expected.id);
+    assertThat(current.number).isEqualTo(expected.number);
+    assertThat(current.subject).isEqualTo(expected.subject);
+    assertThat(current.commitMessage).isEqualTo(expected.commitMessage);
+    assertThat(current.url).isEqualTo(expected.url);
+    assertThat(current.status).isEqualTo(expected.status);
+    assertThat(current.createdOn).isEqualTo(expected.createdOn);
+    assertThat(current.wip).isEqualTo(expected.wip);
+    assertThat(current.isPrivate).isEqualTo(expected.isPrivate);
+  }
+
+  private void assertSameAccount(
+      Supplier<AccountAttribute> currentSupplier, Supplier<AccountAttribute> expectedSupplier) {
+    AccountAttribute current = currentSupplier.get();
+    AccountAttribute expected = expectedSupplier.get();
+    assertThat(current.name).isEqualTo(expected.name);
+    assertThat(current.email).isEqualTo(expected.email);
+    assertThat(current.username).isEqualTo(expected.username);
+  }
+
+  public Supplier<ChangeAttribute> asChangeAttribute(Change change) {
+    ChangeAttribute a = new ChangeAttribute();
+    a.project = change.getProject().get();
+    a.branch = change.getDest().shortName();
+    a.topic = change.getTopic();
+    a.id = change.getKey().get();
+    a.number = change.getId().get();
+    a.subject = change.getSubject();
+    a.commitMessage = "This is a test commit message";
+    a.url = "http://somewhere.com";
+    a.status = change.getStatus();
+    a.createdOn = change.getCreatedOn().getTime() / 1000L;
+    a.wip = change.isWorkInProgress() ? true : null;
+    a.isPrivate = change.isPrivate() ? true : null;
+    return Suppliers.ofInstance(a);
+  }
+
+  @SuppressWarnings("unchecked")
+  private <E extends Event> E roundTrip(E event) {
+    String json = gson.toJson(event);
+    return (E) gson.fromJson(json, event.getClass());
+  }
 }
diff --git a/javatests/com/google/gerrit/server/events/EventJsonTest.java b/javatests/com/google/gerrit/server/events/EventJsonTest.java
index b59641d..6163ea7 100644
--- a/javatests/com/google/gerrit/server/events/EventJsonTest.java
+++ b/javatests/com/google/gerrit/server/events/EventJsonTest.java
@@ -15,32 +15,30 @@
 package com.google.gerrit.server.events;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.Change.Status.NEW;
+import static com.google.gerrit.entities.Change.Status.NEW;
 
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.truth.MapSubject;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.RefUpdateAttribute;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 import com.google.gson.reflect.TypeToken;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import org.junit.Before;
 import org.junit.Test;
 
-public class EventJsonTest extends GerritBaseTests {
+public class EventJsonTest {
   private static final String BRANCH = "mybranch";
   private static final String CHANGE_ID = "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
   private static final int CHANGE_NUM = 1000;
@@ -52,12 +50,7 @@
   private static final double TS2 = 1.254344401E9;
   private static final String URL = "http://somewhere.com";
 
-  // Must match StreamEvents#gson. (In master, the definition is refactored to be hared.)
-  private final Gson gson =
-      new GsonBuilder()
-          .registerTypeAdapter(Supplier.class, new SupplierSerializer())
-          .registerTypeAdapter(Project.NameKey.class, new ProjectNameKeySerializer())
-          .create();
+  private final Gson gson = new EventGsonProvider().get();
 
   @Before
   public void setTimeForTesting() {
@@ -580,7 +573,7 @@
         Change.key(CHANGE_ID),
         Change.id(CHANGE_NUM),
         Account.id(9999),
-        Branch.nameKey(Project.nameKey(PROJECT), BRANCH),
+        BranchNameKey.create(Project.nameKey(PROJECT), BRANCH),
         TimeUtil.nowTs());
   }
 
@@ -591,7 +584,7 @@
   private Supplier<ChangeAttribute> asChangeAttribute(Change change) {
     ChangeAttribute a = new ChangeAttribute();
     a.project = change.getProject().get();
-    a.branch = change.getDest().getShortName();
+    a.branch = change.getDest().shortName();
     a.topic = change.getTopic();
     a.id = change.getKey().get();
     a.number = change.getId().get();
@@ -609,8 +602,9 @@
     // Parse JSON into a raw Java map:
     //  * Doesn't depend on field iteration order.
     //  * Avoids excessively long string literals in asserts.
+    String json = gson.toJson(src);
     Map<Object, Object> map =
-        gson.fromJson(gson.toJson(src), new TypeToken<Map<Object, Object>>() {}.getType());
+        gson.fromJson(json, new TypeToken<Map<Object, Object>>() {}.getType());
     return assertThat(map);
   }
 
diff --git a/javatests/com/google/gerrit/server/events/EventTypesTest.java b/javatests/com/google/gerrit/server/events/EventTypesTest.java
index dd5c7f9..c822d6c 100644
--- a/javatests/com/google/gerrit/server/events/EventTypesTest.java
+++ b/javatests/com/google/gerrit/server/events/EventTypesTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class EventTypesTest extends GerritBaseTests {
+public class EventTypesTest {
   public static class TestEvent extends Event {
     private static final String TYPE = "test-event";
 
diff --git a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
index 4a1f47c..2485613 100644
--- a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
+++ b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
@@ -15,14 +15,17 @@
 package com.google.gerrit.server.extensions.webui;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.access.CoreOrPluginProjectPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -32,16 +35,14 @@
 import com.google.gerrit.server.permissions.PermissionBackendCondition;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
-import org.easymock.EasyMock;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Test;
 
-public class UiActionsTest extends GerritBaseTests {
+public class UiActionsTest {
 
   private static class FakeForProject extends ForProject {
     private boolean allowValueQueries = true;
@@ -112,7 +113,7 @@
 
         @Override
         public Account.Id getAccountId() {
-          return new Account.Id(1);
+          return Account.id(1);
         }
       };
     }
@@ -132,9 +133,7 @@
 
     // Set up the Mock to expect a call of bulkEvaluateTest to only contain cond{1,2} since cond3
     // needs to be identified as duplicate and not called out explicitly.
-    PermissionBackend permissionBackendMock = EasyMock.createMock(PermissionBackend.class);
-    permissionBackendMock.bulkEvaluateTest(ImmutableSet.of(cond1, cond2));
-    EasyMock.replay(permissionBackendMock);
+    PermissionBackend permissionBackendMock = mock(PermissionBackend.class);
 
     UiActions.evaluatePermissionBackendConditions(
         permissionBackendMock, ImmutableList.of(cond1, cond2, cond3));
@@ -143,6 +142,8 @@
     // the value of cond1 and issues no additional call to PermissionBackend.
     forProject.disallowValueQueries();
 
+    verify(permissionBackendMock, only()).bulkEvaluateTest(ImmutableSet.of(cond1, cond2));
+
     // Assert the values of all conditions
     assertThat(cond1.value()).isFalse();
     assertThat(cond2.value()).isTrue();
diff --git a/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java b/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
index cc648bf..8b5705b 100644
--- a/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
+++ b/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
@@ -15,32 +15,31 @@
 package com.google.gerrit.server.fixes;
 
 import static com.google.gerrit.server.edit.tree.TreeModificationSubject.assertThatList;
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.replay;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Comment.Range;
+import com.google.gerrit.entities.FixReplacement;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Comment.Range;
-import com.google.gerrit.reviewdb.client.FixReplacement;
 import com.google.gerrit.server.change.FileContentUtil;
 import com.google.gerrit.server.edit.tree.TreeModification;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
-import org.easymock.EasyMock;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
 
-public class FixReplacementInterpreterTest extends GerritBaseTests {
-  private final FileContentUtil fileContentUtil = createMock(FileContentUtil.class);
-  private final Repository repository = createMock(Repository.class);
-  private final ProjectState projectState = createMock(ProjectState.class);
-  private final ObjectId patchSetCommitId = createMock(ObjectId.class);
+public class FixReplacementInterpreterTest {
+  private final FileContentUtil fileContentUtil = mock(FileContentUtil.class);
+  private final Repository repository = mock(Repository.class);
+  private final ProjectState projectState = mock(ProjectState.class);
+  private final ObjectId patchSetCommitId = mock(ObjectId.class);
   private final String filePath1 = "an/arbitrary/file.txt";
   private final String filePath2 = "another/arbitrary/file.txt";
 
@@ -68,7 +67,6 @@
         new FixReplacement(filePath2, new Range(2, 0, 3, 0), "Another modified content");
     mockFileContent(filePath2, "1st line\n2nd line\n3rd line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications =
         toTreeModifications(fixReplacement, fixReplacement3, fixReplacement2);
     List<TreeModification> sortedTreeModifications = getSortedCopy(treeModifications);
@@ -99,7 +97,6 @@
     FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(2, 0, 3, 0), "");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
     assertThatList(treeModifications)
         .onlyElement()
@@ -114,7 +111,6 @@
         new FixReplacement(filePath1, new Range(2, 0, 2, 0), "A new line\n");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
     assertThatList(treeModifications)
         .onlyElement()
@@ -128,7 +124,6 @@
     FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(1, 6, 3, 1), "and t");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
     assertThatList(treeModifications)
         .onlyElement()
@@ -144,7 +139,6 @@
         new FixReplacement(filePath1, new Range(2, 7, 2, 11), "modification");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications =
         toTreeModifications(fixReplacement1, fixReplacement2);
     assertThatList(treeModifications)
@@ -154,6 +148,22 @@
         .isEqualTo("First line\nA modification\nThird line\n");
   }
 
+  @Test()
+  public void startAfterEndOfLineMarkThrowsAnException() throws Exception {
+    FixReplacement fixReplacement =
+        new FixReplacement(filePath1, new Range(1, 11, 2, 6), "A modification");
+    mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+    assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
+  }
+
+  @Test()
+  public void endAfterEndOfLineMarkThrowsAnException() throws Exception {
+    FixReplacement fixReplacement =
+        new FixReplacement(filePath1, new Range(2, 0, 2, 12), "A modification");
+    mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+    assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
+  }
+
   @Test
   public void replacementsMayTouch() throws Exception {
     FixReplacement fixReplacement1 =
@@ -162,7 +172,6 @@
         new FixReplacement(filePath1, new Range(2, 7, 3, 5), "content");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications =
         toTreeModifications(fixReplacement1, fixReplacement2);
     assertThatList(treeModifications)
@@ -178,7 +187,6 @@
         new FixReplacement(filePath1, new Range(4, 0, 4, 0), "New content");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
     assertThatList(treeModifications)
         .onlyElement()
@@ -188,6 +196,34 @@
   }
 
   @Test
+  public void replacementsCanChangeLastLine() throws Exception {
+    FixReplacement fixReplacement =
+        new FixReplacement(filePath1, new Range(3, 0, 4, 0), "New content\n");
+    mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+    List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
+    assertThatList(treeModifications)
+        .onlyElement()
+        .asChangeFileContentModification()
+        .newContent()
+        .isEqualTo("First line\nSecond line\nNew content\n");
+  }
+
+  @Test
+  public void replacementsCanChangeLastLineWithoutEOLMark() throws Exception {
+    FixReplacement fixReplacement =
+        new FixReplacement(filePath1, new Range(3, 0, 3, 10), "New content\n");
+    mockFileContent(filePath1, "First line\nSecond line\nThird line");
+
+    List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
+    assertThatList(treeModifications)
+        .onlyElement()
+        .asChangeFileContentModification()
+        .newContent()
+        .isEqualTo("First line\nSecond line\nNew content\n");
+  }
+
+  @Test
   public void replacementsCanModifySeveralFilesInAnyOrder() throws Exception {
     FixReplacement fixReplacement1 =
         new FixReplacement(filePath1, new Range(1, 1, 3, 2), "Modified content");
@@ -198,7 +234,6 @@
         new FixReplacement(filePath2, new Range(3, 0, 4, 0), "Second modification\n");
     mockFileContent(filePath2, "1st line\n2nd line\n3rd line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications =
         toTreeModifications(fixReplacement3, fixReplacement1, fixReplacement2);
     List<TreeModification> sortedTreeModifications = getSortedCopy(treeModifications);
@@ -219,7 +254,6 @@
     FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(2, 11, 3, 0), "\r");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
     assertThatList(treeModifications)
         .onlyElement()
@@ -238,7 +272,6 @@
         new FixReplacement(filePath1, new Range(4, 0, 5, 0), "3rd modification\n");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\nFourth line\nFifth line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications =
         toTreeModifications(fixReplacement2, fixReplacement1, fixReplacement3);
     assertThatList(treeModifications)
@@ -255,10 +288,7 @@
         new FixReplacement(filePath1, new Range(5, 0, 5, 0), "A new line\n");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
-
-    exception.expect(ResourceConflictException.class);
-    toTreeModifications(fixReplacement);
+    assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
   }
 
   @Test
@@ -267,10 +297,7 @@
         new FixReplacement(filePath1, new Range(0, 0, 0, 0), "A new line\n");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
-
-    exception.expect(ResourceConflictException.class);
-    toTreeModifications(fixReplacement);
+    assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
   }
 
   @Test
@@ -279,10 +306,7 @@
         new FixReplacement(filePath1, new Range(1, 0, 1, 11), "modified");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
-
-    exception.expect(ResourceConflictException.class);
-    toTreeModifications(fixReplacement);
+    assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
   }
 
   @Test
@@ -291,10 +315,7 @@
         new FixReplacement(filePath1, new Range(3, 0, 3, 11), "modified");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
-
-    exception.expect(ResourceConflictException.class);
-    toTreeModifications(fixReplacement);
+    assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
   }
 
   @Test
@@ -303,16 +324,12 @@
         new FixReplacement(filePath1, new Range(1, -1, 1, 5), "modified");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
-
-    exception.expect(ResourceConflictException.class);
-    toTreeModifications(fixReplacement);
+    assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
   }
 
   private void mockFileContent(String filePath, String fileContent) throws Exception {
-    EasyMock.expect(
-            fileContentUtil.getContent(repository, projectState, patchSetCommitId, filePath))
-        .andReturn(BinaryResult.create(fileContent));
+    when(fileContentUtil.getContent(repository, projectState, patchSetCommitId, filePath))
+        .thenReturn(BinaryResult.create(fileContent));
   }
 
   private List<TreeModification> toTreeModifications(FixReplacement... fixReplacements)
diff --git a/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java b/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java
index 309f726..ba80c02 100644
--- a/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java
+++ b/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java
@@ -15,25 +15,27 @@
 package com.google.gerrit.server.fixes;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class LineIdentifierTest extends GerritBaseTests {
+public class LineIdentifierTest {
   @Test
   public void lineNumberMustBePositive() {
     LineIdentifier lineIdentifier = new LineIdentifier("First line\nSecond line");
-    exception.expect(StringIndexOutOfBoundsException.class);
-    exception.expectMessage("positive");
-    lineIdentifier.getStartIndexOfLine(0);
+    StringIndexOutOfBoundsException thrown =
+        assertThrows(
+            StringIndexOutOfBoundsException.class, () -> lineIdentifier.getStartIndexOfLine(0));
+    assertThat(thrown).hasMessageThat().contains("positive");
   }
 
   @Test
   public void lineNumberMustIndicateAnAvailableLine() {
     LineIdentifier lineIdentifier = new LineIdentifier("First line\nSecond line");
-    exception.expect(StringIndexOutOfBoundsException.class);
-    exception.expectMessage("Line 3 isn't available");
-    lineIdentifier.getStartIndexOfLine(3);
+    StringIndexOutOfBoundsException thrown =
+        assertThrows(
+            StringIndexOutOfBoundsException.class, () -> lineIdentifier.getStartIndexOfLine(3));
+    assertThat(thrown).hasMessageThat().contains("Line 3 isn't available");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/fixes/StringModifierTest.java b/javatests/com/google/gerrit/server/fixes/StringModifierTest.java
index 185b58c..3447248 100644
--- a/javatests/com/google/gerrit/server/fixes/StringModifierTest.java
+++ b/javatests/com/google/gerrit/server/fixes/StringModifierTest.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.fixes;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Before;
 import org.junit.Test;
 
-public class StringModifierTest extends GerritBaseTests {
+public class StringModifierTest {
   private final String originalString = "This is the original, unmodified string.";
   private StringModifier stringModifier;
 
@@ -63,20 +63,20 @@
   @Test
   public void replacedPartsMustNotOverlap() {
     stringModifier.replace(0, 9, "");
-    exception.expect(StringIndexOutOfBoundsException.class);
-    stringModifier.replace(8, 32, "The modified");
+    assertThrows(
+        StringIndexOutOfBoundsException.class, () -> stringModifier.replace(8, 32, "The modified"));
   }
 
   @Test
   public void startIndexMustNotBeGreaterThanEndIndex() {
-    exception.expect(StringIndexOutOfBoundsException.class);
-    stringModifier.replace(10, 9, "something");
+    assertThrows(
+        StringIndexOutOfBoundsException.class, () -> stringModifier.replace(10, 9, "something"));
   }
 
   @Test
   public void startIndexMustNotBeNegative() {
-    exception.expect(StringIndexOutOfBoundsException.class);
-    stringModifier.replace(-1, 9, "something");
+    assertThrows(
+        StringIndexOutOfBoundsException.class, () -> stringModifier.replace(-1, 9, "something"));
   }
 
   @Test
@@ -90,13 +90,17 @@
 
   @Test
   public void startIndexMustNotBeGreaterThanLengthOfString() {
-    exception.expect(StringIndexOutOfBoundsException.class);
-    stringModifier.replace(originalString.length() + 1, originalString.length() + 1, "something");
+    assertThrows(
+        StringIndexOutOfBoundsException.class,
+        () ->
+            stringModifier.replace(
+                originalString.length() + 1, originalString.length() + 1, "something"));
   }
 
   @Test
   public void endIndexMustNotBeGreaterThanLengthOfString() {
-    exception.expect(StringIndexOutOfBoundsException.class);
-    stringModifier.replace(8, originalString.length() + 1, "something");
+    assertThrows(
+        StringIndexOutOfBoundsException.class,
+        () -> stringModifier.replace(8, originalString.length() + 1, "something"));
   }
 }
diff --git a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
index b278bfe..3a8d7e4 100644
--- a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
+++ b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
@@ -18,11 +18,11 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.notedb.DeleteZombieCommentsRefs;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -50,7 +50,7 @@
 @RunWith(JUnit4.class)
 public class DeleteZombieCommentsRefsTest {
   private InMemoryRepositoryManager repoManager = new InMemoryRepositoryManager();
-  private Project.NameKey allUsersProject = new Project.NameKey("All-Users");
+  private Project.NameKey allUsersProject = Project.nameKey("All-Users");
 
   @Test
   public void cleanZombieDraftsSmall() throws Exception {
@@ -169,8 +169,8 @@
   }
 
   private static String getRefName(int changeId, int userId) {
-    Change.Id cId = new Change.Id(changeId);
-    Account.Id aId = new Account.Id(userId);
+    Change.Id cId = Change.id(changeId);
+    Account.Id aId = Account.id(userId);
     return RefNames.refsDraftComments(cId, aId);
   }
 
diff --git a/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java
index ea4a199..7832bec 100644
--- a/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java
@@ -15,16 +15,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.reviewdb.client.Project.NameKey;
-import com.google.gerrit.testing.GerritBaseTests;
-import java.io.IOException;
+import com.google.gerrit.entities.Project.NameKey;
 import java.util.SortedSet;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
 
-public class GitRepositoryManagerTest extends GerritBaseTests {
+public class GitRepositoryManagerTest {
 
   private GitRepositoryManager objectUnderTest;
 
@@ -40,13 +37,12 @@
 
   private static class TestGitRepositoryManager implements GitRepositoryManager {
     @Override
-    public Repository openRepository(NameKey name) throws RepositoryNotFoundException, IOException {
+    public Repository openRepository(NameKey name) {
       throw new UnsupportedOperationException("Not implemented");
     }
 
     @Override
-    public Repository createRepository(NameKey name)
-        throws RepositoryCaseMismatchException, RepositoryNotFoundException, IOException {
+    public Repository createRepository(NameKey name) {
       throw new UnsupportedOperationException("Not implemented");
     }
 
diff --git a/javatests/com/google/gerrit/server/git/GroupCollectorTest.java b/javatests/com/google/gerrit/server/git/GroupCollectorTest.java
index f694299..6175385 100644
--- a/javatests/com/google/gerrit/server/git/GroupCollectorTest.java
+++ b/javatests/com/google/gerrit/server/git/GroupCollectorTest.java
@@ -19,9 +19,8 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.SortedSetMultimap;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -32,7 +31,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class GroupCollectorTest extends GerritBaseTests {
+public class GroupCollectorTest {
   private TestRepository<?> tr;
 
   @Before
@@ -285,7 +284,7 @@
   // TODO(dborowitz): Tests for octopus merges.
 
   private static PatchSet.Id psId(int c, int p) {
-    return new PatchSet.Id(new Change.Id(c), p);
+    return PatchSet.id(Change.id(c), p);
   }
 
   private RevWalk newWalk(ObjectId start, ObjectId branchTip) throws Exception {
diff --git a/javatests/com/google/gerrit/server/git/JGitConfigTest.java b/javatests/com/google/gerrit/server/git/JGitConfigTest.java
new file mode 100644
index 0000000..9f6b47e
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/JGitConfigTest.java
@@ -0,0 +1,84 @@
+// 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.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.config.SitePaths;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class JGitConfigTest {
+
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private SitePaths site;
+  private Path gitPath;
+
+  @Before
+  public void setUp() throws IOException {
+    site = new SitePaths(temporaryFolder.newFolder().toPath());
+    Files.createDirectories(site.etc_dir);
+    gitPath = Files.createDirectories(site.resolve("git"));
+
+    Files.write(
+        site.jgit_config, "[core]\n  trustFolderStat = false\n".getBytes(StandardCharsets.UTF_8));
+    new SystemReaderInstaller(site).start();
+  }
+
+  @Test
+  public void test() throws IOException {
+    try (Repository repo = new FileRepository(gitPath.resolve("foo").toFile())) {
+      assertThat(repo.getConfig().getString("core", null, "trustFolderStat")).isEqualTo("false");
+    }
+  }
+
+  @Test
+  public void openSystemConfigRespectsParent() throws Exception {
+    Config parent = new Config();
+    parent.setString("foo", null, "bar", "value");
+    FileBasedConfig system = SystemReader.getInstance().openSystemConfig(parent, FS.DETECTED);
+    system.load();
+    assertThat(system.getString("core", null, "trustFolderStat")).isEqualTo("false");
+    assertThat(system.getString("foo", null, "bar")).isEqualTo("value");
+  }
+
+  @Test
+  public void openSystemConfigReturnsDifferentInstances() throws Exception {
+    FileBasedConfig system1 = SystemReader.getInstance().openSystemConfig(null, FS.DETECTED);
+    system1.load();
+    assertThat(system1.getString("core", null, "trustFolderStat")).isEqualTo("false");
+
+    FileBasedConfig system2 = SystemReader.getInstance().openSystemConfig(null, FS.DETECTED);
+    system2.load();
+    assertThat(system2.getString("core", null, "trustFolderStat")).isEqualTo("false");
+
+    system1.setString("core", null, "trustFolderStat", "true");
+    assertThat(system1.getString("core", null, "trustFolderStat")).isEqualTo("true");
+    assertThat(system2.getString("core", null, "trustFolderStat")).isEqualTo("false");
+  }
+}
diff --git a/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
index 4948a12..febb142 100644
--- a/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
@@ -16,11 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.ioutil.HostPlatform;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -36,7 +36,7 @@
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
-public class LocalDiskRepositoryManagerTest extends GerritBaseTests {
+public class LocalDiskRepositoryManagerTest {
   @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
 
   private Config cfg;
@@ -52,14 +52,15 @@
     repoManager = new LocalDiskRepositoryManager(site, cfg);
   }
 
-  @Test(expected = IllegalStateException.class)
+  @Test
   public void testThatNullBasePathThrowsAnException() {
-    new LocalDiskRepositoryManager(site, new Config());
+    assertThrows(
+        IllegalStateException.class, () -> new LocalDiskRepositoryManager(site, new Config()));
   }
 
   @Test
   public void projectCreation() throws Exception {
-    Project.NameKey projectA = new Project.NameKey("projectA");
+    Project.NameKey projectA = Project.nameKey("projectA");
     try (Repository repo = repoManager.createRepository(projectA)) {
       assertThat(repo).isNotNull();
     }
@@ -69,112 +70,149 @@
     assertThat(repoManager.list()).containsExactly(projectA);
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithEmptyName() throws Exception {
-    repoManager.createRepository(new Project.NameKey(""));
+    assertThrows(
+        RepositoryNotFoundException.class, () -> repoManager.createRepository(Project.nameKey("")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithTrailingSlash() throws Exception {
-    repoManager.createRepository(new Project.NameKey("projectA/"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("projectA/")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithBackSlash() throws Exception {
-    repoManager.createRepository(new Project.NameKey("a\\projectA"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("a\\projectA")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationAbsolutePath() throws Exception {
-    repoManager.createRepository(new Project.NameKey("/projectA"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("/projectA")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationStartingWithDotDot() throws Exception {
-    repoManager.createRepository(new Project.NameKey("../projectA"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("../projectA")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationContainsDotDot() throws Exception {
-    repoManager.createRepository(new Project.NameKey("a/../projectA"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("a/../projectA")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationDotPathSegment() throws Exception {
-    repoManager.createRepository(new Project.NameKey("a/./projectA"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("a/./projectA")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithTwoSlashes() throws Exception {
-    repoManager.createRepository(new Project.NameKey("a//projectA"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("a//projectA")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithPathSegmentEndingByDotGit() throws Exception {
-    repoManager.createRepository(new Project.NameKey("a/b.git/projectA"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("a/b.git/projectA")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithQuestionMark() throws Exception {
-    repoManager.createRepository(new Project.NameKey("project?A"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("project?A")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithPercentageSign() throws Exception {
-    repoManager.createRepository(new Project.NameKey("project%A"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("project%A")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithWidlcard() throws Exception {
-    repoManager.createRepository(new Project.NameKey("project*A"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("project*A")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithColon() throws Exception {
-    repoManager.createRepository(new Project.NameKey("project:A"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("project:A")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithLessThatSign() throws Exception {
-    repoManager.createRepository(new Project.NameKey("project<A"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("project<A")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithGreaterThatSign() throws Exception {
-    repoManager.createRepository(new Project.NameKey("project>A"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("project>A")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithPipe() throws Exception {
-    repoManager.createRepository(new Project.NameKey("project|A"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("project|A")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithDollarSign() throws Exception {
-    repoManager.createRepository(new Project.NameKey("project$A"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("project$A")));
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testProjectCreationWithCarriageReturn() throws Exception {
-    repoManager.createRepository(new Project.NameKey("project\\rA"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.createRepository(Project.nameKey("project\\rA")));
   }
 
-  @Test(expected = IllegalStateException.class)
+  @Test
   public void testProjectRecreation() throws Exception {
-    repoManager.createRepository(new Project.NameKey("a"));
-    repoManager.createRepository(new Project.NameKey("a"));
+    repoManager.createRepository(Project.nameKey("a"));
+    assertThrows(
+        IllegalStateException.class, () -> repoManager.createRepository(Project.nameKey("a")));
   }
 
-  @Test(expected = IllegalStateException.class)
+  @Test
   public void testProjectRecreationAfterRestart() throws Exception {
-    repoManager.createRepository(new Project.NameKey("a"));
+    repoManager.createRepository(Project.nameKey("a"));
     LocalDiskRepositoryManager newRepoManager = new LocalDiskRepositoryManager(site, cfg);
-    newRepoManager.createRepository(new Project.NameKey("a"));
+    assertThrows(
+        IllegalStateException.class, () -> newRepoManager.createRepository(Project.nameKey("a")));
   }
 
   @Test
   public void openRepositoryCreatedDirectlyOnDisk() throws Exception {
-    Project.NameKey projectA = new Project.NameKey("projectA");
+    Project.NameKey projectA = Project.nameKey("projectA");
     createRepository(repoManager.getBasePath(projectA), projectA.get());
     try (Repository repo = repoManager.openRepository(projectA)) {
       assertThat(repo).isNotNull();
@@ -182,30 +220,36 @@
     assertThat(repoManager.list()).containsExactly(projectA);
   }
 
-  @Test(expected = RepositoryCaseMismatchException.class)
+  @Test
   public void testNameCaseMismatch() throws Exception {
     assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue();
-    repoManager.createRepository(new Project.NameKey("a"));
-    repoManager.createRepository(new Project.NameKey("A"));
+    repoManager.createRepository(Project.nameKey("a"));
+    assertThrows(
+        RepositoryCaseMismatchException.class,
+        () -> repoManager.createRepository(Project.nameKey("A")));
   }
 
-  @Test(expected = RepositoryCaseMismatchException.class)
+  @Test
   public void testNameCaseMismatchWithSymlink() throws Exception {
     assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue();
-    Project.NameKey name = new Project.NameKey("a");
+    Project.NameKey name = Project.nameKey("a");
     repoManager.createRepository(name);
     createSymLink(name, "b.git");
-    repoManager.createRepository(new Project.NameKey("B"));
+    assertThrows(
+        RepositoryCaseMismatchException.class,
+        () -> repoManager.createRepository(Project.nameKey("B")));
   }
 
-  @Test(expected = RepositoryCaseMismatchException.class)
+  @Test
   public void testNameCaseMismatchAfterRestart() throws Exception {
     assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue();
-    Project.NameKey name = new Project.NameKey("a");
+    Project.NameKey name = Project.nameKey("a");
     repoManager.createRepository(name);
 
     LocalDiskRepositoryManager newRepoManager = new LocalDiskRepositoryManager(site, cfg);
-    newRepoManager.createRepository(new Project.NameKey("A"));
+    assertThrows(
+        RepositoryCaseMismatchException.class,
+        () -> newRepoManager.createRepository(Project.nameKey("A")));
   }
 
   @Test
@@ -220,20 +264,22 @@
     Files.createSymbolicLink(symlink, projectDir);
   }
 
-  @Test(expected = RepositoryNotFoundException.class)
+  @Test
   public void testOpenRepositoryInvalidName() throws Exception {
-    repoManager.openRepository(new Project.NameKey("project%?|<>A"));
+    assertThrows(
+        RepositoryNotFoundException.class,
+        () -> repoManager.openRepository(Project.nameKey("project%?|<>A")));
   }
 
   @Test
   public void list() throws Exception {
-    Project.NameKey projectA = new Project.NameKey("projectA");
+    Project.NameKey projectA = Project.nameKey("projectA");
     createRepository(repoManager.getBasePath(projectA), projectA.get());
 
-    Project.NameKey projectB = new Project.NameKey("path/projectB");
+    Project.NameKey projectB = Project.nameKey("path/projectB");
     createRepository(repoManager.getBasePath(projectB), projectB.get());
 
-    Project.NameKey projectC = new Project.NameKey("anotherPath/path/projectC");
+    Project.NameKey projectC = Project.nameKey("anotherPath/path/projectC");
     createRepository(repoManager.getBasePath(projectC), projectC.get());
     // create an invalid git repo named only .git
     repoManager.getBasePath(null).resolve(".git").toFile().mkdir();
diff --git a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
index fc79a6d..4902830 100644
--- a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
@@ -15,16 +15,14 @@
 package com.google.gerrit.server.git;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.easymock.EasyMock.createNiceMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.reset;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -41,7 +39,7 @@
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
-public class MultiBaseLocalDiskRepositoryManagerTest extends GerritBaseTests {
+public class MultiBaseLocalDiskRepositoryManagerTest {
   @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
 
   private Config cfg;
@@ -55,16 +53,15 @@
     site.resolve("git").toFile().mkdir();
     cfg = new Config();
     cfg.setString("gerrit", null, "basePath", "git");
-    configMock = createNiceMock(RepositoryConfig.class);
-    expect(configMock.getAllBasePaths()).andReturn(ImmutableList.of()).anyTimes();
-    replay(configMock);
+    configMock = mock(RepositoryConfig.class);
+    when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of());
     repoManager = new MultiBaseLocalDiskRepositoryManager(site, cfg, configMock);
   }
 
   @Test
   public void defaultRepositoryLocation()
       throws RepositoryCaseMismatchException, RepositoryNotFoundException, IOException {
-    Project.NameKey someProjectKey = new Project.NameKey("someProject");
+    Project.NameKey someProjectKey = Project.nameKey("someProject");
     Repository repo = repoManager.createRepository(someProjectKey);
     assertThat(repo.getDirectory()).isNotNull();
     assertThat(repo.getDirectory().exists()).isTrue();
@@ -89,21 +86,21 @@
   @Test
   public void alternateRepositoryLocation() throws IOException {
     Path alternateBasePath = temporaryFolder.newFolder().toPath();
-    Project.NameKey someProjectKey = new Project.NameKey("someProject");
-    reset(configMock);
-    expect(configMock.getBasePath(someProjectKey)).andReturn(alternateBasePath).anyTimes();
-    expect(configMock.getAllBasePaths()).andReturn(ImmutableList.of(alternateBasePath)).anyTimes();
-    replay(configMock);
+    Project.NameKey someProjectKey = Project.nameKey("someProject");
+    when(configMock.getBasePath(someProjectKey)).thenReturn(alternateBasePath);
+    when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of(alternateBasePath));
 
     Repository repo = repoManager.createRepository(someProjectKey);
     assertThat(repo.getDirectory()).isNotNull();
     assertThat(repo.getDirectory().exists()).isTrue();
-    assertThat(repo.getDirectory().getParent()).isEqualTo(alternateBasePath.toString());
+    assertThat(repo.getDirectory().getParent())
+        .isEqualTo(alternateBasePath.toRealPath().toString());
 
     repo = repoManager.openRepository(someProjectKey);
     assertThat(repo.getDirectory()).isNotNull();
     assertThat(repo.getDirectory().exists()).isTrue();
-    assertThat(repo.getDirectory().getParent()).isEqualTo(alternateBasePath.toString());
+    assertThat(repo.getDirectory().getParent())
+        .isEqualTo(alternateBasePath.toRealPath().toString());
 
     assertThat(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString())
         .isEqualTo(alternateBasePath.toString());
@@ -116,18 +113,16 @@
 
   @Test
   public void listReturnRepoFromProperLocation() throws IOException {
-    Project.NameKey basePathProject = new Project.NameKey("basePathProject");
-    Project.NameKey altPathProject = new Project.NameKey("altPathProject");
-    Project.NameKey misplacedProject1 = new Project.NameKey("misplacedProject1");
-    Project.NameKey misplacedProject2 = new Project.NameKey("misplacedProject2");
+    Project.NameKey basePathProject = Project.nameKey("basePathProject");
+    Project.NameKey altPathProject = Project.nameKey("altPathProject");
+    Project.NameKey misplacedProject1 = Project.nameKey("misplacedProject1");
+    Project.NameKey misplacedProject2 = Project.nameKey("misplacedProject2");
 
     Path alternateBasePath = temporaryFolder.newFolder().toPath();
 
-    reset(configMock);
-    expect(configMock.getBasePath(altPathProject)).andReturn(alternateBasePath).anyTimes();
-    expect(configMock.getBasePath(misplacedProject2)).andReturn(alternateBasePath).anyTimes();
-    expect(configMock.getAllBasePaths()).andReturn(ImmutableList.of(alternateBasePath)).anyTimes();
-    replay(configMock);
+    when(configMock.getBasePath(altPathProject)).thenReturn(alternateBasePath);
+    when(configMock.getBasePath(misplacedProject2)).thenReturn(alternateBasePath);
+    when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of(alternateBasePath));
 
     repoManager.createRepository(basePathProject);
     repoManager.createRepository(altPathProject);
@@ -150,11 +145,14 @@
     }
   }
 
-  @Test(expected = IllegalStateException.class)
+  @Test
   public void testRelativeAlternateLocation() {
-    configMock = createNiceMock(RepositoryConfig.class);
-    expect(configMock.getAllBasePaths()).andReturn(ImmutableList.of(Paths.get("repos"))).anyTimes();
-    replay(configMock);
-    repoManager = new MultiBaseLocalDiskRepositoryManager(site, cfg, configMock);
+    assertThrows(
+        IllegalStateException.class,
+        () -> {
+          configMock = mock(RepositoryConfig.class);
+          when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of(Paths.get("repos")));
+          repoManager = new MultiBaseLocalDiskRepositoryManager(site, cfg, configMock);
+        });
   }
 }
diff --git a/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java b/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java
index 8c17075..a12a8f7 100644
--- a/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java
+++ b/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java
@@ -17,7 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteArray;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.cache.proto.Cache;
 import com.google.protobuf.ByteString;
 import org.eclipse.jgit.lib.ObjectId;
@@ -36,8 +36,7 @@
             0xaa, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
             0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb);
 
-    Cache.PureRevertKeyProto key =
-        PureRevertCache.key(new Project.NameKey("test"), revert, original);
+    Cache.PureRevertKeyProto key = PureRevertCache.key(Project.nameKey("test"), revert, original);
     assertThat(key)
         .isEqualTo(
             Cache.PureRevertKeyProto.newBuilder()
diff --git a/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java b/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java
index 9ad8ffb..99618db 100644
--- a/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java
+++ b/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java
@@ -325,5 +325,10 @@
         throw new IllegalStateException("Repository is not open (refCounter=" + refCounter + ")");
       }
     }
+
+    @Override
+    public String getIdentifier() {
+      return "foo";
+    }
   }
 }
diff --git a/javatests/com/google/gerrit/server/git/TagSetHolderTest.java b/javatests/com/google/gerrit/server/git/TagSetHolderTest.java
index 87ddc75..4fa0903 100644
--- a/javatests/com/google/gerrit/server/git/TagSetHolderTest.java
+++ b/javatests/com/google/gerrit/server/git/TagSetHolderTest.java
@@ -19,15 +19,14 @@
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto;
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class TagSetHolderTest extends GerritBaseTests {
+public class TagSetHolderTest {
   @Test
   public void serializerWithTagSet() throws Exception {
-    TagSetHolder holder = new TagSetHolder(new Project.NameKey("project"));
+    TagSetHolder holder = new TagSetHolder(Project.nameKey("project"));
     holder.setTagSet(new TagSet(holder.getProjectName()));
 
     byte[] serialized = TagSetHolder.Serializer.INSTANCE.serialize(holder);
@@ -46,7 +45,7 @@
 
   @Test
   public void serializerWithoutTagSet() throws Exception {
-    TagSetHolder holder = new TagSetHolder(new Project.NameKey("project"));
+    TagSetHolder holder = new TagSetHolder(Project.nameKey("project"));
 
     byte[] serialized = TagSetHolder.Serializer.INSTANCE.serialize(holder);
     assertThat(TagSetHolderProto.parseFrom(serialized))
diff --git a/javatests/com/google/gerrit/server/git/TagSetTest.java b/javatests/com/google/gerrit/server/git/TagSetTest.java
index 3ac72be..4a3c930 100644
--- a/javatests/com/google/gerrit/server/git/TagSetTest.java
+++ b/javatests/com/google/gerrit/server/git/TagSetTest.java
@@ -24,13 +24,12 @@
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.CachedRefProto;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.TagProto;
 import com.google.gerrit.server.git.TagSet.CachedRef;
 import com.google.gerrit.server.git.TagSet.Tag;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.TypeLiteral;
 import java.lang.reflect.Type;
 import java.util.Arrays;
@@ -43,7 +42,7 @@
 import org.eclipse.jgit.lib.ObjectIdOwnerMap;
 import org.junit.Test;
 
-public class TagSetTest extends GerritBaseTests {
+public class TagSetTest {
   @Test
   public void roundTripToProto() {
     HashMap<String, CachedRef> refs = new HashMap<>();
@@ -60,7 +59,7 @@
     tags.add(
         new Tag(
             ObjectId.fromString("dddddddddddddddddddddddddddddddddddddddd"), newBitSet(2, 4, 6)));
-    TagSet tagSet = new TagSet(new Project.NameKey("project"), refs, tags);
+    TagSet tagSet = new TagSet(Project.nameKey("project"), refs, tags);
 
     TagSetProto proto = tagSet.toProto();
     assertThat(proto)
@@ -156,22 +155,24 @@
 
     Map<String, CachedRef> aRefs = a.getRefsForTesting();
     Map<String, CachedRef> bRefs = b.getRefsForTesting();
-    assertThat(ImmutableSortedSet.copyOf(aRefs.keySet()))
-        .named("ref name set")
+    assertWithMessage("ref name set")
+        .that(ImmutableSortedSet.copyOf(aRefs.keySet()))
         .isEqualTo(ImmutableSortedSet.copyOf(bRefs.keySet()));
     for (String name : aRefs.keySet()) {
       CachedRef aRef = aRefs.get(name);
       CachedRef bRef = bRefs.get(name);
-      assertThat(aRef.get()).named("value of ref %s", name).isEqualTo(bRef.get());
-      assertThat(aRef.flag).named("flag of ref %s", name).isEqualTo(bRef.flag);
+      assertWithMessage("value of ref %s", name).that(aRef.get()).isEqualTo(bRef.get());
+      assertWithMessage("flag of ref %s", name).that(aRef.flag).isEqualTo(bRef.flag);
     }
 
     ObjectIdOwnerMap<Tag> aTags = a.getTagsForTesting();
     ObjectIdOwnerMap<Tag> bTags = b.getTagsForTesting();
-    assertThat(getTagIds(aTags)).named("tag ID set").isEqualTo(getTagIds(bTags));
+    assertWithMessage("tag ID set").that(getTagIds(aTags)).isEqualTo(getTagIds(bTags));
     for (Tag aTag : aTags) {
       Tag bTag = bTags.get(aTag);
-      assertThat(aTag.refFlags).named("flags for tag %s", aTag.name()).isEqualTo(bTag.refFlags);
+      assertWithMessage("flags for tag %s", aTag.name())
+          .that(aTag.refFlags)
+          .isEqualTo(bTag.refFlags);
     }
   }
 
diff --git a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
index dedccc2..690a5cc 100644
--- a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
+++ b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
@@ -22,12 +22,11 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.VersionedMetaData.BatchMetaDataUpdate;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.TestTimeUtil;
 import java.io.IOException;
 import java.util.Arrays;
@@ -51,7 +50,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class VersionedMetaDataTest extends GerritBaseTests {
+public class VersionedMetaDataTest {
   // If you're considering fleshing out this test and making it more comprehensive, please consider
   // instead coming up with a replacement interface for
   // VersionedMetaData/BatchMetaDataUpdate/MetaDataUpdate that is easier to use correctly.
@@ -65,7 +64,7 @@
   @Before
   public void setUp() {
     TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
-    project = new Project.NameKey("repo");
+    project = Project.nameKey("repo");
     repo = new InMemoryRepository(new DfsRepositoryDescription(project.get()));
   }
 
diff --git a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
index 9fc6da1..c749b77 100644
--- a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -18,17 +18,16 @@
 
 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.RefNames;
 import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -44,7 +43,7 @@
 import org.junit.Ignore;
 
 @Ignore
-public class AbstractGroupTest extends GerritBaseTests {
+public class AbstractGroupTest {
   protected static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
   protected static final String SERVER_ID = "server-id";
   protected static final String SERVER_NAME = "Gerrit Server";
@@ -65,9 +64,9 @@
     allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
     repoManager = new InMemoryRepositoryManager();
     allUsersRepo = repoManager.createRepository(allUsersName);
-    serverAccountId = new Account.Id(SERVER_ACCOUNT_NUMBER);
+    serverAccountId = Account.id(SERVER_ACCOUNT_NUMBER);
     serverIdent = new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
-    userId = new Account.Id(USER_ACCOUNT_NUMBER);
+    userId = Account.id(USER_ACCOUNT_NUMBER);
     userIdent = newPersonIdent(userId, serverIdent);
   }
 
@@ -124,9 +123,9 @@
   }
 
   private static Optional<Account> getAccount(Account.Id id) {
-    Account account = new Account(id, TimeUtil.nowTs());
+    Account.Builder account = Account.builder(id, TimeUtil.nowTs());
     account.setFullName("Account " + id);
-    return Optional.of(account);
+    return Optional.of(account.build());
   }
 
   private Optional<GroupDescription.Basic> getGroup(AccountGroup.UUID uuid) {
diff --git a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
index 120f026..e54ab5d 100644
--- a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
@@ -18,10 +18,10 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.AccountGroupByIdAudit;
+import com.google.gerrit.entities.AccountGroupMemberAudit;
 import com.google.gerrit.server.account.GroupUUID;
 import com.google.gerrit.server.group.InternalGroup;
 import java.sql.Timestamp;
@@ -37,7 +37,7 @@
 
   @Before
   public void setUp() throws Exception {
-    auditLogReader = new AuditLogReader(SERVER_ID, allUsersName);
+    auditLogReader = new AuditLogReader(allUsersName);
   }
 
   @Test
@@ -66,7 +66,7 @@
     assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid)).containsExactly(expAudit1);
 
     // User adds account 100002 to the group.
-    Account.Id id = new Account.Id(100002);
+    Account.Id id = Account.id(100002);
     addMembers(uuid, ImmutableSet.of(id));
 
     AccountGroupMemberAudit expAudit2 =
@@ -78,7 +78,7 @@
     // User removes account 100002 from the group.
     removeMembers(uuid, ImmutableSet.of(id));
 
-    expAudit2.removed(userId, getTipTimestamp(uuid));
+    expAudit2 = expAudit2.toBuilder().removed(userId, getTipTimestamp(uuid)).build();
     assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid))
         .containsExactly(expAudit1, expAudit2)
         .inOrder();
@@ -94,8 +94,8 @@
         createExpMemberAudit(groupId, userId, userId, getTipTimestamp(uuid));
     assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid)).containsExactly(expAudit1);
 
-    Account.Id id1 = new Account.Id(100002);
-    Account.Id id2 = new Account.Id(100003);
+    Account.Id id1 = Account.id(100002);
+    Account.Id id2 = Account.id(100003);
     addMembers(uuid, ImmutableSet.of(id1, id2));
 
     AccountGroupMemberAudit expAudit2 =
@@ -118,13 +118,13 @@
 
     addSubgroups(uuid, ImmutableSet.of(subgroupUuid));
 
-    AccountGroupByIdAud expAudit =
+    AccountGroupByIdAudit expAudit =
         createExpGroupAudit(group.getId(), subgroupUuid, userId, getTipTimestamp(uuid));
     assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid)).containsExactly(expAudit);
 
     removeSubgroups(uuid, ImmutableSet.of(subgroupUuid));
 
-    expAudit.removed(userId, getTipTimestamp(uuid));
+    expAudit = expAudit.toBuilder().removed(userId, getTipTimestamp(uuid)).build();
     assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid)).containsExactly(expAudit);
   }
 
@@ -140,9 +140,9 @@
 
     addSubgroups(uuid, ImmutableSet.of(subgroupUuid1, subgroupUuid2));
 
-    AccountGroupByIdAud expAudit1 =
+    AccountGroupByIdAudit expAudit1 =
         createExpGroupAudit(group.getId(), subgroupUuid1, userId, getTipTimestamp(uuid));
-    AccountGroupByIdAud expAudit2 =
+    AccountGroupByIdAudit expAudit2 =
         createExpGroupAudit(group.getId(), subgroupUuid2, userId, getTipTimestamp(uuid));
     assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid))
         .containsExactly(expAudit1, expAudit2)
@@ -158,9 +158,9 @@
         createExpMemberAudit(groupId, userId, userId, getTipTimestamp(uuid));
     assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid)).containsExactly(expMemberAudit);
 
-    Account.Id id1 = new Account.Id(100002);
-    Account.Id id2 = new Account.Id(100003);
-    Account.Id id3 = new Account.Id(100004);
+    Account.Id id1 = Account.id(100002);
+    Account.Id id2 = Account.id(100003);
+    Account.Id id3 = Account.id(100004);
     InternalGroup subgroup1 = createGroupAsUser(2, "test-group-2");
     InternalGroup subgroup2 = createGroupAsUser(3, "test-group-3");
     InternalGroup subgroup3 = createGroupAsUser(4, "test-group-4");
@@ -180,23 +180,23 @@
 
     // Add one subgroup.
     addSubgroups(uuid, ImmutableSet.of(subgroupUuid1));
-    AccountGroupByIdAud expGroupAudit1 =
+    AccountGroupByIdAudit expGroupAudit1 =
         createExpGroupAudit(group.getId(), subgroupUuid1, userId, getTipTimestamp(uuid));
     assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid))
         .containsExactly(expGroupAudit1);
 
     // Remove one account.
     removeMembers(uuid, ImmutableSet.of(id2));
-    expMemberAudit2.removed(userId, getTipTimestamp(uuid));
+    expMemberAudit2 = expMemberAudit2.toBuilder().removed(userId, getTipTimestamp(uuid)).build();
     assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid))
         .containsExactly(expMemberAudit, expMemberAudit1, expMemberAudit2)
         .inOrder();
 
     // Add two subgroups.
     addSubgroups(uuid, ImmutableSet.of(subgroupUuid2, subgroupUuid3));
-    AccountGroupByIdAud expGroupAudit2 =
+    AccountGroupByIdAudit expGroupAudit2 =
         createExpGroupAudit(group.getId(), subgroupUuid2, userId, getTipTimestamp(uuid));
-    AccountGroupByIdAud expGroupAudit3 =
+    AccountGroupByIdAudit expGroupAudit3 =
         createExpGroupAudit(group.getId(), subgroupUuid3, userId, getTipTimestamp(uuid));
     assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid))
         .containsExactly(expGroupAudit1, expGroupAudit2, expGroupAudit3)
@@ -215,15 +215,15 @@
 
     // Remove two subgroups.
     removeSubgroups(uuid, ImmutableSet.of(subgroupUuid1, subgroupUuid3));
-    expGroupAudit1.removed(userId, getTipTimestamp(uuid));
-    expGroupAudit3.removed(userId, getTipTimestamp(uuid));
+    expGroupAudit1 = expGroupAudit1.toBuilder().removed(userId, getTipTimestamp(uuid)).build();
+    expGroupAudit3 = expGroupAudit3.toBuilder().removed(userId, getTipTimestamp(uuid)).build();
     assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid))
         .containsExactly(expGroupAudit1, expGroupAudit2, expGroupAudit3)
         .inOrder();
 
     // Add back one removed subgroup.
     addSubgroups(uuid, ImmutableSet.of(subgroupUuid1));
-    AccountGroupByIdAud expGroupAudit4 =
+    AccountGroupByIdAudit expGroupAudit4 =
         createExpGroupAudit(group.getId(), subgroupUuid1, userId, getTipTimestamp(uuid));
     assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid))
         .containsExactly(expGroupAudit1, expGroupAudit2, expGroupAudit3, expGroupAudit4)
@@ -239,8 +239,8 @@
     InternalGroupCreation groupCreation =
         InternalGroupCreation.builder()
             .setGroupUUID(GroupUUID.make(groupName, serverIdent))
-            .setNameKey(new AccountGroup.NameKey(groupName))
-            .setId(new AccountGroup.Id(next))
+            .setNameKey(AccountGroup.nameKey(groupName))
+            .setId(AccountGroup.id(next))
             .build();
     InternalGroupUpdate groupUpdate =
         authorIdent.equals(serverIdent)
@@ -303,12 +303,21 @@
 
   private static AccountGroupMemberAudit createExpMemberAudit(
       AccountGroup.Id groupId, Account.Id id, Account.Id addedBy, Timestamp addedOn) {
-    return new AccountGroupMemberAudit(
-        new AccountGroupMemberAudit.Key(id, groupId, addedOn), addedBy);
+    return AccountGroupMemberAudit.builder()
+        .groupId(groupId)
+        .memberId(id)
+        .addedOn(addedOn)
+        .addedBy(addedBy)
+        .build();
   }
 
-  private static AccountGroupByIdAud createExpGroupAudit(
+  private static AccountGroupByIdAudit createExpGroupAudit(
       AccountGroup.Id groupId, AccountGroup.UUID uuid, Account.Id addedBy, Timestamp addedOn) {
-    return new AccountGroupByIdAud(new AccountGroupByIdAud.Key(groupId, uuid, addedOn), addedBy);
+    return AccountGroupByIdAudit.builder()
+        .groupId(groupId)
+        .includeUuid(uuid)
+        .addedOn(addedOn)
+        .addedBy(addedBy)
+        .build();
   }
 }
diff --git a/javatests/com/google/gerrit/server/group/db/BUILD b/javatests/com/google/gerrit/server/group/db/BUILD
index b4652c9..3303338 100644
--- a/javatests/com/google/gerrit/server/group/db/BUILD
+++ b/javatests/com/google/gerrit/server/group/db/BUILD
@@ -8,11 +8,11 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/common/data/testing:common-data-test-util",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/extensions/common/testing:common-test-util",
         "//java/com/google/gerrit/git",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/group/db/testing",
         "//java/com/google/gerrit/server/group/testing",
@@ -20,8 +20,8 @@
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//java/com/google/gerrit/truth",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
index c9ba72e..b7fe23d 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -17,23 +17,24 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.server.group.testing.InternalGroupSubject.internalGroups;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
 
 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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.testing.InternalGroupSubject;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.truth.OptionalSubject;
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -55,20 +56,20 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class GroupConfigTest extends GerritBaseTests {
+public class GroupConfigTest {
   private Project.NameKey projectName;
   private Repository repository;
   private TestRepository<?> testRepository;
-  private final AccountGroup.UUID groupUuid = new AccountGroup.UUID("users-XYZ");
-  private final AccountGroup.NameKey groupName = new AccountGroup.NameKey("users");
-  private final AccountGroup.Id groupId = new AccountGroup.Id(123);
+  private final AccountGroup.UUID groupUuid = AccountGroup.uuid("users-XYZ");
+  private final AccountGroup.NameKey groupName = AccountGroup.nameKey("users");
+  private final AccountGroup.Id groupId = AccountGroup.id(123);
   private final AuditLogFormatter auditLogFormatter =
       AuditLogFormatter.createBackedBy(ImmutableSet.of(), ImmutableSet.of(), "server-id");
   private final TimeZone timeZone = TimeZone.getTimeZone("America/Los_Angeles");
 
   @Before
   public void setUp() throws Exception {
-    projectName = new Project.NameKey("Test Repository");
+    projectName = Project.nameKey("Test Repository");
     repository = new InMemoryRepository(new DfsRepositoryDescription("Test Repository"));
     testRepository = new TestRepository<>(repository);
   }
@@ -95,7 +96,7 @@
 
   @Test
   public void nameOfGroupUpdateOverridesGroupCreation() throws Exception {
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("Another name");
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("Another name");
 
     InternalGroupCreation groupCreation =
         getPrefilledGroupCreationBuilder().setNameKey(groupName).build();
@@ -109,26 +110,13 @@
   @Test
   public void nameOfNewGroupMustNotBeEmpty() throws Exception {
     InternalGroupCreation groupCreation =
-        getPrefilledGroupCreationBuilder().setNameKey(new AccountGroup.NameKey("")).build();
+        getPrefilledGroupCreationBuilder().setNameKey(AccountGroup.nameKey("")).build();
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      exception.expectCause(instanceOf(ConfigInvalidException.class));
-      exception.expectMessage("Name of the group " + groupUuid);
-      groupConfig.commit(metaDataUpdate);
-    }
-  }
-
-  @Test
-  public void nameOfNewGroupMustNotBeNull() throws Exception {
-    InternalGroupCreation groupCreation =
-        getPrefilledGroupCreationBuilder().setNameKey(new AccountGroup.NameKey(null)).build();
-    GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
-
-    try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      exception.expectCause(instanceOf(ConfigInvalidException.class));
-      exception.expectMessage("Name of the group " + groupUuid);
-      groupConfig.commit(metaDataUpdate);
+      Throwable thrown = assertThrows(Throwable.class, () -> groupConfig.commit(metaDataUpdate));
+      assertThat(thrown.getCause(), instanceOf(ConfigInvalidException.class));
+      assertThat(thrown).hasMessageThat().contains("Name of the group " + groupUuid);
     }
   }
 
@@ -144,13 +132,13 @@
   @Test
   public void idOfNewGroupMustNotBeNegative() throws Exception {
     InternalGroupCreation groupCreation =
-        getPrefilledGroupCreationBuilder().setId(new AccountGroup.Id(-2)).build();
+        getPrefilledGroupCreationBuilder().setId(AccountGroup.id(-2)).build();
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      exception.expectCause(instanceOf(ConfigInvalidException.class));
-      exception.expectMessage("ID of the group " + groupUuid);
-      groupConfig.commit(metaDataUpdate);
+      Throwable thrown = assertThrows(Throwable.class, () -> groupConfig.commit(metaDataUpdate));
+      assertThat(thrown.getCause(), instanceOf(ConfigInvalidException.class));
+      assertThat(thrown).hasMessageThat().contains("ID of the group " + groupUuid);
     }
   }
 
@@ -207,7 +195,7 @@
 
   @Test
   public void specifiedOwnerGroupUuidIsRespectedForNewGroup() throws Exception {
-    AccountGroup.UUID ownerGroupUuid = new AccountGroup.UUID("anotherOwnerUuid");
+    AccountGroup.UUID ownerGroupUuid = AccountGroup.uuid("anotherOwnerUuid");
 
     InternalGroupCreation groupCreation = getPrefilledGroupCreationBuilder().build();
     InternalGroupUpdate groupUpdate =
@@ -219,32 +207,17 @@
   }
 
   @Test
-  public void ownerGroupUuidOfNewGroupMustNotBeNull() throws Exception {
-    InternalGroupCreation groupCreation = getPrefilledGroupCreationBuilder().build();
-    InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setOwnerGroupUUID(new AccountGroup.UUID(null)).build();
-    GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
-    groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
-
-    try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      exception.expectCause(instanceOf(ConfigInvalidException.class));
-      exception.expectMessage("Owner UUID of the group " + groupUuid);
-      groupConfig.commit(metaDataUpdate);
-    }
-  }
-
-  @Test
   public void ownerGroupUuidOfNewGroupMustNotBeEmpty() throws Exception {
     InternalGroupCreation groupCreation = getPrefilledGroupCreationBuilder().build();
     InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setOwnerGroupUUID(new AccountGroup.UUID("")).build();
+        InternalGroupUpdate.builder().setOwnerGroupUUID(AccountGroup.uuid("")).build();
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
     groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      exception.expectCause(instanceOf(ConfigInvalidException.class));
-      exception.expectMessage("Owner UUID of the group " + groupUuid);
-      groupConfig.commit(metaDataUpdate);
+      Throwable thrown = assertThrows(Throwable.class, () -> groupConfig.commit(metaDataUpdate));
+      assertThat(thrown.getCause(), instanceOf(ConfigInvalidException.class));
+      assertThat(thrown).hasMessageThat().contains("Owner UUID of the group " + groupUuid);
     }
   }
 
@@ -303,8 +276,8 @@
 
   @Test
   public void specifiedMembersAreRespectedForNewGroup() throws Exception {
-    Account.Id member1 = new Account.Id(1);
-    Account.Id member2 = new Account.Id(2);
+    Account.Id member1 = Account.id(1);
+    Account.Id member2 = Account.id(2);
 
     InternalGroupCreation groupCreation = getPrefilledGroupCreationBuilder().build();
     InternalGroupUpdate groupUpdate =
@@ -319,8 +292,8 @@
 
   @Test
   public void specifiedSubgroupsAreRespectedForNewGroup() throws Exception {
-    AccountGroup.UUID subgroup1 = new AccountGroup.UUID("subgroup1");
-    AccountGroup.UUID subgroup2 = new AccountGroup.UUID("subgroup2");
+    AccountGroup.UUID subgroup1 = AccountGroup.uuid("subgroup1");
+    AccountGroup.UUID subgroup2 = AccountGroup.uuid("subgroup2");
 
     InternalGroupCreation groupCreation = getPrefilledGroupCreationBuilder().build();
     InternalGroupUpdate groupUpdate =
@@ -353,9 +326,11 @@
   public void idInConfigMustBeDefined() throws Exception {
     populateGroupConfig(groupUuid, "[group]\n\tname = users\n\townerGroupUuid = owners\n");
 
-    exception.expect(ConfigInvalidException.class);
-    exception.expectMessage("ID of the group " + groupUuid);
-    GroupConfig.loadForGroup(projectName, repository, groupUuid);
+    ConfigInvalidException thrown =
+        assertThrows(
+            ConfigInvalidException.class,
+            () -> GroupConfig.loadForGroup(projectName, repository, groupUuid));
+    assertThat(thrown).hasMessageThat().contains("ID of the group " + groupUuid);
   }
 
   @Test
@@ -363,9 +338,11 @@
     populateGroupConfig(
         groupUuid, "[group]\n\tname = users\n\tid = -5\n\townerGroupUuid = owners\n");
 
-    exception.expect(ConfigInvalidException.class);
-    exception.expectMessage("ID of the group " + groupUuid);
-    GroupConfig.loadForGroup(projectName, repository, groupUuid);
+    ConfigInvalidException thrown =
+        assertThrows(
+            ConfigInvalidException.class,
+            () -> GroupConfig.loadForGroup(projectName, repository, groupUuid));
+    assertThat(thrown).hasMessageThat().contains("ID of the group " + groupUuid);
   }
 
   @Test
@@ -389,9 +366,11 @@
   public void ownerGroupUuidInConfigMustBeDefined() throws Exception {
     populateGroupConfig(groupUuid, "[group]\n\tname = users\n\tid = 42\n");
 
-    exception.expect(ConfigInvalidException.class);
-    exception.expectMessage("Owner UUID of the group " + groupUuid);
-    GroupConfig.loadForGroup(projectName, repository, groupUuid);
+    ConfigInvalidException thrown =
+        assertThrows(
+            ConfigInvalidException.class,
+            () -> GroupConfig.loadForGroup(projectName, repository, groupUuid));
+    assertThat(thrown).hasMessageThat().contains("Owner UUID of the group " + groupUuid);
   }
 
   @Test
@@ -430,12 +409,12 @@
         .value()
         .members()
         .containsExactly(
-            new Account.Id(1),
-            new Account.Id(2),
-            new Account.Id(3),
-            new Account.Id(4),
-            new Account.Id(5),
-            new Account.Id(6));
+            Account.id(1),
+            Account.id(2),
+            Account.id(3),
+            Account.id(4),
+            Account.id(5),
+            Account.id(6));
   }
 
   @Test
@@ -443,9 +422,9 @@
     populateGroupConfig(groupUuid, "[group]\n\tname=users\n\tid = 42\n\townerGroupUuid = owners\n");
     populateMembersFile(groupUuid, "One");
 
-    exception.expect(ConfigInvalidException.class);
-    exception.expectMessage("Invalid file members");
-    loadGroup(groupUuid);
+    ConfigInvalidException thrown =
+        assertThrows(ConfigInvalidException.class, () -> loadGroup(groupUuid));
+    assertThat(thrown).hasMessageThat().contains("Invalid file members");
   }
 
   @Test
@@ -453,9 +432,9 @@
     populateGroupConfig(groupUuid, "[group]\n\tname=users\n\tid = 42\n\townerGroupUuid = owners\n");
     populateMembersFile(groupUuid, "1\t2");
 
-    exception.expect(ConfigInvalidException.class);
-    exception.expectMessage("Invalid file members");
-    loadGroup(groupUuid);
+    ConfigInvalidException thrown =
+        assertThrows(ConfigInvalidException.class, () -> loadGroup(groupUuid));
+    assertThat(thrown).hasMessageThat().contains("Invalid file members");
   }
 
   @Test
@@ -494,12 +473,12 @@
         .value()
         .subgroups()
         .containsExactly(
-            new AccountGroup.UUID("1"),
-            new AccountGroup.UUID("2"),
-            new AccountGroup.UUID("3"),
-            new AccountGroup.UUID("4"),
-            new AccountGroup.UUID("5"),
-            new AccountGroup.UUID("6"));
+            AccountGroup.uuid("1"),
+            AccountGroup.uuid("2"),
+            AccountGroup.uuid("3"),
+            AccountGroup.uuid("4"),
+            AccountGroup.uuid("5"),
+            AccountGroup.uuid("6"));
   }
 
   @Test
@@ -508,7 +487,7 @@
     populateSubgroupsFile(groupUuid, "1\t2 3");
 
     Optional<InternalGroup> group = loadGroup(groupUuid);
-    assertThatGroup(group).value().subgroups().containsExactly(new AccountGroup.UUID("1\t2 3"));
+    assertThatGroup(group).value().subgroups().containsExactly(AccountGroup.uuid("1\t2 3"));
   }
 
   @Test
@@ -520,13 +499,13 @@
     assertThatGroup(group)
         .value()
         .subgroups()
-        .containsExactly(new AccountGroup.UUID("1\t2"), new AccountGroup.UUID("3"));
+        .containsExactly(AccountGroup.uuid("1\t2"), AccountGroup.uuid("3"));
   }
 
   @Test
   public void nameCanBeUpdated() throws Exception {
     createArbitraryGroup(groupUuid);
-    AccountGroup.NameKey newName = new AccountGroup.NameKey("New name");
+    AccountGroup.NameKey newName = AccountGroup.nameKey("New name");
 
     InternalGroupUpdate groupUpdate = InternalGroupUpdate.builder().setName(newName).build();
     updateGroup(groupUuid, groupUpdate);
@@ -536,41 +515,25 @@
   }
 
   @Test
-  public void nameCannotBeUpdatedToNull() throws Exception {
-    createArbitraryGroup(groupUuid);
-
-    GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repository, groupUuid);
-    InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey(null)).build();
-    groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
-
-    try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      exception.expectCause(instanceOf(ConfigInvalidException.class));
-      exception.expectMessage("Name of the group " + groupUuid);
-      groupConfig.commit(metaDataUpdate);
-    }
-  }
-
-  @Test
   public void nameCannotBeUpdatedToEmptyString() throws Exception {
     createArbitraryGroup(groupUuid);
 
     GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repository, groupUuid);
     InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey("")).build();
+        InternalGroupUpdate.builder().setName(AccountGroup.nameKey("")).build();
     groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      exception.expectCause(instanceOf(ConfigInvalidException.class));
-      exception.expectMessage("Name of the group " + groupUuid);
-      groupConfig.commit(metaDataUpdate);
+      Throwable thrown = assertThrows(Throwable.class, () -> groupConfig.commit(metaDataUpdate));
+      assertThat(thrown.getCause(), instanceOf(ConfigInvalidException.class));
+      assertThat(thrown).hasMessageThat().contains("Name of the group " + groupUuid);
     }
   }
 
   @Test
   public void nameCanBeUpdatedToEmptyStringIfExplicitlySpecified() throws Exception {
     createArbitraryGroup(groupUuid);
-    AccountGroup.NameKey emptyName = new AccountGroup.NameKey("");
+    AccountGroup.NameKey emptyName = AccountGroup.nameKey("");
 
     GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repository, groupUuid);
     groupConfig.setAllowSaveEmptyName();
@@ -608,7 +571,7 @@
   @Test
   public void ownerGroupUuidCanBeUpdated() throws Exception {
     createArbitraryGroup(groupUuid);
-    AccountGroup.UUID newOwnerGroupUuid = new AccountGroup.UUID("New owner");
+    AccountGroup.UUID newOwnerGroupUuid = AccountGroup.uuid("New owner");
 
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder().setOwnerGroupUUID(newOwnerGroupUuid).build();
@@ -619,34 +582,18 @@
   }
 
   @Test
-  public void ownerGroupUuidCannotBeUpdatedToNull() throws Exception {
-    createArbitraryGroup(groupUuid);
-
-    GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repository, groupUuid);
-    InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setOwnerGroupUUID(new AccountGroup.UUID(null)).build();
-    groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
-
-    try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      exception.expectCause(instanceOf(ConfigInvalidException.class));
-      exception.expectMessage("Owner UUID of the group " + groupUuid);
-      groupConfig.commit(metaDataUpdate);
-    }
-  }
-
-  @Test
   public void ownerGroupUuidCannotBeUpdatedToEmptyString() throws Exception {
     createArbitraryGroup(groupUuid);
 
     GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repository, groupUuid);
     InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setOwnerGroupUUID(new AccountGroup.UUID("")).build();
+        InternalGroupUpdate.builder().setOwnerGroupUUID(AccountGroup.uuid("")).build();
     groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
 
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
-      exception.expectCause(instanceOf(ConfigInvalidException.class));
-      exception.expectMessage("Owner UUID of the group " + groupUuid);
-      groupConfig.commit(metaDataUpdate);
+      Throwable thrown = assertThrows(Throwable.class, () -> groupConfig.commit(metaDataUpdate));
+      assertThat(thrown.getCause(), instanceOf(ConfigInvalidException.class));
+      assertThat(thrown).hasMessageThat().contains("Owner UUID of the group " + groupUuid);
     }
   }
 
@@ -675,7 +622,7 @@
 
     InternalGroupUpdate laterGroupUpdate =
         InternalGroupUpdate.builder()
-            .setName(new AccountGroup.NameKey("Another name"))
+            .setName(AccountGroup.nameKey("Another name"))
             .setUpdatedOn(updatedOn)
             .build();
     Optional<InternalGroup> group = updateGroup(groupCreation.getGroupUUID(), laterGroupUpdate);
@@ -688,8 +635,8 @@
   @Test
   public void membersCanBeAdded() throws Exception {
     createArbitraryGroup(groupUuid);
-    Account.Id member1 = new Account.Id(1);
-    Account.Id member2 = new Account.Id(2);
+    Account.Id member1 = Account.id(1);
+    Account.Id member2 = Account.id(2);
 
     InternalGroupUpdate groupUpdate1 =
         InternalGroupUpdate.builder()
@@ -710,8 +657,8 @@
   @Test
   public void membersCanBeDeleted() throws Exception {
     createArbitraryGroup(groupUuid);
-    Account.Id member1 = new Account.Id(1);
-    Account.Id member2 = new Account.Id(2);
+    Account.Id member1 = Account.id(1);
+    Account.Id member2 = Account.id(2);
 
     InternalGroupUpdate groupUpdate1 =
         InternalGroupUpdate.builder()
@@ -732,8 +679,8 @@
   @Test
   public void subgroupsCanBeAdded() throws Exception {
     createArbitraryGroup(groupUuid);
-    AccountGroup.UUID subgroup1 = new AccountGroup.UUID("subgroups1");
-    AccountGroup.UUID subgroup2 = new AccountGroup.UUID("subgroups2");
+    AccountGroup.UUID subgroup1 = AccountGroup.uuid("subgroups1");
+    AccountGroup.UUID subgroup2 = AccountGroup.uuid("subgroups2");
 
     InternalGroupUpdate groupUpdate1 =
         InternalGroupUpdate.builder()
@@ -754,8 +701,8 @@
   @Test
   public void subgroupsCanBeDeleted() throws Exception {
     createArbitraryGroup(groupUuid);
-    AccountGroup.UUID subgroup1 = new AccountGroup.UUID("subgroups1");
-    AccountGroup.UUID subgroup2 = new AccountGroup.UUID("subgroups2");
+    AccountGroup.UUID subgroup1 = AccountGroup.uuid("subgroups1");
+    AccountGroup.UUID subgroup2 = AccountGroup.uuid("subgroups2");
 
     InternalGroupUpdate groupUpdate1 =
         InternalGroupUpdate.builder()
@@ -798,13 +745,12 @@
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setDescription("A test group")
-            .setOwnerGroupUUID(new AccountGroup.UUID("another owner"))
+            .setOwnerGroupUUID(AccountGroup.uuid("another owner"))
             .setVisibleToAll(true)
-            .setName(new AccountGroup.NameKey("Another name"))
+            .setName(AccountGroup.nameKey("Another name"))
             .setUpdatedOn(new Timestamp(92900892))
-            .setMemberModification(members -> ImmutableSet.of(new Account.Id(1), new Account.Id(2)))
-            .setSubgroupModification(
-                subgroups -> ImmutableSet.of(new AccountGroup.UUID("subgroup")))
+            .setMemberModification(members -> ImmutableSet.of(Account.id(1), Account.id(2)))
+            .setSubgroupModification(subgroups -> ImmutableSet.of(AccountGroup.uuid("subgroup")))
             .build();
 
     Optional<InternalGroup> createdGroup = createGroup(groupCreation, groupUpdate);
@@ -820,13 +766,12 @@
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setDescription("A test group")
-            .setOwnerGroupUUID(new AccountGroup.UUID("another owner"))
+            .setOwnerGroupUUID(AccountGroup.uuid("another owner"))
             .setVisibleToAll(true)
-            .setName(new AccountGroup.NameKey("Another name"))
+            .setName(AccountGroup.nameKey("Another name"))
             .setUpdatedOn(new Timestamp(92900892))
-            .setMemberModification(members -> ImmutableSet.of(new Account.Id(1), new Account.Id(2)))
-            .setSubgroupModification(
-                subgroups -> ImmutableSet.of(new AccountGroup.UUID("subgroup")))
+            .setMemberModification(members -> ImmutableSet.of(Account.id(1), Account.id(2)))
+            .setSubgroupModification(subgroups -> ImmutableSet.of(AccountGroup.uuid("subgroup")))
             .build();
 
     Optional<InternalGroup> updatedGroup = updateGroup(groupUuid, groupUpdate);
@@ -843,19 +788,18 @@
     InternalGroupUpdate initialGroupUpdate =
         InternalGroupUpdate.builder()
             .setDescription("A test group")
-            .setOwnerGroupUUID(new AccountGroup.UUID("another owner"))
+            .setOwnerGroupUUID(AccountGroup.uuid("another owner"))
             .setVisibleToAll(true)
-            .setName(new AccountGroup.NameKey("Another name"))
+            .setName(AccountGroup.nameKey("Another name"))
             .setUpdatedOn(new Timestamp(92900892))
-            .setMemberModification(members -> ImmutableSet.of(new Account.Id(1), new Account.Id(2)))
-            .setSubgroupModification(
-                subgroups -> ImmutableSet.of(new AccountGroup.UUID("subgroup")))
+            .setMemberModification(members -> ImmutableSet.of(Account.id(1), Account.id(2)))
+            .setSubgroupModification(subgroups -> ImmutableSet.of(AccountGroup.uuid("subgroup")))
             .build();
     createGroup(groupCreation, initialGroupUpdate);
 
     // Only update one of the properties.
     InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey("Another name")).build();
+        InternalGroupUpdate.builder().setName(AccountGroup.nameKey("Another name")).build();
 
     Optional<InternalGroup> updatedGroup = updateGroup(groupCreation.getGroupUUID(), groupUpdate);
     Optional<InternalGroup> reloadedGroup = loadGroup(groupCreation.getGroupUUID());
@@ -870,7 +814,7 @@
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
     commit(groupConfig);
 
-    AccountGroup.NameKey name = new AccountGroup.NameKey("Robots");
+    AccountGroup.NameKey name = AccountGroup.nameKey("Robots");
     InternalGroupUpdate groupUpdate1 = InternalGroupUpdate.builder().setName(name).build();
     groupConfig.setGroupUpdate(groupUpdate1, auditLogFormatter);
     commit(groupConfig);
@@ -907,7 +851,7 @@
     RevCommit commitAfterCreation = getLatestCommitForGroup(groupUuid);
 
     InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey("Another name")).build();
+        InternalGroupUpdate.builder().setName(AccountGroup.nameKey("Another name")).build();
     updateGroup(groupUuid, groupUpdate);
 
     RevCommit commitAfterUpdate = getLatestCommitForGroup(groupUuid);
@@ -990,9 +934,7 @@
     createArbitraryGroup(groupUuid);
 
     InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder()
-            .setOwnerGroupUUID(new AccountGroup.UUID("Another owner"))
-            .build();
+        InternalGroupUpdate.builder().setOwnerGroupUUID(AccountGroup.uuid("Another owner")).build();
     updateGroup(groupUuid, groupUpdate);
 
     RevCommit commitBeforeUpdate = getLatestCommitForGroup(groupUuid);
@@ -1008,8 +950,7 @@
 
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
-            .setMemberModification(
-                members -> Sets.union(members, ImmutableSet.of(new Account.Id(10))))
+            .setMemberModification(members -> Sets.union(members, ImmutableSet.of(Account.id(10))))
             .build();
     updateGroup(groupUuid, groupUpdate);
 
@@ -1027,8 +968,7 @@
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setSubgroupModification(
-                subgroups ->
-                    Sets.union(subgroups, ImmutableSet.of(new AccountGroup.UUID("subgroup"))))
+                subgroups -> Sets.union(subgroups, ImmutableSet.of(AccountGroup.uuid("subgroup"))))
             .build();
     updateGroup(groupUuid, groupUpdate);
 
@@ -1045,7 +985,7 @@
         getPrefilledGroupCreationBuilder().setGroupUUID(groupUuid).build();
 
     InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey("Another name")).build();
+        InternalGroupUpdate.builder().setName(AccountGroup.nameKey("Another name")).build();
 
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
     groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
@@ -1128,7 +1068,7 @@
             .build();
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
-            .setName(new AccountGroup.NameKey("Another name"))
+            .setName(AccountGroup.nameKey("Another name"))
             .setUpdatedOn(createdOn)
             .build();
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
@@ -1161,7 +1101,7 @@
             .build();
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
-            .setName(new AccountGroup.NameKey("Another name"))
+            .setName(AccountGroup.nameKey("Another name"))
             .setUpdatedOn(createdOn)
             .build();
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
@@ -1187,7 +1127,7 @@
 
     createArbitraryGroup(groupUuid);
     InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey("Another name")).build();
+        InternalGroupUpdate.builder().setName(AccountGroup.nameKey("Another name")).build();
     updateGroup(groupUuid, groupUpdate);
 
     RevCommit revCommit = getLatestCommitForGroup(groupUuid);
@@ -1202,7 +1142,7 @@
     createArbitraryGroup(groupUuid);
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
-            .setName(new AccountGroup.NameKey("Another name"))
+            .setName(AccountGroup.nameKey("Another name"))
             .setUpdatedOn(new Timestamp(updatedOnAsSecondsSinceEpoch * 1000))
             .build();
     updateGroup(groupUuid, groupUpdate);
@@ -1220,7 +1160,7 @@
     createArbitraryGroup(groupUuid);
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
-            .setName(new AccountGroup.NameKey("Another name"))
+            .setName(AccountGroup.nameKey("Another name"))
             .setUpdatedOn(updatedOn)
             .build();
     GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repository, groupUuid);
@@ -1248,7 +1188,7 @@
     createArbitraryGroup(groupUuid);
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
-            .setName(new AccountGroup.NameKey("Another name"))
+            .setName(AccountGroup.nameKey("Another name"))
             .setUpdatedOn(updatedOn)
             .build();
     GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repository, groupUuid);
@@ -1281,14 +1221,14 @@
   public void groupCanBeLoadedAtASpecificRevision() throws Exception {
     createArbitraryGroup(groupUuid);
 
-    AccountGroup.NameKey firstName = new AccountGroup.NameKey("Bots");
+    AccountGroup.NameKey firstName = AccountGroup.nameKey("Bots");
     InternalGroupUpdate groupUpdate1 = InternalGroupUpdate.builder().setName(firstName).build();
     updateGroup(groupUuid, groupUpdate1);
 
     RevCommit commitAfterUpdate1 = getLatestCommitForGroup(groupUuid);
 
     InternalGroupUpdate groupUpdate2 =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey("Robots")).build();
+        InternalGroupUpdate.builder().setName(AccountGroup.nameKey("Robots")).build();
     updateGroup(groupUuid, groupUpdate2);
 
     GroupConfig groupConfig =
@@ -1315,7 +1255,7 @@
     InternalGroupCreation groupCreation =
         getPrefilledGroupCreationBuilder().setGroupUUID(groupUuid).build();
     InternalGroupUpdate groupUpdate =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey("Another name")).build();
+        InternalGroupUpdate.builder().setName(AccountGroup.nameKey("Another name")).build();
     createGroup(groupCreation, groupUpdate);
 
     RevCommit revCommit = getLatestCommitForGroup(groupUuid);
@@ -1324,8 +1264,8 @@
 
   @Test
   public void commitMessageOfNewGroupWithMembersContainsFooters() throws Exception {
-    Account account13 = createAccount(new Account.Id(13), "John");
-    Account account7 = createAccount(new Account.Id(7), "Jane");
+    Account account13 = createAccount(Account.id(13), "John");
+    Account account7 = createAccount(Account.id(7), "Jane");
     ImmutableSet<Account> accounts = ImmutableSet.of(account13, account7);
 
     AuditLogFormatter auditLogFormatter =
@@ -1335,7 +1275,7 @@
         getPrefilledGroupCreationBuilder().setGroupUUID(groupUuid).build();
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
-            .setMemberModification(members -> ImmutableSet.of(account13.getId(), account7.getId()))
+            .setMemberModification(members -> ImmutableSet.of(account13.id(), account7.id()))
             .build();
 
     GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
@@ -1349,8 +1289,8 @@
 
   @Test
   public void commitMessageOfNewGroupWithSubgroupsContainsFooters() throws Exception {
-    GroupDescription.Basic group1 = createGroup(new AccountGroup.UUID("129403"), "Bots");
-    GroupDescription.Basic group2 = createGroup(new AccountGroup.UUID("8903493"), "Verifiers");
+    GroupDescription.Basic group1 = createGroup(AccountGroup.uuid("129403"), "Bots");
+    GroupDescription.Basic group2 = createGroup(AccountGroup.uuid("8903493"), "Verifiers");
     ImmutableSet<GroupDescription.Basic> groups = ImmutableSet.of(group1, group2);
 
     AuditLogFormatter auditLogFormatter =
@@ -1374,8 +1314,8 @@
 
   @Test
   public void commitMessageOfMemberAdditionContainsFooters() throws Exception {
-    Account account13 = createAccount(new Account.Id(13), "John");
-    Account account7 = createAccount(new Account.Id(7), "Jane");
+    Account account13 = createAccount(Account.id(13), "John");
+    Account account7 = createAccount(Account.id(7), "Jane");
     ImmutableSet<Account> accounts = ImmutableSet.of(account13, account7);
 
     createArbitraryGroup(groupUuid);
@@ -1385,7 +1325,7 @@
 
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
-            .setMemberModification(members -> ImmutableSet.of(account13.getId(), account7.getId()))
+            .setMemberModification(members -> ImmutableSet.of(account13.id(), account7.id()))
             .build();
     updateGroup(groupUuid, groupUpdate, auditLogFormatter);
 
@@ -1396,8 +1336,8 @@
 
   @Test
   public void commitMessageOfMemberRemovalContainsFooters() throws Exception {
-    Account account13 = createAccount(new Account.Id(13), "John");
-    Account account7 = createAccount(new Account.Id(7), "Jane");
+    Account account13 = createAccount(Account.id(13), "John");
+    Account account7 = createAccount(Account.id(7), "Jane");
     ImmutableSet<Account> accounts = ImmutableSet.of(account13, account7);
 
     createArbitraryGroup(groupUuid);
@@ -1407,13 +1347,13 @@
 
     InternalGroupUpdate groupUpdate1 =
         InternalGroupUpdate.builder()
-            .setMemberModification(members -> ImmutableSet.of(account13.getId(), account7.getId()))
+            .setMemberModification(members -> ImmutableSet.of(account13.id(), account7.id()))
             .build();
     updateGroup(groupUuid, groupUpdate1, auditLogFormatter);
 
     InternalGroupUpdate groupUpdate2 =
         InternalGroupUpdate.builder()
-            .setMemberModification(members -> ImmutableSet.of(account7.getId()))
+            .setMemberModification(members -> ImmutableSet.of(account7.id()))
             .build();
     updateGroup(groupUuid, groupUpdate2, auditLogFormatter);
 
@@ -1423,8 +1363,8 @@
 
   @Test
   public void commitMessageOfSubgroupAdditionContainsFooters() throws Exception {
-    GroupDescription.Basic group1 = createGroup(new AccountGroup.UUID("129403"), "Bots");
-    GroupDescription.Basic group2 = createGroup(new AccountGroup.UUID("8903493"), "Verifiers");
+    GroupDescription.Basic group1 = createGroup(AccountGroup.uuid("129403"), "Bots");
+    GroupDescription.Basic group2 = createGroup(AccountGroup.uuid("8903493"), "Verifiers");
     ImmutableSet<GroupDescription.Basic> groups = ImmutableSet.of(group1, group2);
 
     createArbitraryGroup(groupUuid);
@@ -1446,8 +1386,8 @@
 
   @Test
   public void commitMessageOfSubgroupRemovalContainsFooters() throws Exception {
-    GroupDescription.Basic group1 = createGroup(new AccountGroup.UUID("129403"), "Bots");
-    GroupDescription.Basic group2 = createGroup(new AccountGroup.UUID("8903493"), "Verifiers");
+    GroupDescription.Basic group1 = createGroup(AccountGroup.uuid("129403"), "Bots");
+    GroupDescription.Basic group2 = createGroup(AccountGroup.uuid("8903493"), "Verifiers");
     ImmutableSet<GroupDescription.Basic> groups = ImmutableSet.of(group1, group2);
 
     createArbitraryGroup(groupUuid);
@@ -1478,11 +1418,11 @@
     createArbitraryGroup(groupUuid);
 
     InternalGroupUpdate groupUpdate1 =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey("Old name")).build();
+        InternalGroupUpdate.builder().setName(AccountGroup.nameKey("Old name")).build();
     updateGroup(groupUuid, groupUpdate1);
 
     InternalGroupUpdate groupUpdate2 =
-        InternalGroupUpdate.builder().setName(new AccountGroup.NameKey("New name")).build();
+        InternalGroupUpdate.builder().setName(AccountGroup.nameKey("New name")).build();
     updateGroup(groupUuid, groupUpdate2);
 
     RevCommit revCommit = getLatestCommitForGroup(groupUuid);
@@ -1492,11 +1432,11 @@
 
   @Test
   public void commitMessageFootersCanBeMixed() throws Exception {
-    Account account13 = createAccount(new Account.Id(13), "John");
-    Account account7 = createAccount(new Account.Id(7), "Jane");
+    Account account13 = createAccount(Account.id(13), "John");
+    Account account7 = createAccount(Account.id(7), "Jane");
     ImmutableSet<Account> accounts = ImmutableSet.of(account13, account7);
-    GroupDescription.Basic group1 = createGroup(new AccountGroup.UUID("129403"), "Bots");
-    GroupDescription.Basic group2 = createGroup(new AccountGroup.UUID("8903493"), "Verifiers");
+    GroupDescription.Basic group1 = createGroup(AccountGroup.uuid("129403"), "Bots");
+    GroupDescription.Basic group2 = createGroup(AccountGroup.uuid("8903493"), "Verifiers");
     ImmutableSet<GroupDescription.Basic> groups = ImmutableSet.of(group1, group2);
 
     createArbitraryGroup(groupUuid);
@@ -1506,16 +1446,16 @@
 
     InternalGroupUpdate groupUpdate1 =
         InternalGroupUpdate.builder()
-            .setName(new AccountGroup.NameKey("Old name"))
-            .setMemberModification(members -> ImmutableSet.of(account7.getId()))
+            .setName(AccountGroup.nameKey("Old name"))
+            .setMemberModification(members -> ImmutableSet.of(account7.id()))
             .setSubgroupModification(subgroups -> ImmutableSet.of(group2.getGroupUUID()))
             .build();
     updateGroup(groupUuid, groupUpdate1, auditLogFormatter);
 
     InternalGroupUpdate groupUpdate2 =
         InternalGroupUpdate.builder()
-            .setName(new AccountGroup.NameKey("New name"))
-            .setMemberModification(members -> ImmutableSet.of(account13.getId()))
+            .setName(AccountGroup.nameKey("New name"))
+            .setMemberModification(members -> ImmutableSet.of(account13.id()))
             .setSubgroupModification(subgroups -> ImmutableSet.of(group1.getGroupUUID()))
             .build();
     updateGroup(groupUuid, groupUpdate2, auditLogFormatter);
@@ -1623,7 +1563,7 @@
 
     MetaDataUpdate metaDataUpdate =
         new MetaDataUpdate(
-            GitReferenceUpdated.DISABLED, new Project.NameKey("Test Repository"), repository);
+            GitReferenceUpdated.DISABLED, Project.nameKey("Test Repository"), repository);
     metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
     metaDataUpdate.getCommitBuilder().setAuthor(serverIdent);
     return metaDataUpdate;
@@ -1641,9 +1581,9 @@
   }
 
   private static Account createAccount(Account.Id id, String name) {
-    Account account = new Account(id, TimeUtil.nowTs());
+    Account.Builder account = Account.builder(id, TimeUtil.nowTs());
     account.setFullName(name);
-    return account;
+    return account.build();
   }
 
   private static GroupDescription.Basic createGroup(AccountGroup.UUID uuid, String name) {
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index 2a8d551..df97e88 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.server.group.db;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.common.data.testing.GroupReferenceSubject.groupReferences;
+import static com.google.gerrit.entities.RefNames.REFS_GROUPNAMES;
 import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.commits;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_GROUPNAMES;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -27,19 +27,18 @@
 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.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.testing.CommitInfoSubject;
 import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.GitTestUtil;
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gerrit.truth.ListSubject;
@@ -69,13 +68,13 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class GroupNameNotesTest extends GerritBaseTests {
+public class GroupNameNotesTest {
   private static final String SERVER_NAME = "Gerrit Server";
   private static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
   private static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
 
-  private final AccountGroup.UUID groupUuid = new AccountGroup.UUID("users-XYZ");
-  private final AccountGroup.NameKey groupName = new AccountGroup.NameKey("users");
+  private final AccountGroup.UUID groupUuid = AccountGroup.uuid("users-XYZ");
+  private final AccountGroup.NameKey groupName = AccountGroup.nameKey("users");
 
   private AtomicInteger idCounter;
   private AllUsersName allUsersName;
@@ -105,19 +104,21 @@
 
   @Test
   public void uuidOfNewGroupMustNotBeNull() throws Exception {
-    exception.expect(NullPointerException.class);
-    GroupNameNotes.forNewGroup(allUsersName, repo, null, groupName);
+    assertThrows(
+        NullPointerException.class,
+        () -> GroupNameNotes.forNewGroup(allUsersName, repo, null, groupName));
   }
 
   @Test
   public void nameOfNewGroupMustNotBeNull() throws Exception {
-    exception.expect(NullPointerException.class);
-    GroupNameNotes.forNewGroup(allUsersName, repo, groupUuid, null);
+    assertThrows(
+        NullPointerException.class,
+        () -> GroupNameNotes.forNewGroup(allUsersName, repo, groupUuid, null));
   }
 
   @Test
   public void nameOfNewGroupMayBeEmpty() throws Exception {
-    AccountGroup.NameKey emptyName = new AccountGroup.NameKey("");
+    AccountGroup.NameKey emptyName = AccountGroup.nameKey("");
     createGroup(groupUuid, emptyName);
 
     Optional<GroupReference> groupReference = loadGroup(emptyName);
@@ -128,17 +129,19 @@
   public void newGroupMustNotReuseNameOfAnotherGroup() throws Exception {
     createGroup(groupUuid, groupName);
 
-    AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("AnotherGroup");
-    exception.expect(DuplicateKeyException.class);
-    exception.expectMessage(groupName.get());
-    GroupNameNotes.forNewGroup(allUsersName, repo, anotherGroupUuid, groupName);
+    AccountGroup.UUID anotherGroupUuid = AccountGroup.uuid("AnotherGroup");
+    DuplicateKeyException thrown =
+        assertThrows(
+            DuplicateKeyException.class,
+            () -> GroupNameNotes.forNewGroup(allUsersName, repo, anotherGroupUuid, groupName));
+    assertThat(thrown).hasMessageThat().contains(groupName.get());
   }
 
   @Test
   public void newGroupMayReuseUuidOfAnotherGroup() throws Exception {
     createGroup(groupUuid, groupName);
 
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
     createGroup(groupUuid, anotherName);
 
     Optional<GroupReference> group1 = loadGroup(groupName);
@@ -151,7 +154,7 @@
   public void groupCanBeRenamed() throws Exception {
     createGroup(groupUuid, groupName);
 
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
     renameGroup(groupUuid, groupName, anotherName);
 
     Optional<GroupReference> groupReference = loadGroup(anotherName);
@@ -163,7 +166,7 @@
   public void previousNameOfGroupCannotBeUsedAfterRename() throws Exception {
     createGroup(groupUuid, groupName);
 
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
     renameGroup(groupUuid, groupName, anotherName);
 
     Optional<GroupReference> group = loadGroup(groupName);
@@ -173,61 +176,75 @@
   @Test
   public void groupCannotBeRenamedToNull() throws Exception {
     createGroup(groupUuid, groupName);
-
-    exception.expect(NullPointerException.class);
-    GroupNameNotes.forRename(allUsersName, repo, groupUuid, groupName, null);
+    assertThrows(
+        NullPointerException.class,
+        () -> GroupNameNotes.forRename(allUsersName, repo, groupUuid, groupName, null));
   }
 
   @Test
   public void oldNameOfGroupMustBeSpecifiedForRename() throws Exception {
     createGroup(groupUuid, groupName);
 
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
-    exception.expect(NullPointerException.class);
-    GroupNameNotes.forRename(allUsersName, repo, groupUuid, null, anotherName);
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
+    assertThrows(
+        NullPointerException.class,
+        () -> GroupNameNotes.forRename(allUsersName, repo, groupUuid, null, anotherName));
   }
 
   @Test
   public void groupCannotBeRenamedWhenOldNameIsWrong() throws Exception {
     createGroup(groupUuid, groupName);
 
-    AccountGroup.NameKey anotherOldName = new AccountGroup.NameKey("contributors");
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
-    exception.expect(ConfigInvalidException.class);
-    exception.expectMessage(anotherOldName.get());
-    GroupNameNotes.forRename(allUsersName, repo, groupUuid, anotherOldName, anotherName);
+    AccountGroup.NameKey anotherOldName = AccountGroup.nameKey("contributors");
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
+    ConfigInvalidException thrown =
+        assertThrows(
+            ConfigInvalidException.class,
+            () ->
+                GroupNameNotes.forRename(
+                    allUsersName, repo, groupUuid, anotherOldName, anotherName));
+    assertThat(thrown).hasMessageThat().contains(anotherOldName.get());
   }
 
   @Test
   public void groupCannotBeRenamedToNameOfAnotherGroup() throws Exception {
     createGroup(groupUuid, groupName);
-    AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("admins-ABC");
-    AccountGroup.NameKey anotherGroupName = new AccountGroup.NameKey("admins");
+    AccountGroup.UUID anotherGroupUuid = AccountGroup.uuid("admins-ABC");
+    AccountGroup.NameKey anotherGroupName = AccountGroup.nameKey("admins");
     createGroup(anotherGroupUuid, anotherGroupName);
 
-    exception.expect(DuplicateKeyException.class);
-    exception.expectMessage(anotherGroupName.get());
-    GroupNameNotes.forRename(allUsersName, repo, groupUuid, groupName, anotherGroupName);
+    DuplicateKeyException thrown =
+        assertThrows(
+            DuplicateKeyException.class,
+            () ->
+                GroupNameNotes.forRename(
+                    allUsersName, repo, groupUuid, groupName, anotherGroupName));
+    assertThat(thrown).hasMessageThat().contains(anotherGroupName.get());
   }
 
   @Test
   public void groupCannotBeRenamedWithoutSpecifiedUuid() throws Exception {
     createGroup(groupUuid, groupName);
 
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
-    exception.expect(NullPointerException.class);
-    GroupNameNotes.forRename(allUsersName, repo, null, groupName, anotherName);
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
+    assertThrows(
+        NullPointerException.class,
+        () -> GroupNameNotes.forRename(allUsersName, repo, null, groupName, anotherName));
   }
 
   @Test
   public void groupCannotBeRenamedWhenUuidIsWrong() throws Exception {
     createGroup(groupUuid, groupName);
 
-    AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("admins-ABC");
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
-    exception.expect(ConfigInvalidException.class);
-    exception.expectMessage(groupUuid.get());
-    GroupNameNotes.forRename(allUsersName, repo, anotherGroupUuid, groupName, anotherName);
+    AccountGroup.UUID anotherGroupUuid = AccountGroup.uuid("admins-ABC");
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
+    ConfigInvalidException thrown =
+        assertThrows(
+            ConfigInvalidException.class,
+            () ->
+                GroupNameNotes.forRename(
+                    allUsersName, repo, anotherGroupUuid, groupName, anotherName));
+    assertThat(thrown).hasMessageThat().contains(groupUuid.get());
   }
 
   @Test
@@ -248,8 +265,8 @@
     createGroup(groupUuid, groupName);
     ImmutableList<CommitInfo> commitsAfterCreation = log();
 
-    AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("admins-ABC");
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    AccountGroup.UUID anotherGroupUuid = AccountGroup.uuid("admins-ABC");
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
     createGroup(anotherGroupUuid, anotherName);
 
     ImmutableList<CommitInfo> commitsAfterFurtherGroup = log();
@@ -262,7 +279,7 @@
     createGroup(groupUuid, groupName);
     ImmutableList<CommitInfo> commitsAfterCreation = log();
 
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
     renameGroup(groupUuid, groupName, anotherName);
 
     ImmutableList<CommitInfo> commitsAfterRename = log();
@@ -298,7 +315,7 @@
   public void newCommitIsNotCreatedWhenCommittingGroupRenamingTwice() throws Exception {
     createGroup(groupUuid, groupName);
 
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
     GroupNameNotes groupNameNotes =
         GroupNameNotes.forRename(allUsersName, repo, groupUuid, groupName, anotherName);
 
@@ -323,7 +340,7 @@
   public void commitMessageMentionsGroupRenaming() throws Exception {
     createGroup(groupUuid, groupName);
 
-    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    AccountGroup.NameKey anotherName = AccountGroup.nameKey("admins");
     renameGroup(groupUuid, groupName, anotherName);
 
     ImmutableList<CommitInfo> commits = log();
@@ -341,18 +358,18 @@
 
   @Test
   public void nonExistentGroupCannotBeLoaded() throws Exception {
-    createGroup(new AccountGroup.UUID("contributors-MN"), new AccountGroup.NameKey("contributors"));
+    createGroup(AccountGroup.uuid("contributors-MN"), AccountGroup.nameKey("contributors"));
     createGroup(groupUuid, groupName);
 
-    Optional<GroupReference> group = loadGroup(new AccountGroup.NameKey("admins"));
+    Optional<GroupReference> group = loadGroup(AccountGroup.nameKey("admins"));
     assertThatGroup(group).isAbsent();
   }
 
   @Test
   public void specificGroupCanBeLoaded() throws Exception {
-    createGroup(new AccountGroup.UUID("contributors-MN"), new AccountGroup.NameKey("contributors"));
+    createGroup(AccountGroup.uuid("contributors-MN"), AccountGroup.nameKey("contributors"));
     createGroup(groupUuid, groupName);
-    createGroup(new AccountGroup.UUID("admins-ABC"), new AccountGroup.NameKey("admins"));
+    createGroup(AccountGroup.uuid("admins-ABC"), AccountGroup.nameKey("admins"));
 
     Optional<GroupReference> group = loadGroup(groupName);
     assertThatGroup(group).value().groupUuid().isEqualTo(groupUuid);
@@ -367,11 +384,11 @@
 
   @Test
   public void allGroupsCanBeLoaded() throws Exception {
-    AccountGroup.UUID groupUuid1 = new AccountGroup.UUID("contributors-MN");
-    AccountGroup.NameKey groupName1 = new AccountGroup.NameKey("contributors");
+    AccountGroup.UUID groupUuid1 = AccountGroup.uuid("contributors-MN");
+    AccountGroup.NameKey groupName1 = AccountGroup.nameKey("contributors");
     createGroup(groupUuid1, groupName1);
-    AccountGroup.UUID groupUuid2 = new AccountGroup.UUID("admins-ABC");
-    AccountGroup.NameKey groupName2 = new AccountGroup.NameKey("admins");
+    AccountGroup.UUID groupUuid2 = AccountGroup.uuid("admins-ABC");
+    AccountGroup.NameKey groupName2 = AccountGroup.nameKey("admins");
     createGroup(groupUuid2, groupName2);
 
     ImmutableList<GroupReference> allGroups = GroupNameNotes.loadAllGroups(repo);
@@ -384,7 +401,7 @@
   @Test
   public void loadedGroupsContainGroupsWithDuplicateGroupUuids() throws Exception {
     createGroup(groupUuid, groupName);
-    AccountGroup.NameKey anotherGroupName = new AccountGroup.NameKey("admins");
+    AccountGroup.NameKey anotherGroupName = AccountGroup.nameKey("admins");
     createGroup(groupUuid, anotherGroupName);
 
     ImmutableList<GroupReference> allGroups = GroupNameNotes.loadAllGroups(repo);
@@ -424,10 +441,10 @@
     GroupReference g1 = newGroup("a");
     GroupReference g2 = newGroup("b");
 
-    try (TestRepository<?> tr = new TestRepository<>(repo)) {
+    try (TestRepository<Repository> tr = new TestRepository<>(repo)) {
       ObjectId k1 = getNoteKey(g1);
       ObjectId k2 = getNoteKey(g2);
-      ObjectId k3 = GroupNameNotes.getNoteKey(new AccountGroup.NameKey("c"));
+      ObjectId k3 = GroupNameNotes.getNoteKey(AccountGroup.nameKey("c"));
       PersonIdent ident = newPersonIdent();
       ObjectId origCommitId =
           tr.branch(REFS_GROUPNAMES)
@@ -481,14 +498,14 @@
   @Test
   public void updateGroupNamesRejectsNonOneToOneGroupReferences() throws Exception {
     assertIllegalArgument(
-        new GroupReference(new AccountGroup.UUID("uuid1"), "name1"),
-        new GroupReference(new AccountGroup.UUID("uuid1"), "name2"));
+        new GroupReference(AccountGroup.uuid("uuid1"), "name1"),
+        new GroupReference(AccountGroup.uuid("uuid1"), "name2"));
     assertIllegalArgument(
-        new GroupReference(new AccountGroup.UUID("uuid1"), "name1"),
-        new GroupReference(new AccountGroup.UUID("uuid2"), "name1"));
+        new GroupReference(AccountGroup.uuid("uuid1"), "name1"),
+        new GroupReference(AccountGroup.uuid("uuid2"), "name1"));
     assertIllegalArgument(
-        new GroupReference(new AccountGroup.UUID("uuid1"), "name1"),
-        new GroupReference(new AccountGroup.UUID("uuid1"), "name1"));
+        new GroupReference(AccountGroup.uuid("uuid1"), "name1"),
+        new GroupReference(AccountGroup.uuid("uuid1"), "name1"));
   }
 
   @Test
@@ -529,8 +546,7 @@
     PersonIdent serverIdent = newPersonIdent();
 
     MetaDataUpdate metaDataUpdate =
-        new MetaDataUpdate(
-            GitReferenceUpdated.DISABLED, new Project.NameKey("Test Repository"), repo);
+        new MetaDataUpdate(GitReferenceUpdated.DISABLED, Project.nameKey("Test Repository"), repo);
     metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
     metaDataUpdate.getCommitBuilder().setAuthor(serverIdent);
     return metaDataUpdate;
@@ -538,7 +554,7 @@
 
   private GroupReference newGroup(String name) {
     int id = idCounter.incrementAndGet();
-    return new GroupReference(new AccountGroup.UUID(name + "-" + id), name);
+    return new GroupReference(AccountGroup.uuid(name + "-" + id), name);
   }
 
   private static PersonIdent newPersonIdent() {
@@ -546,7 +562,7 @@
   }
 
   private static ObjectId getNoteKey(GroupReference g) {
-    return GroupNameNotes.getNoteKey(new AccountGroup.NameKey(g.getName()));
+    return GroupNameNotes.getNoteKey(AccountGroup.nameKey(g.getName()));
   }
 
   private void updateAllGroups(PersonIdent ident, GroupReference... groupRefs) throws Exception {
@@ -562,12 +578,13 @@
     try (ObjectInserter inserter = repo.newObjectInserter()) {
       BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
       PersonIdent ident = newPersonIdent();
-      try {
-        GroupNameNotes.updateAllGroups(repo, inserter, bru, Arrays.asList(groupRefs), ident);
-        assert_().fail("Expected IllegalArgumentException");
-      } catch (IllegalArgumentException e) {
-        assertThat(e).hasMessageThat().isEqualTo(GroupNameNotes.UNIQUE_REF_ERROR);
-      }
+      IllegalArgumentException thrown =
+          assertThrows(
+              IllegalArgumentException.class,
+              () ->
+                  GroupNameNotes.updateAllGroups(
+                      repo, inserter, bru, Arrays.asList(groupRefs), ident));
+      assertThat(thrown).hasMessageThat().isEqualTo(GroupNameNotes.UNIQUE_REF_ERROR);
     }
   }
 
diff --git a/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java b/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
index a5b04ee..9025691 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
@@ -17,9 +17,9 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo.warning;
 
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.group.db.testing.GroupTestUtil;
 import java.util.List;
 import org.junit.Test;
@@ -30,7 +30,7 @@
   public void groupNamesRefIsMissing() throws Exception {
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
     assertThat(problems)
         .containsExactly(warning("Group with name 'g-1' doesn't exist in the list of all names"));
   }
@@ -40,7 +40,7 @@
     updateGroupNamesRef("g-2", "[group]\n\tuuid = uuid-2\n\tname = g-2\n");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
     assertThat(problems)
         .containsExactly(warning("Group with name 'g-1' doesn't exist in the list of all names"));
   }
@@ -50,7 +50,7 @@
     updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-1\n\tname = g-1\n");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
     assertThat(problems).isEmpty();
   }
 
@@ -59,7 +59,7 @@
     updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-2\n\tname = g-1\n");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
     assertThat(problems)
         .containsExactly(
             warning(
@@ -72,7 +72,7 @@
     updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-1\n\tname = g-2\n");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
     assertThat(problems)
         .containsExactly(warning("group note of name 'g-1' claims to represent name of 'g-2'"));
   }
@@ -82,7 +82,7 @@
     updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-2\n\tname = g-2\n");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
     assertThat(problems)
         .containsExactly(
             warning(
@@ -97,7 +97,7 @@
     updateGroupNamesRef("g-1", "[invalid");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
     assertThat(problems)
         .containsExactly(
             warning(
@@ -105,7 +105,7 @@
   }
 
   private void updateGroupNamesRef(String groupName, String content) throws Exception {
-    String nameKey = GroupNameNotes.getNoteKey(new AccountGroup.NameKey(groupName)).getName();
+    String nameKey = GroupNameNotes.getNoteKey(AccountGroup.nameKey(groupName)).getName();
     GroupTestUtil.updateGroupFile(
         allUsersRepo, serverIdent, RefNames.REFS_GROUPNAMES, nameKey, content);
   }
diff --git a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
index c69fa20..92a5fbe 100644
--- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -21,37 +21,39 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class AccountFieldTest extends GerritBaseTests {
+public class AccountFieldTest {
   @Test
   public void refStateFieldValues() throws Exception {
     AllUsersName allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
-    Account account = new Account(new Account.Id(1), TimeUtil.nowTs());
+    Account.Builder account = Account.builder(Account.id(1), TimeUtil.nowTs());
     String metaId = "0e39795bb25dc914118224995c53c5c36923a461";
     account.setMetaId(metaId);
     List<String> values =
-        toStrings(AccountField.REF_STATE.get(AccountState.forAccount(allUsersName, account)));
+        toStrings(AccountField.REF_STATE.get(AccountState.forAccount(account.build())));
     assertThat(values).hasSize(1);
     String expectedValue =
-        allUsersName.get() + ":" + RefNames.refsUsers(account.getId()) + ":" + metaId;
+        allUsersName.get() + ":" + RefNames.refsUsers(account.id()) + ":" + metaId;
     assertThat(Iterables.getOnlyElement(values)).isEqualTo(expectedValue);
   }
 
   @Test
   public void externalIdStateFieldValues() throws Exception {
-    Account.Id id = new Account.Id(1);
-    Account account = new Account(id, TimeUtil.nowTs());
+    Account.Id id = Account.id(1);
+    Account account =
+        Account.builder(id, TimeUtil.nowTs())
+            .setMetaId("1234567812345678123456781234567812345678")
+            .build();
     ExternalId extId1 =
         ExternalId.create(
             ExternalId.Key.create(ExternalId.SCHEME_MAILTO, "foo.bar@example.com"),
@@ -69,7 +71,7 @@
     List<String> values =
         toStrings(
             AccountField.EXTERNAL_ID_STATE.get(
-                AccountState.forAccount(null, account, ImmutableSet.of(extId1, extId2))));
+                AccountState.forAccount(account, ImmutableSet.of(extId1, extId2))));
     String expectedValue1 = extId1.key().sha1().name() + ":" + extId1.blobId().name();
     String expectedValue2 = extId2.key().sha1().name() + ":" + extId2.blobId().name();
     assertThat(values).containsExactly(expectedValue1, expectedValue2);
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index 758c304..b817b80 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.index.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
 
@@ -23,12 +24,11 @@
 import com.google.common.collect.Table;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.TestTimeUtil;
 import java.sql.Timestamp;
 import java.util.Collections;
@@ -38,7 +38,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class ChangeFieldTest extends GerritBaseTests {
+public class ChangeFieldTest {
   @Before
   public void setUp() {
     TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
@@ -53,9 +53,9 @@
   public void reviewerFieldValues() {
     Table<ReviewerStateInternal, Account.Id, Timestamp> t = HashBasedTable.create();
     Timestamp t1 = TimeUtil.nowTs();
-    t.put(ReviewerStateInternal.REVIEWER, new Account.Id(1), t1);
+    t.put(ReviewerStateInternal.REVIEWER, Account.id(1), t1);
     Timestamp t2 = TimeUtil.nowTs();
-    t.put(ReviewerStateInternal.CC, new Account.Id(2), t2);
+    t.put(ReviewerStateInternal.CC, Account.id(2), t2);
     ReviewerSet reviewers = ReviewerSet.fromTable(t);
 
     List<String> values = ChangeField.getReviewerFieldValues(reviewers);
@@ -63,7 +63,7 @@
         .containsExactly(
             "REVIEWER,1", "REVIEWER,1," + t1.getTime(), "CC,2", "CC,2," + t2.getTime());
 
-    assertThat(ChangeField.parseReviewerFieldValues(new Change.Id(1), values)).isEqualTo(reviewers);
+    assertThat(ChangeField.parseReviewerFieldValues(Change.id(1), values)).isEqualTo(reviewers);
   }
 
   @Test
@@ -75,7 +75,7 @@
                         SubmitRecord.Status.OK,
                         label(SubmitRecord.Label.Status.MAY, "Label-1", null),
                         label(SubmitRecord.Label.Status.OK, "Label-2", 1))),
-                new Account.Id(1)))
+                Account.id(1)))
         .containsExactly("OK", "MAY,label-1", "OK,label-2", "OK,label-2,0", "OK,label-2,1");
   }
 
@@ -142,7 +142,7 @@
     l.status = status;
     l.label = label;
     if (appliedBy != null) {
-      l.appliedBy = new Account.Id(appliedBy);
+      l.appliedBy = Account.id(appliedBy);
     }
     return l;
   }
@@ -153,8 +153,8 @@
         ChangeField.storedSubmitRecords(recordList).stream()
             .map(s -> new String(s, UTF_8))
             .collect(toList());
-    assertThat(ChangeField.parseSubmitRecords(stored))
-        .named("JSON %s" + stored)
+    assertWithMessage("JSON %s" + stored)
+        .that(ChangeField.parseSubmitRecords(stored))
         .isEqualTo(recordList);
   }
 }
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index fd23da3..f5d3bf7 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -16,32 +16,32 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
+import static com.google.gerrit.entities.Change.Status.MERGED;
+import static com.google.gerrit.entities.Change.Status.NEW;
 import static com.google.gerrit.index.query.Predicate.and;
 import static com.google.gerrit.index.query.Predicate.or;
-import static com.google.gerrit.reviewdb.client.Change.Status.MERGED;
-import static com.google.gerrit.reviewdb.client.Change.Status.NEW;
 import static com.google.gerrit.server.index.change.IndexedChangeQuery.convertOptions;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.AndChangeSource;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
 import com.google.gerrit.server.query.change.OrSource;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
 
-public class ChangeIndexRewriterTest extends GerritBaseTests {
+public class ChangeIndexRewriterTest {
   private static final IndexConfig CONFIG = IndexConfig.createDefault();
 
   private FakeChangeIndex index;
@@ -196,9 +196,8 @@
 
     indexes.setSearchIndex(new FakeChangeIndex(FakeChangeIndex.V1));
 
-    exception.expect(QueryParseException.class);
-    exception.expectMessage("Unsupported index predicate: file:a");
-    rewrite(in);
+    QueryParseException thrown = assertThrows(QueryParseException.class, () -> rewrite(in));
+    assertThat(thrown).hasMessageThat().contains("Unsupported index predicate: file:a");
   }
 
   @Test
@@ -207,9 +206,9 @@
     Predicate<ChangeData> in = parse(q);
     assertEquals(query(in), rewrite(in));
 
-    exception.expect(QueryParseException.class);
-    exception.expectMessage("too many terms in query");
-    rewrite(parse(q + " OR file:d"));
+    QueryParseException thrown =
+        assertThrows(QueryParseException.class, () -> rewrite(parse(q + " OR file:d")));
+    assertThat(thrown).hasMessageThat().contains("too many terms in query");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
index 34c5717..a23ccab 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -15,23 +15,24 @@
 package com.google.gerrit.server.index.change;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.ResultSet;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
 import org.junit.Ignore;
 
 @Ignore
 public class FakeChangeIndex implements ChangeIndex {
-  static final Schema<ChangeData> V1 = new Schema<>(1, ImmutableList.of(ChangeField.STATUS));
+  static final Schema<ChangeData> V1 = new Schema<>(1, false, ImmutableList.of(ChangeField.STATUS));
 
   static final Schema<ChangeData> V2 =
-      new Schema<>(2, ImmutableList.of(ChangeField.STATUS, ChangeField.PATH, ChangeField.UPDATED));
+      new Schema<>(
+          2, false, ImmutableList.of(ChangeField.STATUS, ChangeField.PATH, ChangeField.UPDATED));
 
   private static class Source implements ChangeDataSource {
     private final Predicate<ChangeData> p;
diff --git a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
index a38eabe..c887875 100644
--- a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
+++ b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -15,20 +15,19 @@
 package com.google.gerrit.server.index.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.server.index.change.StalenessChecker.refsAreStale;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.RefState;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import java.util.stream.Stream;
 import org.eclipse.jgit.junit.TestRepository;
@@ -37,14 +36,14 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class StalenessCheckerTest extends GerritBaseTests {
+public class StalenessCheckerTest {
   private static final String SHA1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
   private static final String SHA2 = "badc0feebadc0feebadc0feebadc0feebadc0fee";
 
-  private static final Project.NameKey P1 = new Project.NameKey("project1");
-  private static final Project.NameKey P2 = new Project.NameKey("project2");
+  private static final Project.NameKey P1 = Project.nameKey("project1");
+  private static final Project.NameKey P2 = Project.nameKey("project2");
 
-  private static final Change.Id C = new Change.Id(1234);
+  private static final Change.Id C = Change.id(1234);
 
   private GitRepositoryManager repoManager;
   private Repository r1;
@@ -83,12 +82,7 @@
   }
 
   private static void assertInvalidState(String state) {
-    try {
-      RefState.parseStates(byteArrays(state));
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(IllegalArgumentException.class, () -> RefState.parseStates(byteArrays(state)));
   }
 
   @Test
@@ -155,12 +149,8 @@
   }
 
   private static void assertInvalidPattern(String state) {
-    try {
-      StalenessChecker.parsePatterns(byteArrays(state));
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class, () -> StalenessChecker.parsePatterns(byteArrays(state)));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/ioutil/BUILD b/javatests/com/google/gerrit/server/ioutil/BUILD
index ef02243..ac9530f 100644
--- a/javatests/com/google/gerrit/server/ioutil/BUILD
+++ b/javatests/com/google/gerrit/server/ioutil/BUILD
@@ -10,7 +10,6 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/server/ioutil",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
diff --git a/javatests/com/google/gerrit/server/ioutil/BasicSerializationTest.java b/javatests/com/google/gerrit/server/ioutil/BasicSerializationTest.java
index c4f32c7..fae8559 100644
--- a/javatests/com/google/gerrit/server/ioutil/BasicSerializationTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/BasicSerializationTest.java
@@ -23,14 +23,13 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import org.junit.Test;
 
-public class BasicSerializationTest extends GerritBaseTests {
+public class BasicSerializationTest {
   @Test
   public void testReadVarInt32() throws IOException {
     assertEquals(0x00000000, readVarInt32(r(b(0))));
diff --git a/javatests/com/google/gerrit/server/ioutil/ColumnFormatterTest.java b/javatests/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
index 9f5e60a..fe642ba 100644
--- a/javatests/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
@@ -14,13 +14,12 @@
 
 package com.google.gerrit.server.ioutil;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import org.junit.Assert;
 import org.junit.Test;
 
-public class ColumnFormatterTest extends GerritBaseTests {
+public class ColumnFormatterTest {
   /**
    * Holds an in-memory {@link java.io.PrintWriter} object and allows comparisons of its contents to
    * a supplied string via an assert statement.
diff --git a/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java b/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java
index 40fd71f..9bb6951 100644
--- a/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java
@@ -16,10 +16,9 @@
 
 import static org.junit.Assert.assertEquals;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class HexFormatTest extends GerritBaseTests {
+public class HexFormatTest {
 
   @Test
   public void fromInt() {
diff --git a/javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java b/javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java
index 33b1c4f..048d59d 100644
--- a/javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java
@@ -18,11 +18,10 @@
 import static com.google.common.truth.Truth8.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.List;
 import org.junit.Test;
 
-public class RegexListSearcherTest extends GerritBaseTests {
+public class RegexListSearcherTest {
   private static final ImmutableList<String> EMPTY = ImmutableList.of();
 
   @Test
@@ -58,7 +57,7 @@
   }
 
   private void assertSearchReturns(List<?> expected, String re, List<String> inputs) {
-    assertThat(inputs).isOrdered();
+    assertThat(inputs).isInOrder();
     assertThat(RegexListSearcher.ofStrings(re).search(inputs))
         .containsExactlyElementsIn(expected)
         .inOrder();
diff --git a/javatests/com/google/gerrit/server/ioutil/StringUtilTest.java b/javatests/com/google/gerrit/server/ioutil/StringUtilTest.java
index 817b317..04f806d 100644
--- a/javatests/com/google/gerrit/server/ioutil/StringUtilTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/StringUtilTest.java
@@ -16,10 +16,9 @@
 
 import static org.junit.Assert.assertEquals;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class StringUtilTest extends GerritBaseTests {
+public class StringUtilTest {
   /** Test the boundary condition that the first character of a string should be escaped. */
   @Test
   public void escapeFirstChar() {
diff --git a/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
index 463decf..733d784 100644
--- a/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
+++ b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
@@ -17,26 +17,71 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.truth.Expect;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import org.eclipse.jgit.lib.Config;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
-public class LoggingContextAwareExecutorServiceTest extends GerritBaseTests {
+public class LoggingContextAwareExecutorServiceTest {
   @Rule public final Expect expect = Expect.create();
 
+  @Inject @GerritServerConfig private Config config;
+  @Inject private DynamicSet<PerformanceLogger> performanceLoggers;
+
+  private PerformanceLogger testPerformanceLogger;
+  private RegistrationHandle performanceLoggerRegistrationHandle;
+
+  @Before
+  public void setup() {
+    Injector injector = Guice.createInjector(new InMemoryModule());
+    injector.injectMembers(this);
+
+    testPerformanceLogger =
+        new PerformanceLogger() {
+          @Override
+          public void log(String operation, long durationMs, Metadata metadata) {
+            // do nothing
+          }
+        };
+    performanceLoggerRegistrationHandle = performanceLoggers.add("gerrit", testPerformanceLogger);
+  }
+
+  @After
+  public void cleanup() {
+    performanceLoggerRegistrationHandle.remove();
+  }
+
   @Test
   public void loggingContextPropagationToBackgroundThread() throws Exception {
     assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
     assertForceLogging(false);
-    try (TraceContext traceContext = TraceContext.open().forceLogging().addTag("foo", "bar")) {
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+
+    try (TraceContext traceContext = TraceContext.open().forceLogging().addTag("foo", "bar");
+        PerformanceLogContext performanceLogContext =
+            new PerformanceLogContext(config, performanceLoggers)) {
+      // Create a performance log record.
+      TraceContext.newTimer("test").close();
+
       SortedMap<String, SortedSet<Object>> tagMap = LoggingContext.getInstance().getTags().asMap();
       assertThat(tagMap.keySet()).containsExactly("foo");
       assertThat(tagMap.get("foo")).containsExactly("bar");
       assertForceLogging(true);
+      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
+      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(1);
 
       ExecutorService executor =
           new LoggingContextAwareExecutorService(Executors.newFixedThreadPool(1));
@@ -52,17 +97,32 @@
                 expect
                     .that(LoggingContext.getInstance().shouldForceLogging(null, null, false))
                     .isTrue();
+                expect.that(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
+                expect.that(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(1);
+
+                // Create another performance log record. We expect this to be visible in the outer
+                // thread.
+                TraceContext.newTimer("test2").close();
+                expect.that(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(2);
               })
           .get();
 
-      // Verify that tags and force logging flag in the outer thread are still set.
+      // Verify that logging context values in the outer thread are still set.
       tagMap = LoggingContext.getInstance().getTags().asMap();
       assertThat(tagMap.keySet()).containsExactly("foo");
       assertThat(tagMap.get("foo")).containsExactly("bar");
       assertForceLogging(true);
+      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
+
+      // The performance log record that was added in the inner thread is available in addition to
+      // the performance log record that was created in the outer thread.
+      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(2);
     }
+
     assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
     assertForceLogging(false);
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
   }
 
   private void assertForceLogging(boolean expected) {
diff --git a/javatests/com/google/gerrit/server/logging/MetadataTest.java b/javatests/com/google/gerrit/server/logging/MetadataTest.java
new file mode 100644
index 0000000..f9ae2c1
--- /dev/null
+++ b/javatests/com/google/gerrit/server/logging/MetadataTest.java
@@ -0,0 +1,37 @@
+// 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.logging;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class MetadataTest {
+
+  @Test
+  public void stringForLoggingOmitsEmptyOptionalValuesAndReformatsOptionalValuesThatArePresent() {
+    Metadata metadata = Metadata.builder().accountId(1000001).branchName("refs/heads/foo").build();
+    assertThat(metadata.toStringForLoggingLazy().evaluate())
+        .isEqualTo("Metadata{accountId=1000001, branchName=refs/heads/foo, pluginMetadata=[]}");
+  }
+
+  @Test
+  public void
+      stringForLoggingOmitsEmptyOptionalValuesAndReformatsOptionalValuesThatArePresentNoFieldsSet() {
+    Metadata metadata = Metadata.builder().build();
+    assertThat(metadata.toStringForLoggingLazy().evaluate())
+        .isEqualTo("Metadata{pluginMetadata=[]}");
+  }
+}
diff --git a/javatests/com/google/gerrit/server/logging/MutableTagsTest.java b/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
index 113f26c..f6f3b46 100644
--- a/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
+++ b/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
@@ -15,19 +15,18 @@
 package com.google.gerrit.server.logging;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import org.junit.Before;
 import org.junit.Test;
 
-public class MutableTagsTest extends GerritBaseTests {
+public class MutableTagsTest {
   private MutableTags tags;
 
   @Before
@@ -167,11 +166,7 @@
   }
 
   private void assertNullPointerException(String expectedMessage, Runnable r) {
-    try {
-      r.run();
-      assert_().fail("expected NullPointerException");
-    } catch (NullPointerException e) {
-      assertThat(e.getMessage()).isEqualTo(expectedMessage);
-    }
+    NullPointerException thrown = assertThrows(NullPointerException.class, () -> r.run());
+    assertThat(thrown).hasMessageThat().isEqualTo(expectedMessage);
   }
 }
diff --git a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
new file mode 100644
index 0000000..ed4325d
--- /dev/null
+++ b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
@@ -0,0 +1,382 @@
+// 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.logging;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.metrics.Timer2;
+import com.google.gerrit.metrics.Timer3;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PerformanceLogContextTest {
+  @Inject @GerritServerConfig private Config config;
+  @Inject private DynamicSet<PerformanceLogger> performanceLoggers;
+
+  // In this test setup this gets the DisabledMetricMaker injected. This means it doesn't record any
+  // metric, but performance log records are still created.
+  @Inject private MetricMaker metricMaker;
+
+  private TestPerformanceLogger testPerformanceLogger;
+  private RegistrationHandle performanceLoggerRegistrationHandle;
+
+  @Before
+  public void setup() {
+    Injector injector = Guice.createInjector(new InMemoryModule());
+    injector.injectMembers(this);
+
+    testPerformanceLogger = new TestPerformanceLogger();
+    performanceLoggerRegistrationHandle = performanceLoggers.add("gerrit", testPerformanceLogger);
+  }
+
+  @After
+  public void cleanup() {
+    performanceLoggerRegistrationHandle.remove();
+
+    LoggingContext.getInstance().clearPerformanceLogEntries();
+    LoggingContext.getInstance().performanceLogging(false);
+  }
+
+  @Test
+  public void traceTimersInsidePerformanceLogContextCreatePerformanceLog() {
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+
+    try (PerformanceLogContext traceContext =
+        new PerformanceLogContext(config, performanceLoggers)) {
+      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
+
+      TraceContext.newTimer("test1").close();
+      TraceContext.newTimer("test2", Metadata.builder().accountId(1000000).changeId(123).build())
+          .close();
+
+      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(2);
+    }
+
+    assertThat(testPerformanceLogger.logEntries())
+        .containsExactly(
+            PerformanceLogEntry.create("test1", Metadata.empty()),
+            PerformanceLogEntry.create(
+                "test2", Metadata.builder().accountId(1000000).changeId(123).build()))
+        .inOrder();
+
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+  }
+
+  @Test
+  public void traceTimersOutsidePerformanceLogContextDoNotCreatePerformanceLog() {
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+
+    TraceContext.newTimer("test1").close();
+    TraceContext.newTimer("test2", Metadata.builder().accountId(1000000).changeId(123).build())
+        .close();
+
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+    assertThat(testPerformanceLogger.logEntries()).isEmpty();
+  }
+
+  @Test
+  public void
+      traceTimersInsidePerformanceLogContextDoNotCreatePerformanceLogIfNoPerformanceLoggers() {
+    // Remove test performance logger so that there are no registered performance loggers.
+    performanceLoggerRegistrationHandle.remove();
+
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+
+    try (PerformanceLogContext traceContext =
+        new PerformanceLogContext(config, performanceLoggers)) {
+      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+
+      TraceContext.newTimer("test1").close();
+      TraceContext.newTimer("test2", Metadata.builder().accountId(1000000).changeId(123).build())
+          .close();
+
+      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+    }
+
+    assertThat(testPerformanceLogger.logEntries()).isEmpty();
+
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+  }
+
+  @Test
+  public void timerMetricsInsidePerformanceLogContextCreatePerformanceLog() {
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+
+    try (PerformanceLogContext traceContext =
+        new PerformanceLogContext(config, performanceLoggers)) {
+      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
+
+      Timer0 timer0 =
+          metricMaker.newTimer("test1/latency", new Description("Latency metric for testing"));
+      timer0.start().close();
+
+      Timer1<Integer> timer1 =
+          metricMaker.newTimer(
+              "test2/latency",
+              new Description("Latency metric for testing"),
+              Field.ofInteger("account", Metadata.Builder::accountId).build());
+      timer1.start(1000000).close();
+
+      Timer2<Integer, Integer> timer2 =
+          metricMaker.newTimer(
+              "test3/latency",
+              new Description("Latency metric for testing"),
+              Field.ofInteger("account", Metadata.Builder::accountId).build(),
+              Field.ofInteger("change", Metadata.Builder::changeId).build());
+      timer2.start(1000000, 123).close();
+
+      Timer3<Integer, Integer, String> timer3 =
+          metricMaker.newTimer(
+              "test4/latency",
+              new Description("Latency metric for testing"),
+              Field.ofInteger("account", Metadata.Builder::accountId).build(),
+              Field.ofInteger("change", Metadata.Builder::changeId).build(),
+              Field.ofString("project", Metadata.Builder::projectName).build());
+      timer3.start(1000000, 123, "foo/bar").close();
+
+      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(4);
+    }
+
+    assertThat(testPerformanceLogger.logEntries())
+        .containsExactly(
+            PerformanceLogEntry.create("test1/latency", Metadata.empty()),
+            PerformanceLogEntry.create(
+                "test2/latency", Metadata.builder().accountId(1000000).build()),
+            PerformanceLogEntry.create(
+                "test3/latency", Metadata.builder().accountId(1000000).changeId(123).build()),
+            PerformanceLogEntry.create(
+                "test4/latency",
+                Metadata.builder().accountId(1000000).changeId(123).projectName("foo/bar").build()))
+        .inOrder();
+
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+  }
+
+  @Test
+  public void timerMetricsInsidePerformanceLogContextCreatePerformanceLogNullValuesAllowed() {
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+
+    try (PerformanceLogContext traceContext =
+        new PerformanceLogContext(config, performanceLoggers)) {
+      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
+
+      Timer1<String> timer1 =
+          metricMaker.newTimer(
+              "test1/latency",
+              new Description("Latency metric for testing"),
+              Field.ofString("project", Metadata.Builder::projectName).build());
+      timer1.start(null).close();
+
+      Timer2<String, String> timer2 =
+          metricMaker.newTimer(
+              "test2/latency",
+              new Description("Latency metric for testing"),
+              Field.ofString("project", Metadata.Builder::projectName).build(),
+              Field.ofString("branch", Metadata.Builder::branchName).build());
+      timer2.start(null, null).close();
+
+      Timer3<String, String, String> timer3 =
+          metricMaker.newTimer(
+              "test3/latency",
+              new Description("Latency metric for testing"),
+              Field.ofString("project", Metadata.Builder::projectName).build(),
+              Field.ofString("branch", Metadata.Builder::branchName).build(),
+              Field.ofString("revision", Metadata.Builder::revision).build());
+      timer3.start(null, null, null).close();
+
+      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(3);
+    }
+
+    assertThat(testPerformanceLogger.logEntries())
+        .containsExactly(
+            PerformanceLogEntry.create("test1/latency", Metadata.empty()),
+            PerformanceLogEntry.create("test2/latency", Metadata.empty()),
+            PerformanceLogEntry.create("test3/latency", Metadata.empty()))
+        .inOrder();
+
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+  }
+
+  @Test
+  public void timerMetricsOutsidePerformanceLogContextDoNotCreatePerformanceLog() {
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+
+    Timer0 timer0 =
+        metricMaker.newTimer("test1/latency", new Description("Latency metric for testing"));
+    timer0.start().close();
+
+    Timer1<Integer> timer1 =
+        metricMaker.newTimer(
+            "test2/latency",
+            new Description("Latency metric for testing"),
+            Field.ofInteger("account", Metadata.Builder::accountId).build());
+    timer1.start(1000000).close();
+
+    Timer2<Integer, Integer> timer2 =
+        metricMaker.newTimer(
+            "test3/latency",
+            new Description("Latency metric for testing"),
+            Field.ofInteger("account", Metadata.Builder::accountId).build(),
+            Field.ofInteger("change", Metadata.Builder::changeId).build());
+    timer2.start(1000000, 123).close();
+
+    Timer3<Integer, Integer, String> timer3 =
+        metricMaker.newTimer(
+            "test4/latency",
+            new Description("Latency metric for testing"),
+            Field.ofInteger("account", Metadata.Builder::accountId).build(),
+            Field.ofInteger("change", Metadata.Builder::changeId).build(),
+            Field.ofString("project", Metadata.Builder::projectName).build());
+    timer3.start(1000000, 123, "value3").close();
+
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+    assertThat(testPerformanceLogger.logEntries()).isEmpty();
+  }
+
+  @Test
+  public void
+      timerMetricssInsidePerformanceLogContextDoNotCreatePerformanceLogIfNoPerformanceLoggers() {
+    // Remove test performance logger so that there are no registered performance loggers.
+    performanceLoggerRegistrationHandle.remove();
+
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+
+    try (PerformanceLogContext traceContext =
+        new PerformanceLogContext(config, performanceLoggers)) {
+      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+
+      Timer0 timer0 =
+          metricMaker.newTimer("test1/latency", new Description("Latency metric for testing"));
+      timer0.start().close();
+
+      Timer1<Integer> timer1 =
+          metricMaker.newTimer(
+              "test2/latency",
+              new Description("Latency metric for testing"),
+              Field.ofInteger("accoutn", Metadata.Builder::accountId).build());
+      timer1.start(1000000).close();
+
+      Timer2<Integer, Integer> timer2 =
+          metricMaker.newTimer(
+              "test3/latency",
+              new Description("Latency metric for testing"),
+              Field.ofInteger("account", Metadata.Builder::accountId).build(),
+              Field.ofInteger("change", Metadata.Builder::changeId).build());
+      timer2.start(1000000, 123).close();
+
+      Timer3<Integer, Integer, String> timer3 =
+          metricMaker.newTimer(
+              "test4/latency",
+              new Description("Latency metric for testing"),
+              Field.ofInteger("account", Metadata.Builder::accountId).build(),
+              Field.ofInteger("change", Metadata.Builder::changeId).build(),
+              Field.ofString("project", Metadata.Builder::projectName).build());
+      timer3.start(1000000, 123, "foo/bar").close();
+
+      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+    }
+
+    assertThat(testPerformanceLogger.logEntries()).isEmpty();
+
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+  }
+
+  @Test
+  public void nestingPerformanceLogContextsIsPossible() {
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+
+    try (PerformanceLogContext traceContext1 =
+        new PerformanceLogContext(config, performanceLoggers)) {
+      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
+
+      TraceContext.newTimer("test1").close();
+
+      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(1);
+
+      try (PerformanceLogContext traceContext2 =
+          new PerformanceLogContext(config, performanceLoggers)) {
+        assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+        assertThat(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
+
+        TraceContext.newTimer("test2").close();
+        TraceContext.newTimer("test3").close();
+
+        assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(2);
+      }
+
+      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
+      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(1);
+    }
+    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
+    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
+  }
+
+  private static class TestPerformanceLogger implements PerformanceLogger {
+    private List<PerformanceLogEntry> logEntries = new ArrayList<>();
+
+    @Override
+    public void log(String operation, long durationMs, Metadata metadata) {
+      logEntries.add(PerformanceLogEntry.create(operation, metadata));
+    }
+
+    ImmutableList<PerformanceLogEntry> logEntries() {
+      return ImmutableList.copyOf(logEntries);
+    }
+  }
+
+  @AutoValue
+  abstract static class PerformanceLogEntry {
+    static PerformanceLogEntry create(String operation, Metadata metadata) {
+      return new AutoValue_PerformanceLogContextTest_PerformanceLogEntry(operation, metadata);
+    }
+
+    abstract String operation();
+
+    abstract Metadata metadata();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/logging/TraceContextTest.java b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
index 19b2eeb..13f2035 100644
--- a/javatests/com/google/gerrit/server/logging/TraceContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
@@ -15,18 +15,18 @@
 package com.google.gerrit.server.logging;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.server.logging.TraceContext.TraceIdConsumer;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import org.junit.After;
 import org.junit.Test;
 
-public class TraceContextTest extends GerritBaseTests {
+public class TraceContextTest {
   @After
   public void cleanup() {
     LoggingContext.getInstance().clearTags();
@@ -237,6 +237,22 @@
     }
   }
 
+  @Test
+  public void operationForTraceTimerCannotBeNull() throws Exception {
+    assertThrows(NullPointerException.class, () -> TraceContext.newTimer(null));
+    assertThrows(NullPointerException.class, () -> TraceContext.newTimer(null, Metadata.empty()));
+    assertThrows(
+        NullPointerException.class,
+        () ->
+            TraceContext.newTimer(
+                null, Metadata.builder().accountId(1000000).changeId(123).build()));
+  }
+
+  @Test
+  public void metadataForTraceTimerCannotBeNull() throws Exception {
+    assertThrows(NullPointerException.class, () -> TraceContext.newTimer("test", null));
+  }
+
   private void assertTags(ImmutableMap<String, ImmutableSet<String>> expectedTagMap) {
     SortedMap<String, SortedSet<Object>> actualTagMap =
         LoggingContext.getInstance().getTags().asMap();
diff --git a/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
index f8a613a..9dcb08c 100644
--- a/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
+++ b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
@@ -18,11 +18,10 @@
 
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.MailMessage;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.time.Instant;
 import org.junit.Test;
 
-public class AutoReplyMailFilterTest extends GerritBaseTests {
+public class AutoReplyMailFilterTest {
 
   private AutoReplyMailFilter autoReplyMailFilter = new AutoReplyMailFilter();
 
diff --git a/javatests/com/google/gerrit/server/mail/send/CommentFormatterTest.java b/javatests/com/google/gerrit/server/mail/send/CommentFormatterTest.java
index 78116ed..f4fbc78 100644
--- a/javatests/com/google/gerrit/server/mail/send/CommentFormatterTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/CommentFormatterTest.java
@@ -20,11 +20,10 @@
 import static com.google.gerrit.server.mail.send.CommentFormatter.BlockType.PRE_FORMATTED;
 import static com.google.gerrit.server.mail.send.CommentFormatter.BlockType.QUOTE;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.List;
 import org.junit.Test;
 
-public class CommentFormatterTest extends GerritBaseTests {
+public class CommentFormatterTest {
   private void assertBlock(
       List<CommentFormatter.Block> list, int index, CommentFormatter.BlockType type, String text) {
     CommentFormatter.Block block = list.get(index);
diff --git a/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java b/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java
index 0682bb3..78cefdf 100644
--- a/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java
@@ -16,11 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Collections;
 import org.junit.Test;
 
-public class CommentSenderTest extends GerritBaseTests {
+public class CommentSenderTest {
   private static class TestSender extends CommentSender {
     TestSender() {
       super(null, null, null, null, null);
diff --git a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index 537ebff..74f44a1 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -15,20 +15,17 @@
 package com.google.gerrit.server.mail.send;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.easymock.EasyMock.createStrictMock;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
@@ -37,7 +34,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class FromAddressGeneratorProviderTest extends GerritBaseTests {
+public class FromAddressGeneratorProviderTest {
   private Config config;
   private PersonIdent ident;
   private AccountCache accountCache;
@@ -46,7 +43,7 @@
   public void setUp() throws Exception {
     config = new Config();
     ident = new PersonIdent("NAME", "e@email", 0, 0);
-    accountCache = createStrictMock(AccountCache.class);
+    accountCache = mock(AccountCache.class);
   }
 
   private FromAddressGenerator create() {
@@ -86,12 +83,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name);
     assertThat(r.getEmail()).isEqualTo(email);
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -101,12 +97,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(null, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isNull();
     assertThat(r.getEmail()).isEqualTo(email);
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -116,23 +111,21 @@
     final String name = "A U. Thor";
     final Account.Id user = user(name, null);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name + " (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
   public void USER_NullUser() {
     setFrom("USER");
-    replay(accountCache);
     final Address r = create().from(null);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(ident.getName());
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyZeroInteractions(accountCache);
   }
 
   @Test
@@ -143,12 +136,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name);
     assertThat(r.getEmail()).isEqualTo(email);
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -159,12 +151,11 @@
     final String email = "a.u.thor@test.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name + " (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -176,12 +167,11 @@
     final String email = "a.u.thor@test.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name);
     assertThat(r.getEmail()).isEqualTo(email);
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -193,12 +183,11 @@
     final String email = "a.u.thor@test.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name + " (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -209,12 +198,11 @@
     final String email = "a.u.thor@test.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name);
     assertThat(r.getEmail()).isEqualTo(email);
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -237,23 +225,21 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = userNoLookup(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(ident.getName());
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyZeroInteractions(accountCache);
   }
 
   @Test
   public void SERVER_NullUser() {
     setFrom("SERVER");
-    replay(accountCache);
     final Address r = create().from(null);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(ident.getName());
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyZeroInteractions(accountCache);
   }
 
   @Test
@@ -276,12 +262,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name + " (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -291,12 +276,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(null, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo("Anonymous Coward (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -306,23 +290,21 @@
     final String name = "A U. Thor";
     final Account.Id user = user(name, null);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name + " (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
   public void MIXED_NullUser() {
     setFrom("MIXED");
-    replay(accountCache);
     final Address r = create().from(null);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(ident.getName());
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyZeroInteractions(accountCache);
   }
 
   @Test
@@ -333,12 +315,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo("A " + name + " B");
     assertThat(r.getEmail()).isEqualTo("my.server@email.address");
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -348,42 +329,42 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(null, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo("A Anonymous Coward B");
     assertThat(r.getEmail()).isEqualTo("my.server@email.address");
-    verify(accountCache);
   }
 
   @Test
   public void CUSTOM_NullUser() {
     setFrom("A ${user} B <my.server@email.address>");
 
-    replay(accountCache);
     final Address r = create().from(null);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(ident.getName());
     assertThat(r.getEmail()).isEqualTo("my.server@email.address");
-    verify(accountCache);
   }
 
   private Account.Id user(String name, String email) {
     final AccountState s = makeUser(name, email);
-    expect(accountCache.get(eq(s.getAccount().getId()))).andReturn(Optional.of(s));
-    return s.getAccount().getId();
+    when(accountCache.get(eq(s.account().id()))).thenReturn(Optional.of(s));
+    return s.account().id();
+  }
+
+  private void verifyAccountCacheGet(Account.Id id) {
+    verify(accountCache).get(eq(id));
   }
 
   private Account.Id userNoLookup(String name, String email) {
     final AccountState s = makeUser(name, email);
-    return s.getAccount().getId();
+    return s.account().id();
   }
 
   private AccountState makeUser(String name, String email) {
-    final Account.Id userId = new Account.Id(42);
-    final Account account = new Account(userId, TimeUtil.nowTs());
+    final Account.Id userId = Account.id(42);
+    final Account.Builder account = Account.builder(userId, TimeUtil.nowTs());
     account.setFullName(name);
     account.setPreferredEmail(email);
-    return AccountState.forAccount(new AllUsersName(AllUsersNameProvider.DEFAULT), account);
+    return AccountState.forAccount(account.build());
   }
 }
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index a1f318f..7192c55 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -19,16 +19,17 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.CommentRange;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
@@ -51,9 +52,9 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gerrit.testing.AssertableExecutorService;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.FakeAccountCache;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.TestChanges;
 import com.google.gerrit.testing.TestTimeUtil;
@@ -63,9 +64,11 @@
 import com.google.inject.util.Providers;
 import java.sql.Timestamp;
 import java.util.TimeZone;
+import java.util.concurrent.ExecutorService;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
@@ -75,7 +78,7 @@
 
 @Ignore
 @RunWith(ConfigSuite.class)
-public abstract class AbstractChangeNotesTest extends GerritBaseTests {
+public abstract class AbstractChangeNotesTest {
   private static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
 
   @ConfigSuite.Parameter public Config testConfig;
@@ -91,6 +94,7 @@
   protected Project.NameKey project;
   protected RevWalk rw;
   protected TestRepository<InMemoryRepository> tr;
+  protected AssertableExecutorService assertableFanOutExecutor;
 
   @Inject protected IdentifiedUser.GenericFactory userFactory;
 
@@ -110,20 +114,21 @@
     setTimeForTesting();
 
     serverIdent = new PersonIdent("Gerrit Server", "noreply@gerrit.com", TimeUtil.nowTs(), TZ);
-    project = new Project.NameKey("test-project");
+    project = Project.nameKey("test-project");
     repoManager = new InMemoryRepositoryManager();
     repo = repoManager.createRepository(project);
     tr = new TestRepository<>(repo);
     rw = tr.getRevWalk();
     accountCache = new FakeAccountCache();
-    Account co = new Account(new Account.Id(1), TimeUtil.nowTs());
+    Account.Builder co = Account.builder(Account.id(1), TimeUtil.nowTs());
     co.setFullName("Change Owner");
     co.setPreferredEmail("change@owner.com");
-    accountCache.put(co);
-    Account ou = new Account(new Account.Id(2), TimeUtil.nowTs());
+    accountCache.put(co.build());
+    Account.Builder ou = Account.builder(Account.id(2), TimeUtil.nowTs());
     ou.setFullName("Other Account");
     ou.setPreferredEmail("other@account.com");
-    accountCache.put(ou);
+    accountCache.put(ou.build());
+    assertableFanOutExecutor = new AssertableExecutorService();
 
     injector =
         Guice.createInjector(
@@ -156,13 +161,16 @@
                     .toInstance(serverIdent);
                 bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
                 bind(MetricMaker.class).to(DisabledMetricMaker.class);
+                bind(ExecutorService.class)
+                    .annotatedWith(FanOutExecutor.class)
+                    .toInstance(assertableFanOutExecutor);
               }
             });
 
     injector.injectMembers(this);
     repoManager.createRepository(allUsers);
-    changeOwner = userFactory.create(co.getId());
-    otherUser = userFactory.create(ou.getId());
+    changeOwner = userFactory.create(co.id());
+    otherUser = userFactory.create(ou.id());
     otherUserId = otherUser.getAccountId();
     internalUser = new InternalUser();
   }
@@ -182,7 +190,7 @@
     Change c = TestChanges.newChange(project, changeOwner.getAccountId());
     ChangeUpdate u = newUpdateForNewChange(c, changeOwner);
     u.setChangeId(c.getKey().get());
-    u.setBranch(c.getDest().get());
+    u.setBranch(c.getDest().branch());
     u.setWorkInProgress(workInProgress);
     u.commit();
     return c;
@@ -247,7 +255,7 @@
       Timestamp t,
       String message,
       short side,
-      String commitSHA1,
+      ObjectId commitId,
       boolean unresolved) {
     Comment c =
         new Comment(
@@ -260,7 +268,7 @@
             unresolved);
     c.lineNbr = line;
     c.parentUuid = parentUUID;
-    c.revId = commitSHA1;
+    c.setCommitId(commitId);
     c.setRange(range);
     return c;
   }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
index b4d9738..9112756 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
@@ -20,8 +20,8 @@
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
@@ -31,8 +31,8 @@
   public void keySerializer() throws Exception {
     ChangeNotesCache.Key key =
         ChangeNotesCache.Key.create(
-            new Project.NameKey("project"),
-            new Change.Id(1234),
+            Project.nameKey("project"),
+            Change.id(1234),
             ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
     byte[] serialized = ChangeNotesCache.Key.Serializer.INSTANCE.serialize(key);
     assertThat(ChangeNotesKeyProto.parseFrom(serialized))
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 79dcd5b..013939a 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.util.time.TimeUtil;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -214,6 +214,26 @@
   }
 
   @Test
+  public void parseAssignee() throws Exception {
+    assertParseSucceeds(
+        "Update change\n"
+            + "\n"
+            + "Branch: refs/heads/master\n"
+            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+            + "Patch-set: 1\n"
+            + "Assignee: Change Owner <1@gerrit>\n"
+            + "Subject: This is a test change\n");
+    assertParseSucceeds(
+        "Update change\n"
+            + "\n"
+            + "Branch: refs/heads/master\n"
+            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+            + "Patch-set: 2\n"
+            + "Assignee:\n"
+            + "Subject: This is a test change\n");
+  }
+
+  @Test
   public void parseTopic() throws Exception {
     assertParseSucceeds(
         "Update change\n"
@@ -545,19 +565,12 @@
   }
 
   private void assertParseFails(RevCommit commit) throws Exception {
-    try {
-      newParser(commit).parseAll();
-      fail("Expected parse to fail:\n" + commit.getFullMessage());
-    } catch (ConfigInvalidException e) {
-      // Expected
-    }
+    assertThrows(ConfigInvalidException.class, () -> newParser(commit).parseAll());
   }
 
   private ChangeNotesParser newParser(ObjectId tip) throws Exception {
     walk.reset();
     ChangeNoteJson changeNoteJson = injector.getInstance(ChangeNoteJson.class);
-    LegacyChangeNoteRead reader = injector.getInstance(LegacyChangeNoteRead.class);
-    return new ChangeNotesParser(
-        newChange().getId(), tip, walk, changeNoteJson, reader, args.metrics);
+    return new ChangeNotesParser(newChange().getId(), tip, walk, changeNoteJson, args.metrics);
   }
 }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 2931b17..6ece894 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -27,22 +27,23 @@
 import com.google.common.collect.Iterables;
 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.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.converter.ChangeMessageProtoConverter;
-import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
-import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
+import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AssigneeStatusUpdateProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
@@ -50,23 +51,25 @@
 import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.notedb.ChangeNotesState.ChangeColumns;
 import com.google.gerrit.server.notedb.ChangeNotesState.Serializer;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.TypeLiteral;
 import com.google.protobuf.ByteString;
 import java.lang.reflect.Type;
 import java.sql.Timestamp;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Before;
 import org.junit.Test;
 
-public class ChangeNotesStateTest extends GerritBaseTests {
-  private static final Change.Id ID = new Change.Id(123);
+public class ChangeNotesStateTest {
+  private static final Change.Id ID = Change.id(123);
   private static final ObjectId SHA =
       ObjectId.fromString("1234567812345678123456781234567812345678");
   private static final ByteString SHA_BYTES = ObjectIdConverter.create().toByteString(SHA);
   private static final String CHANGE_KEY = "Iabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";
+  private static final String DEFAULT_SERVER_ID = UUID.randomUUID().toString();
 
   private ChangeColumns cols;
   private ChangeColumnsProto colsProto;
@@ -75,10 +78,10 @@
   public void setUp() throws Exception {
     cols =
         ChangeColumns.builder()
-            .changeKey(new Change.Key(CHANGE_KEY))
+            .changeKey(Change.key(CHANGE_KEY))
             .createdOn(new Timestamp(123456L))
             .lastUpdatedOn(new Timestamp(234567L))
-            .owner(new Account.Id(1000))
+            .owner(Account.id(1000))
             .branch("refs/heads/master")
             .subject("Test change")
             .isPrivate(false)
@@ -98,7 +101,7 @@
         newBuilder()
             .columns(
                 cols.toBuilder()
-                    .changeKey(new Change.Key("Ieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"))
+                    .changeKey(Change.key("Ieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"))
                     .build())
             .build(),
         ChangeNotesStateProto.newBuilder()
@@ -134,7 +137,7 @@
   @Test
   public void serializeOwner() throws Exception {
     assertRoundTrip(
-        newBuilder().columns(cols.toBuilder().owner(new Account.Id(7777)).build()).build(),
+        newBuilder().columns(cols.toBuilder().owner(Account.id(7777)).build()).build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
             .setChangeId(ID.get())
@@ -168,7 +171,7 @@
   public void serializeCurrentPatchSetId() throws Exception {
     assertRoundTrip(
         newBuilder()
-            .columns(cols.toBuilder().currentPatchSetId(new PatchSet.Id(ID, 2)).build())
+            .columns(cols.toBuilder().currentPatchSetId(PatchSet.id(ID, 2)).build())
             .build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
@@ -241,17 +244,6 @@
   }
 
   @Test
-  public void serializeAssignee() throws Exception {
-    assertRoundTrip(
-        newBuilder().columns(cols.toBuilder().assignee(new Account.Id(2000)).build()).build(),
-        ChangeNotesStateProto.newBuilder()
-            .setMetaId(SHA_BYTES)
-            .setChangeId(ID.get())
-            .setColumns(colsProto.toBuilder().setAssignee(2000).setHasAssignee(true))
-            .build());
-  }
-
-  @Test
   public void serializeStatus() throws Exception {
     assertRoundTrip(
         newBuilder().columns(cols.toBuilder().status(Change.Status.MERGED).build()).build(),
@@ -298,7 +290,7 @@
   @Test
   public void serializeRevertOf() throws Exception {
     assertRoundTrip(
-        newBuilder().columns(cols.toBuilder().revertOf(new Change.Id(999)).build()).build(),
+        newBuilder().columns(cols.toBuilder().revertOf(Change.id(999)).build()).build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
             .setChangeId(ID.get())
@@ -307,21 +299,6 @@
   }
 
   @Test
-  public void serializePastAssignees() throws Exception {
-    assertRoundTrip(
-        newBuilder()
-            .pastAssignees(ImmutableSet.of(new Account.Id(2002), new Account.Id(2001)))
-            .build(),
-        ChangeNotesStateProto.newBuilder()
-            .setMetaId(SHA_BYTES)
-            .setChangeId(ID.get())
-            .setColumns(colsProto)
-            .addPastAssignee(2002)
-            .addPastAssignee(2001)
-            .build());
-  }
-
-  @Test
   public void serializeHashtags() throws Exception {
     assertRoundTrip(
         newBuilder().hashtags(ImmutableSet.of("tag2", "tag1")).build(),
@@ -336,25 +313,29 @@
 
   @Test
   public void serializePatchSets() throws Exception {
-    PatchSet ps1 = new PatchSet(new PatchSet.Id(ID, 1));
-    ps1.setUploader(new Account.Id(2000));
-    ps1.setRevision(new RevId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
-    ps1.setCreatedOn(cols.createdOn());
+    PatchSet ps1 =
+        PatchSet.builder()
+            .id(PatchSet.id(ID, 1))
+            .commitId(ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
+            .uploader(Account.id(2000))
+            .createdOn(cols.createdOn())
+            .build();
     ByteString ps1Bytes = toByteString(ps1, PatchSetProtoConverter.INSTANCE);
     assertThat(ps1Bytes.size()).isEqualTo(66);
 
-    PatchSet ps2 = new PatchSet(new PatchSet.Id(ID, 2));
-    ps2.setUploader(new Account.Id(3000));
-    ps2.setRevision(new RevId("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
-    ps2.setCreatedOn(cols.lastUpdatedOn());
+    PatchSet ps2 =
+        PatchSet.builder()
+            .id(PatchSet.id(ID, 2))
+            .commitId(ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))
+            .uploader(Account.id(3000))
+            .createdOn(cols.lastUpdatedOn())
+            .build();
     ByteString ps2Bytes = toByteString(ps2, PatchSetProtoConverter.INSTANCE);
     assertThat(ps2Bytes.size()).isEqualTo(66);
     assertThat(ps2Bytes).isNotEqualTo(ps1Bytes);
 
     assertRoundTrip(
-        newBuilder()
-            .patchSets(ImmutableMap.of(ps2.getId(), ps2, ps1.getId(), ps1).entrySet())
-            .build(),
+        newBuilder().patchSets(ImmutableMap.of(ps2.id(), ps2, ps1.id(), ps1).entrySet()).build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
             .setChangeId(ID.get())
@@ -367,28 +348,31 @@
   @Test
   public void serializeApprovals() throws Exception {
     PatchSetApproval a1 =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(
-                new PatchSet.Id(ID, 1), new Account.Id(2001), new LabelId("Code-Review")),
-            (short) 1,
-            new Timestamp(1212L));
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    PatchSet.id(ID, 1), Account.id(2001), LabelId.create("Code-Review")))
+            .value(1)
+            .granted(new Timestamp(1212L))
+            .build();
     ByteString a1Bytes = toByteString(a1, PatchSetApprovalProtoConverter.INSTANCE);
     assertThat(a1Bytes.size()).isEqualTo(43);
 
     PatchSetApproval a2 =
-        new PatchSetApproval(
-            new PatchSetApproval.Key(
-                new PatchSet.Id(ID, 1), new Account.Id(2002), new LabelId("Verified")),
-            (short) -1,
-            new Timestamp(3434L));
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    PatchSet.id(ID, 1), Account.id(2002), LabelId.create("Verified")))
+            .value(-1)
+            .granted(new Timestamp(3434L))
+            .build();
     ByteString a2Bytes = toByteString(a2, PatchSetApprovalProtoConverter.INSTANCE);
     assertThat(a2Bytes.size()).isEqualTo(49);
     assertThat(a2Bytes).isNotEqualTo(a1Bytes);
 
     assertRoundTrip(
         newBuilder()
-            .approvals(
-                ImmutableListMultimap.of(a2.getPatchSetId(), a2, a1.getPatchSetId(), a1).entries())
+            .approvals(ImmutableListMultimap.of(a2.patchSetId(), a2, a1.patchSetId(), a1).entries())
             .build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
@@ -406,11 +390,8 @@
             .reviewers(
                 ReviewerSet.fromTable(
                     ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
-                        .put(ReviewerStateInternal.CC, new Account.Id(2001), new Timestamp(1212L))
-                        .put(
-                            ReviewerStateInternal.REVIEWER,
-                            new Account.Id(2002),
-                            new Timestamp(3434L))
+                        .put(ReviewerStateInternal.CC, Account.id(2001), new Timestamp(1212L))
+                        .put(ReviewerStateInternal.REVIEWER, Account.id(2002), new Timestamp(3434L))
                         .build()))
             .build(),
         ChangeNotesStateProto.newBuilder()
@@ -503,11 +484,8 @@
             .pendingReviewers(
                 ReviewerSet.fromTable(
                     ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
-                        .put(ReviewerStateInternal.CC, new Account.Id(2001), new Timestamp(1212L))
-                        .put(
-                            ReviewerStateInternal.REVIEWER,
-                            new Account.Id(2002),
-                            new Timestamp(3434L))
+                        .put(ReviewerStateInternal.CC, Account.id(2001), new Timestamp(1212L))
+                        .put(ReviewerStateInternal.REVIEWER, Account.id(2002), new Timestamp(3434L))
                         .build()))
             .build(),
         ChangeNotesStateProto.newBuilder()
@@ -564,9 +542,7 @@
   @Test
   public void serializeAllPastReviewers() throws Exception {
     assertRoundTrip(
-        newBuilder()
-            .allPastReviewers(ImmutableList.of(new Account.Id(2002), new Account.Id(2001)))
-            .build(),
+        newBuilder().allPastReviewers(ImmutableList.of(Account.id(2002), Account.id(2001))).build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
             .setChangeId(ID.get())
@@ -584,13 +560,13 @@
                 ImmutableList.of(
                     ReviewerStatusUpdate.create(
                         new Timestamp(1212L),
-                        new Account.Id(1000),
-                        new Account.Id(2002),
+                        Account.id(1000),
+                        Account.id(2002),
                         ReviewerStateInternal.CC),
                     ReviewerStatusUpdate.create(
                         new Timestamp(3434L),
-                        new Account.Id(1000),
-                        new Account.Id(2001),
+                        Account.id(1000),
+                        Account.id(2001),
                         ReviewerStateInternal.REVIEWER)))
             .build(),
         ChangeNotesStateProto.newBuilder()
@@ -613,6 +589,35 @@
   }
 
   @Test
+  public void serializeAssigneeUpdates() throws Exception {
+    assertRoundTrip(
+        newBuilder()
+            .assigneeUpdates(
+                ImmutableList.of(
+                    AssigneeStatusUpdate.create(
+                        new Timestamp(1212L), Account.id(1000), Optional.of(Account.id(2001))),
+                    AssigneeStatusUpdate.create(
+                        new Timestamp(3434L), Account.id(1000), Optional.empty())))
+            .build(),
+        ChangeNotesStateProto.newBuilder()
+            .setMetaId(SHA_BYTES)
+            .setChangeId(ID.get())
+            .setColumns(colsProto)
+            .addAssigneeUpdate(
+                AssigneeStatusUpdateProto.newBuilder()
+                    .setDate(1212L)
+                    .setUpdatedBy(1000)
+                    .setCurrentAssignee(2001)
+                    .setHasCurrentAssignee(true))
+            .addAssigneeUpdate(
+                AssigneeStatusUpdateProto.newBuilder()
+                    .setDate(3434L)
+                    .setUpdatedBy(1000)
+                    .setHasCurrentAssignee(false))
+            .build());
+  }
+
+  @Test
   public void serializeSubmitRecords() throws Exception {
     SubmitRecord sr1 = new SubmitRecord();
     sr1.status = SubmitRecord.Status.OK;
@@ -635,19 +640,19 @@
   public void serializeChangeMessages() throws Exception {
     ChangeMessage m1 =
         new ChangeMessage(
-            new ChangeMessage.Key(ID, "uuid1"),
-            new Account.Id(1000),
+            ChangeMessage.key(ID, "uuid1"),
+            Account.id(1000),
             new Timestamp(1212L),
-            new PatchSet.Id(ID, 1));
+            PatchSet.id(ID, 1));
     ByteString m1Bytes = toByteString(m1, ChangeMessageProtoConverter.INSTANCE);
     assertThat(m1Bytes.size()).isEqualTo(35);
 
     ChangeMessage m2 =
         new ChangeMessage(
-            new ChangeMessage.Key(ID, "uuid2"),
-            new Account.Id(2000),
+            ChangeMessage.key(ID, "uuid2"),
+            Account.id(2000),
             new Timestamp(3434L),
-            new PatchSet.Id(ID, 2));
+            PatchSet.id(ID, 2));
     ByteString m2Bytes = toByteString(m2, ChangeMessageProtoConverter.INSTANCE);
     assertThat(m2Bytes.size()).isEqualTo(35);
     assertThat(m2Bytes).isNotEqualTo(m1Bytes);
@@ -668,31 +673,30 @@
     Comment c1 =
         new Comment(
             new Comment.Key("uuid1", "file1", 1),
-            new Account.Id(1001),
+            Account.id(1001),
             new Timestamp(1212L),
             (short) 1,
             "message 1",
             "serverId",
             false);
-    c1.setRevId(new RevId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
+    c1.setCommitId(ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
     String c1Json = Serializer.GSON.toJson(c1);
 
     Comment c2 =
         new Comment(
             new Comment.Key("uuid2", "file2", 2),
-            new Account.Id(1002),
+            Account.id(1002),
             new Timestamp(3434L),
             (short) 2,
             "message 2",
             "serverId",
             true);
-    c2.setRevId(new RevId("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
+    c2.setCommitId(ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
     String c2Json = Serializer.GSON.toJson(c2);
 
     assertRoundTrip(
         newBuilder()
-            .publishedComments(
-                ImmutableListMultimap.of(new RevId(c2.revId), c2, new RevId(c1.revId), c1))
+            .publishedComments(ImmutableListMultimap.of(c2.getCommitId(), c2, c1.getCommitId(), c1))
             .build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
@@ -704,14 +708,26 @@
   }
 
   @Test
+  public void serializeUpdateCount() throws Exception {
+    assertRoundTrip(
+        newBuilder().updateCount(234).build(),
+        ChangeNotesStateProto.newBuilder()
+            .setMetaId(SHA_BYTES)
+            .setChangeId(ID.get())
+            .setColumns(colsProto)
+            .setUpdateCount(234)
+            .build());
+  }
+
+  @Test
   public void changeNotesStateMethods() throws Exception {
     assertThatSerializedClass(ChangeNotesState.class)
         .hasAutoValueMethods(
             ImmutableMap.<String, Type>builder()
                 .put("metaId", ObjectId.class)
                 .put("changeId", Change.Id.class)
+                .put("serverId", String.class)
                 .put("columns", ChangeColumns.class)
-                .put("pastAssignees", new TypeLiteral<ImmutableSet<Account.Id>>() {}.getType())
                 .put("hashtags", new TypeLiteral<ImmutableSet<String>>() {}.getType())
                 .put(
                     "patchSets",
@@ -728,11 +744,15 @@
                 .put(
                     "reviewerUpdates",
                     new TypeLiteral<ImmutableList<ReviewerStatusUpdate>>() {}.getType())
+                .put(
+                    "assigneeUpdates",
+                    new TypeLiteral<ImmutableList<AssigneeStatusUpdate>>() {}.getType())
                 .put("submitRecords", new TypeLiteral<ImmutableList<SubmitRecord>>() {}.getType())
                 .put("changeMessages", new TypeLiteral<ImmutableList<ChangeMessage>>() {}.getType())
                 .put(
                     "publishedComments",
-                    new TypeLiteral<ImmutableListMultimap<RevId, Comment>>() {}.getType())
+                    new TypeLiteral<ImmutableListMultimap<ObjectId, Comment>>() {}.getType())
+                .put("updateCount", int.class)
                 .build());
   }
 
@@ -751,7 +771,6 @@
                 .put("topic", String.class)
                 .put("originalSubject", String.class)
                 .put("submissionId", String.class)
-                .put("assignee", Account.Id.class)
                 .put("status", Change.Status.class)
                 .put("isPrivate", boolean.class)
                 .put("workInProgress", boolean.class)
@@ -764,36 +783,37 @@
   @Test
   public void patchSetFields() throws Exception {
     assertThatSerializedClass(PatchSet.class)
-        .hasFields(
+        .hasAutoValueMethods(
             ImmutableMap.<String, Type>builder()
                 .put("id", PatchSet.Id.class)
-                .put("revision", RevId.class)
+                .put("commitId", ObjectId.class)
                 .put("uploader", Account.Id.class)
                 .put("createdOn", Timestamp.class)
-                .put("groups", String.class)
-                .put("pushCertificate", String.class)
-                .put("description", String.class)
+                .put("groups", new TypeLiteral<ImmutableList<String>>() {}.getType())
+                .put("pushCertificate", new TypeLiteral<Optional<String>>() {}.getType())
+                .put("description", new TypeLiteral<Optional<String>>() {}.getType())
                 .build());
   }
 
   @Test
   public void patchSetApprovalFields() throws Exception {
     assertThatSerializedClass(PatchSetApproval.Key.class)
-        .hasFields(
+        .hasAutoValueMethods(
             ImmutableMap.<String, Type>builder()
                 .put("patchSetId", PatchSet.Id.class)
                 .put("accountId", Account.Id.class)
-                .put("categoryId", LabelId.class)
+                .put("labelId", LabelId.class)
                 .build());
     assertThatSerializedClass(PatchSetApproval.class)
-        .hasFields(
+        .hasAutoValueMethods(
             ImmutableMap.<String, Type>builder()
                 .put("key", PatchSetApproval.Key.class)
                 .put("value", short.class)
                 .put("granted", Timestamp.class)
-                .put("tag", String.class)
+                .put("tag", new TypeLiteral<Optional<String>>() {}.getType())
                 .put("realAccountId", Account.Id.class)
                 .put("postSubmit", boolean.class)
+                .put("toBuilder", PatchSetApproval.Builder.class)
                 .build());
   }
 
@@ -832,6 +852,19 @@
   }
 
   @Test
+  public void assigneeStatusUpdateMethods() throws Exception {
+    assertThatSerializedClass(AssigneeStatusUpdate.class)
+        .hasAutoValueMethods(
+            ImmutableMap.of(
+                "date",
+                Timestamp.class,
+                "updatedBy",
+                Account.Id.class,
+                "currentAssignee",
+                new TypeLiteral<Optional<Account.Id>>() {}.getType()));
+  }
+
+  @Test
   public void submitRecordFields() throws Exception {
     assertThatSerializedClass(SubmitRecord.class)
         .hasFields(
@@ -861,7 +894,7 @@
   @Test
   public void changeMessageFields() throws Exception {
     assertThatSerializedClass(ChangeMessage.Key.class)
-        .hasFields(ImmutableMap.of("changeId", Change.Id.class, "uuid", String.class));
+        .hasAutoValueMethods(ImmutableMap.of("changeId", Change.Id.class, "uuid", String.class));
     assertThatSerializedClass(ChangeMessage.class)
         .hasFields(
             ImmutableMap.<String, Type>builder()
@@ -905,10 +938,22 @@
                 .put("revId", String.class)
                 .put("serverId", String.class)
                 .put("unresolved", boolean.class)
-                .put("legacyFormat", boolean.class)
                 .build());
   }
 
+  @Test
+  public void serializeServerId() throws Exception {
+    assertRoundTrip(
+        newBuilder().serverId(DEFAULT_SERVER_ID).build(),
+        ChangeNotesStateProto.newBuilder()
+            .setMetaId(SHA_BYTES)
+            .setChangeId(ID.get())
+            .setServerId(DEFAULT_SERVER_ID)
+            .setHasServerId(true)
+            .setColumns(colsProto.toBuilder())
+            .build());
+  }
+
   private static ChangeNotesStateProto toProto(ChangeNotesState state) throws Exception {
     return ChangeNotesStateProto.parseFrom(Serializer.INSTANCE.serialize(state));
   }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index a6c0224..145e914 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -16,15 +16,17 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
-import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.entities.RefNames.changeMetaRef;
+import static com.google.gerrit.entities.RefNames.refsDraftComments;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REMOVED;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Comparator.comparing;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-import static org.junit.Assert.fail;
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
@@ -35,18 +37,17 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.CommentRange;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ReviewerSet;
@@ -73,7 +74,6 @@
   @Inject private DraftCommentNotes.Factory draftNotesFactory;
 
   @Inject private ChangeNoteJson changeNoteJson;
-  @Inject private LegacyChangeNoteRead legacyChangeNoteRead;
 
   @Inject private @GerritServerId String serverId;
 
@@ -101,7 +101,7 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getCurrentPatchSet().getDescription()).isEqualTo(description);
+    assertThat(notes.getCurrentPatchSet().description()).hasValue(description);
 
     description = "new, now more descriptive!";
     update = newUpdate(c, changeOwner);
@@ -109,7 +109,7 @@
     update.commit();
 
     notes = newNotes(c);
-    assertThat(notes.getCurrentPatchSet().getDescription()).isEqualTo(description);
+    assertThat(notes.getCurrentPatchSet().description()).hasValue(description);
   }
 
   @Test
@@ -119,7 +119,7 @@
     RevCommit commit = tr.commit().message("PS2").create();
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.putComment(
-        Status.PUBLISHED,
+        Comment.Status.PUBLISHED,
         newComment(
             c.currentPatchSetId(),
             "a.txt",
@@ -131,14 +131,14 @@
             TimeUtil.nowTs(),
             "Comment",
             (short) 1,
-            commit.name(),
+            commit,
             false));
     update.setTag(tag);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
 
-    ImmutableListMultimap<RevId, Comment> comments = notes.getComments();
+    ImmutableListMultimap<ObjectId, Comment> comments = notes.getComments();
     assertThat(comments).hasSize(1);
     assertThat(comments.entries().asList().get(0).getValue().tag).isEqualTo(tag);
   }
@@ -162,7 +162,7 @@
 
     ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals = notes.getApprovals();
     assertThat(approvals).hasSize(1);
-    assertThat(approvals.entries().asList().get(0).getValue().getTag()).isEqualTo(tag2);
+    assertThat(approvals.entries().asList().get(0).getValue().tag()).hasValue(tag2);
   }
 
   @Test
@@ -181,7 +181,7 @@
     RevCommit commit = tr.commit().message("PS2").create();
     update = newUpdate(c, changeOwner);
     update.putComment(
-        Status.PUBLISHED,
+        Comment.Status.PUBLISHED,
         newComment(
             c.currentPatchSetId(),
             "a.txt",
@@ -193,7 +193,7 @@
             TimeUtil.nowTs(),
             "Comment",
             (short) 1,
-            commit.name(),
+            commit,
             false));
     update.setChangeMessage("coverage verification");
     update.setTag(coverageTag);
@@ -209,10 +209,10 @@
     ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals = notes.getApprovals();
     assertThat(approvals).hasSize(1);
     PatchSetApproval approval = approvals.entries().asList().get(0).getValue();
-    assertThat(approval.getTag()).isEqualTo(integrationTag);
-    assertThat(approval.getValue()).isEqualTo(-1);
+    assertThat(approval.tag()).hasValue(integrationTag);
+    assertThat(approval.value()).isEqualTo(-1);
 
-    ImmutableListMultimap<RevId, Comment> comments = notes.getComments();
+    ImmutableListMultimap<ObjectId, Comment> comments = notes.getComments();
     assertThat(comments).hasSize(1);
     assertThat(comments.entries().asList().get(0).getValue().tag).isEqualTo(coverageTag);
 
@@ -236,17 +236,17 @@
     List<PatchSetApproval> psas = notes.getApprovals().get(c.currentPatchSetId());
     assertThat(psas).hasSize(2);
 
-    assertThat(psas.get(0).getPatchSetId()).isEqualTo(c.currentPatchSetId());
-    assertThat(psas.get(0).getAccountId().get()).isEqualTo(1);
-    assertThat(psas.get(0).getLabel()).isEqualTo("Code-Review");
-    assertThat(psas.get(0).getValue()).isEqualTo((short) -1);
-    assertThat(psas.get(0).getGranted()).isEqualTo(truncate(after(c, 2000)));
+    assertThat(psas.get(0).patchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(0).accountId().get()).isEqualTo(1);
+    assertThat(psas.get(0).label()).isEqualTo("Code-Review");
+    assertThat(psas.get(0).value()).isEqualTo((short) -1);
+    assertThat(psas.get(0).granted()).isEqualTo(truncate(after(c, 2000)));
 
-    assertThat(psas.get(1).getPatchSetId()).isEqualTo(c.currentPatchSetId());
-    assertThat(psas.get(1).getAccountId().get()).isEqualTo(1);
-    assertThat(psas.get(1).getLabel()).isEqualTo("Verified");
-    assertThat(psas.get(1).getValue()).isEqualTo((short) 1);
-    assertThat(psas.get(1).getGranted()).isEqualTo(psas.get(0).getGranted());
+    assertThat(psas.get(1).patchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(1).accountId().get()).isEqualTo(1);
+    assertThat(psas.get(1).label()).isEqualTo("Verified");
+    assertThat(psas.get(1).value()).isEqualTo((short) 1);
+    assertThat(psas.get(1).granted()).isEqualTo(psas.get(0).granted());
   }
 
   @Test
@@ -268,18 +268,18 @@
     assertThat(psas).hasSize(2);
 
     PatchSetApproval psa1 = Iterables.getOnlyElement(psas.get(ps1));
-    assertThat(psa1.getPatchSetId()).isEqualTo(ps1);
-    assertThat(psa1.getAccountId().get()).isEqualTo(1);
-    assertThat(psa1.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa1.getValue()).isEqualTo((short) -1);
-    assertThat(psa1.getGranted()).isEqualTo(truncate(after(c, 2000)));
+    assertThat(psa1.patchSetId()).isEqualTo(ps1);
+    assertThat(psa1.accountId().get()).isEqualTo(1);
+    assertThat(psa1.label()).isEqualTo("Code-Review");
+    assertThat(psa1.value()).isEqualTo((short) -1);
+    assertThat(psa1.granted()).isEqualTo(truncate(after(c, 2000)));
 
     PatchSetApproval psa2 = Iterables.getOnlyElement(psas.get(ps2));
-    assertThat(psa2.getPatchSetId()).isEqualTo(ps2);
-    assertThat(psa2.getAccountId().get()).isEqualTo(1);
-    assertThat(psa2.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa2.getValue()).isEqualTo((short) +1);
-    assertThat(psa2.getGranted()).isEqualTo(truncate(after(c, 4000)));
+    assertThat(psa2.patchSetId()).isEqualTo(ps2);
+    assertThat(psa2.accountId().get()).isEqualTo(1);
+    assertThat(psa2.label()).isEqualTo("Code-Review");
+    assertThat(psa2.value()).isEqualTo((short) +1);
+    assertThat(psa2.granted()).isEqualTo(truncate(after(c, 4000)));
   }
 
   @Test
@@ -292,8 +292,8 @@
     ChangeNotes notes = newNotes(c);
     PatchSetApproval psa =
         Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
-    assertThat(psa.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa.getValue()).isEqualTo((short) -1);
+    assertThat(psa.label()).isEqualTo("Code-Review");
+    assertThat(psa.value()).isEqualTo((short) -1);
 
     update = newUpdate(c, changeOwner);
     update.putApproval("Code-Review", (short) 1);
@@ -301,8 +301,8 @@
 
     notes = newNotes(c);
     psa = Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
-    assertThat(psa.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa.getValue()).isEqualTo((short) 1);
+    assertThat(psa.label()).isEqualTo("Code-Review");
+    assertThat(psa.value()).isEqualTo((short) 1);
   }
 
   @Test
@@ -321,17 +321,17 @@
     List<PatchSetApproval> psas = notes.getApprovals().get(c.currentPatchSetId());
     assertThat(psas).hasSize(2);
 
-    assertThat(psas.get(0).getPatchSetId()).isEqualTo(c.currentPatchSetId());
-    assertThat(psas.get(0).getAccountId().get()).isEqualTo(1);
-    assertThat(psas.get(0).getLabel()).isEqualTo("Code-Review");
-    assertThat(psas.get(0).getValue()).isEqualTo((short) -1);
-    assertThat(psas.get(0).getGranted()).isEqualTo(truncate(after(c, 2000)));
+    assertThat(psas.get(0).patchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(0).accountId().get()).isEqualTo(1);
+    assertThat(psas.get(0).label()).isEqualTo("Code-Review");
+    assertThat(psas.get(0).value()).isEqualTo((short) -1);
+    assertThat(psas.get(0).granted()).isEqualTo(truncate(after(c, 2000)));
 
-    assertThat(psas.get(1).getPatchSetId()).isEqualTo(c.currentPatchSetId());
-    assertThat(psas.get(1).getAccountId().get()).isEqualTo(2);
-    assertThat(psas.get(1).getLabel()).isEqualTo("Code-Review");
-    assertThat(psas.get(1).getValue()).isEqualTo((short) 1);
-    assertThat(psas.get(1).getGranted()).isEqualTo(truncate(after(c, 3000)));
+    assertThat(psas.get(1).patchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(1).accountId().get()).isEqualTo(2);
+    assertThat(psas.get(1).label()).isEqualTo("Code-Review");
+    assertThat(psas.get(1).value()).isEqualTo((short) 1);
+    assertThat(psas.get(1).granted()).isEqualTo(truncate(after(c, 3000)));
   }
 
   @Test
@@ -344,9 +344,9 @@
     ChangeNotes notes = newNotes(c);
     PatchSetApproval psa =
         Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
-    assertThat(psa.getAccountId().get()).isEqualTo(1);
-    assertThat(psa.getLabel()).isEqualTo("Not-For-Long");
-    assertThat(psa.getValue()).isEqualTo((short) 1);
+    assertThat(psa.accountId().get()).isEqualTo(1);
+    assertThat(psa.label()).isEqualTo("Not-For-Long");
+    assertThat(psa.value()).isEqualTo((short) 1);
 
     update = newUpdate(c, changeOwner);
     update.removeApproval("Not-For-Long");
@@ -356,8 +356,12 @@
     assertThat(notes.getApprovals())
         .containsExactlyEntriesIn(
             ImmutableListMultimap.of(
-                psa.getPatchSetId(),
-                new PatchSetApproval(psa.getKey(), (short) 0, update.getWhen())));
+                psa.patchSetId(),
+                PatchSetApproval.builder()
+                    .key(psa.key())
+                    .value(0)
+                    .granted(update.getWhen())
+                    .build()));
   }
 
   @Test
@@ -370,9 +374,9 @@
     ChangeNotes notes = newNotes(c);
     PatchSetApproval psa =
         Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
-    assertThat(psa.getAccountId()).isEqualTo(otherUserId);
-    assertThat(psa.getLabel()).isEqualTo("Not-For-Long");
-    assertThat(psa.getValue()).isEqualTo((short) 1);
+    assertThat(psa.accountId()).isEqualTo(otherUserId);
+    assertThat(psa.label()).isEqualTo("Not-For-Long");
+    assertThat(psa.value()).isEqualTo((short) 1);
 
     update = newUpdate(c, changeOwner);
     update.removeApprovalFor(otherUserId, "Not-For-Long");
@@ -382,8 +386,12 @@
     assertThat(notes.getApprovals())
         .containsExactlyEntriesIn(
             ImmutableListMultimap.of(
-                psa.getPatchSetId(),
-                new PatchSetApproval(psa.getKey(), (short) 0, update.getWhen())));
+                psa.patchSetId(),
+                PatchSetApproval.builder()
+                    .key(psa.key())
+                    .value(0)
+                    .granted(update.getWhen())
+                    .build()));
 
     // Add back approval on same label.
     update = newUpdate(c, otherUser);
@@ -392,9 +400,9 @@
 
     notes = newNotes(c);
     psa = Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
-    assertThat(psa.getAccountId()).isEqualTo(otherUserId);
-    assertThat(psa.getLabel()).isEqualTo("Not-For-Long");
-    assertThat(psa.getValue()).isEqualTo((short) 2);
+    assertThat(psa.accountId()).isEqualTo(otherUserId);
+    assertThat(psa.label()).isEqualTo("Not-For-Long");
+    assertThat(psa.value()).isEqualTo((short) 2);
   }
 
   @Test
@@ -408,17 +416,17 @@
     ChangeNotes notes = newNotes(c);
     ImmutableList<PatchSetApproval> approvals =
         notes.getApprovals().get(c.currentPatchSetId()).stream()
-            .sorted(comparing(a -> a.getAccountId().get()))
+            .sorted(comparing(a -> a.accountId().get()))
             .collect(toImmutableList());
     assertThat(approvals).hasSize(2);
 
-    assertThat(approvals.get(0).getAccountId()).isEqualTo(changeOwner.getAccountId());
-    assertThat(approvals.get(0).getLabel()).isEqualTo("Code-Review");
-    assertThat(approvals.get(0).getValue()).isEqualTo((short) 1);
+    assertThat(approvals.get(0).accountId()).isEqualTo(changeOwner.getAccountId());
+    assertThat(approvals.get(0).label()).isEqualTo("Code-Review");
+    assertThat(approvals.get(0).value()).isEqualTo((short) 1);
 
-    assertThat(approvals.get(1).getAccountId()).isEqualTo(otherUser.getAccountId());
-    assertThat(approvals.get(1).getLabel()).isEqualTo("Code-Review");
-    assertThat(approvals.get(1).getValue()).isEqualTo((short) -1);
+    assertThat(approvals.get(1).accountId()).isEqualTo(otherUser.getAccountId());
+    assertThat(approvals.get(1).label()).isEqualTo("Code-Review");
+    assertThat(approvals.get(1).value()).isEqualTo((short) -1);
   }
 
   @Test
@@ -448,12 +456,12 @@
     ChangeNotes notes = newNotes(c);
     List<PatchSetApproval> approvals = Lists.newArrayList(notes.getApprovals().values());
     assertThat(approvals).hasSize(2);
-    assertThat(approvals.get(0).getLabel()).isEqualTo("Verified");
-    assertThat(approvals.get(0).getValue()).isEqualTo((short) 1);
-    assertThat(approvals.get(0).isPostSubmit()).isFalse();
-    assertThat(approvals.get(1).getLabel()).isEqualTo("Code-Review");
-    assertThat(approvals.get(1).getValue()).isEqualTo((short) 2);
-    assertThat(approvals.get(1).isPostSubmit()).isTrue();
+    assertThat(approvals.get(0).label()).isEqualTo("Verified");
+    assertThat(approvals.get(0).value()).isEqualTo((short) 1);
+    assertThat(approvals.get(0).postSubmit()).isFalse();
+    assertThat(approvals.get(1).label()).isEqualTo("Code-Review");
+    assertThat(approvals.get(1).value()).isEqualTo((short) 2);
+    assertThat(approvals.get(1).postSubmit()).isTrue();
   }
 
   @Test
@@ -488,26 +496,26 @@
 
     List<PatchSetApproval> approvals = Lists.newArrayList(notes.getApprovals().values());
     assertThat(approvals).hasSize(3);
-    assertThat(approvals.get(0).getAccountId()).isEqualTo(ownerId);
-    assertThat(approvals.get(0).getLabel()).isEqualTo("Verified");
-    assertThat(approvals.get(0).getValue()).isEqualTo(1);
-    assertThat(approvals.get(0).isPostSubmit()).isFalse();
-    assertThat(approvals.get(1).getAccountId()).isEqualTo(ownerId);
-    assertThat(approvals.get(1).getLabel()).isEqualTo("Code-Review");
-    assertThat(approvals.get(1).getValue()).isEqualTo(2);
-    assertThat(approvals.get(1).isPostSubmit()).isFalse(); // During submit.
-    assertThat(approvals.get(2).getAccountId()).isEqualTo(otherId);
-    assertThat(approvals.get(2).getLabel()).isEqualTo("Other-Label");
-    assertThat(approvals.get(2).getValue()).isEqualTo(2);
-    assertThat(approvals.get(2).isPostSubmit()).isTrue();
+    assertThat(approvals.get(0).accountId()).isEqualTo(ownerId);
+    assertThat(approvals.get(0).label()).isEqualTo("Verified");
+    assertThat(approvals.get(0).value()).isEqualTo(1);
+    assertThat(approvals.get(0).postSubmit()).isFalse();
+    assertThat(approvals.get(1).accountId()).isEqualTo(ownerId);
+    assertThat(approvals.get(1).label()).isEqualTo("Code-Review");
+    assertThat(approvals.get(1).value()).isEqualTo(2);
+    assertThat(approvals.get(1).postSubmit()).isFalse(); // During submit.
+    assertThat(approvals.get(2).accountId()).isEqualTo(otherId);
+    assertThat(approvals.get(2).label()).isEqualTo("Other-Label");
+    assertThat(approvals.get(2).value()).isEqualTo(2);
+    assertThat(approvals.get(2).postSubmit()).isTrue();
   }
 
   @Test
   public void multipleReviewers() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
-    update.putReviewer(otherUser.getAccount().getId(), REVIEWER);
+    update.putReviewer(changeOwner.getAccount().id(), REVIEWER);
+    update.putReviewer(otherUser.getAccount().id(), REVIEWER);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
@@ -516,8 +524,8 @@
         .isEqualTo(
             ReviewerSet.fromTable(
                 ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
-                    .put(REVIEWER, new Account.Id(1), ts)
-                    .put(REVIEWER, new Account.Id(2), ts)
+                    .put(REVIEWER, Account.id(1), ts)
+                    .put(REVIEWER, Account.id(2), ts)
                     .build()));
   }
 
@@ -525,8 +533,8 @@
   public void reviewerTypes() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
-    update.putReviewer(otherUser.getAccount().getId(), CC);
+    update.putReviewer(changeOwner.getAccount().id(), REVIEWER);
+    update.putReviewer(otherUser.getAccount().id(), CC);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
@@ -535,8 +543,8 @@
         .isEqualTo(
             ReviewerSet.fromTable(
                 ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
-                    .put(REVIEWER, new Account.Id(1), ts)
-                    .put(CC, new Account.Id(2), ts)
+                    .put(REVIEWER, Account.id(1), ts)
+                    .put(CC, Account.id(2), ts)
                     .build()));
   }
 
@@ -544,29 +552,29 @@
   public void oneReviewerMultipleTypes() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewer(otherUser.getAccount().getId(), REVIEWER);
+    update.putReviewer(otherUser.getAccount().id(), REVIEWER);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
     Timestamp ts = new Timestamp(update.getWhen().getTime());
     assertThat(notes.getReviewers())
-        .isEqualTo(ReviewerSet.fromTable(ImmutableTable.of(REVIEWER, new Account.Id(2), ts)));
+        .isEqualTo(ReviewerSet.fromTable(ImmutableTable.of(REVIEWER, Account.id(2), ts)));
 
     update = newUpdate(c, otherUser);
-    update.putReviewer(otherUser.getAccount().getId(), CC);
+    update.putReviewer(otherUser.getAccount().id(), CC);
     update.commit();
 
     notes = newNotes(c);
     ts = new Timestamp(update.getWhen().getTime());
     assertThat(notes.getReviewers())
-        .isEqualTo(ReviewerSet.fromTable(ImmutableTable.of(CC, new Account.Id(2), ts)));
+        .isEqualTo(ReviewerSet.fromTable(ImmutableTable.of(CC, Account.id(2), ts)));
   }
 
   @Test
   public void removeReviewer() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewer(otherUser.getAccount().getId(), REVIEWER);
+    update.putReviewer(otherUser.getAccount().id(), REVIEWER);
     update.commit();
 
     update = newUpdate(c, changeOwner);
@@ -580,17 +588,17 @@
     ChangeNotes notes = newNotes(c);
     List<PatchSetApproval> psas = notes.getApprovals().get(c.currentPatchSetId());
     assertThat(psas).hasSize(2);
-    assertThat(psas.get(0).getAccountId()).isEqualTo(changeOwner.getAccount().getId());
-    assertThat(psas.get(1).getAccountId()).isEqualTo(otherUser.getAccount().getId());
+    assertThat(psas.get(0).accountId()).isEqualTo(changeOwner.getAccount().id());
+    assertThat(psas.get(1).accountId()).isEqualTo(otherUser.getAccount().id());
 
     update = newUpdate(c, changeOwner);
-    update.removeReviewer(otherUser.getAccount().getId());
+    update.removeReviewer(otherUser.getAccount().id());
     update.commit();
 
     notes = newNotes(c);
     psas = notes.getApprovals().get(c.currentPatchSetId());
     assertThat(psas).hasSize(1);
-    assertThat(psas.get(0).getAccountId()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(psas.get(0).accountId()).isEqualTo(changeOwner.getAccount().id());
   }
 
   @Test
@@ -736,6 +744,35 @@
   }
 
   @Test
+  public void assigneeStatusUpdateChangeNotes() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, otherUser);
+    update.setAssignee(otherUserId);
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.removeAssignee();
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.setAssignee(changeOwner.getAccountId());
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.setAssignee(otherUserId);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    ImmutableList<AssigneeStatusUpdate> statusUpdates = notes.getAssigneeUpdates();
+    assertThat(statusUpdates).hasSize(4);
+    assertThat(statusUpdates.get(3).updatedBy()).isEqualTo(otherUserId);
+    assertThat(statusUpdates.get(3).currentAssignee()).hasValue(otherUserId);
+    assertThat(statusUpdates.get(2).currentAssignee()).isEmpty();
+    assertThat(statusUpdates.get(1).currentAssignee()).hasValue(changeOwner.getAccountId());
+    assertThat(statusUpdates.get(0).currentAssignee()).hasValue(otherUserId);
+  }
+
+  @Test
   public void hashtagCommit() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -819,14 +856,17 @@
 
     // Trying to set another Change-Id fails
     String otherChangeId = "I577fb248e474018276351785930358ec0450e9f7";
-    update = newUpdate(c, changeOwner);
-    exception.expect(IllegalArgumentException.class);
-    exception.expectMessage(
-        "The Change-Id was already set to "
-            + c.getKey()
-            + ", so we cannot set this Change-Id: "
-            + otherChangeId);
-    update.setChangeId(otherChangeId);
+    ChangeUpdate failingUpdate = newUpdate(c, changeOwner);
+    IllegalArgumentException thrown =
+        assertThrows(
+            IllegalArgumentException.class, () -> failingUpdate.setChangeId(otherChangeId));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "The Change-Id was already set to "
+                + c.getKey()
+                + ", so we cannot set this Change-Id: "
+                + otherChangeId);
   }
 
   @Test
@@ -834,7 +874,7 @@
     Change c = newChange();
 
     ChangeNotes notes = newNotes(c);
-    Branch.NameKey expectedBranch = new Branch.NameKey(project, "refs/heads/master");
+    BranchNameKey expectedBranch = BranchNameKey.create(project, "refs/heads/master");
     assertThat(notes.getChange().getDest()).isEqualTo(expectedBranch);
 
     // An update doesn't affect the branch
@@ -849,7 +889,7 @@
     update.setBranch(otherBranch);
     update.commit();
     assertThat(newNotes(c).getChange().getDest())
-        .isEqualTo(new Branch.NameKey(project, otherBranch));
+        .isEqualTo(BranchNameKey.create(project, otherBranch));
   }
 
   @Test
@@ -956,7 +996,7 @@
     c.setCurrentPatchSet(c.currentPatchSetId(), "  " + trimmedSubj, c.getOriginalSubject());
     ChangeUpdate update = newUpdateForNewChange(c, changeOwner);
     update.setChangeId(c.getKey().get());
-    update.setBranch(c.getDest().get());
+    update.setBranch(c.getDest().branch());
     update.commit();
 
     ChangeNotes notes = newNotes(c);
@@ -968,7 +1008,7 @@
     c.setCurrentPatchSet(c.currentPatchSetId(), tabSubj, c.getOriginalSubject());
     update = newUpdateForNewChange(c, changeOwner);
     update.setChangeId(c.getKey().get());
-    update.setBranch(c.getDest().get());
+    update.setBranch(c.getDest().branch());
     update.commit();
 
     notes = newNotes(c);
@@ -977,7 +1017,7 @@
 
   @Test
   public void commitChangeNotesUnique() throws Exception {
-    // PatchSetId -> RevId must be a one to one mapping
+    // PatchSetId -> ObjectId must be a one to one mapping
     Change c = newChange();
 
     ChangeNotes notes = newNotes(c);
@@ -990,19 +1030,15 @@
     update.setCommit(rw, commit);
     update.commit();
 
-    try {
-      newNotes(c);
-      fail("Expected IOException");
-    } catch (StorageException e) {
-      assertCause(
-          e,
-          ConfigInvalidException.class,
-          "Multiple revisions parsed for patch set 1:"
-              + " RevId{"
-              + commit.name()
-              + "} and "
-              + ps.getRevision().get());
-    }
+    StorageException e = assertThrows(StorageException.class, () -> newNotes(c));
+    assertCause(
+        e,
+        ConfigInvalidException.class,
+        "Multiple revisions parsed for patch set 1:"
+            + " "
+            + commit.name()
+            + " and "
+            + ps.commitId().name());
   }
 
   @Test
@@ -1012,32 +1048,32 @@
     // ps1 created by newChange()
     ChangeNotes notes = newNotes(c);
     PatchSet ps1 = notes.getCurrentPatchSet();
-    assertThat(notes.getChange().currentPatchSetId()).isEqualTo(ps1.getId());
+    assertThat(notes.getChange().currentPatchSetId()).isEqualTo(ps1.id());
     assertThat(notes.getChange().getSubject()).isEqualTo("Change subject");
     assertThat(notes.getChange().getOriginalSubject()).isEqualTo("Change subject");
-    assertThat(ps1.getId()).isEqualTo(new PatchSet.Id(c.getId(), 1));
-    assertThat(ps1.getUploader()).isEqualTo(changeOwner.getAccountId());
+    assertThat(ps1.id()).isEqualTo(PatchSet.id(c.getId(), 1));
+    assertThat(ps1.uploader()).isEqualTo(changeOwner.getAccountId());
 
     // ps2 by other user
     RevCommit commit = incrementPatchSet(c, otherUser);
     notes = newNotes(c);
     PatchSet ps2 = notes.getCurrentPatchSet();
-    assertThat(ps2.getId()).isEqualTo(new PatchSet.Id(c.getId(), 2));
+    assertThat(ps2.id()).isEqualTo(PatchSet.id(c.getId(), 2));
     assertThat(notes.getChange().getSubject()).isEqualTo("PS2");
     assertThat(notes.getChange().getOriginalSubject()).isEqualTo("Change subject");
-    assertThat(notes.getChange().currentPatchSetId()).isEqualTo(ps2.getId());
-    assertThat(ps2.getRevision().get()).isNotEqualTo(ps1.getRevision());
-    assertThat(ps2.getRevision().get()).isEqualTo(commit.name());
-    assertThat(ps2.getUploader()).isEqualTo(otherUser.getAccountId());
-    assertThat(ps2.getCreatedOn()).isEqualTo(notes.getChange().getLastUpdatedOn());
+    assertThat(notes.getChange().currentPatchSetId()).isEqualTo(ps2.id());
+    assertThat(ps2.commitId()).isNotEqualTo(ps1.commitId());
+    assertThat(ps2.commitId()).isEqualTo(commit);
+    assertThat(ps2.uploader()).isEqualTo(otherUser.getAccountId());
+    assertThat(ps2.createdOn()).isEqualTo(notes.getChange().getLastUpdatedOn());
 
     // comment on ps1, current patch set is still ps2
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setPatchSetId(ps1.getId());
+    update.setPatchSetId(ps1.id());
     update.setChangeMessage("Comment on old patch set.");
     update.commit();
     notes = newNotes(c);
-    assertThat(notes.getChange().currentPatchSetId()).isEqualTo(ps2.getId());
+    assertThat(notes.getChange().currentPatchSetId()).isEqualTo(ps2.id());
   }
 
   @Test
@@ -1053,7 +1089,7 @@
     update.putApproval("Code-Review", (short) 1);
     update.setChangeMessage("This is a message");
     update.putComment(
-        Status.PUBLISHED,
+        Comment.Status.PUBLISHED,
         newComment(
             c.currentPatchSetId(),
             "a.txt",
@@ -1065,7 +1101,7 @@
             TimeUtil.nowTs(),
             "Comment",
             (short) 1,
-            commit.name(),
+            commit,
             false));
     update.commit();
 
@@ -1098,14 +1134,14 @@
     PatchSet.Id psId1 = c.currentPatchSetId();
 
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getPatchSets().get(psId1).getGroups()).isEmpty();
+    assertThat(notes.getPatchSets().get(psId1).groups()).isEmpty();
 
     // ps1
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setGroups(ImmutableList.of("a", "b"));
     update.commit();
     notes = newNotes(c);
-    assertThat(notes.getPatchSets().get(psId1).getGroups()).containsExactly("a", "b").inOrder();
+    assertThat(notes.getPatchSets().get(psId1).groups()).containsExactly("a", "b").inOrder();
 
     incrementCurrentPatchSetFieldOnly(c);
     PatchSet.Id psId2 = c.currentPatchSetId();
@@ -1114,8 +1150,8 @@
     update.setGroups(ImmutableList.of("d"));
     update.commit();
     notes = newNotes(c);
-    assertThat(notes.getPatchSets().get(psId2).getGroups()).containsExactly("d");
-    assertThat(notes.getPatchSets().get(psId1).getGroups()).containsExactly("a", "b").inOrder();
+    assertThat(notes.getPatchSets().get(psId2).groups()).containsExactly("d");
+    assertThat(notes.getPatchSets().get(psId1).groups()).containsExactly("a", "b").inOrder();
   }
 
   @Test
@@ -1144,8 +1180,8 @@
     readNote(notes, commit);
 
     Map<PatchSet.Id, PatchSet> patchSets = notes.getPatchSets();
-    assertThat(patchSets.get(psId1).getPushCertificate()).isNull();
-    assertThat(patchSets.get(psId2).getPushCertificate()).isEqualTo(pushCert);
+    assertThat(patchSets.get(psId1).pushCertificate()).isEmpty();
+    assertThat(patchSets.get(psId2).pushCertificate()).hasValue(pushCert);
     assertThat(notes.getComments()).isEmpty();
 
     // comment on ps2
@@ -1153,7 +1189,7 @@
     update.setPatchSetId(psId2);
     Timestamp ts = TimeUtil.nowTs();
     update.putComment(
-        Status.PUBLISHED,
+        Comment.Status.PUBLISHED,
         newComment(
             psId2,
             "a.txt",
@@ -1165,15 +1201,15 @@
             ts,
             "Comment",
             (short) 1,
-            commit.name(),
+            commit,
             false));
     update.commit();
 
     notes = newNotes(c);
 
     patchSets = notes.getPatchSets();
-    assertThat(patchSets.get(psId1).getPushCertificate()).isNull();
-    assertThat(patchSets.get(psId2).getPushCertificate()).isEqualTo(pushCert);
+    assertThat(patchSets.get(psId1).pushCertificate()).isEmpty();
+    assertThat(patchSets.get(psId2).pushCertificate()).hasValue(pushCert);
     assertThat(notes.getComments()).isNotEmpty();
   }
 
@@ -1203,13 +1239,13 @@
     List<PatchSetApproval> psas = notes.getApprovals().get(c.currentPatchSetId());
     assertThat(psas).hasSize(2);
 
-    assertThat(psas.get(0).getAccountId()).isEqualTo(changeOwner.getAccount().getId());
-    assertThat(psas.get(0).getLabel()).isEqualTo("Verified");
-    assertThat(psas.get(0).getValue()).isEqualTo((short) 1);
+    assertThat(psas.get(0).accountId()).isEqualTo(changeOwner.getAccount().id());
+    assertThat(psas.get(0).label()).isEqualTo("Verified");
+    assertThat(psas.get(0).value()).isEqualTo((short) 1);
 
-    assertThat(psas.get(1).getAccountId()).isEqualTo(otherUser.getAccount().getId());
-    assertThat(psas.get(1).getLabel()).isEqualTo("Code-Review");
-    assertThat(psas.get(1).getValue()).isEqualTo((short) 2);
+    assertThat(psas.get(1).accountId()).isEqualTo(otherUser.getAccount().id());
+    assertThat(psas.get(1).label()).isEqualTo("Code-Review");
+    assertThat(psas.get(1).value()).isEqualTo((short) 2);
   }
 
   @Test
@@ -1235,10 +1271,10 @@
               time1,
               message1,
               (short) 0,
-              "abcd1234abcd1234abcd1234abcd1234abcd1234",
+              ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"),
               false);
       update1.setPatchSetId(psId);
-      update1.putComment(Status.PUBLISHED, comment1);
+      update1.putComment(Comment.Status.PUBLISHED, comment1);
       updateManager.add(update1);
 
       ChangeUpdate update2 = newUpdate(c, otherUser);
@@ -1260,12 +1296,7 @@
     try (ChangeNotesRevWalk rw = ChangeNotesCommit.newRevWalk(repo)) {
       ChangeNotesParser notesWithComments =
           new ChangeNotesParser(
-              c.getId(),
-              commitWithComments.copy(),
-              rw,
-              changeNoteJson,
-              legacyChangeNoteRead,
-              args.metrics);
+              c.getId(), commitWithComments.copy(), rw, changeNoteJson, args.metrics);
       ChangeNotesState state = notesWithComments.parseAll();
       assertThat(state.approvals()).isEmpty();
       assertThat(state.publishedComments()).hasSize(1);
@@ -1274,12 +1305,7 @@
     try (ChangeNotesRevWalk rw = ChangeNotesCommit.newRevWalk(repo)) {
       ChangeNotesParser notesWithApprovals =
           new ChangeNotesParser(
-              c.getId(),
-              commitWithApprovals.copy(),
-              rw,
-              changeNoteJson,
-              legacyChangeNoteRead,
-              args.metrics);
+              c.getId(), commitWithApprovals.copy(), rw, changeNoteJson, args.metrics);
 
       ChangeNotesState state = notesWithApprovals.parseAll();
       assertThat(state.approvals()).hasSize(1);
@@ -1317,25 +1343,25 @@
 
     PatchSetApproval approval1 =
         newNotes(c1).getApprovals().get(c1.currentPatchSetId()).iterator().next();
-    assertThat(approval1.getLabel()).isEqualTo("Verified");
+    assertThat(approval1.label()).isEqualTo("Verified");
 
     PatchSetApproval approval2 =
         newNotes(c2).getApprovals().get(c2.currentPatchSetId()).iterator().next();
-    assertThat(approval2.getLabel()).isEqualTo("Code-Review");
+    assertThat(approval2.label()).isEqualTo("Code-Review");
   }
 
   @Test
   public void changeMessageOnePatchSet() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.putReviewer(changeOwner.getAccount().id(), REVIEWER);
     update.setChangeMessage("Just a little code change.\n");
     update.commit();
 
     ChangeNotes notes = newNotes(c);
     ChangeMessage cm = Iterables.getOnlyElement(notes.getChangeMessages());
     assertThat(cm.getMessage()).isEqualTo("Just a little code change.\n");
-    assertThat(cm.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm.getAuthor()).isEqualTo(changeOwner.getAccount().id());
     assertThat(cm.getPatchSetId()).isEqualTo(c.currentPatchSetId());
   }
 
@@ -1343,7 +1369,7 @@
   public void noChangeMessage() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.putReviewer(changeOwner.getAccount().id(), REVIEWER);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
@@ -1360,7 +1386,7 @@
     ChangeNotes notes = newNotes(c);
     ChangeMessage cm1 = Iterables.getOnlyElement(notes.getChangeMessages());
     assertThat(cm1.getMessage()).isEqualTo("Testing trailing double newline\n\n");
-    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().id());
   }
 
   @Test
@@ -1379,14 +1405,14 @@
                 + "Testing paragraph 2\n"
                 + "\n"
                 + "Testing paragraph 3");
-    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().id());
   }
 
   @Test
   public void changeMessagesMultiplePatchSets() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.putReviewer(changeOwner.getAccount().id(), REVIEWER);
     update.setChangeMessage("This is the change message for the first PS.");
     update.commit();
     PatchSet.Id ps1 = c.currentPatchSetId();
@@ -1404,12 +1430,12 @@
     ChangeMessage cm1 = notes.getChangeMessages().get(0);
     assertThat(cm1.getPatchSetId()).isEqualTo(ps1);
     assertThat(cm1.getMessage()).isEqualTo("This is the change message for the first PS.");
-    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().id());
 
     ChangeMessage cm2 = notes.getChangeMessages().get(1);
     assertThat(cm2.getPatchSetId()).isEqualTo(ps2);
     assertThat(cm2.getMessage()).isEqualTo("This is the change message for the second PS.");
-    assertThat(cm2.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm2.getAuthor()).isEqualTo(changeOwner.getAccount().id());
     assertThat(cm2.getPatchSetId()).isEqualTo(ps2);
   }
 
@@ -1417,14 +1443,14 @@
   public void changeMessageMultipleInOnePatchSet() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.putReviewer(changeOwner.getAccount().id(), REVIEWER);
     update.setChangeMessage("First change message.\n");
     update.commit();
 
     PatchSet.Id ps1 = c.currentPatchSetId();
 
     update = newUpdate(c, changeOwner);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.putReviewer(changeOwner.getAccount().id(), REVIEWER);
     update.setChangeMessage("Second change message.\n");
     update.commit();
 
@@ -1433,10 +1459,10 @@
     List<ChangeMessage> cm = notes.getChangeMessages();
     assertThat(cm).hasSize(2);
     assertThat(cm.get(0).getMessage()).isEqualTo("First change message.\n");
-    assertThat(cm.get(0).getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm.get(0).getAuthor()).isEqualTo(changeOwner.getAccount().id());
     assertThat(cm.get(0).getPatchSetId()).isEqualTo(ps1);
     assertThat(cm.get(1).getMessage()).isEqualTo("Second change message.\n");
-    assertThat(cm.get(1).getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm.get(1).getAuthor()).isEqualTo(changeOwner.getAccount().id());
     assertThat(cm.get(1).getPatchSetId()).isEqualTo(ps1);
   }
 
@@ -1445,7 +1471,7 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
     PatchSet.Id psId = c.currentPatchSetId();
-    RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
 
     Comment comment =
         newComment(
@@ -1459,14 +1485,14 @@
             TimeUtil.nowTs(),
             "message",
             (short) 1,
-            revId.get(),
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment);
+    update.putComment(Comment.Status.PUBLISHED, comment);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getComments()).isEqualTo(ImmutableListMultimap.of(revId, comment));
+    assertThat(notes.getComments()).isEqualTo(ImmutableListMultimap.of(commitId, comment));
   }
 
   @Test
@@ -1474,7 +1500,7 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
     PatchSet.Id psId = c.currentPatchSetId();
-    RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     CommentRange range = new CommentRange(1, 0, 2, 0);
 
     Comment comment =
@@ -1489,14 +1515,14 @@
             TimeUtil.nowTs(),
             "message",
             (short) 1,
-            revId.get(),
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment);
+    update.putComment(Comment.Status.PUBLISHED, comment);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getComments()).isEqualTo(ImmutableListMultimap.of(revId, comment));
+    assertThat(notes.getComments()).isEqualTo(ImmutableListMultimap.of(commitId, comment));
   }
 
   @Test
@@ -1504,7 +1530,7 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
     PatchSet.Id psId = c.currentPatchSetId();
-    RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     CommentRange range = new CommentRange(0, 0, 0, 0);
 
     Comment comment =
@@ -1519,14 +1545,14 @@
             TimeUtil.nowTs(),
             "message",
             (short) 1,
-            revId.get(),
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment);
+    update.putComment(Comment.Status.PUBLISHED, comment);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getComments()).isEqualTo(ImmutableListMultimap.of(revId, comment));
+    assertThat(notes.getComments()).isEqualTo(ImmutableListMultimap.of(commitId, comment));
   }
 
   @Test
@@ -1534,7 +1560,7 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
     PatchSet.Id psId = c.currentPatchSetId();
-    RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     CommentRange range = new CommentRange(1, 2, 3, 4);
 
     Comment comment =
@@ -1549,18 +1575,18 @@
             TimeUtil.nowTs(),
             "message",
             (short) 1,
-            revId.get(),
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment);
+    update.putComment(Comment.Status.PUBLISHED, comment);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getComments()).isEqualTo(ImmutableListMultimap.of(revId, comment));
+    assertThat(notes.getComments()).isEqualTo(ImmutableListMultimap.of(commitId, comment));
   }
 
   @Test
-  public void patchLineCommentNotesFormatMultiplePatchSetsSameRevId() throws Exception {
+  public void patchLineCommentNotesFormatMultiplePatchSetsSameCommitId() throws Exception {
     Change c = newChange();
     PatchSet.Id psId1 = c.currentPatchSetId();
     incrementPatchSet(c);
@@ -1574,7 +1600,7 @@
     CommentRange range1 = new CommentRange(1, 1, 2, 1);
     CommentRange range2 = new CommentRange(2, 1, 3, 1);
     Timestamp time = TimeUtil.nowTs();
-    RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
 
     Comment comment1 =
         newComment(
@@ -1588,7 +1614,7 @@
             time,
             message1,
             (short) 0,
-            revId.get(),
+            commitId,
             false);
     Comment comment2 =
         newComment(
@@ -1602,7 +1628,7 @@
             time,
             message2,
             (short) 0,
-            revId.get(),
+            commitId,
             false);
     Comment comment3 =
         newComment(
@@ -1616,23 +1642,23 @@
             time,
             message3,
             (short) 0,
-            revId.get(),
+            commitId,
             false);
 
     ChangeUpdate update = newUpdate(c, otherUser);
     update.setPatchSetId(psId2);
-    update.putComment(Status.PUBLISHED, comment3);
-    update.putComment(Status.PUBLISHED, comment2);
-    update.putComment(Status.PUBLISHED, comment1);
+    update.putComment(Comment.Status.PUBLISHED, comment3);
+    update.putComment(Comment.Status.PUBLISHED, comment2);
+    update.putComment(Comment.Status.PUBLISHED, comment1);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
     assertThat(notes.getComments())
         .isEqualTo(
             ImmutableListMultimap.of(
-                revId, comment1,
-                revId, comment2,
-                revId, comment3));
+                commitId, comment1,
+                commitId, comment2,
+                commitId, comment3));
   }
 
   @Test
@@ -1645,7 +1671,7 @@
     CommentRange range = new CommentRange(1, 1, 2, 1);
     Timestamp time = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
-    RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
 
     Comment comment =
         newComment(
@@ -1659,25 +1685,25 @@
             time,
             message,
             (short) 1,
-            revId.get(),
+            commitId,
             false);
     comment.setRealAuthor(changeOwner.getAccountId());
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment);
+    update.putComment(Comment.Status.PUBLISHED, comment);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
 
-    assertThat(notes.getComments()).isEqualTo(ImmutableListMultimap.of(revId, comment));
+    assertThat(notes.getComments()).isEqualTo(ImmutableListMultimap.of(commitId, comment));
   }
 
   @Test
   public void patchLineCommentNotesFormatWeirdUser() throws Exception {
-    Account account = new Account(new Account.Id(3), TimeUtil.nowTs());
+    Account.Builder account = Account.builder(Account.id(3), TimeUtil.nowTs());
     account.setFullName("Weird\n\u0002<User>\n");
     account.setPreferredEmail(" we\r\nird@ex>ample<.com");
-    accountCache.put(account);
-    IdentifiedUser user = userFactory.create(account.getId());
+    accountCache.put(account.build());
+    IdentifiedUser user = userFactory.create(Account.id(3));
 
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, user);
@@ -1698,16 +1724,16 @@
             time,
             "comment",
             (short) 1,
-            "abcd1234abcd1234abcd1234abcd1234abcd1234",
+            ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"),
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment);
+    update.putComment(Comment.Status.PUBLISHED, comment);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
 
     assertThat(notes.getComments())
-        .isEqualTo(ImmutableListMultimap.of(new RevId(comment.revId), comment));
+        .isEqualTo(ImmutableListMultimap.of(comment.getCommitId(), comment));
   }
 
   @Test
@@ -1716,8 +1742,8 @@
     ChangeUpdate update = newUpdate(c, otherUser);
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
-    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
-    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    ObjectId commitId1 = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId2 = ObjectId.fromString("abcd4567abcd4567abcd4567abcd4567abcd4567");
     String messageForBase = "comment for base";
     String messageForPS = "comment for ps";
     CommentRange range = new CommentRange(1, 1, 2, 1);
@@ -1736,10 +1762,10 @@
             now,
             messageForBase,
             (short) 0,
-            rev1,
+            commitId1,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, commentForBase);
+    update.putComment(Comment.Status.PUBLISHED, commentForBase);
     update.commit();
 
     update = newUpdate(c, otherUser);
@@ -1755,17 +1781,17 @@
             now,
             messageForPS,
             (short) 1,
-            rev2,
+            commitId2,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, commentForPS);
+    update.putComment(Comment.Status.PUBLISHED, commentForPS);
     update.commit();
 
     assertThat(newNotes(c).getComments())
         .containsExactlyEntriesIn(
             ImmutableListMultimap.of(
-                new RevId(rev1), commentForBase,
-                new RevId(rev2), commentForPS));
+                commitId1, commentForBase,
+                commitId2, commentForPS));
   }
 
   @Test
@@ -1773,7 +1799,7 @@
     Change c = newChange();
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
-    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id psId = c.currentPatchSetId();
     String filename = "filename";
@@ -1794,10 +1820,10 @@
             timeForComment1,
             "comment 1",
             side,
-            rev,
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment1);
+    update.putComment(Comment.Status.PUBLISHED, comment1);
     update.commit();
 
     update = newUpdate(c, otherUser);
@@ -1813,17 +1839,17 @@
             timeForComment2,
             "comment 2",
             side,
-            rev,
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment2);
+    update.putComment(Comment.Status.PUBLISHED, comment2);
     update.commit();
 
     assertThat(newNotes(c).getComments())
         .containsExactlyEntriesIn(
             ImmutableListMultimap.of(
-                new RevId(rev), comment1,
-                new RevId(rev), comment2))
+                commitId, comment1,
+                commitId, comment2))
         .inOrder();
   }
 
@@ -1831,7 +1857,7 @@
   public void patchLineCommentMultipleOnePatchsetMultipleFiles() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
-    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id psId = c.currentPatchSetId();
     String filename1 = "filename1";
@@ -1852,10 +1878,10 @@
             now,
             "comment 1",
             side,
-            rev,
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment1);
+    update.putComment(Comment.Status.PUBLISHED, comment1);
     update.commit();
 
     update = newUpdate(c, otherUser);
@@ -1871,17 +1897,17 @@
             now,
             "comment 2",
             side,
-            rev,
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment2);
+    update.putComment(Comment.Status.PUBLISHED, comment2);
     update.commit();
 
     assertThat(newNotes(c).getComments())
         .containsExactlyEntriesIn(
             ImmutableListMultimap.of(
-                new RevId(rev), comment1,
-                new RevId(rev), comment2))
+                commitId, comment1,
+                commitId, comment2))
         .inOrder();
   }
 
@@ -1889,8 +1915,8 @@
   public void patchLineCommentMultiplePatchsets() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
-    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
-    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    ObjectId commitId1 = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId2 = ObjectId.fromString("abcd4567abcd4567abcd4567abcd4567abcd4567");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     String filename = "filename1";
@@ -1910,10 +1936,10 @@
             now,
             "comment on ps1",
             side,
-            rev1,
+            commitId1,
             false);
     update.setPatchSetId(ps1);
-    update.putComment(Status.PUBLISHED, comment1);
+    update.putComment(Comment.Status.PUBLISHED, comment1);
     update.commit();
 
     incrementPatchSet(c);
@@ -1933,24 +1959,24 @@
             now,
             "comment on ps2",
             side,
-            rev2,
+            commitId2,
             false);
     update.setPatchSetId(ps2);
-    update.putComment(Status.PUBLISHED, comment2);
+    update.putComment(Comment.Status.PUBLISHED, comment2);
     update.commit();
 
     assertThat(newNotes(c).getComments())
         .containsExactlyEntriesIn(
             ImmutableListMultimap.of(
-                new RevId(rev1), comment1,
-                new RevId(rev2), comment2));
+                commitId1, comment1,
+                commitId2, comment2));
   }
 
   @Test
   public void patchLineCommentSingleDraftToPublished() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
-    String rev = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    ObjectId commitId = ObjectId.fromString("abcd4567abcd4567abcd4567abcd4567abcd4567");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     String filename = "filename1";
@@ -1970,26 +1996,26 @@
             now,
             "comment on ps1",
             side,
-            rev,
+            commitId,
             false);
     update.setPatchSetId(ps1);
-    update.putComment(Status.DRAFT, comment1);
+    update.putComment(Comment.Status.DRAFT, comment1);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
     assertThat(notes.getDraftComments(otherUserId))
-        .containsExactlyEntriesIn(ImmutableListMultimap.of(new RevId(rev), comment1));
+        .containsExactlyEntriesIn(ImmutableListMultimap.of(commitId, comment1));
     assertThat(notes.getComments()).isEmpty();
 
     update = newUpdate(c, otherUser);
     update.setPatchSetId(ps1);
-    update.putComment(Status.PUBLISHED, comment1);
+    update.putComment(Comment.Status.PUBLISHED, comment1);
     update.commit();
 
     notes = newNotes(c);
     assertThat(notes.getDraftComments(otherUserId)).isEmpty();
     assertThat(notes.getComments())
-        .containsExactlyEntriesIn(ImmutableListMultimap.of(new RevId(rev), comment1));
+        .containsExactlyEntriesIn(ImmutableListMultimap.of(commitId, comment1));
   }
 
   @Test
@@ -1997,7 +2023,7 @@
     Change c = newChange();
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
-    String rev = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    ObjectId commitId = ObjectId.fromString("abcd4567abcd4567abcd4567abcd4567abcd4567");
     CommentRange range1 = new CommentRange(1, 1, 2, 2);
     CommentRange range2 = new CommentRange(2, 2, 3, 3);
     String filename = "filename1";
@@ -2020,7 +2046,7 @@
             now,
             "comment on ps1",
             side,
-            rev,
+            commitId,
             false);
     Comment comment2 =
         newComment(
@@ -2034,32 +2060,32 @@
             now,
             "other on ps1",
             side,
-            rev,
+            commitId,
             false);
-    update.putComment(Status.DRAFT, comment1);
-    update.putComment(Status.DRAFT, comment2);
+    update.putComment(Comment.Status.DRAFT, comment1);
+    update.putComment(Comment.Status.DRAFT, comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
     assertThat(notes.getDraftComments(otherUserId))
         .containsExactlyEntriesIn(
             ImmutableListMultimap.of(
-                new RevId(rev), comment1,
-                new RevId(rev), comment2))
+                commitId, comment1,
+                commitId, comment2))
         .inOrder();
     assertThat(notes.getComments()).isEmpty();
 
     // Publish first draft.
     update = newUpdate(c, otherUser);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment1);
+    update.putComment(Comment.Status.PUBLISHED, comment1);
     update.commit();
 
     notes = newNotes(c);
     assertThat(notes.getDraftComments(otherUserId))
-        .containsExactlyEntriesIn(ImmutableListMultimap.of(new RevId(rev), comment2));
+        .containsExactlyEntriesIn(ImmutableListMultimap.of(commitId, comment2));
     assertThat(notes.getComments())
-        .containsExactlyEntriesIn(ImmutableListMultimap.of(new RevId(rev), comment1));
+        .containsExactlyEntriesIn(ImmutableListMultimap.of(commitId, comment1));
   }
 
   @Test
@@ -2067,8 +2093,8 @@
     Change c = newChange();
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
-    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
-    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    ObjectId commitId1 = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId2 = ObjectId.fromString("abcd4567abcd4567abcd4567abcd4567abcd4567");
     CommentRange range1 = new CommentRange(1, 1, 2, 2);
     CommentRange range2 = new CommentRange(2, 2, 3, 3);
     String filename = "filename1";
@@ -2090,7 +2116,7 @@
             now,
             "comment on base",
             (short) 0,
-            rev1,
+            commitId1,
             false);
     Comment psComment =
         newComment(
@@ -2104,27 +2130,27 @@
             now,
             "comment on ps",
             (short) 1,
-            rev2,
+            commitId2,
             false);
 
-    update.putComment(Status.DRAFT, baseComment);
-    update.putComment(Status.DRAFT, psComment);
+    update.putComment(Comment.Status.DRAFT, baseComment);
+    update.putComment(Comment.Status.DRAFT, psComment);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
     assertThat(notes.getDraftComments(otherUserId))
         .containsExactlyEntriesIn(
             ImmutableListMultimap.of(
-                new RevId(rev1), baseComment,
-                new RevId(rev2), psComment));
+                commitId1, baseComment,
+                commitId2, psComment));
     assertThat(notes.getComments()).isEmpty();
 
     // Publish both comments.
     update = newUpdate(c, otherUser);
     update.setPatchSetId(psId);
 
-    update.putComment(Status.PUBLISHED, baseComment);
-    update.putComment(Status.PUBLISHED, psComment);
+    update.putComment(Comment.Status.PUBLISHED, baseComment);
+    update.putComment(Comment.Status.PUBLISHED, psComment);
     update.commit();
 
     notes = newNotes(c);
@@ -2132,16 +2158,15 @@
     assertThat(notes.getComments())
         .containsExactlyEntriesIn(
             ImmutableListMultimap.of(
-                new RevId(rev1), baseComment,
-                new RevId(rev2), psComment));
+                commitId1, baseComment,
+                commitId2, psComment));
   }
 
   @Test
   public void patchLineCommentsDeleteAllDrafts() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
-    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
-    ObjectId objId = ObjectId.fromString(rev);
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id psId = c.currentPatchSetId();
     String filename = "filename";
@@ -2161,15 +2186,15 @@
             now,
             "comment on ps1",
             side,
-            rev,
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.DRAFT, comment);
+    update.putComment(Comment.Status.DRAFT, comment);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
     assertThat(notes.getDraftComments(otherUserId)).hasSize(1);
-    assertThat(notes.getDraftCommentNotes().getNoteMap().contains(objId)).isTrue();
+    assertThat(notes.getDraftCommentNotes().getNoteMap().contains(commitId)).isTrue();
 
     update = newUpdate(c, otherUser);
     update.setPatchSetId(psId);
@@ -2185,10 +2210,8 @@
   public void patchLineCommentsDeleteAllDraftsForOneRevision() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
-    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
-    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
-    ObjectId objId1 = ObjectId.fromString(rev1);
-    ObjectId objId2 = ObjectId.fromString(rev2);
+    ObjectId commitId1 = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId2 = ObjectId.fromString("abcd4567abcd4567abcd4567abcd4567abcd4567");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     String filename = "filename1";
@@ -2208,10 +2231,10 @@
             now,
             "comment on ps1",
             side,
-            rev1,
+            commitId1,
             false);
     update.setPatchSetId(ps1);
-    update.putComment(Status.DRAFT, comment1);
+    update.putComment(Comment.Status.DRAFT, comment1);
     update.commit();
 
     incrementPatchSet(c);
@@ -2231,10 +2254,10 @@
             now,
             "comment on ps2",
             side,
-            rev2,
+            commitId2,
             false);
     update.setPatchSetId(ps2);
-    update.putComment(Status.DRAFT, comment2);
+    update.putComment(Comment.Status.DRAFT, comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
@@ -2248,15 +2271,15 @@
     notes = newNotes(c);
     assertThat(notes.getDraftComments(otherUserId)).hasSize(1);
     NoteMap noteMap = notes.getDraftCommentNotes().getNoteMap();
-    assertThat(noteMap.contains(objId1)).isTrue();
-    assertThat(noteMap.contains(objId2)).isFalse();
+    assertThat(noteMap.contains(commitId1)).isTrue();
+    assertThat(noteMap.contains(commitId2)).isFalse();
   }
 
   @Test
   public void addingPublishedCommentDoesNotCreateNoOpCommitOnEmptyDraftRef() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
-    String rev = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    ObjectId commitId = ObjectId.fromString("abcd4567abcd4567abcd4567abcd4567abcd4567");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     String filename = "filename1";
@@ -2276,9 +2299,9 @@
             now,
             "comment on ps1",
             side,
-            rev,
+            commitId,
             false);
-    update.putComment(Status.PUBLISHED, comment);
+    update.putComment(Comment.Status.PUBLISHED, comment);
     update.commit();
 
     assertThat(repo.exactRef(changeMetaRef(c.getId()))).isNotNull();
@@ -2289,7 +2312,7 @@
   @Test
   public void addingPublishedCommentDoesNotCreateNoOpCommitOnNonEmptyDraftRef() throws Exception {
     Change c = newChange();
-    String rev = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    ObjectId commitId = ObjectId.fromString("abcd4567abcd4567abcd4567abcd4567abcd4567");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     String filename = "filename1";
@@ -2309,9 +2332,9 @@
             now,
             "draft comment on ps1",
             side,
-            rev,
+            commitId,
             false);
-    update.putComment(Status.DRAFT, draft);
+    update.putComment(Comment.Status.DRAFT, draft);
     update.commit();
 
     String draftRef = refsDraftComments(c.getId(), otherUser.getAccountId());
@@ -2331,9 +2354,9 @@
             now,
             "comment on ps1",
             side,
-            rev,
+            commitId,
             false);
-    update.putComment(Status.PUBLISHED, pub);
+    update.putComment(Comment.Status.PUBLISHED, pub);
     update.commit();
 
     assertThat(exactRefAllUsers(draftRef)).isEqualTo(old);
@@ -2344,7 +2367,7 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
     String uuid = "uuid";
-    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     String messageForBase = "comment for base";
     Timestamp now = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
@@ -2361,14 +2384,14 @@
             now,
             messageForBase,
             (short) 0,
-            rev,
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment);
+    update.putComment(Comment.Status.PUBLISHED, comment);
     update.commit();
 
     assertThat(newNotes(c).getComments())
-        .containsExactlyEntriesIn(ImmutableListMultimap.of(new RevId(rev), comment));
+        .containsExactlyEntriesIn(ImmutableListMultimap.of(commitId, comment));
   }
 
   @Test
@@ -2376,7 +2399,7 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
     String uuid = "uuid";
-    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     String messageForBase = "comment for base";
     Timestamp now = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
@@ -2393,22 +2416,22 @@
             now,
             messageForBase,
             (short) 0,
-            rev,
+            commitId,
             false);
     update.setPatchSetId(psId);
-    update.putComment(Status.PUBLISHED, comment);
+    update.putComment(Comment.Status.PUBLISHED, comment);
     update.commit();
 
     assertThat(newNotes(c).getComments())
-        .containsExactlyEntriesIn(ImmutableListMultimap.of(new RevId(rev), comment));
+        .containsExactlyEntriesIn(ImmutableListMultimap.of(commitId, comment));
   }
 
   @Test
   public void putCommentsForMultipleRevisions() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
-    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
-    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    ObjectId commitId1 = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId2 = ObjectId.fromString("abcd4567abcd4567abcd4567abcd4567abcd4567");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     String filename = "filename1";
@@ -2432,7 +2455,7 @@
             now,
             "comment on ps1",
             side,
-            rev1,
+            commitId1,
             false);
     Comment comment2 =
         newComment(
@@ -2446,10 +2469,10 @@
             now,
             "comment on ps2",
             side,
-            rev2,
+            commitId2,
             false);
-    update.putComment(Status.DRAFT, comment1);
-    update.putComment(Status.DRAFT, comment2);
+    update.putComment(Comment.Status.DRAFT, comment1);
+    update.putComment(Comment.Status.DRAFT, comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
@@ -2458,8 +2481,8 @@
 
     update = newUpdate(c, otherUser);
     update.setPatchSetId(ps2);
-    update.putComment(Status.PUBLISHED, comment1);
-    update.putComment(Status.PUBLISHED, comment2);
+    update.putComment(Comment.Status.PUBLISHED, comment1);
+    update.putComment(Comment.Status.PUBLISHED, comment2);
     update.commit();
 
     notes = newNotes(c);
@@ -2470,7 +2493,7 @@
   @Test
   public void publishSubsetOfCommentsOnRevision() throws Exception {
     Change c = newChange();
-    RevId rev1 = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId1 = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     short side = (short) 1;
@@ -2490,7 +2513,7 @@
             now,
             "comment1",
             side,
-            rev1.get(),
+            commitId1,
             false);
     Comment comment2 =
         newComment(
@@ -2504,24 +2527,25 @@
             now,
             "comment2",
             side,
-            rev1.get(),
+            commitId1,
             false);
-    update.putComment(Status.DRAFT, comment1);
-    update.putComment(Status.DRAFT, comment2);
+    update.putComment(Comment.Status.DRAFT, comment1);
+    update.putComment(Comment.Status.DRAFT, comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getDraftComments(otherUserId).get(rev1)).containsExactly(comment1, comment2);
+    assertThat(notes.getDraftComments(otherUserId).get(commitId1))
+        .containsExactly(comment1, comment2);
     assertThat(notes.getComments()).isEmpty();
 
     update = newUpdate(c, otherUser);
     update.setPatchSetId(ps1);
-    update.putComment(Status.PUBLISHED, comment2);
+    update.putComment(Comment.Status.PUBLISHED, comment2);
     update.commit();
 
     notes = newNotes(c);
-    assertThat(notes.getDraftComments(otherUserId).get(rev1)).containsExactly(comment1);
-    assertThat(notes.getComments().get(rev1)).containsExactly(comment2);
+    assertThat(notes.getDraftComments(otherUserId).get(commitId1)).containsExactly(comment1);
+    assertThat(notes.getComments().get(commitId1)).containsExactly(comment2);
   }
 
   @Test
@@ -2535,15 +2559,15 @@
     assertThat(msg.getMessage()).isEqualTo("A message.");
     assertThat(msg.getAuthor()).isNull();
 
-    update = newUpdate(c, internalUser);
-    exception.expect(IllegalStateException.class);
-    update.putApproval("Code-Review", (short) 1);
+    ChangeUpdate failingUpdate = newUpdate(c, internalUser);
+    assertThrows(
+        IllegalStateException.class, () -> failingUpdate.putApproval("Code-Review", (short) 1));
   }
 
   @Test
   public void filterOutAndFixUpZombieDraftComments() throws Exception {
     Change c = newChange();
-    RevId rev1 = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    ObjectId commitId1 = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     short side = (short) 1;
@@ -2562,7 +2586,7 @@
             now,
             "comment on ps1",
             side,
-            rev1.get(),
+            commitId1,
             false);
     Comment comment2 =
         newComment(
@@ -2576,10 +2600,10 @@
             now,
             "another comment",
             side,
-            rev1.get(),
+            commitId1,
             false);
-    update.putComment(Status.DRAFT, comment1);
-    update.putComment(Status.DRAFT, comment2);
+    update.putComment(Comment.Status.DRAFT, comment1);
+    update.putComment(Comment.Status.DRAFT, comment2);
     update.commit();
 
     String refName = refsDraftComments(c.getId(), otherUserId);
@@ -2587,7 +2611,7 @@
 
     update = newUpdate(c, otherUser);
     update.setPatchSetId(ps1);
-    update.putComment(Status.PUBLISHED, comment2);
+    update.putComment(Comment.Status.PUBLISHED, comment2);
     update.commit();
     assertThat(exactRefAllUsers(refName)).isNotNull();
     assertThat(exactRefAllUsers(refName)).isNotEqualTo(oldDraftId);
@@ -2604,16 +2628,16 @@
 
     // Looking at drafts directly shows the zombie comment.
     DraftCommentNotes draftNotes = draftNotesFactory.create(c.getId(), otherUserId);
-    assertThat(draftNotes.load().getComments().get(rev1)).containsExactly(comment1, comment2);
+    assertThat(draftNotes.load().getComments().get(commitId1)).containsExactly(comment1, comment2);
 
     // Zombie comment is filtered out of drafts via ChangeNotes.
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getDraftComments(otherUserId).get(rev1)).containsExactly(comment1);
-    assertThat(notes.getComments().get(rev1)).containsExactly(comment2);
+    assertThat(notes.getDraftComments(otherUserId).get(commitId1)).containsExactly(comment1);
+    assertThat(notes.getComments().get(commitId1)).containsExactly(comment2);
 
     update = newUpdate(c, otherUser);
     update.setPatchSetId(ps1);
-    update.putComment(Status.PUBLISHED, comment1);
+    update.putComment(Comment.Status.PUBLISHED, comment1);
     update.commit();
 
     // Updating an unrelated comment causes the zombie comment to get fixed up.
@@ -2624,7 +2648,7 @@
   public void updateCommentsInSequentialUpdates() throws Exception {
     Change c = newChange();
     CommentRange range = new CommentRange(1, 1, 2, 1);
-    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
 
     ChangeUpdate update1 = newUpdate(c, otherUser);
     Comment comment1 =
@@ -2639,9 +2663,9 @@
             new Timestamp(update1.getWhen().getTime()),
             "comment 1",
             (short) 1,
-            rev,
+            commitId,
             false);
-    update1.putComment(Status.PUBLISHED, comment1);
+    update1.putComment(Comment.Status.PUBLISHED, comment1);
 
     ChangeUpdate update2 = newUpdate(c, otherUser);
     Comment comment2 =
@@ -2656,9 +2680,9 @@
             new Timestamp(update2.getWhen().getTime()),
             "comment 2",
             (short) 1,
-            rev,
+            commitId,
             false);
-    update2.putComment(Status.PUBLISHED, comment2);
+    update2.putComment(Comment.Status.PUBLISHED, comment2);
 
     try (NoteDbUpdateManager manager = updateManagerFactory.create(project)) {
       manager.add(update1);
@@ -2667,7 +2691,7 @@
     }
 
     ChangeNotes notes = newNotes(c);
-    List<Comment> comments = notes.getComments().get(new RevId(rev));
+    List<Comment> comments = notes.getComments().get(commitId);
     assertThat(comments).hasSize(2);
     assertThat(comments.get(0).message).isEqualTo("comment 1");
     assertThat(comments.get(1).message).isEqualTo("comment 2");
@@ -2697,7 +2721,7 @@
     int numComments = notes.getComments().size();
 
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setPatchSetId(new PatchSet.Id(c.getId(), c.currentPatchSetId().get() + 1));
+    update.setPatchSetId(PatchSet.id(c.getId(), c.currentPatchSetId().get() + 1));
     update.setChangeMessage("Should be ignored");
     update.putApproval("Code-Review", (short) 2);
     CommentRange range = new CommentRange(1, 1, 2, 1);
@@ -2713,9 +2737,9 @@
             new Timestamp(update.getWhen().getTime()),
             "comment",
             (short) 1,
-            "abcd1234abcd1234abcd1234abcd1234abcd1234",
+            ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"),
             false);
-    update.putComment(Status.PUBLISHED, comment);
+    update.putComment(Comment.Status.PUBLISHED, comment);
     update.commit();
 
     notes = newNotes(c);
@@ -2734,7 +2758,7 @@
     assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(2);
 
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setPatchSetId(new PatchSet.Id(c.getId(), 1));
+    update.setPatchSetId(PatchSet.id(c.getId(), 1));
     update.setCurrentPatchSet();
     update.commit();
     assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(1);
@@ -2751,7 +2775,7 @@
 
     // Delete PS1, PS2 becomes current.
     update = newUpdate(c, changeOwner);
-    update.setPatchSetId(new PatchSet.Id(c.getId(), 1));
+    update.setPatchSetId(PatchSet.id(c.getId(), 1));
     update.setPatchSetState(PatchSetState.DELETED);
     update.commit();
     assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(2);
@@ -2930,8 +2954,8 @@
   public void pendingReviewers() throws Exception {
     Address adr1 = new Address("Foo Bar1", "foo.bar1@gerritcodereview.com");
     Address adr2 = new Address("Foo Bar2", "foo.bar2@gerritcodereview.com");
-    Account.Id ownerId = changeOwner.getAccount().getId();
-    Account.Id otherUserId = otherUser.getAccount().getId();
+    Account.Id ownerId = changeOwner.getAccount().id();
+    Account.Id otherUserId = otherUser.getAccount().id();
 
     ChangeNotes notes = newNotes(newChange());
     assertThat(notes.getPendingReviewers().asTable()).isEmpty();
@@ -3006,9 +3030,9 @@
   public void setRevertOfToCurrentChangeFails() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    exception.expect(IllegalArgumentException.class);
-    exception.expectMessage("A change cannot revert itself");
-    update.setRevertOf(c.getId().get());
+    IllegalArgumentException thrown =
+        assertThrows(IllegalArgumentException.class, () -> update.setRevertOf(c.getId().get()));
+    assertThat(thrown).hasMessageThat().contains("A change cannot revert itself");
   }
 
   @Test
@@ -3016,9 +3040,58 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setRevertOf(newChange().getId().get());
-    exception.expect(StorageException.class);
-    exception.expectMessage("Given ChangeUpdate is only allowed on initial commit");
+    StorageException thrown = assertThrows(StorageException.class, () -> update.commit());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Given ChangeUpdate is only allowed on initial commit");
+  }
+
+  @Test
+  public void updateCount() throws Exception {
+    Change c = newChange();
+    assertThat(newNotes(c).getUpdateCount()).isEqualTo(1);
+
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putApproval("Code-Review", (short) -1);
     update.commit();
+    assertThat(newNotes(c).getUpdateCount()).isEqualTo(2);
+
+    update = newUpdate(c, changeOwner);
+    update.putApproval("Code-Review", (short) 1);
+    update.commit();
+    assertThat(newNotes(c).getUpdateCount()).isEqualTo(3);
+  }
+
+  @Test
+  public void createPatchSetAfterPatchSetDeletion() throws Exception {
+    Change c = newChange();
+    assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(1);
+
+    // Create PS2.
+    incrementCurrentPatchSetFieldOnly(c);
+    RevCommit commit = tr.commit().message("PS" + c.currentPatchSetId().get()).create();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setCommit(rw, commit);
+    update.setGroups(ImmutableList.of(commit.name()));
+    update.commit();
+    assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(2);
+
+    // Delete PS2.
+    update = newUpdate(c, changeOwner);
+    update.setPatchSetState(PatchSetState.DELETED);
+    update.commit();
+    c = newNotes(c).getChange();
+    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
+
+    // Create another PS2
+    incrementCurrentPatchSetFieldOnly(c);
+    commit = tr.commit().message("PS" + c.currentPatchSetId().get()).create();
+    update = newUpdate(c, changeOwner);
+    update.setPatchSetState(PatchSetState.PUBLISHED);
+    update.setCommit(rw, commit);
+    update.setGroups(ImmutableList.of(commit.name()));
+    update.commit();
+    assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(2);
   }
 
   private String readNote(ChangeNotes notes, ObjectId noteId) throws Exception {
@@ -3042,11 +3115,11 @@
         break;
       }
     }
-    assertThat(cause)
-        .named(
+    assertWithMessage(
             expectedClass.getSimpleName()
                 + " in causal chain of:\n"
                 + Throwables.getStackTraceAsString(e))
+        .that(cause)
         .isNotNull();
     assertThat(cause.getMessage()).isEqualTo(expectedMsg);
   }
diff --git a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
index 4dd2005..c2620dc 100644
--- a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
@@ -16,20 +16,20 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Comment;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import java.sql.Timestamp;
 import java.time.ZonedDateTime;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.ObjectId;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-public class CommentTimestampAdapterTest extends GerritBaseTests {
+public class CommentTimestampAdapterTest {
   /** Arbitrary time outside of a DST transition, as an ISO instant. */
   private static final String NON_DST_STR = "2017-02-07T10:20:30.123Z";
 
@@ -153,14 +153,14 @@
     Comment c =
         new Comment(
             new Comment.Key("uuid", "filename", 1),
-            new Account.Id(100),
+            Account.id(100),
             NON_DST_TS,
             (short) 0,
             "message",
             "serverId",
             false);
     c.lineNbr = 1;
-    c.revId = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    c.setCommitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
 
     String json = gson.toJson(c);
     assertThat(json).contains("\"writtenOn\": \"" + NON_DST_STR_TRUNC + "\",");
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 5552572..97781a4 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -19,9 +19,9 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -44,8 +44,8 @@
     ChangeUpdate update = newUpdateForNewChange(c, changeOwner);
     update.putApproval("Verified", (short) 1);
     update.putApproval("Code-Review", (short) -1);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
-    update.putReviewer(otherUser.getAccount().getId(), CC);
+    update.putReviewer(changeOwner.getAccount().id(), REVIEWER);
+    update.putReviewer(otherUser.getAccount().id(), CC);
     update.commit();
     assertThat(update.getRefName()).isEqualTo("refs/changes/01/1/meta");
 
@@ -199,10 +199,13 @@
 
   @Test
   public void anonymousUser() throws Exception {
-    Account anon = new Account(new Account.Id(3), TimeUtil.nowTs());
+    Account anon =
+        Account.builder(Account.id(3), TimeUtil.nowTs())
+            .setMetaId("1234567812345678123456781234567812345678")
+            .build();
     accountCache.put(anon);
     Change c = newChange();
-    ChangeUpdate update = newUpdate(c, userFactory.create(anon.getId()));
+    ChangeUpdate update = newUpdate(c, userFactory.create(anon.id()));
     update.setChangeMessage("Comment on the change.");
     update.commit();
 
@@ -241,7 +244,7 @@
   public void noChangeMessage() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.putReviewer(changeOwner.getAccount().id(), REVIEWER);
     update.commit();
 
     assertBodyEquals(
@@ -311,7 +314,7 @@
     c.setCurrentPatchSet(c.currentPatchSetId(), "  " + c.getSubject(), c.getOriginalSubject());
     ChangeUpdate update = newUpdateForNewChange(c, changeOwner);
     update.setChangeId(c.getKey().get());
-    update.setBranch(c.getDest().get());
+    update.setBranch(c.getDest().branch());
     update.commit();
 
     assertBodyEquals(
@@ -332,7 +335,7 @@
     c.setCurrentPatchSet(c.currentPatchSetId(), "\t\t" + c.getSubject(), c.getOriginalSubject());
     update = newUpdateForNewChange(c, changeOwner);
     update.setChangeId(c.getKey().get());
-    update.setBranch(c.getDest().get());
+    update.setBranch(c.getDest().branch());
     update.commit();
 
     assertBodyEquals(
diff --git a/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java b/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java
new file mode 100644
index 0000000..bf49884
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java
@@ -0,0 +1,98 @@
+// 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.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.server.util.time.TimeUtil;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class DraftCommentNotesTest extends AbstractChangeNotesTest {
+
+  @Test
+  public void createAndPublishCommentInOneAction_runsDraftOperationAsynchronously()
+      throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, otherUser);
+    update.setPatchSetId(c.currentPatchSetId());
+    update.putComment(Comment.Status.PUBLISHED, comment(c.currentPatchSetId()));
+    update.commit();
+
+    assertThat(newNotes(c).getDraftComments(otherUserId)).isEmpty();
+    assertableFanOutExecutor.assertInteractions(1);
+  }
+
+  @Test
+  public void createAndPublishComment_runsPublishDraftOperationAsynchronously() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, otherUser);
+
+    update.setPatchSetId(c.currentPatchSetId());
+    update.putComment(Comment.Status.DRAFT, comment(c.currentPatchSetId()));
+    update.commit();
+    assertThat(newNotes(c).getDraftComments(otherUserId)).hasSize(1);
+    assertableFanOutExecutor.assertInteractions(0);
+
+    update = newUpdate(c, otherUser);
+    update.putComment(Comment.Status.PUBLISHED, comment(c.currentPatchSetId()));
+    update.commit();
+
+    assertThat(newNotes(c).getDraftComments(otherUserId)).isEmpty();
+    assertableFanOutExecutor.assertInteractions(1);
+  }
+
+  @Test
+  public void createAndDeleteDraftComment_runsDraftOperationSynchronously() throws Exception {
+    Change c = newChange();
+
+    ChangeUpdate update = newUpdate(c, otherUser);
+    update.setPatchSetId(c.currentPatchSetId());
+    update.putComment(Comment.Status.DRAFT, comment(c.currentPatchSetId()));
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).hasSize(1);
+    assertableFanOutExecutor.assertInteractions(0);
+
+    update = newUpdate(c, otherUser);
+    update.setPatchSetId(c.currentPatchSetId());
+    update.deleteComment(comment(c.currentPatchSetId()));
+    update.commit();
+
+    notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).isEmpty();
+    assertableFanOutExecutor.assertInteractions(0);
+  }
+
+  private Comment comment(PatchSet.Id psId) {
+    return newComment(
+        psId,
+        "filename",
+        "uuid",
+        null,
+        0,
+        otherUser,
+        null,
+        TimeUtil.nowTs(),
+        "comment",
+        (short) 0,
+        ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"),
+        false);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/IntBlobTest.java b/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
index 1cbe61d..333c229 100644
--- a/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
+++ b/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.git.LockFailureException;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import java.io.IOException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -44,7 +44,7 @@
 
   @Before
   public void setUp() throws Exception {
-    projectName = new Project.NameKey("repo");
+    projectName = Project.nameKey("repo");
     repo = new InMemoryRepository(new DfsRepositoryDescription(projectName.get()));
     tr = new TestRepository<>(repo);
     rw = tr.getRevWalk();
@@ -59,12 +59,7 @@
   public void parseNonBlob() throws Exception {
     String refName = "refs/foo/master";
     tr.branch(refName).commit().create();
-    try {
-      IntBlob.parse(repo, refName);
-      assert_().fail("Expected IncorrectObjectTypeException");
-    } catch (IncorrectObjectTypeException e) {
-      // Expected.
-    }
+    assertThrows(IncorrectObjectTypeException.class, () -> IntBlob.parse(repo, refName));
   }
 
   @Test
@@ -85,12 +80,9 @@
   public void parseInvalid() throws Exception {
     String refName = "refs/foo";
     ObjectId id = tr.update(refName, tr.blob("1 2 3"));
-    try {
-      IntBlob.parse(repo, refName);
-      assert_().fail("Expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e).hasMessageThat().isEqualTo("invalid value in refs/foo blob at " + id.name());
-    }
+    StorageException thrown =
+        assertThrows(StorageException.class, () -> IntBlob.parse(repo, refName));
+    assertThat(thrown).hasMessageThat().isEqualTo("invalid value in refs/foo blob at " + id.name());
   }
 
   @Test
@@ -180,19 +172,19 @@
   @Test
   public void storeWrongOldId() throws Exception {
     String refName = "refs/foo";
-    try {
-      IntBlob.store(
-          repo,
-          rw,
-          projectName,
-          refName,
-          ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
-          123,
-          GitReferenceUpdated.DISABLED);
-      assert_().fail("expected LockFailureException");
-    } catch (LockFailureException e) {
-      assertThat(e.getFailedRefs()).containsExactly("refs/foo");
-    }
+    LockFailureException thrown =
+        assertThrows(
+            LockFailureException.class,
+            () ->
+                IntBlob.store(
+                    repo,
+                    rw,
+                    projectName,
+                    refName,
+                    ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
+                    123,
+                    GitReferenceUpdated.DISABLED));
+    assertThat(thrown.getFailedRefs()).containsExactly("refs/foo");
     assertThat(IntBlob.parse(repo, refName)).isEmpty();
   }
 
diff --git a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
index 74cc507..a768eaf 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -15,22 +15,27 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-import static org.junit.Assert.fail;
 
+import com.github.rholder.retry.BlockStrategy;
 import com.github.rholder.retry.Retryer;
 import com.github.rholder.retry.RetryerBuilder;
 import com.github.rholder.retry.StopStrategies;
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Expect;
 import com.google.common.util.concurrent.Runnables;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import java.io.IOException;
-import java.util.concurrent.ExecutionException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -41,11 +46,14 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
-public class RepoSequenceTest extends GerritBaseTests {
+public class RepoSequenceTest {
+  @Rule public final Expect expect = Expect.create();
+
   // Don't sleep in tests.
-  private static final Retryer<RefUpdate> RETRYER =
+  private static final Retryer<ImmutableList<Integer>> RETRYER =
       RepoSequence.retryerBuilder().withBlockStrategy(t -> {}).build();
 
   private InMemoryRepositoryManager repoManager;
@@ -54,7 +62,7 @@
   @Before
   public void setUp() throws Exception {
     repoManager = new InMemoryRepositoryManager();
-    project = new Project.NameKey("project");
+    project = Project.nameKey("project");
     repoManager.createRepository(project);
   }
 
@@ -66,13 +74,13 @@
       RepoSequence s = newSequence(name, 1, batchSize);
       for (int i = 1; i <= max; i++) {
         try {
-          assertThat(s.next()).named("i=" + i + " for " + name).isEqualTo(i);
+          assertWithMessage("i=" + i + " for " + name).that(s.next()).isEqualTo(i);
         } catch (StorageException e) {
           throw new AssertionError("failed batchSize=" + batchSize + ", i=" + i, e);
         }
       }
-      assertThat(s.acquireCount)
-          .named("acquireCount for " + name)
+      assertWithMessage("acquireCount for " + name)
+          .that(s.acquireCount)
           .isEqualTo(divCeil(max, batchSize));
     }
   }
@@ -160,7 +168,7 @@
     RepoSequence s = newSequence("id", 1, 10, bgUpdate, RETRYER);
     assertThat(doneBgUpdate.get()).isFalse();
     assertThat(s.next()).isEqualTo(1234);
-    // Single acquire call that results in 2 ref reads.
+    // Two acquire calls, but only one successful.
     assertThat(s.acquireCount).isEqualTo(1);
     assertThat(doneBgUpdate.get()).isTrue();
   }
@@ -168,9 +176,11 @@
   @Test
   public void failOnInvalidValue() throws Exception {
     ObjectId id = writeBlob("id", "not a number");
-    exception.expect(StorageException.class);
-    exception.expectMessage("invalid value in refs/sequences/id blob at " + id.name());
-    newSequence("id", 1, 3).next();
+    StorageException thrown =
+        assertThrows(StorageException.class, () -> newSequence("id", 1, 3).next());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("invalid value in refs/sequences/id blob at " + id.name());
   }
 
   @Test
@@ -178,13 +188,9 @@
     try (Repository repo = repoManager.openRepository(project);
         TestRepository<Repository> tr = new TestRepository<>(repo)) {
       tr.branch(RefNames.REFS_SEQUENCES + "id").commit().create();
-      try {
-        newSequence("id", 1, 3).next();
-        fail();
-      } catch (StorageException e) {
-        assertThat(e.getCause()).isInstanceOf(ExecutionException.class);
-        assertThat(e.getCause().getCause()).isInstanceOf(IncorrectObjectTypeException.class);
-      }
+      StorageException e =
+          assertThrows(StorageException.class, () -> newSequence("id", 1, 3).next());
+      assertThat(e.getCause()).isInstanceOf(IncorrectObjectTypeException.class);
     }
   }
 
@@ -197,12 +203,84 @@
             1,
             10,
             () -> writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))),
-            RetryerBuilder.<RefUpdate>newBuilder()
+            RetryerBuilder.<ImmutableList<Integer>>newBuilder()
                 .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                 .build());
-    exception.expect(StorageException.class);
-    exception.expectMessage("Failed to update refs/sequences/id: LOCK_FAILURE");
-    s.next();
+    StorageException thrown = assertThrows(StorageException.class, () -> s.next());
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Failed to update refs/sequences/id: LOCK_FAILURE");
+  }
+
+  @Test
+  public void idCanBeRetrievedFromOtherThreadWhileWaitingToRetry() throws Exception {
+    // Seed existing ref value.
+    writeBlob("id", "1");
+
+    // Let the first update of the sequence fail with LOCK_FAILURE, so that the update is retried.
+    CountDownLatch lockFailure = new CountDownLatch(1);
+    CountDownLatch parallelSuccessfulSequenceGeneration = new CountDownLatch(1);
+    AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
+    Runnable bgUpdate =
+        () -> {
+          if (!doneBgUpdate.getAndSet(true)) {
+            writeBlob("id", "1234");
+          }
+        };
+
+    BlockStrategy blockStrategy =
+        t -> {
+          // Keep blocking until we verified that another thread can retrieve a sequence number
+          // while we are blocking here.
+          lockFailure.countDown();
+          parallelSuccessfulSequenceGeneration.await();
+        };
+
+    // Use batch size = 1 to make each call go to NoteDb.
+    RepoSequence s =
+        newSequence(
+            "id",
+            1,
+            1,
+            bgUpdate,
+            RepoSequence.retryerBuilder().withBlockStrategy(blockStrategy).build());
+
+    assertThat(doneBgUpdate.get()).isFalse();
+
+    // Start a thread to get a sequence number. This thread needs to update the sequence in NoteDb,
+    // but due to the background update (see bgUpdate) the first attempt to update NoteDb fails
+    // with LOCK_FAILURE. RepoSequence uses a retryer to retry the NoteDb update on LOCK_FAILURE,
+    // but our block strategy ensures that this retry only happens after isBlocking was set to
+    // false.
+    Future<?> future =
+        Executors.newFixedThreadPool(1)
+            .submit(
+                () -> {
+                  // The background update sets the next available sequence number to 1234. Then the
+                  // test thread retrieves one sequence number, so that the next available sequence
+                  // number for this thread is 1235.
+                  expect.that(s.next()).isEqualTo(1235);
+                });
+
+    // Wait until the LOCK_FAILURE has happened and the block strategy was entered.
+    lockFailure.await();
+
+    // Verify that the background update was done now.
+    assertThat(doneBgUpdate.get()).isTrue();
+
+    // Verify that we can retrieve a sequence number while the other thread is blocked. If the
+    // s.next() call hangs it means that the RepoSequence.counterLock was not released before the
+    // background thread started to block for retry. In this case the test would time out.
+    assertThat(s.next()).isEqualTo(1234);
+
+    // Stop blocking the retry of the background thread (and verify that it was still blocked).
+    parallelSuccessfulSequenceGeneration.countDown();
+
+    // Wait until the background thread is done.
+    future.get();
+
+    // Two successful acquire calls (because batch size == 1).
+    assertThat(s.acquireCount).isEqualTo(2);
   }
 
   @Test
@@ -260,7 +338,7 @@
       final int start,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate> retryer) {
+      Retryer<ImmutableList<Integer>> retryer) {
     return new RepoSequence(
         repoManager,
         GitReferenceUpdated.DISABLED,
diff --git a/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java b/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
index 6c63c5f..52a81ad 100644
--- a/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
+++ b/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
@@ -15,18 +15,18 @@
 package com.google.gerrit.server.patch;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.jgit.diff.ReplaceEdit;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.List;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.EditList;
 import org.junit.Test;
 
-public class IntraLineLoaderTest extends GerritBaseTests {
+public class IntraLineLoaderTest {
 
   @Test
   public void rewriteAtStartOfLineIsRecognized() throws Exception {
@@ -88,22 +88,30 @@
   // TODO: expected failure
   // the current code does not work on the first line
   // and the insert marker is in the wrong location
-  @Test(expected = AssertionError.class)
+  @Test
   public void preferInsertAtLineBreak2() throws Exception {
-    String a = "  abc\n    def\n";
-    String b = "    abc\n      def\n";
-    assertThat(intraline(a, b))
-        .isEqualTo(ref().insert("  ").common("  abc\n").insert("  ").common("  def\n").edits);
+    assertThrows(
+        AssertionError.class,
+        () -> {
+          String a = "  abc\n    def\n";
+          String b = "    abc\n      def\n";
+          assertThat(intraline(a, b))
+              .isEqualTo(ref().insert("  ").common("  abc\n").insert("  ").common("  def\n").edits);
+        });
   }
 
   // TODO: expected failure
   // the current code does not work on the first line
-  @Test(expected = AssertionError.class)
+  @Test
   public void preferDeleteAtLineBreak() throws Exception {
-    String a = "    abc\n      def\n";
-    String b = "  abc\n    def\n";
-    assertThat(intraline(a, b))
-        .isEqualTo(ref().remove("  ").common("  abc\n").remove("  ").common("  def\n").edits);
+    assertThrows(
+        AssertionError.class,
+        () -> {
+          String a = "    abc\n      def\n";
+          String b = "  abc\n    def\n";
+          assertThat(intraline(a, b))
+              .isEqualTo(ref().remove("  ").common("  abc\n").remove("  ").common("  def\n").edits);
+        });
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/patch/PatchListEntryTest.java b/javatests/com/google/gerrit/server/patch/PatchListEntryTest.java
index 4ed5f4b..7ac1c31 100644
--- a/javatests/com/google/gerrit/server/patch/PatchListEntryTest.java
+++ b/javatests/com/google/gerrit/server/patch/PatchListEntryTest.java
@@ -19,11 +19,10 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Patch;
 import org.junit.Test;
 
-public class PatchListEntryTest extends GerritBaseTests {
+public class PatchListEntryTest {
   @Test
   public void empty1() {
     final String name = "empty-file";
diff --git a/javatests/com/google/gerrit/server/patch/PatchListTest.java b/javatests/com/google/gerrit/server/patch/PatchListTest.java
index ccdd040..e224191a 100644
--- a/javatests/com/google/gerrit/server/patch/PatchListTest.java
+++ b/javatests/com/google/gerrit/server/patch/PatchListTest.java
@@ -16,8 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Patch;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
@@ -26,7 +25,7 @@
 import java.util.Arrays;
 import org.junit.Test;
 
-public class PatchListTest extends GerritBaseTests {
+public class PatchListTest {
   @Test
   public void fileOrder() {
     String[] names = {
diff --git a/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java b/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
index ff9ac41..305e81b 100644
--- a/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
+++ b/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
@@ -18,10 +18,9 @@
 import static com.google.gerrit.server.permissions.DefaultPermissionMappings.refPermission;
 
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class DefaultPermissionsMappingTest extends GerritBaseTests {
+public class DefaultPermissionsMappingTest {
   @Test
   public void stringToRefPermission() {
     assertThat(refPermission("doesnotexist")).isEmpty();
diff --git a/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.java b/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.java
index f40c3bc..7aa73a7 100644
--- a/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.java
+++ b/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.permissions;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isValidPluginPermission;
 
 import com.google.common.collect.ImmutableList;
@@ -30,8 +30,8 @@
         ImmutableList.of("plugin-foo-a", "plugin-foo-a-b");
 
     for (String permission : validPluginPermissions) {
-      assertThat(isValidPluginPermission(permission))
-          .named("valid plugin permission: %s", permission)
+      assertWithMessage("valid plugin permission: %s", permission)
+          .that(isValidPluginPermission(permission))
           .isTrue();
     }
   }
@@ -48,8 +48,8 @@
             "plugin-foo-a1");
 
     for (String permission : invalidPluginPermissions) {
-      assertThat(isValidPluginPermission(permission))
-          .named("invalid plugin permission: %s", permission)
+      assertWithMessage("invalid plugin permission: %s", permission)
+          .that(isValidPluginPermission(permission))
           .isFalse();
     }
   }
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 14fa01c..7f2f5a4 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -15,284 +15,205 @@
 package com.google.gerrit.server.permissions;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.common.data.Permission.EDIT_TOPIC_NAME;
 import static com.google.gerrit.common.data.Permission.LABEL;
 import static com.google.gerrit.common.data.Permission.OWNER;
 import static com.google.gerrit.common.data.Permission.PUSH;
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.common.data.Permission.SUBMIT;
+import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.ADMIN;
-import static com.google.gerrit.server.project.testing.Util.DEVS;
-import static com.google.gerrit.server.project.testing.Util.allow;
-import static com.google.gerrit.server.project.testing.Util.allowExclusive;
-import static com.google.gerrit.server.project.testing.Util.block;
-import static com.google.gerrit.server.project.testing.Util.deny;
-import static com.google.gerrit.server.project.testing.Util.doNotInherit;
-import static com.google.gerrit.testing.InMemoryRepositoryManager.newRepository;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.PermissionRange;
-import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.InvalidNameException;
-import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
-import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.InternalUser;
-import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.AllUsersNameProvider;
-import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefPattern;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-public class RefControlTest extends GerritBaseTests {
-  private void assertAdminsAreOwnersAndDevsAreNot() {
-    ProjectControl uBlah = user(local, DEVS);
-    ProjectControl uAdmin = user(local, DEVS, ADMIN);
+public class RefControlTest {
+  private static final AccountGroup.UUID ADMIN = AccountGroup.uuid("test.admin");
+  private static final AccountGroup.UUID DEVS = AccountGroup.uuid("test.devs");
 
-    assertThat(uBlah.isOwner()).named("not owner").isFalse();
-    assertThat(uAdmin.isOwner()).named("is owner").isTrue();
+  private void assertAdminsAreOwnersAndDevsAreNot() throws Exception {
+    ProjectControl uBlah = user(localKey, DEVS);
+    ProjectControl uAdmin = user(localKey, DEVS, ADMIN);
+
+    assertWithMessage("not owner").that(uBlah.isOwner()).isFalse();
+    assertWithMessage("is owner").that(uAdmin.isOwner()).isTrue();
   }
 
   private void assertOwner(String ref, ProjectControl u) {
-    assertThat(u.controlForRef(ref).isOwner()).named("OWN " + ref).isTrue();
+    assertWithMessage("OWN " + ref).that(u.controlForRef(ref).isOwner()).isTrue();
   }
 
   private void assertNotOwner(ProjectControl u) {
-    assertThat(u.isOwner()).named("not owner").isFalse();
+    assertWithMessage("not owner").that(u.isOwner()).isFalse();
   }
 
   private void assertAllRefsAreVisible(ProjectControl u) {
-    assertThat(u.allRefsAreVisible(Collections.emptySet())).named("all refs visible").isTrue();
+    assertWithMessage("all refs visible")
+        .that(u.allRefsAreVisible(Collections.emptySet()))
+        .isTrue();
   }
 
   private void assertAllRefsAreNotVisible(ProjectControl u) {
-    assertThat(u.allRefsAreVisible(Collections.emptySet())).named("all refs NOT visible").isFalse();
+    assertWithMessage("all refs NOT visible")
+        .that(u.allRefsAreVisible(Collections.emptySet()))
+        .isFalse();
   }
 
   private void assertNotOwner(String ref, ProjectControl u) {
-    assertThat(u.controlForRef(ref).isOwner()).named("NOT OWN " + ref).isFalse();
+    assertWithMessage("NOT OWN " + ref).that(u.controlForRef(ref).isOwner()).isFalse();
   }
 
   private void assertCanAccess(ProjectControl u) {
     boolean access = u.asForProject().testOrFalse(ProjectPermission.ACCESS);
-    assertThat(access).named("can access").isTrue();
+    assertWithMessage("can access").that(access).isTrue();
   }
 
   private void assertAccessDenied(ProjectControl u) {
     boolean access = u.asForProject().testOrFalse(ProjectPermission.ACCESS);
-    assertThat(access).named("cannot access").isFalse();
+    assertWithMessage("cannot access").that(access).isFalse();
   }
 
   private void assertCanRead(String ref, ProjectControl u) {
-    assertThat(u.controlForRef(ref).hasReadPermissionOnRef(true))
-        // This should be false but the test relies on inheritance into refs/tags
-        .named("can read " + ref)
+    assertWithMessage("can read " + ref)
+        .that(
+            u.controlForRef(ref)
+                .hasReadPermissionOnRef(
+                    true)) // This should be false but the test relies on inheritance into refs/tags
         .isTrue();
   }
 
   private void assertCannotRead(String ref, ProjectControl u) {
-    assertThat(u.controlForRef(ref).hasReadPermissionOnRef(true))
-        // This should be false but the test relies on inheritance into refs/tags
-        .named("cannot read " + ref)
+    assertWithMessage("cannot read " + ref)
+        .that(
+            u.controlForRef(ref)
+                .hasReadPermissionOnRef(
+                    true)) // This should be false but the test relies on inheritance into refs/tags
         .isFalse();
   }
 
   private void assertCanSubmit(String ref, ProjectControl u) {
-    assertThat(u.controlForRef(ref).canSubmit(false)).named("can submit " + ref).isTrue();
+    assertWithMessage("can submit " + ref).that(u.controlForRef(ref).canSubmit(false)).isTrue();
   }
 
   private void assertCannotSubmit(String ref, ProjectControl u) {
-    assertThat(u.controlForRef(ref).canSubmit(false)).named("can submit " + ref).isFalse();
+    assertWithMessage("can submit " + ref).that(u.controlForRef(ref).canSubmit(false)).isFalse();
   }
 
   private void assertCanUpload(ProjectControl u) {
-    assertThat(u.canPushToAtLeastOneRef()).named("can upload").isTrue();
+    assertWithMessage("can upload").that(u.canPushToAtLeastOneRef()).isTrue();
   }
 
   private void assertCreateChange(String ref, ProjectControl u) {
     boolean create = u.asForProject().ref(ref).testOrFalse(RefPermission.CREATE_CHANGE);
-    assertThat(create).named("can create change " + ref).isTrue();
+    assertWithMessage("can create change " + ref).that(create).isTrue();
   }
 
   private void assertCannotUpload(ProjectControl u) {
-    assertThat(u.canPushToAtLeastOneRef()).named("cannot upload").isFalse();
+    assertWithMessage("cannot upload").that(u.canPushToAtLeastOneRef()).isFalse();
   }
 
   private void assertCannotCreateChange(String ref, ProjectControl u) {
     boolean create = u.asForProject().ref(ref).testOrFalse(RefPermission.CREATE_CHANGE);
-    assertThat(create).named("cannot create change " + ref).isFalse();
+    assertWithMessage("cannot create change " + ref).that(create).isFalse();
   }
 
   private void assertCanUpdate(String ref, ProjectControl u) {
     boolean update = u.asForProject().ref(ref).testOrFalse(RefPermission.UPDATE);
-    assertThat(update).named("can update " + ref).isTrue();
+    assertWithMessage("can update " + ref).that(update).isTrue();
   }
 
   private void assertCannotUpdate(String ref, ProjectControl u) {
     boolean update = u.asForProject().ref(ref).testOrFalse(RefPermission.UPDATE);
-    assertThat(update).named("cannot update " + ref).isFalse();
+    assertWithMessage("cannot update " + ref).that(update).isFalse();
   }
 
   private void assertCanForceUpdate(String ref, ProjectControl u) {
     boolean update = u.asForProject().ref(ref).testOrFalse(RefPermission.FORCE_UPDATE);
-    assertThat(update).named("can force push " + ref).isTrue();
+    assertWithMessage("can force push " + ref).that(update).isTrue();
   }
 
   private void assertCannotForceUpdate(String ref, ProjectControl u) {
     boolean update = u.asForProject().ref(ref).testOrFalse(RefPermission.FORCE_UPDATE);
-    assertThat(update).named("cannot force push " + ref).isFalse();
+    assertWithMessage("cannot force push " + ref).that(update).isFalse();
   }
 
   private void assertCanVote(int score, PermissionRange range) {
-    assertThat(range.contains(score)).named("can vote " + score).isTrue();
+    assertWithMessage("can vote " + score).that(range.contains(score)).isTrue();
   }
 
   private void assertCannotVote(int score, PermissionRange range) {
-    assertThat(range.contains(score)).named("cannot vote " + score).isFalse();
+    assertWithMessage("cannot vote " + score).that(range.contains(score)).isFalse();
   }
 
-  private final AllProjectsName allProjectsName =
-      new AllProjectsName(AllProjectsNameProvider.DEFAULT);
-  private final AllUsersName allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
-  private final AccountGroup.UUID fixers = new AccountGroup.UUID("test.fixers");
-  private final Map<Project.NameKey, ProjectState> all = new HashMap<>();
-  private Project.NameKey localKey = new Project.NameKey("local");
-  private ProjectConfig local;
-  private Project.NameKey parentKey = new Project.NameKey("parent");
-  private ProjectConfig parent;
-  private InMemoryRepositoryManager repoManager;
-  private ProjectCache projectCache;
-  private PermissionCollection.Factory sectionSorter;
-  private ChangeControl.Factory changeControlFactory;
+  private final AccountGroup.UUID fixers = AccountGroup.uuid("test.fixers");
+  private final Project.NameKey localKey = Project.nameKey("local");
+  private final Project.NameKey parentKey = Project.nameKey("parent");
 
-  @Inject private PermissionBackend permissionBackend;
-  @Inject private CapabilityCollection.Factory capabilityCollectionFactory;
+  @Inject private AllProjectsName allProjectsName;
+  @Inject private AllUsersName allUsersName;
+  @Inject private InMemoryRepositoryManager repoManager;
+  @Inject private MetaDataUpdate.Server metaDataUpdateFactory;
+  @Inject private ProjectCache projectCache;
+  @Inject private ProjectControl.Factory projectControlFactory;
+  @Inject private ProjectOperations projectOperations;
   @Inject private SchemaCreator schemaCreator;
   @Inject private SingleVersionListener singleVersionListener;
   @Inject private ThreadLocalRequestContext requestContext;
-  @Inject private DefaultRefFilter.Factory refFilterFactory;
-  @Inject private TransferConfig transferConfig;
-  @Inject private MetricMaker metricMaker;
-  @Inject private ProjectConfig.Factory projectConfigFactory;
-  @Inject private RefVisibilityControl refVisibilityControl;
 
   @Before
   public void setUp() throws Exception {
-    repoManager = new InMemoryRepositoryManager();
-    projectCache =
-        new ProjectCache() {
-          @Override
-          public ProjectState getAllProjects() {
-            return get(allProjectsName);
-          }
-
-          @Override
-          public ProjectState getAllUsers() {
-            return null;
-          }
-
-          @Override
-          public ProjectState get(Project.NameKey projectName) {
-            return all.get(projectName);
-          }
-
-          @Override
-          public void evict(Project p) {}
-
-          @Override
-          public void remove(Project p) {}
-
-          @Override
-          public void remove(Project.NameKey name) {}
-
-          @Override
-          public ImmutableSortedSet<Project.NameKey> all() {
-            return ImmutableSortedSet.of();
-          }
-
-          @Override
-          public ImmutableSortedSet<Project.NameKey> byName(String prefix) {
-            return ImmutableSortedSet.of();
-          }
-
-          @Override
-          public void onCreateProject(Project.NameKey newProjectName) {}
-
-          @Override
-          public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
-            return Collections.emptySet();
-          }
-
-          @Override
-          public ProjectState checkedGet(Project.NameKey projectName) throws IOException {
-            return all.get(projectName);
-          }
-
-          @Override
-          public void evict(Project.NameKey p) {}
-
-          @Override
-          public ProjectState checkedGet(Project.NameKey projectName, boolean strict)
-              throws Exception {
-            return all.get(projectName);
-          }
-        };
-
     Injector injector = Guice.createInjector(new InMemoryModule());
     injector.injectMembers(this);
 
-    try {
-      Repository repo = repoManager.createRepository(allProjectsName);
-      ProjectConfig allProjects =
-          projectConfigFactory.create(new Project.NameKey(allProjectsName.get()));
-      allProjects.load(repo);
-      LabelType cr = Util.codeReview();
-      allProjects.getLabelSections().put(cr.getName(), cr);
-      add(allProjects);
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RuntimeException(e);
-    }
+    // Tests previously used ProjectConfig.Factory to create ProjectConfigs without going through
+    // the ProjectCache, which was wrong. Manually call getInstance so we don't store it in a
+    // field that is accessible to test methods.
+    ProjectConfig.Factory projectConfigFactory = injector.getInstance(ProjectConfig.Factory.class);
 
     singleVersionListener.start();
     try {
@@ -301,84 +222,111 @@
       singleVersionListener.stop();
     }
 
-    Cache<SectionSortCache.EntryKey, SectionSortCache.EntryVal> c =
-        CacheBuilder.newBuilder().build();
-    sectionSorter = new PermissionCollection.Factory(new SectionSortCache(c), metricMaker);
+    // Clear out All-Projects and use the lowest-level API possible for project creation, so the
+    // only ACL entries are exactly what is initialized by this test, and we aren't subject to
+    // changing defaults in SchemaCreator or ProjectCreator.
+    try (Repository allProjectsRepo = repoManager.createRepository(allProjectsName);
+        TestRepository<Repository> tr = new TestRepository<>(allProjectsRepo)) {
+      tr.delete(REFS_CONFIG);
+      try (MetaDataUpdate md = metaDataUpdateFactory.create(allProjectsName)) {
+        ProjectConfig allProjectsConfig = projectConfigFactory.create(allProjectsName);
+        allProjectsConfig.load(md);
+        LabelType cr = TestLabels.codeReview();
+        allProjectsConfig.getLabelSections().put(cr.getName(), cr);
+        allProjectsConfig.commit(md);
+      }
+    }
 
-    parent = projectConfigFactory.create(parentKey);
-    parent.load(newRepository(parentKey));
-    add(parent);
-
-    local = projectConfigFactory.create(localKey);
-    local.load(newRepository(localKey));
-    add(local);
-    local.getProject().setParentName(parentKey);
+    repoManager.createRepository(parentKey).close();
+    repoManager.createRepository(localKey).close();
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(localKey)) {
+      ProjectConfig newLocal = projectConfigFactory.create(localKey);
+      newLocal.load(md);
+      newLocal.getProject().setParentName(parentKey);
+      newLocal.commit(md);
+    }
 
     requestContext.setContext(() -> null);
-
-    changeControlFactory = injector.getInstance(ChangeControl.Factory.class);
   }
 
   @After
-  public void tearDown() {
+  public void tearDown() throws Exception {
     requestContext.setContext(null);
   }
 
   @Test
   public void ownerProject() throws Exception {
-    allow(local, OWNER, ADMIN, "refs/*");
-
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(ADMIN))
+        .update();
     assertAdminsAreOwnersAndDevsAreNot();
   }
 
   @Test
   public void denyOwnerProject() throws Exception {
-    allow(local, OWNER, ADMIN, "refs/*");
-    deny(local, OWNER, DEVS, "refs/*");
-
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(ADMIN))
+        .add(deny(OWNER).ref("refs/*").group(DEVS))
+        .update();
     assertAdminsAreOwnersAndDevsAreNot();
   }
 
   @Test
   public void blockOwnerProject() throws Exception {
-    allow(local, OWNER, ADMIN, "refs/*");
-    block(local, OWNER, DEVS, "refs/*");
-
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(ADMIN))
+        .add(block(OWNER).ref("refs/*").group(DEVS))
+        .update();
     assertAdminsAreOwnersAndDevsAreNot();
   }
 
   @Test
   public void allRefsAreVisibleForRegularProject() throws Exception {
-    allow(local, READ, DEVS, "refs/*");
-    allow(local, READ, DEVS, "refs/groups/*");
-    allow(local, READ, DEVS, "refs/users/default");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(DEVS))
+        .add(allow(READ).ref("refs/groups/*").group(DEVS))
+        .add(allow(READ).ref("refs/users/default").group(DEVS))
+        .update();
 
-    assertAllRefsAreVisible(user(local, DEVS));
+    assertAllRefsAreVisible(user(localKey, DEVS));
   }
 
   @Test
   public void allRefsAreNotVisibleForAllUsers() throws Exception {
-    ProjectConfig allUsers = projectConfigFactory.create(allUsersName);
-    allUsers.load(newRepository(allUsersName));
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(DEVS))
+        .add(allow(READ).ref("refs/groups/*").group(DEVS))
+        .add(allow(READ).ref("refs/users/default").group(DEVS))
+        .update();
 
-    allow(allUsers, READ, DEVS, "refs/*");
-    allow(allUsers, READ, DEVS, "refs/groups/*");
-    allow(allUsers, READ, DEVS, "refs/users/default");
-
-    assertAllRefsAreNotVisible(user(allUsers, DEVS));
+    assertAllRefsAreNotVisible(user(allUsersName, DEVS));
   }
 
   @Test
   public void userRefIsVisibleForInternalUser() throws Exception {
-    internalUser(local).controlForRef("refs/users/default").asForRef().check(RefPermission.READ);
+    internalUser(localKey).controlForRef("refs/users/default").asForRef().check(RefPermission.READ);
   }
 
   @Test
   public void branchDelegation1() throws Exception {
-    allow(local, OWNER, ADMIN, "refs/*");
-    allow(local, OWNER, DEVS, "refs/heads/x/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(ADMIN))
+        .add(allow(OWNER).ref("refs/heads/x/*").group(DEVS))
+        .update();
 
-    ProjectControl uDev = user(local, DEVS);
+    ProjectControl uDev = user(localKey, DEVS);
     assertNotOwner(uDev);
 
     assertOwner("refs/heads/x/*", uDev);
@@ -391,12 +339,16 @@
 
   @Test
   public void branchDelegation2() throws Exception {
-    allow(local, OWNER, ADMIN, "refs/*");
-    allow(local, OWNER, DEVS, "refs/heads/x/*");
-    allow(local, OWNER, fixers, "refs/heads/x/y/*");
-    doNotInherit(local, OWNER, "refs/heads/x/y/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(ADMIN))
+        .add(allow(OWNER).ref("refs/heads/x/*").group(DEVS))
+        .add(allow(OWNER).ref("refs/heads/x/y/*").group(fixers))
+        .setExclusiveGroup(permissionKey(OWNER).ref("refs/heads/x/y/*"), true)
+        .update();
 
-    ProjectControl uDev = user(local, DEVS);
+    ProjectControl uDev = user(localKey, DEVS);
     assertNotOwner(uDev);
 
     assertOwner("refs/heads/x/*", uDev);
@@ -405,7 +357,7 @@
     assertNotOwner("refs/*", uDev);
     assertNotOwner("refs/heads/master", uDev);
 
-    ProjectControl uFix = user(local, fixers);
+    ProjectControl uFix = user(localKey, fixers);
     assertNotOwner(uFix);
 
     assertOwner("refs/heads/x/y/*", uFix);
@@ -418,53 +370,41 @@
 
   @Test
   public void inheritRead_SingleBranchDeniesUpload() throws Exception {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
-    allow(local, READ, REGISTERED_USERS, "refs/heads/foobar");
-    doNotInherit(local, READ, "refs/heads/foobar");
-    doNotInherit(local, PUSH, "refs/for/refs/heads/foobar");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(PUSH).ref("refs/for/refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/foobar").group(REGISTERED_USERS))
+        .setExclusiveGroup(permissionKey(READ).ref("refs/heads/foobar"), true)
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/for/refs/heads/foobar"), true)
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCanUpload(u);
     assertCreateChange("refs/heads/master", u);
     assertCannotCreateChange("refs/heads/foobar", u);
   }
 
   @Test
-  public void blockPushDrafts() throws Exception {
-    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*");
-    allow(local, PUSH, REGISTERED_USERS, "refs/drafts/*");
-
-    ProjectControl u = user(local);
-    assertCreateChange("refs/heads/master", u);
-    assertThat(u.controlForRef("refs/drafts/master").canPerform(PUSH)).isFalse();
-  }
-
-  @Test
-  public void blockPushDraftsUnblockAdmin() throws Exception {
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*");
-    allow(parent, PUSH, ADMIN, "refs/drafts/*");
-    allow(local, PUSH, REGISTERED_USERS, "refs/drafts/*");
-
-    ProjectControl u = user(local);
-    ProjectControl a = user(local, "a", ADMIN);
-
-    assertThat(a.controlForRef("refs/drafts/master").canPerform(PUSH))
-        .named("push is allowed")
-        .isTrue();
-    assertThat(u.controlForRef("refs/drafts/master").canPerform(PUSH))
-        .named("push is not allowed")
-        .isFalse();
-  }
-
-  @Test
   public void inheritRead_SingleBranchDoesNotOverrideInherited() throws Exception {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
-    allow(local, READ, REGISTERED_USERS, "refs/heads/foobar");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(PUSH).ref("refs/for/refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/foobar").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCanUpload(u);
     assertCreateChange("refs/heads/master", u);
     assertCreateChange("refs/heads/foobar", u);
@@ -472,31 +412,50 @@
 
   @Test
   public void inheritDuplicateSections() throws Exception {
-    allow(parent, READ, ADMIN, "refs/*");
-    allow(local, READ, DEVS, "refs/heads/*");
-    assertCanAccess(user(local, "a", ADMIN));
-
-    local = projectConfigFactory.create(localKey);
-    local.load(newRepository(localKey));
-    local.getProject().setParentName(parentKey);
-    allow(local, READ, DEVS, "refs/*");
-    assertCanAccess(user(local, "d", DEVS));
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(ADMIN))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(DEVS))
+        .update();
+    assertCanAccess(user(localKey, "a", ADMIN));
+    assertCanAccess(user(localKey, "d", DEVS));
   }
 
   @Test
   public void inheritRead_OverrideWithDeny() throws Exception {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    deny(local, READ, REGISTERED_USERS, "refs/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(deny(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
-    assertAccessDenied(user(local));
+    assertAccessDenied(user(localKey));
   }
 
   @Test
   public void inheritRead_AppendWithDenyOfRef() throws Exception {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    deny(local, READ, REGISTERED_USERS, "refs/heads/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(deny(READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCanAccess(u);
     assertCanRead("refs/master", u);
     assertCanRead("refs/tags/foobar", u);
@@ -505,11 +464,19 @@
 
   @Test
   public void inheritRead_OverridesAndDeniesOfRef() throws Exception {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    deny(local, READ, REGISTERED_USERS, "refs/*");
-    allow(local, READ, REGISTERED_USERS, "refs/heads/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(deny(READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCanAccess(u);
     assertCannotRead("refs/foobar", u);
     assertCannotRead("refs/tags/foobar", u);
@@ -518,11 +485,19 @@
 
   @Test
   public void inheritSubmit_OverridesAndDeniesOfRef() throws Exception {
-    allow(parent, SUBMIT, REGISTERED_USERS, "refs/*");
-    deny(local, SUBMIT, REGISTERED_USERS, "refs/*");
-    allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(deny(SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCannotSubmit("refs/foobar", u);
     assertCannotSubmit("refs/tags/foobar", u);
     assertCanSubmit("refs/heads/foobar", u);
@@ -530,46 +505,73 @@
 
   @Test
   public void cannotUploadToAnyRef() throws Exception {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    allow(local, READ, DEVS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/for/refs/heads/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/*").group(DEVS))
+        .add(allow(PUSH).ref("refs/for/refs/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCannotUpload(u);
     assertCannotCreateChange("refs/heads/master", u);
   }
 
   @Test
   public void usernamePatternCanUploadToAnyRef() throws Exception {
-    allow(local, PUSH, REGISTERED_USERS, "refs/heads/users/${username}/*");
-    ProjectControl u = user(local, "a-registered-user");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/users/${username}/*").group(REGISTERED_USERS))
+        .update();
+    ProjectControl u = user(localKey, "a-registered-user");
     assertCanUpload(u);
   }
 
   @Test
   public void usernamePatternRegExpCanUploadToAnyRef() throws Exception {
-    allow(local, PUSH, REGISTERED_USERS, "^refs/heads/users/${username}/(public|private)/.+");
-    ProjectControl u = user(local, "a-registered-user");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(
+            allow(PUSH)
+                .ref("^refs/heads/users/${username}/(public|private)/.+")
+                .group(REGISTERED_USERS))
+        .update();
+    ProjectControl u = user(localKey, "a-registered-user");
     assertCanUpload(u);
     assertCanUpdate("refs/heads/users/a-registered-user/private/a", u);
   }
 
   @Test
   public void usernamePatternNonRegex() throws Exception {
-    allow(local, READ, DEVS, "refs/sb/${username}/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/sb/${username}/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, "u", DEVS);
-    ProjectControl d = user(local, "d", DEVS);
+    ProjectControl u = user(localKey, "u", DEVS);
+    ProjectControl d = user(localKey, "d", DEVS);
     assertCannotRead("refs/sb/d/heads/foobar", u);
     assertCanRead("refs/sb/d/heads/foobar", d);
   }
 
   @Test
   public void usernamePatternWithRegex() throws Exception {
-    allow(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("^refs/sb/${username}/heads/.*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, "d.v", DEVS);
-    ProjectControl d = user(local, "dev", DEVS);
+    ProjectControl u = user(localKey, "d.v", DEVS);
+    ProjectControl d = user(localKey, "dev", DEVS);
     assertCanAccess(u);
     assertCanAccess(d);
     assertCannotRead("refs/sb/dev/heads/foobar", u);
@@ -578,48 +580,80 @@
 
   @Test
   public void usernameEmailPatternWithRegex() throws Exception {
-    allow(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("^refs/sb/${username}/heads/.*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, "d.v@ger-rit.org", DEVS);
-    ProjectControl d = user(local, "dev@ger-rit.org", DEVS);
+    ProjectControl u = user(localKey, "d.v@ger-rit.org", DEVS);
+    ProjectControl d = user(localKey, "dev@ger-rit.org", DEVS);
     assertCannotRead("refs/sb/dev@ger-rit.org/heads/foobar", u);
     assertCanRead("refs/sb/dev@ger-rit.org/heads/foobar", d);
   }
 
   @Test
   public void sortWithRegex() throws Exception {
-    allow(local, READ, DEVS, "^refs/heads/.*");
-    allow(parent, READ, ANONYMOUS_USERS, "^refs/heads/.*-QA-.*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("^refs/heads/.*").group(DEVS))
+        .update();
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("^refs/heads/.*-QA-.*").group(ANONYMOUS_USERS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
-    ProjectControl d = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
+    ProjectControl d = user(localKey, DEVS);
     assertCanRead("refs/heads/foo-QA-bar", u);
     assertCanRead("refs/heads/foo-QA-bar", d);
   }
 
   @Test
   public void blockRule_ParentBlocksChild() throws Exception {
-    allow(local, PUSH, DEVS, "refs/tags/*");
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*");
-    ProjectControl u = user(local, DEVS);
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/tags/*").group(DEVS))
+        .update();
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/tags/*").group(ANONYMOUS_USERS))
+        .update();
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/tags/V10", u);
   }
 
   @Test
   public void blockRule_ParentBlocksChildEvenIfAlreadyBlockedInChild() throws Exception {
-    allow(local, PUSH, DEVS, "refs/tags/*");
-    block(local, PUSH, ANONYMOUS_USERS, "refs/tags/*");
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/tags/*").group(DEVS))
+        .add(block(PUSH).ref("refs/tags/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/tags/*").group(ANONYMOUS_USERS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/tags/V10", u);
   }
 
   @Test
   public void blockPartialRangeLocally() throws Exception {
-    block(local, LABEL + "Code-Review", +1, +2, DEVS, "refs/heads/master");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/master").group(DEVS).range(+1, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
 
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(2, range);
@@ -627,10 +661,18 @@
 
   @Test
   public void blockLabelRange_ParentBlocksChild() throws Exception {
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
-    block(parent, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
 
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-1, range);
@@ -641,11 +683,19 @@
 
   @Test
   public void blockLabelRange_ParentBlocksChildEvenIfAlreadyBlockedInChild() throws Exception {
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
-    block(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
-    block(parent, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
 
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-1, range);
@@ -656,197 +706,317 @@
 
   @Test
   public void inheritSubmit_AllowInChildDoesntAffectUnblockInParent() throws Exception {
-    block(parent, SUBMIT, ANONYMOUS_USERS, "refs/heads/*");
-    allow(parent, SUBMIT, REGISTERED_USERS, "refs/heads/*");
-    allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(SUBMIT).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
-    assertThat(u.controlForRef("refs/heads/master").canPerform(SUBMIT))
-        .named("submit is allowed")
+    ProjectControl u = user(localKey);
+    assertWithMessage("submit is allowed")
+        .that(u.controlForRef("refs/heads/master").canPerform(SUBMIT))
         .isTrue();
   }
 
   @Test
   public void unblockNoForce() throws Exception {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCanUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockForce() throws Exception {
-    PermissionRule r = block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    r.setForce(true);
-    allow(local, PUSH, DEVS, "refs/heads/*").setForce(true);
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS).force(true))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS).force(true))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCanForceUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockRead_NotPossible() throws Exception {
-    block(parent, READ, ANONYMOUS_USERS, "refs/*");
-    allow(parent, READ, ADMIN, "refs/*");
-    allow(local, READ, ANONYMOUS_USERS, "refs/*");
-    allow(local, READ, ADMIN, "refs/*");
-    ProjectControl u = user(local);
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(READ).ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow(READ).ref("refs/*").group(ADMIN))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow(READ).ref("refs/*").group(ADMIN))
+        .update();
+
+    ProjectControl u = user(localKey);
     assertCannotRead("refs/heads/master", u);
   }
 
   @Test
   public void unblockForceWithAllowNoForce_NotPossible() throws Exception {
-    PermissionRule r = block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    r.setForce(true);
-    allow(local, PUSH, DEVS, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS).force(true))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotForceUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockMoreSpecificRef_Fails() throws Exception {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/master");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockMoreSpecificRefInLocal_Fails() throws Exception {
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/master");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockMoreSpecificRefWithExclusiveFlag() throws Exception {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/master", true);
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/heads/master"), true)
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCanUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockVoteMoreSpecificRefWithExclusiveFlag() throws Exception {
-    String perm = LABEL + "Code-Review";
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .add(allowLabel("Code-Review").ref("refs/heads/master").group(DEVS).range(-2, 2))
+        .setExclusiveGroup(labelPermissionKey("Code-Review").ref("refs/heads/master"), true)
+        .update();
 
-    block(local, perm, -1, 1, ANONYMOUS_USERS, "refs/heads/*");
-    allowExclusive(local, perm, -2, 2, DEVS, "refs/heads/master");
-
-    ProjectControl u = user(local, DEVS);
-    PermissionRange range = u.controlForRef("refs/heads/master").getRange(perm);
+    ProjectControl u = user(localKey, DEVS);
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-2, range);
   }
 
   @Test
   public void unblockFromParentDoesNotAffectChild() throws Exception {
-    allow(parent, PUSH, DEVS, "refs/heads/master", true);
-    block(local, PUSH, DEVS, "refs/heads/master");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/heads/master"), true)
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/master").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockFromParentDoesNotAffectChildDifferentGroups() throws Exception {
-    allow(parent, PUSH, DEVS, "refs/heads/master", true);
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/master");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/heads/master"), true)
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/master").group(ANONYMOUS_USERS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockMoreSpecificRefInLocalWithExclusiveFlag_Fails() throws Exception {
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/master", true);
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/heads/master"), true)
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
   public void blockMoreSpecificRefWithinProject() throws Exception {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/secret");
-    allow(local, PUSH, DEVS, "refs/heads/*", true);
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/secret").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS))
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/heads/*"), true)
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/secret", u);
     assertCanUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockOtherPermissionWithMoreSpecificRefAndExclusiveFlag_Fails() throws Exception {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/master");
-    allow(local, SUBMIT, DEVS, "refs/heads/master", true);
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .add(allow(SUBMIT).ref("refs/heads/master").group(DEVS))
+        .setExclusiveGroup(permissionKey(SUBMIT).ref("refs/heads/master"), true)
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockLargerScope_Fails() throws Exception {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/master");
-    allow(local, PUSH, DEVS, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/master").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
   public void unblockInLocal_Fails() throws Exception {
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, fixers, "refs/heads/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/*").group(fixers))
+        .update();
 
-    ProjectControl f = user(local, fixers);
+    ProjectControl f = user(localKey, fixers);
     assertCannotUpdate("refs/heads/master", f);
   }
 
   @Test
   public void unblockInParentBlockInLocal() throws Exception {
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(parent, PUSH, DEVS, "refs/heads/*");
-    block(local, PUSH, DEVS, "refs/heads/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl d = user(local, DEVS);
+    ProjectControl d = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", d);
   }
 
   @Test
   public void unblockForceEditTopicName() throws Exception {
-    block(local, EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(EDIT_TOPIC_NAME).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(EDIT_TOPIC_NAME).ref("refs/heads/*").group(DEVS).force(true))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
-    assertThat(u.controlForRef("refs/heads/master").canForceEditTopicName())
-        .named("u can edit topic name")
+    ProjectControl u = user(localKey, DEVS);
+    assertWithMessage("u can edit topic name")
+        .that(u.controlForRef("refs/heads/master").canForceEditTopicName())
         .isTrue();
   }
 
   @Test
   public void unblockInLocalForceEditTopicName_Fails() throws Exception {
-    block(parent, EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(EDIT_TOPIC_NAME).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(EDIT_TOPIC_NAME).ref("refs/heads/*").group(DEVS).force(true))
+        .update();
 
-    ProjectControl u = user(local, REGISTERED_USERS);
-    assertThat(u.controlForRef("refs/heads/master").canForceEditTopicName())
-        .named("u can't edit topic name")
+    ProjectControl u = user(localKey, REGISTERED_USERS);
+    assertWithMessage("u can't edit topic name")
+        .that(u.controlForRef("refs/heads/master").canForceEditTopicName())
         .isFalse();
   }
 
   @Test
   public void unblockRange() throws Exception {
-    block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, +1))
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-2, range);
     assertCanVote(2, range);
@@ -854,10 +1024,14 @@
 
   @Test
   public void unblockRangeOnMoreSpecificRef_Fails() throws Exception {
-    block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/master");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, +1))
+        .add(allowLabel("Code-Review").ref("refs/heads/master").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
@@ -865,10 +1039,15 @@
 
   @Test
   public void unblockRangeOnLargerScope_Fails() throws Exception {
-    block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/master");
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(
+            blockLabel("Code-Review").ref("refs/heads/master").group(ANONYMOUS_USERS).range(-1, +1))
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
@@ -876,9 +1055,13 @@
 
   @Test
   public void nonconfiguredCannotVote() throws Exception {
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, REGISTERED_USERS);
+    ProjectControl u = user(localKey, REGISTERED_USERS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-1, range);
     assertCannotVote(1, range);
@@ -886,10 +1069,18 @@
 
   @Test
   public void unblockInLocalRange_Fails() throws Exception {
-    block(parent, LABEL + "Code-Review", -1, 1, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
@@ -897,9 +1088,13 @@
 
   @Test
   public void unblockRangeForChangeOwner() throws Exception {
-    allow(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(CHANGE_OWNER).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range =
         u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review", true);
     assertCanVote(-2, range);
@@ -908,9 +1103,13 @@
 
   @Test
   public void unblockRangeForNotChangeOwner() throws Exception {
-    allow(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(CHANGE_OWNER).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
@@ -918,9 +1117,13 @@
 
   @Test
   public void blockChangeOwnerVote() throws Exception {
-    block(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(CHANGE_OWNER).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
@@ -928,10 +1131,14 @@
 
   @Test
   public void unionOfPermissibleVotes() throws Exception {
-    allow(local, LABEL + "Code-Review", -1, +1, DEVS, "refs/heads/*");
-    allow(local, LABEL + "Code-Review", -2, +2, REGISTERED_USERS, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-1, +1))
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-2, range);
     assertCanVote(2, range);
@@ -939,10 +1146,14 @@
 
   @Test
   public void unionOfPermissibleVotesPermissionOrder() throws Exception {
-    allow(local, LABEL + "Code-Review", -2, +2, REGISTERED_USERS, "refs/heads/*");
-    allow(local, LABEL + "Code-Review", -1, +1, DEVS, "refs/heads/*");
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-1, +1))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-2, range);
     assertCanVote(2, range);
@@ -950,11 +1161,19 @@
 
   @Test
   public void unionOfBlockedVotes() throws Exception {
-    allow(parent, LABEL + "Code-Review", -1, +1, DEVS, "refs/heads/*");
-    block(parent, LABEL + "Code-Review", -2, +2, REGISTERED_USERS, "refs/heads/*");
-    block(local, LABEL + "Code-Review", -2, +1, REGISTERED_USERS, "refs/heads/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-1, +1))
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +1))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-1, range);
     assertCannotVote(1, range);
@@ -962,10 +1181,18 @@
 
   @Test
   public void blockOwner() throws Exception {
-    block(parent, OWNER, ANONYMOUS_USERS, "refs/*");
-    allow(local, OWNER, DEVS, "refs/*");
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(OWNER).ref("refs/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(DEVS))
+        .update();
 
-    assertThat(user(local, DEVS).isOwner()).isFalse();
+    assertThat(user(localKey, DEVS).isOwner()).isFalse();
   }
 
   @Test
@@ -977,14 +1204,16 @@
     RefPattern.validate("^refs/heads/review/${username}/.+");
   }
 
-  @Test(expected = InvalidNameException.class)
+  @Test
   public void testValidateBadRefPatternDoubleCaret() throws Exception {
-    RefPattern.validate("^^refs/*");
+    assertThrows(InvalidNameException.class, () -> RefPattern.validate("^^refs/*"));
   }
 
-  @Test(expected = InvalidNameException.class)
+  @Test
   public void testValidateBadRefPatternDanglingCharacter() throws Exception {
-    RefPattern.validate("^refs/heads/tmp/sdk/[0-9]{3,3}_R[1-9][A-Z][0-9]{3,3}*");
+    assertThrows(
+        InvalidNameException.class,
+        () -> RefPattern.validate("^refs/heads/tmp/sdk/[0-9]{3,3}_R[1-9][A-Z][0-9]{3,3}*"));
   }
 
   @Test
@@ -992,71 +1221,23 @@
     RefPattern.validate("^refs/heads/tmp/sdk/[0-9]{3,3}_R[1-9][A-Z][0-9]{3,3}");
   }
 
-  private InMemoryRepository add(ProjectConfig pc) {
-    List<CommentLinkInfo> commentLinks = null;
-
-    InMemoryRepository repo;
-    try {
-      repo = repoManager.createRepository(pc.getName());
-      if (pc.getProject() == null) {
-        pc.load(repo);
-      }
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RuntimeException(e);
-    }
-    all.put(
-        pc.getName(),
-        new ProjectState(
-            projectCache,
-            allProjectsName,
-            allUsersName,
-            repoManager,
-            commentLinks,
-            capabilityCollectionFactory,
-            transferConfig,
-            metricMaker,
-            pc));
-    return repo;
+  private ProjectState getProjectState(Project.NameKey nameKey) throws Exception {
+    return projectCache.checkedGet(nameKey, true);
   }
 
-  private ProjectControl internalUser(ProjectConfig local) throws Exception {
-    return new ProjectControl(
-        Collections.emptySet(),
-        Collections.emptySet(),
-        sectionSorter,
-        changeControlFactory,
-        permissionBackend,
-        refVisibilityControl,
-        repoManager,
-        refFilterFactory,
-        allUsersName,
-        new InternalUser(),
-        newProjectState(local));
+  private ProjectControl internalUser(Project.NameKey localKey) throws Exception {
+    return projectControlFactory.create(new InternalUser(), getProjectState(localKey));
   }
 
-  private ProjectControl user(ProjectConfig local, AccountGroup.UUID... memberOf) {
-    return user(local, null, memberOf);
+  private ProjectControl user(Project.NameKey localKey, AccountGroup.UUID... memberOf)
+      throws Exception {
+    return user(localKey, null, memberOf);
   }
 
   private ProjectControl user(
-      ProjectConfig local, @Nullable String name, AccountGroup.UUID... memberOf) {
-    return new ProjectControl(
-        Collections.emptySet(),
-        Collections.emptySet(),
-        sectionSorter,
-        changeControlFactory,
-        permissionBackend,
-        refVisibilityControl,
-        repoManager,
-        refFilterFactory,
-        allUsersName,
-        new MockUser(name, memberOf),
-        newProjectState(local));
-  }
-
-  private ProjectState newProjectState(ProjectConfig local) {
-    add(local);
-    return all.get(local.getProject().getNameKey());
+      Project.NameKey localKey, @Nullable String name, AccountGroup.UUID... memberOf)
+      throws Exception {
+    return projectControlFactory.create(new MockUser(name, memberOf), getProjectState(localKey));
   }
 
   private static class MockUser extends CurrentUser {
diff --git a/javatests/com/google/gerrit/server/plugins/AutoRegisterModulesTest.java b/javatests/com/google/gerrit/server/plugins/AutoRegisterModulesTest.java
index 0ffd4ac..55c9bc3 100644
--- a/javatests/com/google/gerrit/server/plugins/AutoRegisterModulesTest.java
+++ b/javatests/com/google/gerrit/server/plugins/AutoRegisterModulesTest.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.plugins;
 
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.expectLastCall;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.annotations.Export;
@@ -31,26 +32,17 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.jar.Manifest;
-import org.easymock.EasyMockSupport;
 import org.junit.Test;
 
 public class AutoRegisterModulesTest {
 
   @Test
   public void shouldRegisterSshCommand() throws InvalidPluginException {
-    EasyMockSupport ems = new EasyMockSupport();
-    ModuleGenerator sshModule = ems.createNiceMock(ModuleGenerator.class);
+    ModuleGenerator sshModule = mock(ModuleGenerator.class);
+    PluginGuiceEnvironment env = mock(PluginGuiceEnvironment.class);
 
-    PluginGuiceEnvironment env = ems.createNiceMock(PluginGuiceEnvironment.class);
-    expect(env.hasSshModule()).andReturn(true);
-    expect(env.newSshModuleGenerator()).andReturn(sshModule);
-
-    sshModule.setPluginName("test_plugin_name");
-    expectLastCall();
-    sshModule.export(anyObject(Export.class), eq(TestSshCommand.class));
-    expectLastCall();
-
-    ems.replayAll();
+    when(env.hasSshModule()).thenReturn(true);
+    when(env.newSshModuleGenerator()).thenReturn(sshModule);
 
     PluginContentScanner scanner = new TestPluginContextScanner();
     ClassLoader classLoader = this.getClass().getClassLoader();
@@ -59,7 +51,8 @@
         new AutoRegisterModules("test_plugin_name", env, scanner, classLoader);
     objectUnderTest.discover();
 
-    ems.verifyAll();
+    verify(sshModule).setPluginName("test_plugin_name");
+    verify(sshModule).export(any(Export.class), eq(TestSshCommand.class));
   }
 
   @Export(value = "test")
diff --git a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
index cf6d50f..61a2d2f 100644
--- a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
+++ b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
@@ -14,27 +14,31 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.eclipse.jgit.lib.Constants.R_REFS;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+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.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.project.CommitsCollection;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.InMemoryTestEnvironment;
 import com.google.inject.Inject;
@@ -44,12 +48,13 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
 /** Unit tests for {@link CommitsCollection}. */
-public class CommitsCollectionTest extends GerritBaseTests {
+public class CommitsCollectionTest {
   @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
 
   @Inject private AccountManager accountManager;
@@ -58,10 +63,10 @@
   @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
   @Inject protected AllProjectsName allProjects;
   @Inject private CommitsCollection commits;
-  @Inject private ProjectConfig.Factory projectConfigFactory;
+  @Inject private ProjectOperations projectOperations;
 
   private TestRepository<InMemoryRepository> repo;
-  private ProjectConfig project;
+  private Project.NameKey project;
 
   @Before
   public void setUp() throws Exception {
@@ -69,17 +74,22 @@
 
     Account.Id user = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
     testEnvironment.setApiUser(user);
+    project = projectOperations.newProject().create();
+    repo = new TestRepository<>(repoManager.openRepository(project));
+  }
 
-    Project.NameKey name = new Project.NameKey("project");
-    InMemoryRepository inMemoryRepo = repoManager.createRepository(name);
-    project = projectConfigFactory.create(name);
-    project.load(inMemoryRepo);
-    repo = new TestRepository<>(inMemoryRepo);
+  @After
+  public void tearDown() {
+    repo.getRepository().close();
   }
 
   @Test
   public void canReadCommitWhenAllRefsVisible() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     ObjectId id = repo.branch("master").commit().create();
     ProjectState state = readProjectState();
     RevWalk rw = repo.getRevWalk();
@@ -90,8 +100,12 @@
 
   @Test
   public void canReadCommitIfTwoRefsVisible() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
-    allow(project, READ, REGISTERED_USERS, "refs/heads/branch2");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/branch1").group(REGISTERED_USERS))
+        .add(allow(READ).ref("refs/heads/branch2").group(REGISTERED_USERS))
+        .update();
 
     ObjectId id1 = repo.branch("branch1").commit().create();
     ObjectId id2 = repo.branch("branch2").commit().create();
@@ -106,8 +120,12 @@
 
   @Test
   public void canReadCommitIfRefVisible() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
-    deny(project, READ, REGISTERED_USERS, "refs/heads/branch2");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/branch1").group(REGISTERED_USERS))
+        .add(deny(READ).ref("refs/heads/branch2").group(REGISTERED_USERS))
+        .update();
 
     ObjectId id1 = repo.branch("branch1").commit().create();
     ObjectId id2 = repo.branch("branch2").commit().create();
@@ -122,8 +140,12 @@
 
   @Test
   public void canReadCommitIfReachableFromVisibleRef() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
-    deny(project, READ, REGISTERED_USERS, "refs/heads/branch2");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/branch1").group(REGISTERED_USERS))
+        .add(deny(READ).ref("refs/heads/branch2").group(REGISTERED_USERS))
+        .update();
 
     RevCommit parent1 = repo.commit().create();
     repo.branch("branch1").commit().parent(parent1).create();
@@ -140,7 +162,11 @@
 
   @Test
   public void cannotReadAfterRollbackWithRestrictedRead() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/branch1").group(REGISTERED_USERS))
+        .update();
 
     RevCommit parent1 = repo.commit().create();
     ObjectId id1 = repo.branch("branch1").commit().parent(parent1).create();
@@ -159,7 +185,11 @@
 
   @Test
   public void canReadAfterRollbackWithAllRefsVisible() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     RevCommit parent1 = repo.commit().create();
     ObjectId id1 = repo.branch("branch1").commit().parent(parent1).create();
@@ -177,41 +207,19 @@
   }
 
   private ProjectState readProjectState() throws Exception {
-    return projectCache.get(project.getName());
-  }
-
-  protected void allow(ProjectConfig project, String permission, AccountGroup.UUID id, String ref)
-      throws Exception {
-    Util.allow(project, permission, id, ref);
-    saveProjectConfig(project);
-  }
-
-  protected void deny(ProjectConfig project, String permission, AccountGroup.UUID id, String ref)
-      throws Exception {
-    Util.deny(project, permission, id, ref);
-    saveProjectConfig(project);
-  }
-
-  protected void saveProjectConfig(ProjectConfig cfg) throws Exception {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(cfg.getName())) {
-      cfg.commit(md);
-    }
-    projectCache.evict(cfg.getProject());
+    return projectCache.get(project);
   }
 
   private void setUpPermissions() throws Exception {
-    ImmutableList<AccountGroup.UUID> admins = getAdmins();
-
     // Remove read permissions for all users besides admin, because by default
     // Anonymous user group has ALLOW READ permission in refs/*.
     // This method is idempotent, so is safe to call on every test setup.
-    ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
-    for (AccessSection sec : pc.getAccessSections()) {
-      sec.removePermission(Permission.READ);
-    }
-    for (AccountGroup.UUID admin : admins) {
-      allow(pc, Permission.READ, admin, "refs/*");
-    }
+    TestProjectUpdate.Builder u = projectOperations.allProjectsForUpdate();
+    projectCache.checkedGet(allProjects).getConfig().getAccessSectionNames().stream()
+        .filter(sec -> sec.startsWith(R_REFS))
+        .forEach(sec -> u.remove(permissionKey(Permission.READ).ref(sec)));
+    getAdmins().forEach(admin -> u.add(allow(Permission.READ).ref("refs/*").group(admin)));
+    u.update();
   }
 
   private ImmutableList<AccountGroup.UUID> getAdmins() {
diff --git a/javatests/com/google/gerrit/server/project/GroupListTest.java b/javatests/com/google/gerrit/server/project/GroupListTest.java
index 08aca9f..518f85d 100644
--- a/javatests/com/google/gerrit/server/project/GroupListTest.java
+++ b/javatests/com/google/gerrit/server/project/GroupListTest.java
@@ -14,22 +14,19 @@
 
 package com.google.gerrit.server.project;
 
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.createNiceMock;
-import static org.easymock.EasyMock.expectLastCall;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.ValidationError;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
@@ -37,8 +34,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class GroupListTest extends GerritBaseTests {
-  private static final Project.NameKey PROJECT = new Project.NameKey("project");
+public class GroupListTest {
+  private static final Project.NameKey PROJECT = Project.nameKey("project");
   private static final String TEXT =
       "# UUID                                  \tGroup Name\n"
           + "#\n"
@@ -49,14 +46,13 @@
 
   @Before
   public void setup() throws IOException {
-    ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
-    replay(sink);
+    ValidationError.Sink sink = mock(ValidationError.Sink.class);
     groupList = GroupList.parse(PROJECT, TEXT, sink);
   }
 
   @Test
   public void byUUID() throws Exception {
-    AccountGroup.UUID uuid = new AccountGroup.UUID("d96b998f8a66ff433af50befb975d0e2bb6e0999");
+    AccountGroup.UUID uuid = AccountGroup.uuid("d96b998f8a66ff433af50befb975d0e2bb6e0999");
 
     GroupReference groupReference = groupList.byUUID(uuid);
 
@@ -66,7 +62,7 @@
 
   @Test
   public void put() {
-    AccountGroup.UUID uuid = new AccountGroup.UUID("abc");
+    AccountGroup.UUID uuid = AccountGroup.uuid("abc");
     GroupReference groupReference = new GroupReference(uuid, "Hutzliputz");
 
     groupList.put(uuid, groupReference);
@@ -81,7 +77,7 @@
     Collection<GroupReference> result = groupList.references();
 
     assertEquals(2, result.size());
-    AccountGroup.UUID uuid = new AccountGroup.UUID("ebe31c01aec2c9ac3b3c03e87a47450829ff4310");
+    AccountGroup.UUID uuid = AccountGroup.uuid("ebe31c01aec2c9ac3b3c03e87a47450829ff4310");
     GroupReference expected = new GroupReference(uuid, "Administrators");
 
     assertTrue(result.contains(expected));
@@ -92,27 +88,24 @@
     Set<AccountGroup.UUID> result = groupList.uuids();
 
     assertEquals(2, result.size());
-    AccountGroup.UUID expected = new AccountGroup.UUID("ebe31c01aec2c9ac3b3c03e87a47450829ff4310");
+    AccountGroup.UUID expected = AccountGroup.uuid("ebe31c01aec2c9ac3b3c03e87a47450829ff4310");
     assertTrue(result.contains(expected));
   }
 
   @Test
   public void validationError() throws Exception {
-    ValidationError.Sink sink = createMock(ValidationError.Sink.class);
-    sink.error(anyObject(ValidationError.class));
-    expectLastCall().times(2);
-    replay(sink);
+    ValidationError.Sink sink = mock(ValidationError.Sink.class);
     groupList = GroupList.parse(PROJECT, TEXT.replace("\t", "    "), sink);
-    verify(sink);
+    verify(sink, times(2)).error(any(ValidationError.class));
   }
 
   @Test
   public void retainAll() throws Exception {
-    AccountGroup.UUID uuid = new AccountGroup.UUID("d96b998f8a66ff433af50befb975d0e2bb6e0999");
+    AccountGroup.UUID uuid = AccountGroup.uuid("d96b998f8a66ff433af50befb975d0e2bb6e0999");
     groupList.retainUUIDs(Collections.singleton(uuid));
 
     assertNotNull(groupList.byUUID(uuid));
-    assertNull(groupList.byUUID(new AccountGroup.UUID("ebe31c01aec2c9ac3b3c03e87a47450829ff4310")));
+    assertNull(groupList.byUUID(AccountGroup.uuid("ebe31c01aec2c9ac3b3c03e87a47450829ff4310")));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 3436153..0dd6436 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.gerrit.reviewdb.client.BooleanProjectConfig.REQUIRE_CHANGE_ID;
+import static com.google.gerrit.entities.BooleanProjectConfig.REQUIRE_CHANGE_ID;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -26,18 +26,17 @@
 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.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 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.ValidationError;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.project.testing.Util;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.server.project.testing.TestLabels;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -62,9 +61,12 @@
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
-public class ProjectConfigTest extends GerritBaseTests {
+public class ProjectConfigTest {
   private static final String LABEL_SCORES_CONFIG =
-      "  copyMinScore = "
+      "  copyAnyScore = "
+          + !LabelType.DEF_COPY_ANY_SCORE
+          + "\n"
+          + "  copyMinScore = "
           + !LabelType.DEF_COPY_MIN_SCORE
           + "\n"
           + "  copyMaxScore = "
@@ -88,8 +90,8 @@
   @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
 
   private final GroupReference developers =
-      new GroupReference(new AccountGroup.UUID("X"), "Developers");
-  private final GroupReference staff = new GroupReference(new AccountGroup.UUID("Y"), "Staff");
+      new GroupReference(AccountGroup.uuid("X"), "Developers");
+  private final GroupReference staff = new GroupReference(AccountGroup.uuid("Y"), "Staff");
 
   private SitePaths sitePaths;
   private ProjectConfig.Factory factory;
@@ -260,6 +262,7 @@
     ProjectConfig cfg = read(rev);
     Map<String, LabelType> labels = cfg.getLabelSections();
     LabelType type = labels.entrySet().iterator().next().getValue();
+    assertThat(type.isCopyAnyScore()).isNotEqualTo(LabelType.DEF_COPY_ANY_SCORE);
     assertThat(type.isCopyMinScore()).isNotEqualTo(LabelType.DEF_COPY_MIN_SCORE);
     assertThat(type.isCopyMaxScore()).isNotEqualTo(LabelType.DEF_COPY_MAX_SCORE);
     assertThat(type.isCopyAllScoresOnMergeFirstParentUpdate())
@@ -319,17 +322,17 @@
                 + "\tsubmit = group Staff\n"
                 + "  upload = group Developers\n"
                 + "  read = group Developers\n"
-                + "[accounts]\n"
-                + "  sameGroupVisibility = group Staff\n"
-                + "[contributor-agreement \"Individual\"]\n"
-                + "  description = A new description\n"
-                + "  accepted = group Staff\n"
-                + "  agreementUrl = http://www.example.com/agree\n"
-                + "\texcludeProjects = ^/theirproject\n"
                 + "[label \"CustomLabel\"]\n"
                 + LABEL_SCORES_CONFIG
                 + "\tfunction = MaxWithBlock\n" // label gets this function when it is created
-                + "\tdefaultValue = 0\n"); //  label gets this value when it is created
+                + "\tdefaultValue = 0\n" //  label gets this value when it is created
+                + "[accounts]\n"
+                + "\tsameGroupVisibility = group Staff\n"
+                + "[contributor-agreement \"Individual\"]\n"
+                + "\tdescription = A new description\n"
+                + "\tagreementUrl = http://www.example.com/agree\n"
+                + "\taccepted = group Staff\n"
+                + "\texcludeProjects = ^/theirproject\n");
   }
 
   @Test
@@ -341,11 +344,11 @@
     cfg.getLabelSections()
         .put(
             "My-Label",
-            Util.category(
+            TestLabels.label(
                 "My-Label",
-                Util.value(-1, "Negative"),
-                Util.value(0, "No score"),
-                Util.value(1, "Positive")));
+                TestLabels.value(-1, "Negative"),
+                TestLabels.value(0, "No score"),
+                TestLabels.value(1, "Positive")));
     rev = commit(cfg);
     assertThat(text(rev, "project.config"))
         .isEqualTo(
@@ -422,7 +425,7 @@
 
   @Test
   public void readUnexistingPluginConfig() throws Exception {
-    ProjectConfig cfg = factory.create(new Project.NameKey("test"));
+    ProjectConfig cfg = factory.create(Project.nameKey("test"));
     cfg.load(db);
     PluginConfig pluginCfg = cfg.getPluginConfig("somePlugin");
     assertThat(pluginCfg.getNames()).isEmpty();
@@ -625,7 +628,7 @@
 
   @Test
   public void readOtherProjectIgnoresAllProjectsBaseConfig() throws Exception {
-    ProjectConfig cfg = factory.create(new Project.NameKey("test"));
+    ProjectConfig cfg = factory.create(Project.nameKey("test"));
     cfg.load(db);
     assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
         .isEqualTo(InheritableBoolean.INHERIT);
@@ -641,6 +644,118 @@
         .isEqualTo(InheritableBoolean.INHERIT);
   }
 
+  @Test
+  public void accountsSectionIsUnsetIfNoSameGroupVisibilityIsSet() throws Exception {
+    RevCommit rev =
+        tr.commit()
+            .add(
+                "project.config",
+                "[commentlink \"bugzilla\"]\n"
+                    + "\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n"
+                    + "\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n"
+                    + "[accounts]\n"
+                    + "  sameGroupVisibility = group Staff\n")
+            .create();
+    update(rev);
+
+    ProjectConfig cfg = read(rev);
+    cfg.getAccountsSection().setSameGroupVisibility(ImmutableList.of());
+    rev = commit(cfg);
+    assertThat(text(rev, "project.config"))
+        .isEqualTo(
+            "[commentlink \"bugzilla\"]\n\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
+  }
+
+  @Test
+  public void contributorSectionIsUnsetIfNoContributorAgreementIsSet() throws Exception {
+    RevCommit rev =
+        tr.commit()
+            .add(
+                "project.config",
+                "[commentlink \"bugzilla\"]\n"
+                    + "\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n"
+                    + "\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n"
+                    + "[contributor-agreement \"Individual\"]\n"
+                    + "  accepted = group Developers\n"
+                    + "  accepted = group Staff\n")
+            .create();
+    update(rev);
+
+    ProjectConfig cfg = read(rev);
+    ContributorAgreement section = cfg.getContributorAgreement("Individual");
+    section.setAccepted(ImmutableList.of());
+    rev = commit(cfg);
+    assertThat(text(rev, "project.config"))
+        .isEqualTo(
+            "[commentlink \"bugzilla\"]\n\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
+  }
+
+  @Test
+  public void notifySectionIsUnsetIfNoNotificationsAreSet() throws Exception {
+    RevCommit rev =
+        tr.commit()
+            .add(
+                "project.config",
+                "[commentlink \"bugzilla\"]\n"
+                    + "\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n"
+                    + "\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n"
+                    + "[notify \"name\"]\n"
+                    + "  email = example@example.com\n")
+            .create();
+    update(rev);
+
+    ProjectConfig cfg = read(rev);
+    cfg.getNotifyConfigs().clear();
+    rev = commit(cfg);
+    assertThat(text(rev, "project.config"))
+        .isEqualTo(
+            "[commentlink \"bugzilla\"]\n\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
+  }
+
+  @Test
+  public void commentLinkSectionIsUnsetIfNoCommentLinksAreSet() throws Exception {
+    RevCommit rev =
+        tr.commit()
+            .add(
+                "project.config",
+                "[commentlink \"bugzilla\"]\n"
+                    + "\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n"
+                    + "\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n"
+                    + "[notify \"name\"]\n"
+                    + "  email = example@example.com\n")
+            .create();
+    update(rev);
+
+    ProjectConfig cfg = read(rev);
+    cfg.getCommentLinkSections().clear();
+    rev = commit(cfg);
+    assertThat(text(rev, "project.config"))
+        .isEqualTo("[notify \"name\"]\n\temail = example@example.com\n");
+  }
+
+  @Test
+  public void pluginSectionIsUnsetIfAllPluginConfigsAreEmpty() throws Exception {
+    RevCommit rev =
+        tr.commit()
+            .add(
+                "project.config",
+                "[commentlink \"bugzilla\"]\n"
+                    + "\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n"
+                    + "\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n"
+                    + "[plugin \"somePlugin\"]\n"
+                    + "  key = value\n")
+            .create();
+    update(rev);
+
+    ProjectConfig cfg = read(rev);
+    PluginConfig pluginCfg = cfg.getPluginConfig("somePlugin");
+    pluginCfg.unset("key");
+    rev = commit(cfg);
+    assertThat(text(rev, "project.config"))
+        .isEqualTo(
+            "[commentlink \"bugzilla\"]\n\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
+  }
+
   private Path writeDefaultAllProjectsConfig(String... lines) throws IOException {
     Path dir = sitePaths.etc_dir.resolve(ALL_PROJECTS.get());
     Files.createDirectories(dir);
@@ -648,7 +763,7 @@
   }
 
   private ProjectConfig read(RevCommit rev) throws IOException, ConfigInvalidException {
-    ProjectConfig cfg = factory.create(new Project.NameKey("test"));
+    ProjectConfig cfg = factory.create(Project.nameKey("test"));
     cfg.load(db, rev);
     return cfg;
   }
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index a2b2866..e7f0812 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -15,13 +15,17 @@
 package com.google.gerrit.server.query.account;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
 import com.google.gerrit.extensions.api.access.PermissionInfo;
@@ -47,8 +51,6 @@
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -79,6 +81,7 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testing.GerritServerTests;
+import com.google.gerrit.testing.GerritTestName;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
@@ -93,10 +96,13 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 
 @Ignore
 public abstract class AbstractQueryAccountsTest extends GerritServerTests {
+  @Rule public final GerritTestName testName = new GerritTestName();
+
   @Inject protected Accounts accounts;
 
   @Inject @ServerInitiated protected Provider<AccountsUpdate> accountsUpdate;
@@ -253,7 +259,7 @@
     addEmails(user1, secondaryEmail);
 
     AccountInfo user2 = newAccount("user");
-    requestContext.setContext(newRequestContext(new Account.Id(user2._accountId)));
+    requestContext.setContext(newRequestContext(Account.id(user2._accountId)));
 
     if (getSchemaVersion() < 5) {
       assertMissingField(AccountField.PREFERRED_EMAIL);
@@ -340,7 +346,7 @@
     AccountInfo user2 = newAccountWithFullName("jroe", "Jane Roe");
 
     AccountInfo user3 = newAccount("user");
-    requestContext.setContext(newRequestContext(new Account.Id(user3._accountId)));
+    requestContext.setContext(newRequestContext(Account.id(user3._accountId)));
 
     assertQuery("notexisting");
     assertQuery("Not Existing");
@@ -575,13 +581,13 @@
     String[] secondaryEmails = new String[] {"dfg@example.com", "hij@example.com"};
     addEmails(otherUser, secondaryEmails);
 
-    requestContext.setContext(newRequestContext(new Account.Id(user._accountId)));
+    requestContext.setContext(newRequestContext(Account.id(user._accountId)));
 
     List<AccountInfo> result = newQuery(otherUser.username).withSuggest(true).get();
     assertThat(result.get(0).secondaryEmails).isNull();
-
-    exception.expect(AuthException.class);
-    newQuery(otherUser.username).withOption(ListAccountsOption.ALL_EMAILS).get();
+    assertThrows(
+        AuthException.class,
+        () -> newQuery(otherUser.username).withOption(ListAccountsOption.ALL_EMAILS).get());
   }
 
   @Test
@@ -600,7 +606,7 @@
     AccountInfo user1 = newAccountWithFullName("tester", "Test Usre");
 
     // update account without reindex so that account index is stale
-    Account.Id accountId = new Account.Id(user1._accountId);
+    Account.Id accountId = Account.id(user1._accountId);
     String newName = "Test User";
     try (Repository repo = repoManager.openRepository(allUsers)) {
       MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsers, repo);
@@ -625,19 +631,22 @@
   public void rawDocument() throws Exception {
     AccountInfo userInfo = gApi.accounts().id(admin.getAccountId().get()).get();
 
+    Schema<AccountState> schema = indexes.getSearchIndex().getSchema();
     Optional<FieldBundle> rawFields =
         indexes
             .getSearchIndex()
             .getRaw(
-                new Account.Id(userInfo._accountId),
+                Account.id(userInfo._accountId),
                 QueryOptions.create(
-                    IndexConfig.createDefault(),
-                    0,
-                    1,
-                    indexes.getSearchIndex().getSchema().getStoredFields().keySet()));
+                    IndexConfig.createDefault(), 0, 1, schema.getStoredFields().keySet()));
 
     assertThat(rawFields).isPresent();
-    assertThat(rawFields.get().getValue(AccountField.ID)).isEqualTo(userInfo._accountId);
+    if (schema.useLegacyNumericFields()) {
+      assertThat(rawFields.get().getValue(AccountField.ID)).isEqualTo(userInfo._accountId);
+    } else {
+      assertThat(Integer.valueOf(rawFields.get().getValue(AccountField.ID_STR)))
+          .isEqualTo(userInfo._accountId);
+    }
 
     // The field EXTERNAL_ID_STATE is only supported from schema version 6.
     if (getSchemaVersion() < 6) {
@@ -695,7 +704,7 @@
     in.name = name;
     in.createEmptyCommit = true;
     gApi.projects().create(in);
-    return new Project.NameKey(name);
+    return Project.nameKey(name);
   }
 
   protected void blockRead(Project.NameKey project, GroupInfo group) throws RestApiException {
@@ -750,7 +759,7 @@
       return null;
     }
 
-    String suffix = getSanitizedMethodName();
+    String suffix = testName.getSanitizedMethodName();
     if (name.contains("@")) {
       return name + "." + suffix;
     }
@@ -777,7 +786,7 @@
   }
 
   private void addEmails(AccountInfo account, String... emails) throws Exception {
-    Account.Id id = new Account.Id(account._accountId);
+    Account.Id id = Account.id(account._accountId);
     for (String email : emails) {
       accountManager.link(id, AuthRequest.forEmail(email));
     }
@@ -802,15 +811,15 @@
       throws Exception {
     List<AccountInfo> result = query.get();
     Iterable<Integer> ids = ids(result);
-    assertThat(ids)
-        .named(format(query, result, accounts))
+    assertWithMessage(format(query, result, accounts))
+        .that(ids)
         .containsExactlyElementsIn(ids(accounts))
         .inOrder();
     return result;
   }
 
   protected void assertAccounts(List<AccountState> accounts, AccountInfo... expectedAccounts) {
-    assertThat(accounts.stream().map(a -> a.getAccount().getId().get()).collect(toList()))
+    assertThat(accounts.stream().map(a -> a.account().id().get()).collect(toList()))
         .containsExactlyElementsIn(
             Arrays.asList(expectedAccounts).stream().map(a -> a._accountId).collect(toList()));
   }
@@ -860,8 +869,8 @@
   }
 
   protected void assertMissingField(FieldDef<AccountState, ?> field) {
-    assertThat(getSchema().hasField(field))
-        .named("schema %s has field %s", getSchemaVersion(), field.getName())
+    assertWithMessage("schema %s has field %s", getSchemaVersion(), field.getName())
+        .that(getSchema().hasField(field))
         .isFalse();
   }
 
diff --git a/javatests/com/google/gerrit/server/query/account/BUILD b/javatests/com/google/gerrit/server/query/account/BUILD
index ba0f779..5c910a0 100644
--- a/javatests/com/google/gerrit/server/query/account/BUILD
+++ b/javatests/com/google/gerrit/server/query/account/BUILD
@@ -9,19 +9,20 @@
     srcs = ABSTRACT_QUERY_TEST,
     visibility = ["//visibility:public"],
     runtime_deps = [
+        "//java/com/google/gerrit/lucene",
         "//prolog:gerrit-prolog-common",
     ],
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/lifecycle",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
     ],
@@ -39,7 +40,7 @@
         ":abstract_query_tests",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:jgit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index ff45c08..558658b 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -16,14 +16,15 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.allow;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
-import static com.google.gerrit.server.project.testing.Util.verified;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
@@ -40,11 +41,21 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Streams;
 import com.google.common.truth.ThrowableSubject;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
 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.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AssigneeInput;
@@ -74,14 +85,6 @@
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -93,6 +96,7 @@
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.VersionedAccountQueries;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeTriplet;
@@ -107,7 +111,6 @@
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.util.ManualRequestContext;
@@ -159,7 +162,7 @@
   @Inject protected AllUsersName allUsersName;
   @Inject protected BatchUpdate.Factory updateFactory;
   @Inject protected ChangeInserter.Factory changeFactory;
-  @Inject protected ChangeQueryBuilder queryBuilder;
+  @Inject protected Provider<ChangeQueryBuilder> queryBuilderProvider;
   @Inject protected GerritApi gApi;
   @Inject protected IdentifiedUser.GenericFactory userFactory;
   @Inject protected ChangeIndexCollection indexes;
@@ -180,7 +183,9 @@
   @Inject protected ProjectCache projectCache;
   @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
   @Inject protected IdentifiedUser.GenericFactory identifiedUserFactory;
-  @Inject protected ProjectConfig.Factory projectConfigFactory;
+
+  @Inject private ProjectConfig.Factory projectConfigFactory;
+  @Inject private ProjectOperations projectOperations;
 
   protected Injector injector;
   protected LifecycleManager lifecycle;
@@ -416,13 +421,6 @@
 
   @Test
   public void byPrivate() throws Exception {
-    if (getSchemaVersion() < 40) {
-      assertMissingField(ChangeField.PRIVATE);
-      assertFailingQuery(
-          "is:private", "'is:private' operator is not supported by change index version");
-      return;
-    }
-
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo), userId);
     Account.Id user2 =
@@ -447,12 +445,6 @@
 
   @Test
   public void byWip() throws Exception {
-    if (getSchemaVersion() < 42) {
-      assertMissingField(ChangeField.WIP);
-      assertFailingQuery("is:wip", "'is:wip' operator is not supported by change index version");
-      return;
-    }
-
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo), userId);
 
@@ -469,24 +461,7 @@
   }
 
   @Test
-  public void excludeWipChangeFromReviewersDashboardsBeforeSchema42() throws Exception {
-    assume().that(getSchemaVersion()).isLessThan(42);
-
-    assertMissingField(ChangeField.WIP);
-    assertFailingQuery("is:wip", "'is:wip' operator is not supported by change index version");
-
-    Account.Id user1 = createAccount("user1");
-    TestRepository<Repo> repo = createProject("repo");
-    Change change1 = insert(repo, newChangeWorkInProgress(repo), userId);
-    assertQuery("reviewer:" + user1, change1);
-    gApi.changes().id(change1.getChangeId()).setWorkInProgress();
-    assertQuery("reviewer:" + user1, change1);
-  }
-
-  @Test
   public void excludeWipChangeFromReviewersDashboards() throws Exception {
-    assume().that(getSchemaVersion()).isAtLeast(42);
-
     Account.Id user1 = createAccount("user1");
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChangeWorkInProgress(repo), userId);
@@ -504,17 +479,7 @@
   }
 
   @Test
-  public void byStartedBeforeSchema44() throws Exception {
-    assume().that(getSchemaVersion()).isLessThan(44);
-    assertMissingField(ChangeField.STARTED);
-    assertFailingQuery(
-        "is:started", "'is:started' operator is not supported by change index version");
-  }
-
-  @Test
   public void byStarted() throws Exception {
-    assume().that(getSchemaVersion()).isAtLeast(44);
-
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChangeWorkInProgress(repo));
 
@@ -550,9 +515,7 @@
 
   @Test
   public void restorePendingReviewers() throws Exception {
-    assume().that(getSchemaVersion()).isAtLeast(44);
-
-    Project.NameKey project = new Project.NameKey("repo");
+    Project.NameKey project = Project.nameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
@@ -569,7 +532,7 @@
             .reviewer(user2.toString(), ReviewerState.CC, false)
             .reviewer(email1)
             .reviewer(email2, ReviewerState.CC, false);
-    gApi.changes().id(change1.getId().get()).revision("current").review(in);
+    gApi.changes().id(change1.getId().get()).current().review(in);
 
     List<ChangeInfo> changeInfos =
         assertQuery(newQuery("is:wip").withOption(DETAILED_LABELS), change1);
@@ -579,8 +542,7 @@
         changeInfos.get(0).pendingReviewers;
     assertThat(pendingReviewers).isNotNull();
 
-    assertReviewers(
-        pendingReviewers.get(ReviewerState.REVIEWER), userId.toString(), user1.toString(), email1);
+    assertReviewers(pendingReviewers.get(ReviewerState.REVIEWER), user1.toString(), email1);
     assertReviewers(pendingReviewers.get(ReviewerState.CC), user2.toString(), email2);
     assertReviewers(pendingReviewers.get(ReviewerState.REMOVED));
 
@@ -710,10 +672,10 @@
     assertQuery(searchOperator + "\"John Smith\"");
 
     // By invalid query.
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("invalid value");
     // SchemaUtil.getNameParts will return an empty set for query only containing these characters.
-    assertQuery(searchOperator + "@.- /_");
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> assertQuery(searchOperator + "@.- /_"));
+    assertThat(thrown).hasMessageThat().contains("invalid value");
   }
 
   private Change createChange(TestRepository<Repo> repo, PersonIdent person) throws Exception {
@@ -1054,20 +1016,24 @@
   public void byLabelMulti() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Project.NameKey project =
-        new Project.NameKey(repo.getRepository().getDescription().getRepositoryName());
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+        Project.nameKey(repo.getRepository().getDescription().getRepositoryName());
 
     LabelType verified =
-        category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
-    cfg.getLabelSections().put(verified.getName(), verified);
-
-    String heads = RefNames.REFS_HEADS + "*";
-    allow(cfg, Permission.forLabel(verified().getName()), -1, 1, REGISTERED_USERS, heads);
-
+        label("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      ProjectConfig cfg = projectConfigFactory.create(project);
+      cfg.load(md);
+      cfg.getLabelSections().put(verified.getName(), verified);
       cfg.commit(md);
     }
-    projectCache.evict(cfg.getProject());
+    projectCache.evict(project);
+
+    String heads = RefNames.REFS_HEADS + "*";
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref(heads).group(REGISTERED_USERS).range(-1, 1))
+        .update();
 
     ReviewInput reviewVerified = new ReviewInput().label("Verified", 1);
     ChangeInserter ins = newChange(repo);
@@ -1204,9 +1170,9 @@
       }
       String q = "status:new limit:" + i;
       List<ChangeInfo> results = newQuery(q).get();
-      assertThat(results).named(q).hasSize(expectedSize);
-      assertThat(results.get(results.size() - 1)._moreChanges)
-          .named(q)
+      assertWithMessage(q).that(results).hasSize(expectedSize);
+      assertWithMessage(q)
+          .that(results.get(results.size() - 1)._moreChanges)
           .isEqualTo(expectedMoreChanges);
       assertThat(results.get(0)._number).isEqualTo(last.getId().get());
     }
@@ -1378,15 +1344,6 @@
 
   @Test
   public void byExtension() throws Exception {
-    if (getSchemaVersion() < 52) {
-      assertMissingField(ChangeField.EXTENSION);
-      String unsupportedOperatorMsg =
-          "'extension' operator is not supported by change index version";
-      assertFailingQuery("extension:txt", unsupportedOperatorMsg);
-      assertFailingQuery("ext:txt", unsupportedOperatorMsg);
-      return;
-    }
-
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc"));
     Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC"));
@@ -1410,15 +1367,6 @@
 
   @Test
   public void byOnlyExtensions() throws Exception {
-    if (getSchemaVersion() < 53) {
-      assertMissingField(ChangeField.ONLY_EXTENSIONS);
-      String unsupportedOperatorMessage =
-          "'onlyextensions' operator is not supported by change index version";
-      assertFailingQuery("onlyextensions:txt,jpg", unsupportedOperatorMessage);
-      assertFailingQuery("onlyexts:txt,jpg", unsupportedOperatorMessage);
-      return;
-    }
-
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc"));
     Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H"));
@@ -1466,14 +1414,6 @@
 
   @Test
   public void byFooter() throws Exception {
-    if (getSchemaVersion() < 54) {
-      assertMissingField(ChangeField.FOOTER);
-      assertFailingQuery(
-          "footer:Change-Id=I3d2b978ed455f835d1dad2daa920be0b0ec2ae36",
-          "'footer' operator is not supported by change index version");
-      return;
-    }
-
     TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create());
     Change change1 = insert(repo, newChangeForCommit(repo, commit1));
@@ -1523,15 +1463,6 @@
 
   @Test
   public void byDirectory() throws Exception {
-    if (getSchemaVersion() < 55) {
-      assertMissingField(ChangeField.DIRECTORY);
-      String unsupportedOperatorMessage =
-          "'directory' operator is not supported by change index version";
-      assertFailingQuery("directory:src/java", unsupportedOperatorMessage);
-      assertFailingQuery("dir:src/java", unsupportedOperatorMessage);
-      return;
-    }
-
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChangeWithFiles(repo, "src/foo.h", "src/foo.cc"));
     Change change2 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
@@ -1598,8 +1529,6 @@
 
   @Test
   public void byDirectoryRegex() throws Exception {
-    assume().that(getSchemaVersion()).isAtLeast(55);
-
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
     Change change2 =
@@ -1874,11 +1803,11 @@
 
     // change is visible to group ONLY when access is granted
     grant(
-        new Project.NameKey("repo"),
+        Project.nameKey("repo"),
         "refs/*",
         Permission.READ,
         false,
-        new AccountGroup.UUID(gApi.groups().id(g1).get().id));
+        AccountGroup.uuid(gApi.groups().id(g1).get().id));
     assertQuery(q + " visibleto:" + g1, change1);
 
     // Both changes are visible to InternalUser
@@ -1929,7 +1858,7 @@
       ProjectConfig config = projectConfigFactory.read(md);
       AccessSection s = config.getAccessSection(ref, true);
       Permission p = s.getPermission(permission, true);
-      PermissionRule rule = Util.newRule(config, groupUUID);
+      PermissionRule rule = new PermissionRule(new GroupReference(groupUUID, groupUUID.get()));
       rule.setForce(force);
       p.add(rule);
       config.commit(md);
@@ -2010,7 +1939,7 @@
 
   @Test
   public void byDraftByExcludesZombieDrafts() throws Exception {
-    Project.NameKey project = new Project.NameKey("repo");
+    Project.NameKey project = Project.nameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     Change change = insert(repo, newChange(repo));
     Change.Id id = change.getId();
@@ -2179,8 +2108,15 @@
     assertQuery("conflicts:" + change2.getId().get(), change1);
     assertQuery("is:mergeable", change2, change1);
 
-    gApi.changes().id(change1.getChangeId()).revision("current").review(ReviewInput.approve());
-    gApi.changes().id(change1.getChangeId()).revision("current").submit();
+    gApi.changes().id(change1.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(change1.getChangeId()).current().submit();
+
+    // If a change gets submitted, the remaining open changes get reindexed asynchronously to update
+    // their mergeability information. If the further assertions in this test are done before the
+    // asynchronous reindex completed they fail because the mergeability information in the index
+    // was not updated yet. To avoid this flakiness reindexAfterRefUpdate is switched off for the
+    // tests and we index change2 synchronously here.
+    gApi.changes().id(change2.getChangeId()).index();
 
     assertQuery("status:open conflicts:" + change2.getId().get());
     assertQuery("status:open is:mergeable");
@@ -2249,7 +2185,7 @@
 
     assertQuery("is:reviewer");
     assertQuery("reviewer:self");
-    gApi.changes().id(change3.getChangeId()).revision("current").review(ReviewInput.recommend());
+    gApi.changes().id(change3.getChangeId()).current().review(ReviewInput.recommend());
     assertQuery("is:reviewer", change3);
     assertQuery("reviewer:self", change3);
 
@@ -2330,7 +2266,7 @@
 
   @Test
   public void reviewerAndCcByEmail() throws Exception {
-    Project.NameKey project = new Project.NameKey("repo");
+    Project.NameKey project = Project.nameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
@@ -2353,30 +2289,17 @@
     rin.state = ReviewerState.CC;
     gApi.changes().id(change2.getId().get()).addReviewer(rin);
 
-    if (getSchemaVersion() >= 41) {
-      assertQuery("reviewer:\"" + userByEmailWithName + "\"", change1);
-      assertQuery("cc:\"" + userByEmailWithName + "\"", change2);
+    assertQuery("reviewer:\"" + userByEmailWithName + "\"", change1);
+    assertQuery("cc:\"" + userByEmailWithName + "\"", change2);
 
-      // Omitting the name:
-      assertQuery("reviewer:\"" + userByEmail + "\"", change1);
-      assertQuery("cc:\"" + userByEmail + "\"", change2);
-    } else {
-      assertMissingField(ChangeField.REVIEWER_BY_EMAIL);
-
-      assertFailingQuery(
-          "reviewer:\"" + userByEmailWithName + "\"", "User " + userByEmailWithName + " not found");
-      assertFailingQuery(
-          "cc:\"" + userByEmailWithName + "\"", "User " + userByEmailWithName + " not found");
-
-      // Omitting the name:
-      assertFailingQuery("reviewer:\"" + userByEmail + "\"", "User " + userByEmail + " not found");
-      assertFailingQuery("cc:\"" + userByEmail + "\"", "User " + userByEmail + " not found");
-    }
+    // Omitting the name:
+    assertQuery("reviewer:\"" + userByEmail + "\"", change1);
+    assertQuery("cc:\"" + userByEmail + "\"", change2);
   }
 
   @Test
   public void reviewerAndCcByEmailWithQueryForDifferentUser() throws Exception {
-    Project.NameKey project = new Project.NameKey("repo");
+    Project.NameKey project = Project.nameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
@@ -2398,17 +2321,8 @@
     rin.state = ReviewerState.CC;
     gApi.changes().id(change2.getId().get()).addReviewer(rin);
 
-    if (getSchemaVersion() >= 41) {
-      assertQuery("reviewer:\"someone@example.com\"");
-      assertQuery("cc:\"someone@example.com\"");
-    } else {
-      assertMissingField(ChangeField.REVIEWER_BY_EMAIL);
-
-      String someoneEmail = "someone@example.com";
-      assertFailingQuery(
-          "reviewer:\"" + someoneEmail + "\"", "User " + someoneEmail + " not found");
-      assertFailingQuery("cc:\"" + someoneEmail + "\"", "User " + someoneEmail + " not found");
-    }
+    assertQuery("reviewer:\"someone@example.com\"");
+    assertQuery("cc:\"someone@example.com\"");
   }
 
   @Test
@@ -2511,7 +2425,7 @@
   public void byCommitsOnBranchNotMergedSkipsMissingChanges() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     ObjectId missing =
-        repo.branch(new PatchSet.Id(new Change.Id(987654), 1).toRefName())
+        repo.branch(PatchSet.id(Change.id(987654), 1).toRefName())
             .commit()
             .message("No change for this commit")
             .insertChangeId()
@@ -2526,7 +2440,7 @@
     List<String> shas = new ArrayList<>(n + extra.size());
     extra.forEach(i -> shas.add(i.name()));
     List<Integer> expectedIds = new ArrayList<>(n);
-    Branch.NameKey dest = null;
+    BranchNameKey dest = null;
     for (int i = 0; i < n; i++) {
       ChangeInserter ins = newChange(repo);
       insert(repo, ins);
@@ -2542,15 +2456,15 @@
           queryProvider.get().byCommitsOnBranchNotMerged(repo.getRepository(), dest, shas, i);
       Iterable<Integer> ids = FluentIterable.from(cds).transform(in -> in.getId().get());
       String name = "limit " + i;
-      assertThat(ids).named(name).hasSize(n);
-      assertThat(ids).named(name).containsExactlyElementsIn(expectedIds);
+      assertWithMessage(name).that(ids).hasSize(n);
+      assertWithMessage(name).that(ids).containsExactlyElementsIn(expectedIds);
     }
   }
 
   @Test
   public void reindexIfStale() throws Exception {
     Account.Id user = createAccount("user");
-    Project.NameKey project = new Project.NameKey("repo");
+    Project.NameKey project = Project.nameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     Change change = insert(repo, newChange(repo));
     String changeId = change.getKey().get();
@@ -2563,8 +2477,7 @@
     assertThat(indexer.reindexIfStale(project, change.getId()).get()).isFalse();
 
     // Delete edit ref behind index's back.
-    RefUpdate ru =
-        repo.getRepository().updateRef(RefNames.refsEdit(user, change.getId(), ps.getId()));
+    RefUpdate ru = repo.getRepository().updateRef(RefNames.refsEdit(user, change.getId(), ps.id()));
     ru.setForceUpdate(true);
     assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
 
@@ -2648,13 +2561,6 @@
 
   @Test
   public void revertOf() throws Exception {
-    if (getSchemaVersion() < 45) {
-      assertMissingField(ChangeField.REVERT_OF);
-      assertFailingQuery(
-          "revertof:1", "'revertof' operator is not supported by change index version");
-      return;
-    }
-
     TestRepository<Repo> repo = createProject("repo");
     // Create two commits and revert second commit (initial commit can't be reverted)
     Change initial = insert(repo, newChange(repo));
@@ -2667,8 +2573,7 @@
     gApi.changes().id(changeToRevert.id).current().submit();
 
     ChangeInfo changeThatReverts = gApi.changes().id(changeToRevert.id).revert().get();
-    assertQueryByIds(
-        "revertof:" + changeToRevert._number, new Change.Id(changeThatReverts._number));
+    assertQueryByIds("revertof:" + changeToRevert._number, Change.id(changeThatReverts._number));
   }
 
   /** Change builder for helping in tests for dashboard sections. */
@@ -3123,27 +3028,27 @@
     Change change1 = insert(repo, newChange(repo));
     Change change2 = insert(repo, newChangeForBranch(repo, "stable"));
 
-    String queries =
+    String queryListText =
         "query1\tproject:repo\n"
             + "query2\tproject:repo status:open\n"
             + "query3\tproject:repo branch:stable\n"
             + "query4\tproject:repo branch:other";
 
     try (TestRepository<Repo> allUsers =
-        new TestRepository<>(repoManager.openRepository(allUsersName))) {
-      String refsUsers = RefNames.refsUsers(userId);
-      allUsers.branch(refsUsers).commit().add("queries", queries).create();
-
-      Ref userRef = allUsers.getRepository().exactRef(refsUsers);
-      assertThat(userRef).isNotNull();
+            new TestRepository<>(repoManager.openRepository(allUsersName));
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName)) {
+      VersionedAccountQueries queries = VersionedAccountQueries.forUser(userId);
+      queries.load(md);
+      queries.setQueryList(queryListText);
+      queries.commit(md);
     }
 
     assertThatQueryException("query:foo").hasMessageThat().isEqualTo("Unknown named query: foo");
 
     assertQuery("query:query1", change2, change1);
     assertQuery("query:query2", change2, change1);
-    gApi.changes().id(change1.getChangeId()).revision("current").review(ReviewInput.approve());
-    gApi.changes().id(change1.getChangeId()).revision("current").submit();
+    gApi.changes().id(change1.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(change1.getChangeId()).current().submit();
     assertQuery("query:query2", change2);
     assertQuery("query:query3", change2);
     assertQuery("query:query4");
@@ -3221,6 +3126,7 @@
 
     assertQuery(ChangeIndexPredicate.none());
 
+    ChangeQueryBuilder queryBuilder = queryBuilderProvider.get();
     for (Predicate<ChangeData> matchingOneChange :
         ImmutableList.of(
             // One index query, one post-filtering query.
@@ -3293,7 +3199,7 @@
       branch = "refs/heads/" + branch;
     }
 
-    Change.Id id = new Change.Id(seq.nextChangeId());
+    Change.Id id = Change.id(seq.nextChangeId());
     ChangeInserter ins =
         changeFactory
             .create(id, commit, branch)
@@ -3321,7 +3227,7 @@
       Timestamp createdOn)
       throws Exception {
     Project.NameKey project =
-        new Project.NameKey(repo.getRepository().getDescription().getRepositoryName());
+        Project.nameKey(repo.getRepository().getDescription().getRepositoryName());
     Account.Id ownerId = owner != null ? owner : userId;
     IdentifiedUser user = userFactory.create(ownerId);
     try (BatchUpdate bu = updateFactory.create(project, user, createdOn)) {
@@ -3340,7 +3246,7 @@
 
     PatchSetInserter inserter =
         patchSetFactory
-            .create(changeNotesFactory.createChecked(c), new PatchSet.Id(c.getId(), n), commit)
+            .create(changeNotesFactory.createChecked(c), PatchSet.id(c.getId(), n), commit)
             .setFireRevisionCreated(false)
             .setValidate(false);
     try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.nowTs());
@@ -3380,7 +3286,7 @@
 
   protected TestRepository<Repo> createProject(String name) throws Exception {
     gApi.projects().create(name).get();
-    return new TestRepository<>(repoManager.openRepository(new Project.NameKey(name)));
+    return new TestRepository<>(repoManager.openRepository(Project.nameKey(name)));
   }
 
   protected TestRepository<Repo> createProject(String name, String parent) throws Exception {
@@ -3388,7 +3294,7 @@
     input.name = name;
     input.parent = parent;
     gApi.projects().create(input).get();
-    return new TestRepository<>(repoManager.openRepository(new Project.NameKey(name)));
+    return new TestRepository<>(repoManager.openRepository(Project.nameKey(name)));
   }
 
   protected QueryRequest newQuery(Object query) {
@@ -3412,8 +3318,8 @@
       throws Exception {
     List<ChangeInfo> result = query.get();
     Iterable<Change.Id> ids = ids(result);
-    assertThat(ids)
-        .named(format(query.getQuery(), ids, changes))
+    assertWithMessage(format(query.getQuery(), ids, changes))
+        .that(ids)
         .containsExactlyElementsIn(Arrays.asList(changes))
         .inOrder();
     return result;
@@ -3425,8 +3331,8 @@
             .map(ChangeData::getId)
             .collect(toImmutableList());
     Change.Id[] expectedIds = Arrays.stream(changes).map(Change::getId).toArray(Change.Id[]::new);
-    assertThat(actualIds)
-        .named(format(predicate.toString(), actualIds, expectedIds))
+    assertWithMessage(format(predicate.toString(), actualIds, expectedIds))
+        .that(actualIds)
         .containsExactlyElementsIn(expectedIds)
         .inOrder();
   }
@@ -3457,7 +3363,7 @@
           .append(c.changeId)
           .append("), ")
           .append("dest=")
-          .append(new Branch.NameKey(new Project.NameKey(c.project), c.branch))
+          .append(BranchNameKey.create(Project.nameKey(c.project), c.branch))
           .append(", ")
           .append("status=")
           .append(c.status)
@@ -3478,7 +3384,7 @@
   }
 
   protected static Iterable<Change.Id> ids(Iterable<ChangeInfo> changes) {
-    return Streams.stream(changes).map(c -> new Change.Id(c._number)).collect(toList());
+    return Streams.stream(changes).map(c -> Change.id(c._number)).collect(toList());
   }
 
   protected static long lastUpdatedMs(Change c) {
@@ -3515,8 +3421,8 @@
   }
 
   protected void assertMissingField(FieldDef<ChangeData, ?> field) {
-    assertThat(getSchema().hasField(field))
-        .named("schema %s has field %s", getSchemaVersion(), field.getName())
+    assertWithMessage("schema %s has field %s", getSchemaVersion(), field.getName())
+        .that(getSchema().hasField(field))
         .isFalse();
   }
 
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index 8347484..d0162d3 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -1,7 +1,10 @@
 load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:junit.bzl", "junit_tests")
 
-ABSTRACT_QUERY_TEST = ["AbstractQueryChangesTest.java"]
+ABSTRACT_QUERY_TEST = [
+    "AbstractQueryChangesTest.java",
+    "LuceneQueryChangesTest.java",
+]
 
 java_library(
     name = "abstract_query_tests",
@@ -9,48 +12,53 @@
     srcs = ABSTRACT_QUERY_TEST,
     visibility = ["//visibility:public"],
     runtime_deps = [
+        "//java/com/google/gerrit/lucene",
         "//prolog:gerrit-prolog-common",
     ],
     deps = [
+        "//java/com/google/gerrit/acceptance/testsuite/project",
         "//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/index",
         "//java/com/google/gerrit/lifecycle",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/project/testing:project-test-util",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/truth",
     ],
 )
 
-LUCENE_QUERY_TEST = ["LuceneQueryChangesTest.java"]
+LUCENE_QUERY_TEST = [
+    "LuceneQueryChangesLatestIndexVersionTest.java",
+    "LuceneQueryChangesPreviousIndexVersionTest.java",
+]
 
-junit_tests(
-    name = "lucene_query_test",
+[junit_tests(
+    name = f[:f.index(".")],
     size = "large",
-    srcs = LUCENE_QUERY_TEST,
+    srcs = [f],
     visibility = ["//visibility:public"],
     deps = [
         ":abstract_query_tests",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/truth",
     ],
-)
+) for f in LUCENE_QUERY_TEST]
 
 junit_tests(
     name = "small_tests",
@@ -61,15 +69,15 @@
     ),
     visibility = ["//visibility:public"],
     deps = [
-        "//java/com/google/gerrit/exceptions",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/proto/testing",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/cache/testing",
+        "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib:jgit",
         "//lib/truth",
         "//lib/truth:truth-proto-extension",
         "//proto:cache_java_proto",
diff --git a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
index aba0018..e42230f 100644
--- a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
@@ -17,26 +17,36 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.TestChanges;
+import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class ChangeDataTest extends GerritBaseTests {
+public class ChangeDataTest {
   @Test
   public void setPatchSetsClearsCurrentPatchSet() throws Exception {
-    Project.NameKey project = new Project.NameKey("project");
-    ChangeData cd = ChangeData.createForTest(project, new Change.Id(1), 1);
-    cd.setChange(TestChanges.newChange(project, new Account.Id(1000)));
+    Project.NameKey project = Project.nameKey("project");
+    ChangeData cd = ChangeData.createForTest(project, Change.id(1), 1, ObjectId.zeroId());
+    cd.setChange(TestChanges.newChange(project, Account.id(1000)));
     PatchSet curr1 = cd.currentPatchSet();
-    int currId = curr1.getId().get();
-    PatchSet ps1 = new PatchSet(new PatchSet.Id(cd.getId(), currId + 1));
-    PatchSet ps2 = new PatchSet(new PatchSet.Id(cd.getId(), currId + 2));
+    int currId = curr1.id().get();
+    PatchSet ps1 = newPatchSet(cd.getId(), currId + 1);
+    PatchSet ps2 = newPatchSet(cd.getId(), currId + 2);
     cd.setPatchSets(ImmutableList.of(ps1, ps2));
     PatchSet curr2 = cd.currentPatchSet();
     assertThat(curr2).isNotSameInstanceAs(curr1);
   }
+
+  private static PatchSet newPatchSet(Change.Id changeId, int num) {
+    return PatchSet.builder()
+        .id(PatchSet.id(changeId, num))
+        .commitId(ObjectId.zeroId())
+        .uploader(Account.id(1234))
+        .createdOn(TimeUtil.nowTs())
+        .build();
+  }
 }
diff --git a/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
index e550f8e..00c1a80 100644
--- a/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
@@ -25,11 +25,10 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.gerrit.server.cache.proto.Cache.ConflictKeyProto;
-import com.google.gerrit.testing.GerritBaseTests;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class ConflictKeyTest extends GerritBaseTests {
+public class ConflictKeyTest {
   @Test
   public void ffOnlyPreservesInputOrder() {
     ObjectId id1 = ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee");
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesLatestIndexVersionTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesLatestIndexVersionTest.java
new file mode 100644
index 0000000..52a9170
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesLatestIndexVersionTest.java
@@ -0,0 +1,26 @@
+// 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.query.change;
+
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.IndexConfig;
+import org.eclipse.jgit.lib.Config;
+
+public class LuceneQueryChangesLatestIndexVersionTest extends LuceneQueryChangesTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return IndexConfig.createForLucene();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesPreviousIndexVersionTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesPreviousIndexVersionTest.java
new file mode 100644
index 0000000..62483fa
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesPreviousIndexVersionTest.java
@@ -0,0 +1,36 @@
+// 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.query.change;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.gerrit.testing.IndexVersions;
+import org.eclipse.jgit.lib.Config;
+
+public class LuceneQueryChangesPreviousIndexVersionTest extends LuceneQueryChangesTest {
+  @ConfigSuite.Default
+  public static Config againstPreviousIndexVersion() {
+    // the current schema version is already tested by the inherited default config suite
+    return Iterables.getOnlyElement(
+        IndexVersions.asConfigMap(
+                ChangeSchemaDefinitions.INSTANCE,
+                IndexVersions.getWithoutLatest(ChangeSchemaDefinitions.INSTANCE),
+                "againstIndexVersion",
+                IndexConfig.createForLucene())
+            .values());
+  }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
index 2ea198f..6a83fb9 100644
--- a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -14,37 +14,21 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
-import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
-import com.google.gerrit.testing.IndexConfig;
-import com.google.gerrit.testing.IndexVersions;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
-import java.util.List;
-import java.util.Map;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
-public class LuceneQueryChangesTest extends AbstractQueryChangesTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return IndexConfig.createForLucene();
-  }
-
-  @ConfigSuite.Configs
-  public static Map<String, Config> againstPreviousIndexVersion() {
-    // the current schema version is already tested by the inherited default config suite
-    List<Integer> schemaVersions = IndexVersions.getWithoutLatest(ChangeSchemaDefinitions.INSTANCE);
-    return IndexVersions.asConfigMap(
-        ChangeSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig());
-  }
-
+public abstract class LuceneQueryChangesTest extends AbstractQueryChangesTest {
   @Override
   protected Injector createInjector() {
     Config luceneConfig = new Config(config);
@@ -76,8 +60,10 @@
     Change change1 = insert(repo, newChange(repo), userId);
     String nameEmail = user.asIdentifiedUser().getNameEmail();
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("Cannot create full-text query with value: \\");
-    assertQuery("owner: \"" + nameEmail + "\"\\", change1);
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> assertQuery("owner: \"" + nameEmail + "\"\\", change1));
+    assertThat(thrown).hasMessageThat().contains("Cannot create full-text query with value: \\");
   }
 }
diff --git a/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java b/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
index 135e9c26..72fdd14 100644
--- a/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
+++ b/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
@@ -17,13 +17,13 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import java.util.Arrays;
+import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class RegexPathPredicateTest extends GerritBaseTests {
+public class RegexPathPredicateTest {
   @Test
   public void prefixOnlyOptimization() {
     RegexPathPredicate p = predicate("^a/b/.*");
@@ -83,7 +83,8 @@
 
   private static ChangeData change(String... files) {
     Arrays.sort(files);
-    ChangeData cd = ChangeData.createForTest(new Project.NameKey("project"), new Change.Id(1), 1);
+    ChangeData cd =
+        ChangeData.createForTest(Project.nameKey("project"), Change.id(1), 1, ObjectId.zeroId());
     cd.setCurrentFilePaths(Arrays.asList(files));
     return cd;
   }
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index 4a3c755..d80eac0 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -15,11 +15,15 @@
 package com.google.gerrit.server.query.group;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -32,8 +36,6 @@
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -57,6 +59,7 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testing.GerritServerTests;
+import com.google.gerrit.testing.GerritTestName;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
@@ -68,10 +71,13 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 
 @Ignore
 public abstract class AbstractQueryGroupsTest extends GerritServerTests {
+  @Rule public final GerritTestName testName = new GerritTestName();
+
   @Inject protected Accounts accounts;
 
   @Inject @ServerInitiated protected Provider<AccountsUpdate> accountsUpdate;
@@ -184,7 +190,7 @@
 
   @Test
   public void byInname() throws Exception {
-    String namePart = getSanitizedMethodName();
+    String namePart = testName.getSanitizedMethodName();
     namePart = CharMatcher.is('_').removeFrom(namePart);
 
     GroupInfo group1 = createGroup("group-" + namePart);
@@ -204,9 +210,9 @@
 
     assertQuery("description:non-existing");
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("description operator requires a value");
-    assertQuery("description:\"\"");
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> assertQuery("description:\"\""));
+    assertThat(thrown).hasMessageThat().contains("description operator requires a value");
   }
 
   @Test
@@ -340,7 +346,7 @@
 
     // update group in the database so that group index is stale
     String newDescription = "barY";
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID(group1.id);
+    AccountGroup.UUID groupUuid = AccountGroup.uuid(group1.id);
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder().setDescription(newDescription).build();
     groupsUpdateProvider.get().updateGroupInNoteDb(groupUuid, groupUpdate);
@@ -356,7 +362,7 @@
   @Test
   public void rawDocument() throws Exception {
     GroupInfo group1 = createGroup(name("group1"));
-    AccountGroup.UUID uuid = new AccountGroup.UUID(group1.id);
+    AccountGroup.UUID uuid = AccountGroup.uuid(group1.id);
 
     Optional<FieldBundle> rawFields =
         indexes
@@ -376,7 +382,7 @@
   @Test
   public void byDeletedGroup() throws Exception {
     GroupInfo group = createGroup(name("group"));
-    AccountGroup.UUID uuid = new AccountGroup.UUID(group.id);
+    AccountGroup.UUID uuid = AccountGroup.uuid(group.id);
     String query = "uuid:" + uuid;
     assertQuery(query, group);
 
@@ -459,8 +465,8 @@
       throws Exception {
     List<GroupInfo> result = query.get();
     Iterable<String> uuids = uuids(result);
-    assertThat(uuids)
-        .named(format(query, result, groups))
+    assertWithMessage(format(query, result, groups))
+        .that(uuids)
         .containsExactlyElementsIn(uuids(groups))
         .inOrder();
     return result;
@@ -535,7 +541,7 @@
       return null;
     }
 
-    return name + "_" + getSanitizedMethodName();
+    return name + "_" + testName.getSanitizedMethodName();
   }
 
   protected int getSchemaVersion() {
diff --git a/javatests/com/google/gerrit/server/query/group/BUILD b/javatests/com/google/gerrit/server/query/group/BUILD
index 1271f4e..e14350f 100644
--- a/javatests/com/google/gerrit/server/query/group/BUILD
+++ b/javatests/com/google/gerrit/server/query/group/BUILD
@@ -8,17 +8,18 @@
     testonly = True,
     srcs = ABSTRACT_QUERY_TEST,
     visibility = ["//visibility:public"],
+    runtime_deps = ["//java/com/google/gerrit/lucene"],
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/lifecycle",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
     ],
@@ -36,7 +37,7 @@
         ":abstract_query_tests",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:jgit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index 08ef2b0..dfd7928 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -15,12 +15,16 @@
 package com.google.gerrit.server.query.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
 import com.google.gerrit.extensions.api.access.PermissionInfo;
@@ -37,8 +41,6 @@
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -57,6 +59,7 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testing.GerritServerTests;
+import com.google.gerrit.testing.GerritTestName;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
@@ -68,10 +71,13 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 
 @Ignore
 public abstract class AbstractQueryProjectsTest extends GerritServerTests {
+  @Rule public final GerritTestName testName = new GerritTestName();
+
   @Inject protected Accounts accounts;
 
   @Inject @ServerInitiated protected Provider<AccountsUpdate> accountsUpdate;
@@ -185,7 +191,7 @@
 
   @Test
   public void byInname() throws Exception {
-    String namePart = getSanitizedMethodName();
+    String namePart = testName.getSanitizedMethodName();
     namePart = CharMatcher.is('_').removeFrom(namePart);
 
     ProjectInfo project1 = createProject(name("project1-" + namePart));
@@ -207,9 +213,9 @@
 
     assertQuery("description:non-existing");
 
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("description operator requires a value");
-    assertQuery("description:\"\"");
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> assertQuery("description:\"\""));
+    assertThat(thrown).hasMessageThat().contains("description operator requires a value");
   }
 
   @Test
@@ -224,16 +230,18 @@
 
   @Test
   public void byState_emptyQuery() throws Exception {
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("state operator requires a value");
-    assertQuery("state:\"\"");
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> assertQuery("state:\"\""));
+    assertThat(thrown).hasMessageThat().contains("state operator requires a value");
   }
 
   @Test
   public void byState_badQuery() throws Exception {
-    exception.expect(BadRequestException.class);
-    exception.expectMessage("state operator must be either 'active' or 'read-only'");
-    assertQuery("state:bla");
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> assertQuery("state:bla"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("state operator must be either 'active' or 'read-only'");
   }
 
   @Test
@@ -375,8 +383,8 @@
       throws Exception {
     List<ProjectInfo> result = query.get();
     Iterable<String> names = names(result);
-    assertThat(names)
-        .named(format(query, result, projects))
+    assertWithMessage(format(query, result, projects))
+        .that(names)
         .containsExactlyElementsIn(names(projects))
         .inOrder();
     return result;
@@ -443,6 +451,6 @@
       return null;
     }
 
-    return name + "_" + getSanitizedMethodName();
+    return name + "_" + testName.getSanitizedMethodName();
   }
 }
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index 5afc7da..984d824 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -8,18 +8,19 @@
     testonly = True,
     srcs = ABSTRACT_QUERY_TEST,
     visibility = ["//visibility:public"],
+    runtime_deps = ["//java/com/google/gerrit/lucene"],
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/lifecycle",
-        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
     ],
 )
@@ -36,7 +37,7 @@
         ":abstract_query_tests",
         "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:jgit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/rules/BUILD b/javatests/com/google/gerrit/server/rules/BUILD
index 1e335db..250b0ce 100644
--- a/javatests/com/google/gerrit/server/rules/BUILD
+++ b/javatests/com/google/gerrit/server/rules/BUILD
@@ -8,14 +8,15 @@
     runtime_deps = ["//prolog:gerrit-prolog-common"],
     deps = [
         "//java/com/google/gerrit/common:server",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/project/testing:project-test-util",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/mockito",
         "//lib/prolog:runtime",
         "//lib/truth",
     ],
diff --git a/javatests/com/google/gerrit/server/rules/GerritCommonTest.java b/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
index 180c16b..9d7afbc 100644
--- a/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
+++ b/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.server.rules;
 
-import static org.easymock.EasyMock.expect;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.AbstractModule;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
@@ -29,7 +32,6 @@
 import java.io.PushbackReader;
 import java.io.StringReader;
 import java.util.Arrays;
-import org.easymock.EasyMock;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
@@ -56,10 +58,10 @@
 
   @Override
   protected void setUpEnvironment(PrologEnvironment env) throws Exception {
-    LabelTypes labelTypes = new LabelTypes(Arrays.asList(Util.codeReview(), Util.verified()));
-    ChangeData cd = EasyMock.createMock(ChangeData.class);
-    expect(cd.getLabelTypes()).andStubReturn(labelTypes);
-    EasyMock.replay(cd);
+    LabelTypes labelTypes =
+        new LabelTypes(Arrays.asList(TestLabels.codeReview(), TestLabels.verified()));
+    ChangeData cd = mock(ChangeData.class);
+    when(cd.getLabelTypes()).thenReturn(labelTypes);
     env.set(StoredValues.CHANGE_DATA, cd);
   }
 
@@ -82,11 +84,14 @@
       throw new CompileException("Cannot consult " + nameTerm);
     }
 
-    exception.expect(ReductionLimitException.class);
-    exception.expectMessage("exceeded reduction limit of 1300");
-    env.once(
-        Prolog.BUILTIN,
-        "call",
-        new StructureTerm(":", SymbolTerm.create("user"), SymbolTerm.create("loopy")));
+    ReductionLimitException thrown =
+        assertThrows(
+            ReductionLimitException.class,
+            () ->
+                env.once(
+                    Prolog.BUILTIN,
+                    "call",
+                    new StructureTerm(":", SymbolTerm.create("user"), SymbolTerm.create("loopy"))));
+    assertThat(thrown).hasMessageThat().contains("exceeded reduction limit of 1300");
   }
 }
diff --git a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
index 14124fa..d8af0e5 100644
--- a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
+++ b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
@@ -19,12 +19,11 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.LabelId;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -32,9 +31,9 @@
 import java.util.List;
 import org.junit.Test;
 
-public class IgnoreSelfApprovalRuleTest extends GerritBaseTests {
-  private static final Change.Id CHANGE_ID = new Change.Id(100);
-  private static final PatchSet.Id PS_ID = new PatchSet.Id(CHANGE_ID, 1);
+public class IgnoreSelfApprovalRuleTest {
+  private static final Change.Id CHANGE_ID = Change.id(100);
+  private static final PatchSet.Id PS_ID = PatchSet.id(CHANGE_ID, 1);
   private static final LabelType VERIFIED = makeLabel("Verified");
   private static final Account.Id USER1 = makeAccount(100001);
 
@@ -82,16 +81,14 @@
   }
 
   private static PatchSetApproval makeApproval(LabelId labelId, Account.Id accountId, int value) {
-    PatchSetApproval.Key key = makeKey(PS_ID, accountId, labelId);
-    return new PatchSetApproval(key, (short) value, Date.from(Instant.now()));
-  }
-
-  private static PatchSetApproval.Key makeKey(
-      PatchSet.Id psId, Account.Id accountId, LabelId labelId) {
-    return new PatchSetApproval.Key(psId, accountId, labelId);
+    return PatchSetApproval.builder()
+        .key(PatchSetApproval.key(PS_ID, accountId, labelId))
+        .value(value)
+        .granted(Date.from(Instant.now()))
+        .build();
   }
 
   private static Account.Id makeAccount(int account) {
-    return new Account.Id(account);
+    return Account.id(account);
   }
 }
diff --git a/javatests/com/google/gerrit/server/rules/PrologRuleEvaluatorTest.java b/javatests/com/google/gerrit/server/rules/PrologRuleEvaluatorTest.java
index 6eb0747..8622b32 100644
--- a/javatests/com/google/gerrit/server/rules/PrologRuleEvaluatorTest.java
+++ b/javatests/com/google/gerrit/server/rules/PrologRuleEvaluatorTest.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class PrologRuleEvaluatorTest extends GerritBaseTests {
+public class PrologRuleEvaluatorTest {
 
   @Test
   public void validLabelNamesAreKept() {
diff --git a/javatests/com/google/gerrit/server/rules/PrologTestCase.java b/javatests/com/google/gerrit/server/rules/PrologTestCase.java
index f4d8eac..c2b6dbb 100644
--- a/javatests/com/google/gerrit/server/rules/PrologTestCase.java
+++ b/javatests/com/google/gerrit/server/rules/PrologTestCase.java
@@ -19,7 +19,6 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.Guice;
 import com.google.inject.Module;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
@@ -45,7 +44,7 @@
 
 /** Base class for any tests written in Prolog. */
 @Ignore
-public abstract class PrologTestCase extends GerritBaseTests {
+public abstract class PrologTestCase {
   private static final SymbolTerm test_1 = SymbolTerm.intern("test", 1);
 
   private String pkg;
diff --git a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
index e5890c9..d5ddeff 100644
--- a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
+++ b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
@@ -21,20 +21,20 @@
 import static com.google.gerrit.server.schema.testing.AllProjectsCreatorTestUtil.getAllProjectsWithoutDefaultAcls;
 import static com.google.gerrit.server.schema.testing.AllProjectsCreatorTestUtil.getDefaultAllProjectsWithAllDefaultSections;
 import static com.google.gerrit.server.schema.testing.AllProjectsCreatorTestUtil.readAllProjectsConfig;
+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.extensions.client.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.GroupUUID;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.Sequences;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.inject.Inject;
 import org.eclipse.jgit.lib.Config;
@@ -43,7 +43,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class AllProjectsCreatorTest extends GerritBaseTests {
+public class AllProjectsCreatorTest {
   private static final LabelType TEST_LABEL =
       new LabelType(
           "Test-Label",
@@ -127,7 +127,7 @@
     allProjectsCreator.create(allProjectsInput);
 
     Config config = readAllProjectsConfig(repoManager, allProjectsName);
-    assertThat(config.getString("project", null, "description")).isEqualTo(testDescription);
+    assertThat(config).stringValue("project", null, "description").isEqualTo(testDescription);
   }
 
   @Test
@@ -143,7 +143,7 @@
     allProjectsCreator.create(allProjectsInput);
 
     Config config = readAllProjectsConfig(repoManager, allProjectsName);
-    assertThat(config.getBoolean("submit", null, "rejectEmptyCommit", false)).isTrue();
+    assertThat(config).booleanValue("submit", null, "rejectEmptyCommit", false).isTrue();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
index 5c1b201..059b7f3 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
@@ -15,15 +15,15 @@
 package com.google.gerrit.server.schema;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.server.schema.NoteDbSchemaUpdater.requiredUpgrades;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.notedb.IntBlob;
 import com.google.gerrit.server.notedb.RepoSequence;
 import com.google.gerrit.server.notedb.Sequences;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.TestUpdateUI;
 import java.io.IOException;
@@ -44,7 +43,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Test;
 
-public class NoteDbSchemaUpdaterTest extends GerritBaseTests {
+public class NoteDbSchemaUpdaterTest {
   @Test
   public void requiredUpgradesFromNoVersion() throws Exception {
     assertThat(requiredUpgrades(0, versions(10))).containsExactly(10).inOrder();
@@ -62,26 +61,20 @@
 
   @Test
   public void downgradeNotSupported() throws Exception {
-    try {
-      requiredUpgrades(14, versions(10, 11, 12, 13));
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .contains("Cannot downgrade NoteDb schema from version 14 to 13");
-    }
+    StorageException thrown =
+        assertThrows(StorageException.class, () -> requiredUpgrades(14, versions(10, 11, 12, 13)));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Cannot downgrade NoteDb schema from version 14 to 13");
   }
 
   @Test
   public void skipToFirstVersionNotSupported() throws Exception {
     ImmutableSortedSet<Integer> versions = versions(10, 11, 12);
     assertThat(requiredUpgrades(9, versions)).containsExactly(10, 11, 12).inOrder();
-    try {
-      requiredUpgrades(8, versions);
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e).hasMessageThat().contains("Cannot skip NoteDb schema from version 8 to 10");
-    }
+    StorageException thrown =
+        assertThrows(StorageException.class, () -> requiredUpgrades(8, versions));
+    assertThat(thrown).hasMessageThat().contains("Cannot skip NoteDb schema from version 8 to 10");
   }
 
   private static class TestUpdate {
@@ -231,12 +224,8 @@
             seedGroupSequenceRef();
           }
         };
-    try {
-      u.update();
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e).hasMessageThat().contains("NoteDb change migration was not completed");
-    }
+    StorageException thrown = assertThrows(StorageException.class, () -> u.update());
+    assertThat(thrown).hasMessageThat().contains("NoteDb change migration was not completed");
     assertThat(u.getMessages()).isEmpty();
     assertThat(u.readVersion()).isEmpty();
   }
@@ -250,12 +239,8 @@
             setNotesMigrationConfig();
           }
         };
-    try {
-      u.update();
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e).hasMessageThat().contains("upgrade to 2.16.x first");
-    }
+    StorageException thrown = assertThrows(StorageException.class, () -> u.update());
+    assertThat(thrown).hasMessageThat().contains("upgrade to 2.16.x first");
     assertThat(u.getMessages()).isEmpty();
     assertThat(u.readVersion()).isEmpty();
   }
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
new file mode 100644
index 0000000..a5fd4a2
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.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.schema;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import com.google.inject.ProvisionException;
+import java.io.IOException;
+import java.nio.file.Paths;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public class NoteDbSchemaVersionCheckTest {
+  private NoteDbSchemaVersionManager versionManager;
+  private SitePaths sitePaths;
+
+  @Before
+  public void setup() throws Exception {
+    AllProjectsName allProjectsName = new AllProjectsName("All-Projects");
+    GitRepositoryManager repoManager = new InMemoryRepositoryManager();
+    repoManager.createRepository(allProjectsName);
+    versionManager = new NoteDbSchemaVersionManager(allProjectsName, repoManager);
+    versionManager.init();
+
+    sitePaths = new SitePaths(Paths.get("/tmp/foo"));
+  }
+
+  @Test
+  public void shouldNotFailIfCurrentVersionIsExpected() {
+    new NoteDbSchemaVersionCheck(versionManager, sitePaths, new Config()).start();
+    // No exceptions should be thrown
+  }
+
+  @Test
+  public void shouldFailIfCurrentVersionIsOneMoreThanExpected() throws IOException {
+    versionManager.increment(NoteDbSchemaVersions.LATEST);
+
+    ProvisionException e =
+        assertThrows(
+            ProvisionException.class,
+            () -> new NoteDbSchemaVersionCheck(versionManager, sitePaths, new Config()).start());
+
+    assertThat(e)
+        .hasMessageThat()
+        .contains("Unsupported schema version " + (NoteDbSchemaVersions.LATEST + 1));
+  }
+
+  @Test
+  public void
+      shouldNotFailWithExperimentalRollingUpgradeEnabledAndCurrentVersionIsOneMoreThanExpected()
+          throws IOException {
+    Config gerritConfig = new Config();
+    gerritConfig.setBoolean("gerrit", null, "experimentalRollingUpgrade", true);
+    versionManager.increment(NoteDbSchemaVersions.LATEST);
+
+    NoteDbSchemaVersionCheck versionCheck =
+        new NoteDbSchemaVersionCheck(versionManager, sitePaths, gerritConfig);
+    versionCheck.start();
+    // No exceptions should be thrown
+  }
+}
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
index 9c62d7f..38e19f7 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
@@ -15,20 +15,19 @@
 package com.google.gerrit.server.schema;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_VERSION;
+import static com.google.gerrit.entities.RefNames.REFS_VERSION;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Before;
 import org.junit.Test;
 
-public class NoteDbSchemaVersionManagerTest extends GerritBaseTests {
+public class NoteDbSchemaVersionManagerTest {
   private NoteDbSchemaVersionManager manager;
   private TestRepository<?> tr;
 
@@ -55,14 +54,10 @@
   public void readInvalid() throws Exception {
     ObjectId blobId = tr.blob(" 1 2 3 ");
     tr.update(REFS_VERSION, blobId);
-    try {
-      manager.read();
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo("invalid value in refs/meta/version blob at " + blobId.name());
-    }
+    StorageException thrown = assertThrows(StorageException.class, () -> manager.read());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("invalid value in refs/meta/version blob at " + blobId.name());
   }
 
   @Test
@@ -81,13 +76,9 @@
   @Test
   public void incrementWrongOldVersion() throws Exception {
     tr.update(REFS_VERSION, tr.blob("123"));
-    try {
-      manager.increment(456);
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo("Expected old version 456 for refs/meta/version, found 123");
-    }
+    StorageException thrown = assertThrows(StorageException.class, () -> manager.increment(456));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Expected old version 456 for refs/meta/version, found 123");
   }
 }
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
index 7bc3848..31697fd 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
@@ -24,11 +24,10 @@
 import com.google.common.collect.Streams;
 import com.google.common.reflect.ClassPath;
 import com.google.common.reflect.ClassPath.ClassInfo;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.stream.IntStream;
 import org.junit.Test;
 
-public class NoteDbSchemaVersionsTest extends GerritBaseTests {
+public class NoteDbSchemaVersionsTest {
   @Test
   public void testGuessVersion() {
     assertThat(guessVersion(getClass())).isEmpty();
diff --git a/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java b/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java
index c5c956c..da22f76 100644
--- a/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java
+++ b/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java
@@ -17,7 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -103,7 +103,7 @@
       return factory
           .read(
               new MetaDataUpdate(
-                  GitReferenceUpdated.DISABLED, new Project.NameKey(ALL_PROJECTS), repo, null))
+                  GitReferenceUpdated.DISABLED, Project.nameKey(ALL_PROJECTS), repo, null))
           .getConfig();
     }
   }
diff --git a/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
index 35f580c..c92a8e0 100644
--- a/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
+++ b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.inject.Inject;
 import java.util.ArrayList;
@@ -36,7 +35,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class SchemaCreatorImplTest extends GerritBaseTests {
+public class SchemaCreatorImplTest {
   @Inject private AllProjectsName allProjects;
 
   @Inject private GitRepositoryManager repoManager;
@@ -82,7 +81,7 @@
   private void assertValueRange(LabelType label, Integer... range) {
     List<Integer> rangeList = Arrays.asList(range);
     assertThat(rangeList).isNotEmpty();
-    assertThat(rangeList).isStrictlyOrdered();
+    assertThat(rangeList).isInStrictOrder();
 
     assertThat(label.getValues().stream().map(v -> (int) v.getValue()))
         .containsExactlyElementsIn(rangeList)
diff --git a/javatests/com/google/gerrit/server/schema/TestGroup.java b/javatests/com/google/gerrit/server/schema/TestGroup.java
index 4627e8b..cca6d6c 100644
--- a/javatests/com/google/gerrit/server/schema/TestGroup.java
+++ b/javatests/com/google/gerrit/server/schema/TestGroup.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.schema;
 
 import com.google.auto.value.AutoValue;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.util.time.TimeUtil;
 import java.sql.Timestamp;
 import java.util.Optional;
@@ -47,7 +47,7 @@
     public abstract Builder setNameKey(AccountGroup.NameKey nameKey);
 
     public Builder setName(String name) {
-      return setNameKey(new AccountGroup.NameKey(name));
+      return setNameKey(AccountGroup.nameKey(name));
     }
 
     public abstract Builder setGroupUuid(AccountGroup.UUID uuid);
@@ -66,10 +66,9 @@
 
     public AccountGroup build() {
       TestGroup testGroup = autoBuild();
-      AccountGroup.NameKey name = testGroup.getNameKey().orElse(new AccountGroup.NameKey("users"));
-      AccountGroup.Id id = testGroup.getId().orElse(new AccountGroup.Id(Math.abs(name.hashCode())));
-      AccountGroup.UUID uuid =
-          testGroup.getGroupUuid().orElse(new AccountGroup.UUID(name + "-UUID"));
+      AccountGroup.NameKey name = testGroup.getNameKey().orElse(AccountGroup.nameKey("users"));
+      AccountGroup.Id id = testGroup.getId().orElse(AccountGroup.id(Math.abs(name.hashCode())));
+      AccountGroup.UUID uuid = testGroup.getGroupUuid().orElse(AccountGroup.uuid(name + "-UUID"));
       Timestamp createdOn = testGroup.getCreatedOn().orElseGet(TimeUtil::nowTs);
       AccountGroup accountGroup = new AccountGroup(name, id, uuid, createdOn);
       testGroup.getOwnerGroupUuid().ifPresent(accountGroup::setOwnerGroupUUID);
diff --git a/javatests/com/google/gerrit/server/update/BUILD b/javatests/com/google/gerrit/server/update/BUILD
index c98b35f..d1030b5 100644
--- a/javatests/com/google/gerrit/server/update/BUILD
+++ b/javatests/com/google/gerrit/server/update/BUILD
@@ -4,15 +4,23 @@
     name = "small_tests",
     size = "small",
     srcs = glob(["*.java"]),
+    runtime_deps = [
+        "//java/com/google/gerrit/lucene",
+        "//prolog:gerrit-prolog-common",
+    ],
     deps = [
-        "//java/com/google/gerrit/reviewdb:server",
+        "//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/server",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
     ],
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index cca844e..a2f800a 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -14,36 +14,67 @@
 
 package com.google.gerrit.server.update;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.RequestId;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.notedb.TooManyUpdatesException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryTestEnvironment;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
-public class BatchUpdateTest extends GerritBaseTests {
-  @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
+public class BatchUpdateTest {
+  private static final int MAX_UPDATES = 4;
 
-  @Inject private GitRepositoryManager repoManager;
+  @Rule
+  public InMemoryTestEnvironment testEnvironment =
+      new InMemoryTestEnvironment(
+          () -> {
+            Config cfg = new Config();
+            cfg.setInt("change", null, "maxUpdates", MAX_UPDATES);
+            return cfg;
+          });
+
   @Inject private BatchUpdate.Factory batchUpdateFactory;
+  @Inject private ChangeInserter.Factory changeInserterFactory;
+  @Inject private ChangeNotes.Factory changeNotesFactory;
+  @Inject private GitRepositoryManager repoManager;
+  @Inject private PatchSetInserter.Factory patchSetInserterFactory;
   @Inject private Provider<CurrentUser> user;
+  @Inject private Sequences sequences;
 
   private Project.NameKey project;
   private TestRepository<Repository> repo;
 
   @Before
   public void setUp() throws Exception {
-    project = new Project.NameKey("test");
+    project = Project.nameKey("test");
 
     Repository inMemoryRepo = repoManager.createRepository(project);
     repo = new TestRepository<>(inMemoryRepo);
@@ -68,4 +99,220 @@
     assertThat(repo.getRepository().exactRef("refs/heads/master").getObjectId())
         .isEqualTo(branchCommit.getId());
   }
+
+  @Test
+  public void cannotExceedMaxUpdates() throws Exception {
+    Change.Id id = createChangeWithUpdates(MAX_UPDATES);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(id, new AddMessageOp("Excessive update"));
+      ResourceConflictException thrown =
+          assertThrows(ResourceConflictException.class, () -> bu.execute());
+      assertThat(thrown)
+          .hasMessageThat()
+          .isEqualTo(TooManyUpdatesException.message(id, MAX_UPDATES));
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES);
+    assertThat(getMetaId(id)).isEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void cannotExceedMaxUpdatesCountingMultipleChangeUpdatesInSingleBatch() throws Exception {
+    Change.Id id = createChangeWithTwoPatchSets(MAX_UPDATES - 1);
+
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(id, new AddMessageOp("Update on PS1", PatchSet.id(id, 1)));
+      bu.addOp(id, new AddMessageOp("Update on PS2", PatchSet.id(id, 2)));
+      ResourceConflictException thrown =
+          assertThrows(ResourceConflictException.class, () -> bu.execute());
+      assertThat(thrown)
+          .hasMessageThat()
+          .isEqualTo(TooManyUpdatesException.message(id, MAX_UPDATES));
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES - 1);
+    assertThat(getMetaId(id)).isEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void exceedingMaxUpdatesAllowedWithCompleteNoOp() throws Exception {
+    Change.Id id = createChangeWithUpdates(MAX_UPDATES);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(
+          id,
+          new BatchUpdateOp() {
+            @Override
+            public boolean updateChange(ChangeContext ctx) {
+              return false;
+            }
+          });
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES);
+    assertThat(getMetaId(id)).isEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void exceedingMaxUpdatesAllowedWithNoOpAfterPopulatingUpdate() throws Exception {
+    Change.Id id = createChangeWithUpdates(MAX_UPDATES);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(
+          id,
+          new BatchUpdateOp() {
+            @Override
+            public boolean updateChange(ChangeContext ctx) {
+              ctx.getUpdate(ctx.getChange().currentPatchSetId()).setChangeMessage("No-op");
+              return false;
+            }
+          });
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES);
+    assertThat(getMetaId(id)).isEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void exceedingMaxUpdatesAllowedWithSubmit() throws Exception {
+    Change.Id id = createChangeWithUpdates(MAX_UPDATES);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(id, new SubmitOp());
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES + 1);
+    assertThat(getMetaId(id)).isNotEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void exceedingMaxUpdatesAllowedWithSubmitAfterOtherOp() throws Exception {
+    Change.Id id = createChangeWithTwoPatchSets(MAX_UPDATES - 1);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(id, new AddMessageOp("Message on PS1", PatchSet.id(id, 1)));
+      bu.addOp(id, new SubmitOp());
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES + 1);
+    assertThat(getMetaId(id)).isNotEqualTo(oldMetaId);
+  }
+  // Not possible to write a variant of this test that submits first and adds a message second in
+  // the same batch, since submit always comes last.
+
+  @Test
+  public void exceedingMaxUpdatesAllowedWithAbandon() throws Exception {
+    Change.Id id = createChangeWithUpdates(MAX_UPDATES);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(
+          id,
+          new BatchUpdateOp() {
+            @Override
+            public boolean updateChange(ChangeContext ctx) {
+              ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+              update.setChangeMessage("Abandon");
+              update.setStatus(Change.Status.ABANDONED);
+              return true;
+            }
+          });
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES + 1);
+    assertThat(getMetaId(id)).isNotEqualTo(oldMetaId);
+  }
+
+  private Change.Id createChangeWithUpdates(int totalUpdates) throws Exception {
+    checkArgument(totalUpdates > 0);
+    checkArgument(totalUpdates <= MAX_UPDATES);
+    Change.Id id = Change.id(sequences.nextChangeId());
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.insertChange(
+          changeInserterFactory.create(
+              id, repo.commit().message("Change").insertChangeId().create(), "refs/heads/master"));
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(1);
+    for (int i = 2; i <= totalUpdates; i++) {
+      try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+        bu.addOp(id, new AddMessageOp("Update " + i));
+        bu.execute();
+      }
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(totalUpdates);
+    return id;
+  }
+
+  private Change.Id createChangeWithTwoPatchSets(int totalUpdates) throws Exception {
+    Change.Id id = createChangeWithUpdates(totalUpdates - 1);
+    ChangeNotes notes = changeNotesFactory.create(project, id);
+
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      ObjectId commitId = repo.amend(notes.getCurrentPatchSet().commitId()).message("PS2").create();
+      bu.addOp(
+          id,
+          patchSetInserterFactory
+              .create(notes, PatchSet.id(id, 2), commitId)
+              .setMessage("Add PS2"));
+      bu.execute();
+    }
+
+    assertThat(getUpdateCount(id)).isEqualTo(totalUpdates);
+    return id;
+  }
+
+  private static class AddMessageOp implements BatchUpdateOp {
+    private final String message;
+    @Nullable private final PatchSet.Id psId;
+
+    AddMessageOp(String message) {
+      this(message, null);
+    }
+
+    AddMessageOp(String message, PatchSet.Id psId) {
+      this.message = message;
+      this.psId = psId;
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx) throws Exception {
+      PatchSet.Id psIdToUpdate = psId;
+      if (psIdToUpdate == null) {
+        psIdToUpdate = ctx.getChange().currentPatchSetId();
+      } else {
+        checkState(
+            ctx.getNotes().getPatchSets().containsKey(psIdToUpdate),
+            "%s not in %s",
+            psIdToUpdate,
+            ctx.getNotes().getPatchSets().keySet());
+      }
+      ctx.getUpdate(psIdToUpdate).setChangeMessage(message);
+      return true;
+    }
+  }
+
+  private int getUpdateCount(Change.Id changeId) throws Exception {
+    return changeNotesFactory.create(project, changeId).getUpdateCount();
+  }
+
+  private ObjectId getMetaId(Change.Id changeId) throws Exception {
+    return repo.getRepository().exactRef(RefNames.changeMetaRef(changeId)).getObjectId();
+  }
+
+  private static class SubmitOp implements BatchUpdateOp {
+    @Override
+    public boolean updateChange(ChangeContext ctx) throws Exception {
+      SubmitRecord sr = new SubmitRecord();
+      sr.status = SubmitRecord.Status.OK;
+      SubmitRecord.Label cr = new SubmitRecord.Label();
+      cr.status = SubmitRecord.Label.Status.OK;
+      cr.appliedBy = ctx.getAccountId();
+      cr.label = "Code-Review";
+      sr.labels = ImmutableList.of(cr);
+      ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+      update.merge(new RequestId(), ImmutableList.of(sr));
+      update.setChangeMessage("Submitted");
+      return true;
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/server/update/RepoViewTest.java b/javatests/com/google/gerrit/server/update/RepoViewTest.java
index b41c66c..b37e302 100644
--- a/javatests/com/google/gerrit/server/update/RepoViewTest.java
+++ b/javatests/com/google/gerrit/server/update/RepoViewTest.java
@@ -18,8 +18,7 @@
 import static com.google.common.truth.Truth8.assertThat;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -31,7 +30,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class RepoViewTest extends GerritBaseTests {
+public class RepoViewTest {
   private static final String MASTER = "refs/heads/master";
   private static final String BRANCH = "refs/heads/branch";
 
@@ -42,7 +41,7 @@
   @Before
   public void setUp() throws Exception {
     InMemoryRepositoryManager repoManager = new InMemoryRepositoryManager();
-    Project.NameKey project = new Project.NameKey("project");
+    Project.NameKey project = Project.nameKey("project");
     repo = repoManager.createRepository(project);
     tr = new TestRepository<>(repo);
     tr.branch(MASTER).commit().create();
diff --git a/javatests/com/google/gerrit/server/util/IdGeneratorTest.java b/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
index e702656..808eca8 100644
--- a/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
+++ b/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
@@ -17,11 +17,10 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.HashSet;
 import org.junit.Test;
 
-public class IdGeneratorTest extends GerritBaseTests {
+public class IdGeneratorTest {
   @Test
   public void test1234() {
     final HashSet<Integer> seen = new HashSet<>();
diff --git a/javatests/com/google/gerrit/server/util/LabelVoteTest.java b/javatests/com/google/gerrit/server/util/LabelVoteTest.java
index 3048b75..bda99a8 100644
--- a/javatests/com/google/gerrit/server/util/LabelVoteTest.java
+++ b/javatests/com/google/gerrit/server/util/LabelVoteTest.java
@@ -15,14 +15,13 @@
 package com.google.gerrit.server.util;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.server.util.LabelVote.parse;
 import static com.google.gerrit.server.util.LabelVote.parseWithEquals;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class LabelVoteTest extends GerritBaseTests {
+public class LabelVoteTest {
   @Test
   public void labelVoteParse() {
     assertLabelVoteEquals(parse("Code-Review-2"), "Code-Review", -2);
@@ -83,11 +82,6 @@
   }
 
   private void assertParseWithEqualsFails(String value) {
-    try {
-      parseWithEquals(value);
-      assert_().fail("expected IllegalArgumentException when parsing \"%s\"", value);
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(IllegalArgumentException.class, () -> parseWithEquals(value));
   }
 }
diff --git a/javatests/com/google/gerrit/server/util/MostSpecificComparatorTest.java b/javatests/com/google/gerrit/server/util/MostSpecificComparatorTest.java
index 9ea17f3..025bf84 100644
--- a/javatests/com/google/gerrit/server/util/MostSpecificComparatorTest.java
+++ b/javatests/com/google/gerrit/server/util/MostSpecificComparatorTest.java
@@ -16,10 +16,9 @@
 
 import static org.junit.Assert.assertTrue;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
-public class MostSpecificComparatorTest extends GerritBaseTests {
+public class MostSpecificComparatorTest {
 
   private MostSpecificComparator cmp;
 
diff --git a/javatests/com/google/gerrit/server/util/SocketUtilTest.java b/javatests/com/google/gerrit/server/util/SocketUtilTest.java
index 018b8db..25114f9 100644
--- a/javatests/com/google/gerrit/server/util/SocketUtilTest.java
+++ b/javatests/com/google/gerrit/server/util/SocketUtilTest.java
@@ -14,17 +14,18 @@
 
 package com.google.gerrit.server.util;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.util.SocketUtil.hostname;
 import static com.google.gerrit.server.util.SocketUtil.isIPv6;
 import static com.google.gerrit.server.util.SocketUtil.parse;
 import static com.google.gerrit.server.util.SocketUtil.resolve;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.net.InetAddress.getByName;
 import static java.net.InetSocketAddress.createUnresolved;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -32,7 +33,7 @@
 import java.net.UnknownHostException;
 import org.junit.Test;
 
-public class SocketUtilTest extends GerritBaseTests {
+public class SocketUtilTest {
   @Test
   public void testIsIPv6() throws UnknownHostException {
     final InetAddress ipv6 = getByName("1:2:3:4:5:6:7:8");
@@ -105,16 +106,16 @@
 
   @Test
   public void testParseInvalidIPv6() {
-    exception.expect(IllegalArgumentException.class);
-    exception.expectMessage("invalid IPv6: [:3");
-    parse("[:3", 80);
+    IllegalArgumentException thrown =
+        assertThrows(IllegalArgumentException.class, () -> parse("[:3", 80));
+    assertThat(thrown).hasMessageThat().contains("invalid IPv6: [:3");
   }
 
   @Test
   public void testParseInvalidPort() {
-    exception.expect(IllegalArgumentException.class);
-    exception.expectMessage("invalid port: localhost:A");
-    parse("localhost:A", 80);
+    IllegalArgumentException thrown =
+        assertThrows(IllegalArgumentException.class, () -> parse("localhost:A", 80));
+    assertThat(thrown).hasMessageThat().contains("invalid port: localhost:A");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/util/git/BUILD b/javatests/com/google/gerrit/server/util/git/BUILD
index 0cb7b8a..883898f 100644
--- a/javatests/com/google/gerrit/server/util/git/BUILD
+++ b/javatests/com/google/gerrit/server/util/git/BUILD
@@ -8,20 +8,18 @@
     ),
     visibility = ["//visibility:public"],
     deps = [
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server/util/git",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//java/com/google/gerrit/truth",
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
+        "//lib:jgit",
+        "//lib:jgit-junit",
         "//lib:protobuf",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/commons:codec",
         "//lib/guice",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
         "//lib/truth:truth-proto-extension",
diff --git a/javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java b/javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java
index c5e683f..531785f 100644
--- a/javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java
+++ b/javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java
@@ -17,20 +17,19 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.SubmoduleSubscription;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-public class SubmoduleSectionParserTest extends GerritBaseTests {
+public class SubmoduleSectionParserTest {
   private static final String THIS_SERVER = "http://localhost/";
 
   @Test
   public void followMasterBranch() throws Exception {
-    Project.NameKey p = new Project.NameKey("proj");
+    Project.NameKey p = Project.nameKey("proj");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -40,7 +39,7 @@
             + p.get()
             + "\n"
             + "branch = master\n");
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
@@ -48,14 +47,14 @@
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
             new SubmoduleSubscription(
-                targetBranch, new Branch.NameKey(p, "master"), "localpath-to-a"));
+                targetBranch, BranchNameKey.create(p, "master"), "localpath-to-a"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
 
   @Test
   public void followMatchingBranch() throws Exception {
-    Project.NameKey p = new Project.NameKey("a");
+    Project.NameKey p = Project.nameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -66,32 +65,32 @@
             + "\n"
             + "branch = .\n");
 
-    Branch.NameKey targetBranch1 = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch1 = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res1 =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch1).parseAllSections();
 
     Set<SubmoduleSubscription> expected1 =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch1, new Branch.NameKey(p, "master"), "a"));
+            new SubmoduleSubscription(targetBranch1, BranchNameKey.create(p, "master"), "a"));
 
     assertThat(res1).containsExactlyElementsIn(expected1);
 
-    Branch.NameKey targetBranch2 = new Branch.NameKey(new Project.NameKey("project"), "somebranch");
+    BranchNameKey targetBranch2 = BranchNameKey.create(Project.nameKey("project"), "somebranch");
 
     Set<SubmoduleSubscription> res2 =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch2).parseAllSections();
 
     Set<SubmoduleSubscription> expected2 =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch2, new Branch.NameKey(p, "somebranch"), "a"));
+            new SubmoduleSubscription(targetBranch2, BranchNameKey.create(p, "somebranch"), "a"));
 
     assertThat(res2).containsExactlyElementsIn(expected2);
   }
 
   @Test
   public void followAnotherBranch() throws Exception {
-    Project.NameKey p = new Project.NameKey("a");
+    Project.NameKey p = Project.nameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -102,21 +101,21 @@
             + "\n"
             + "branch = anotherbranch\n");
 
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
 
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p, "anotherbranch"), "a"));
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p, "anotherbranch"), "a"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
 
   @Test
   public void withAnotherURI() throws Exception {
-    Project.NameKey p = new Project.NameKey("a");
+    Project.NameKey p = Project.nameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -127,21 +126,21 @@
             + "\n"
             + "branch = master\n");
 
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
 
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p, "master"), "a"));
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p, "master"), "a"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
 
   @Test
   public void withSlashesInProjectName() throws Exception {
-    Project.NameKey p = new Project.NameKey("project/with/slashes/a");
+    Project.NameKey p = Project.nameKey("project/with/slashes/a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -152,21 +151,21 @@
             + "\n"
             + "branch = master\n");
 
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
 
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p, "master"), "a"));
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p, "master"), "a"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
 
   @Test
   public void withSlashesInPath() throws Exception {
-    Project.NameKey p = new Project.NameKey("a");
+    Project.NameKey p = Project.nameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -177,22 +176,23 @@
             + "\n"
             + "branch = master\n");
 
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
 
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p, "master"), "a/b/c/d/e"));
+            new SubmoduleSubscription(
+                targetBranch, BranchNameKey.create(p, "master"), "a/b/c/d/e"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
 
   @Test
   public void withMoreSections() throws Exception {
-    Project.NameKey p1 = new Project.NameKey("a");
-    Project.NameKey p2 = new Project.NameKey("b");
+    Project.NameKey p1 = Project.nameKey("a");
+    Project.NameKey p2 = Project.nameKey("b");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -209,23 +209,23 @@
             + "\n"
             + "		branch = master\n");
 
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
 
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p1, "master"), "a"),
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p2, "master"), "b"));
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p1, "master"), "a"),
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p2, "master"), "b"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
 
   @Test
   public void withSubProjectFound() throws Exception {
-    Project.NameKey p1 = new Project.NameKey("a/b");
-    Project.NameKey p2 = new Project.NameKey("b");
+    Project.NameKey p1 = Project.nameKey("a/b");
+    Project.NameKey p2 = Project.nameKey("b");
     Config cfg = new Config();
     cfg.fromText(
         "\n"
@@ -242,25 +242,25 @@
             + "\n"
             + "branch = .\n");
 
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
 
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p2, "master"), "b"),
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p1, "master"), "a/b"));
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p2, "master"), "b"),
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p1, "master"), "a/b"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
 
   @Test
   public void withAnInvalidSection() throws Exception {
-    Project.NameKey p1 = new Project.NameKey("a");
-    Project.NameKey p2 = new Project.NameKey("b");
-    Project.NameKey p3 = new Project.NameKey("d");
-    Project.NameKey p4 = new Project.NameKey("e");
+    Project.NameKey p1 = Project.nameKey("a");
+    Project.NameKey p2 = Project.nameKey("b");
+    Project.NameKey p3 = Project.nameKey("d");
+    Project.NameKey p4 = Project.nameKey("e");
     Config cfg = new Config();
     cfg.fromText(
         "\n"
@@ -293,15 +293,15 @@
             + "\n"
             + "    branch = refs/heads/master\n");
 
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
 
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p1, "master"), "a"),
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p4, "master"), "e"));
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p1, "master"), "a"),
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p4, "master"), "e"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
@@ -317,7 +317,7 @@
             // Project "a" doesn't exist
             + "branch = .\\n");
 
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
@@ -327,7 +327,7 @@
 
   @Test
   public void withSectionToOtherServer() throws Exception {
-    Project.NameKey p1 = new Project.NameKey("a");
+    Project.NameKey p1 = Project.nameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -338,7 +338,7 @@
             + "\n"
             + "branch = .");
 
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
@@ -348,7 +348,7 @@
 
   @Test
   public void withRelativeURI() throws Exception {
-    Project.NameKey p1 = new Project.NameKey("a");
+    Project.NameKey p1 = Project.nameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -359,21 +359,21 @@
             + "\n"
             + "branch = master\n");
 
-    Branch.NameKey targetBranch = new Branch.NameKey(new Project.NameKey("project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
 
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p1, "master"), "a"));
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p1, "master"), "a"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
 
   @Test
   public void withDeepRelativeURI() throws Exception {
-    Project.NameKey p1 = new Project.NameKey("a");
+    Project.NameKey p1 = Project.nameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -384,22 +384,21 @@
             + "\n"
             + "branch = master\n");
 
-    Branch.NameKey targetBranch =
-        new Branch.NameKey(new Project.NameKey("nested/project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("nested/project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
 
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p1, "master"), "a"));
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p1, "master"), "a"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
 
   @Test
   public void withOverlyDeepRelativeURI() throws Exception {
-    Project.NameKey p1 = new Project.NameKey("nested/a");
+    Project.NameKey p1 = Project.nameKey("nested/a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -410,15 +409,14 @@
             + "\n"
             + "branch = master\n");
 
-    Branch.NameKey targetBranch =
-        new Branch.NameKey(new Project.NameKey("nested/project"), "master");
+    BranchNameKey targetBranch = BranchNameKey.create(Project.nameKey("nested/project"), "master");
 
     Set<SubmoduleSubscription> res =
         new SubmoduleSectionParser(cfg, THIS_SERVER, targetBranch).parseAllSections();
 
     Set<SubmoduleSubscription> expected =
         Sets.newHashSet(
-            new SubmoduleSubscription(targetBranch, new Branch.NameKey(p1, "master"), "a"));
+            new SubmoduleSubscription(targetBranch, BranchNameKey.create(p1, "master"), "a"));
 
     assertThat(res).containsExactlyElementsIn(expected);
   }
diff --git a/javatests/com/google/gerrit/sshd/BUILD b/javatests/com/google/gerrit/sshd/BUILD
index c010d7c..3e11ff2 100644
--- a/javatests/com/google/gerrit/sshd/BUILD
+++ b/javatests/com/google/gerrit/sshd/BUILD
@@ -6,7 +6,6 @@
     deps = [
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/sshd",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib/mina:sshd",
         "//lib/truth",
     ],
diff --git a/javatests/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java b/javatests/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
index b663849..777cb4f 100644
--- a/javatests/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
+++ b/javatests/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
@@ -17,13 +17,12 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.extensions.api.projects.ConfigValue;
-import com.google.gerrit.testing.GerritBaseTests;
 import java.util.Collections;
 import java.util.Map;
 import org.junit.Before;
 import org.junit.Test;
 
-public class ProjectConfigParamParserTest extends GerritBaseTests {
+public class ProjectConfigParamParserTest {
 
   private CreateProjectCommand cmd;
 
diff --git a/javatests/com/google/gerrit/testing/GerritJUnitTest.java b/javatests/com/google/gerrit/testing/GerritJUnitTest.java
index 430f48f..56dda08 100644
--- a/javatests/com/google/gerrit/testing/GerritJUnitTest.java
+++ b/javatests/com/google/gerrit/testing/GerritJUnitTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.testing;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import org.junit.Test;
@@ -68,7 +68,7 @@
           () -> {
             throw new MyException("foo");
           });
-      assert_().fail("expected AssertionError");
+      assertWithMessage("expected AssertionError").fail();
     } catch (AssertionError e) {
       assertThat(e).hasMessageThat().contains(IllegalStateException.class.getSimpleName());
       assertThat(e).hasMessageThat().contains(MyException.class.getSimpleName());
@@ -81,7 +81,7 @@
   public void assertThrowsThrowsAssertionErrorWhenNothingThrown() {
     try {
       assertThrows(MyException.class, () -> {});
-      assert_().fail("expected AssertionError");
+      assertWithMessage("expected AssertionError").fail();
     } catch (AssertionError e) {
       assertThat(e).hasMessageThat().contains(MyException.class.getSimpleName());
       assertThat(e).hasCauseThat().isNull();
diff --git a/javatests/com/google/gerrit/testing/IndexVersionsTest.java b/javatests/com/google/gerrit/testing/IndexVersionsTest.java
index 36247f8..0362ddc 100644
--- a/javatests/com/google/gerrit/testing/IndexVersionsTest.java
+++ b/javatests/com/google/gerrit/testing/IndexVersionsTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.testing.IndexVersions.ALL;
 import static com.google.gerrit.testing.IndexVersions.CURRENT;
 import static com.google.gerrit.testing.IndexVersions.PREVIOUS;
@@ -25,7 +26,7 @@
 import java.util.List;
 import org.junit.Test;
 
-public class IndexVersionsTest extends GerritBaseTests {
+public class IndexVersionsTest {
   private static final ChangeSchemaDefinitions SCHEMA_DEF = ChangeSchemaDefinitions.INSTANCE;
 
   @Test
@@ -133,8 +134,8 @@
   }
 
   private void assertIllegalArgument(String value, String expectedMessage) {
-    exception.expect(IllegalArgumentException.class);
-    exception.expectMessage(expectedMessage);
-    get(value);
+    IllegalArgumentException thrown =
+        assertThrows(IllegalArgumentException.class, () -> get(value));
+    assertThat(thrown).hasMessageThat().contains(expectedMessage);
   }
 }
diff --git a/javatests/com/google/gerrit/util/http/BUILD b/javatests/com/google/gerrit/util/http/BUILD
index 8f6afbb..4711faa 100644
--- a/javatests/com/google/gerrit/util/http/BUILD
+++ b/javatests/com/google/gerrit/util/http/BUILD
@@ -4,12 +4,10 @@
     name = "http_tests",
     srcs = glob(["**/*.java"]),
     deps = [
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//java/com/google/gerrit/util/http",
         "//javatests/com/google/gerrit/util/http/testutil",
         "//lib:junit",
         "//lib:servlet-api-without-neverlink",
-        "//lib/easymock",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/util/http/RequestUtilTest.java b/javatests/com/google/gerrit/util/http/RequestUtilTest.java
index adda5e7..bef9d4b1 100644
--- a/javatests/com/google/gerrit/util/http/RequestUtilTest.java
+++ b/javatests/com/google/gerrit/util/http/RequestUtilTest.java
@@ -18,11 +18,10 @@
 import static com.google.gerrit.util.http.RequestUtil.getEncodedPathInfo;
 import static com.google.gerrit.util.http.RequestUtil.getRestPathWithoutIds;
 
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 import org.junit.Test;
 
-public class RequestUtilTest extends GerritBaseTests {
+public class RequestUtilTest {
   @Test
   public void getEncodedPathInfo_emptyContextPath() {
     assertThat(getEncodedPathInfo(fakeRequest("", "/s", "/foo/bar"))).isEqualTo("/foo/bar");
diff --git a/javatests/com/google/gerrit/util/http/testutil/BUILD b/javatests/com/google/gerrit/util/http/testutil/BUILD
index d7ac5bd..3a67d45 100644
--- a/javatests/com/google/gerrit/util/http/testutil/BUILD
+++ b/javatests/com/google/gerrit/util/http/testutil/BUILD
@@ -7,8 +7,8 @@
     visibility = ["//visibility:public"],
     deps = [
         "//lib:guava",
+        "//lib:jgit",
         "//lib:servlet-api",
         "//lib/httpcomponents:httpclient",
-        "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
index a4175e3..0bb4de4 100644
--- a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
+++ b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
@@ -66,6 +67,7 @@
   private String contextPath;
   private String servletPath;
   private String path;
+  private String method;
 
   public FakeHttpServletRequest() {
     this("gerrit.example.com", 80, "", SERVLET_PATH);
@@ -80,6 +82,7 @@
     attributes = Maps.newConcurrentMap();
     parameters = LinkedListMultimap.create();
     headers = LinkedListMultimap.create();
+    method = "GET";
   }
 
   @Override
@@ -104,6 +107,11 @@
 
   @Override
   public String getContentType() {
+    List<String> contentType = headers.get("Content-Type");
+    if (contentType != null && !contentType.isEmpty()) {
+      return contentType.get(0);
+    }
+
     return null;
   }
 
@@ -257,7 +265,15 @@
 
   @Override
   public Cookie[] getCookies() {
-    return new Cookie[0];
+    return Splitter.on(";").splitToList(Strings.nullToEmpty(getHeader("Cookie"))).stream()
+        .filter(s -> !s.isEmpty())
+        .map(
+            (String cookieValue) -> {
+              String[] kv = cookieValue.split("=");
+              return new Cookie(kv[0], kv[1]);
+            })
+        .collect(toList())
+        .toArray(new Cookie[0]);
   }
 
   @Override
@@ -288,7 +304,11 @@
 
   @Override
   public String getMethod() {
-    return "GET";
+    return method;
+  }
+
+  public void setMethod(String method) {
+    this.method = method;
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
index 9a98ecd..f39b875 100644
--- a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
+++ b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
@@ -161,7 +161,7 @@
 
   @Override
   public void addCookie(Cookie cookie) {
-    throw new UnsupportedOperationException();
+    addHeader("Set-Cookie", cookie.getName() + "=" + cookie.getValue());
   }
 
   @Override
diff --git a/lib/BUILD b/lib/BUILD
index 2ea8f54..2e5668e 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -36,6 +36,49 @@
 )
 
 java_library(
+    name = "jgit",
+    data = ["//lib:LICENSE-jgit"],
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit:jgit"],
+    runtime_deps = [
+        ":javaewah",
+        "//lib/log:api",
+    ],
+)
+
+java_library(
+    name = "jgit-archive",
+    data = ["//lib:LICENSE-jgit"],
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit.archive:jgit-archive"],
+    runtime_deps = [":jgit"],
+)
+
+java_library(
+    name = "jgit-junit",
+    testonly = True,
+    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit.junit:junit"],
+    runtime_deps = [":jgit"],
+)
+
+java_library(
+    name = "jgit-servlet",
+    data = ["//lib:LICENSE-jgit"],
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit.http.server:jgit-servlet"],
+    runtime_deps = [":jgit"],
+)
+
+java_library(
+    name = "javaewah",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@javaewah//jar"],
+)
+
+java_library(
     name = "protobuf",
     data = ["//lib:LICENSE-protobuf"],
     visibility = ["//visibility:public"],
@@ -71,6 +114,7 @@
     name = "caffeine",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = [
+        "//java/com/google/gerrit/acceptance:__pkg__",
         "//java/com/google/gerrit/server/cache/mem:__pkg__",
     ],
     exports = ["@caffeine//jar"],
@@ -85,6 +129,7 @@
     name = "caffeine-guava",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = [
+        "//java/com/google/gerrit/acceptance:__pkg__",
         "//java/com/google/gerrit/server/cache/mem:__pkg__",
     ],
     exports = [":caffeine-guava-renamed"],
@@ -442,13 +487,6 @@
 )
 
 java_library(
-    name = "javassist",
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = ["@javassist//jar"],
-)
-
-java_library(
     name = "soy",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
@@ -485,6 +523,18 @@
     exports = ["@icu4j//jar"],
 )
 
+java_library(
+    name = "javax-annotation",
+    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
+    visibility = [
+        "//java/com/google/gerrit/acceptance:__pkg__",
+        "//java/com/google/gerrit/extensions:__pkg__",
+        "//java/com/google/gerrit/server:__pkg__",
+        "//plugins:__pkg__",
+    ],
+    exports = ["@javax-annotation//jar"],
+)
+
 sh_test(
     name = "nongoogle_test",
     srcs = ["nongoogle_test.sh"],
diff --git a/lib/LICENSE-shadycss b/lib/LICENSE-shadycss
new file mode 100644
index 0000000..0fe5c52
--- /dev/null
+++ b/lib/LICENSE-shadycss
@@ -0,0 +1,20 @@
+# License
+
+Everything in this repo is BSD style license unless otherwise specified.
+
+Copyright (c) 2015 The Polymer Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+* Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/lib/easymock/BUILD b/lib/easymock/BUILD
deleted file mode 100644
index 90c9673..0000000
--- a/lib/easymock/BUILD
+++ /dev/null
@@ -1,26 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-
-java_library(
-    name = "easymock",
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = ["@easymock//jar"],
-    runtime_deps = [
-        ":cglib-3_2",
-        ":objenesis",
-    ],
-)
-
-java_library(
-    name = "cglib-3_2",
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = ["@cglib-3_2//jar"],
-)
-
-java_library(
-    name = "objenesis",
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = ["@objenesis//jar"],
-)
diff --git a/lib/errorprone/BUILD b/lib/errorprone/BUILD
new file mode 100644
index 0000000..456860a
--- /dev/null
+++ b/lib/errorprone/BUILD
@@ -0,0 +1,8 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+    name = "annotations",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@error-prone-annotations//jar"],
+)
diff --git a/lib/greenmail/BUILD b/lib/greenmail/BUILD
index e8845e2..68da16a 100644
--- a/lib/greenmail/BUILD
+++ b/lib/greenmail/BUILD
@@ -17,7 +17,7 @@
     data = ["//lib:LICENSE-Apache2.0"],
     exports = ["@greenmail//jar"],
     runtime_deps = select({
-        "//:java9": POST_JDK8_DEPS,
+        "//:java11": POST_JDK8_DEPS,
         "//:java_next": POST_JDK8_DEPS,
         "//conditions:default": [],
     }),
diff --git a/lib/guava.bzl b/lib/guava.bzl
index c36bf14..18a8355 100644
--- a/lib/guava.bzl
+++ b/lib/guava.bzl
@@ -1,5 +1,5 @@
-GUAVA_VERSION = "27.1-jre"
+GUAVA_VERSION = "28.1-jre"
 
-GUAVA_BIN_SHA1 = "e47b59c893079b87743cdcfb6f17ca95c08c592c"
+GUAVA_BIN_SHA1 = "b0e91dcb6a44ffb6221b5027e12a5cb34b841145"
 
 GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
diff --git a/lib/highlightjs/building.md b/lib/highlightjs/building.md
index fcd2864..18c5746 100644
--- a/lib/highlightjs/building.md
+++ b/lib/highlightjs/building.md
@@ -26,27 +26,10 @@
     $>  ln -s ../../highlightjs-closure-templates/soy.js src/languages/soy.js
     $>  mkdir test/detect/soy && ln -s ../../../highlightjs-closure-templates/test/detect/soy/default.txt test/detect/soy/default.txt
     $>  npm install
-    $>  node tools/build.js -n
+    $>  node tools/build.js
 
-The resulting JS file will appear in the "build" directory of the Highlight.js
-repo under the name "highlight.pack.js".
-
-## Minification
-
-Minify the file using closure-compiler using the command below.
-
-    $> wget https://dl.google.com/closure-compiler/compiler-latest.zip
-
-    $> unzip compiler-latest.zip
-
-    $> mv closure-compiler-*.jar closure-compiler.jar
-
-    $>  java -jar ./closure-compiler.jar \
-            --js build/highlight.js \
-            --js_output_file build/highlight.min.js
-
-Copy the header comment that appears on the first line of
-build/highlight.pack.js and add it to the start of build/highlight.min.js.
+The resulting minified JS file will appear in the "build" directory of the Highlight.js
+repo under the name "highlight.min.js".
 
 ## Finish
 
diff --git a/lib/highlightjs/highlight.min.js b/lib/highlightjs/highlight.min.js
index 959e442..458cb0c 100644
--- a/lib/highlightjs/highlight.min.js
+++ b/lib/highlightjs/highlight.min.js
@@ -1,633 +1,3679 @@
-var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}};$jscomp.arrayIterator=function(a){return{next:$jscomp.arrayIteratorImpl(a)}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
-$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a};$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
-$jscomp.SYMBOL_PREFIX="jscomp_symbol_";$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};$jscomp.SymbolClass=function(a,b){this.$jscomp$symbol$id_=a;$jscomp.defineProperty(this,"description",{configurable:!0,writable:!0,value:b})};$jscomp.SymbolClass.prototype.toString=function(){return this.$jscomp$symbol$id_};
-$jscomp.Symbol=function(){function a(c){if(this instanceof a)throw new TypeError("Symbol is not a constructor");return new $jscomp.SymbolClass($jscomp.SYMBOL_PREFIX+(c||"")+"_"+b++,c)}var b=0;return a}();
-$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var a=$jscomp.global.Symbol.iterator;a||(a=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("Symbol.iterator"));"function"!=typeof Array.prototype[a]&&$jscomp.defineProperty(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return $jscomp.iteratorPrototype($jscomp.arrayIteratorImpl(this))}});$jscomp.initSymbolIterator=function(){}};
-$jscomp.initSymbolAsyncIterator=function(){$jscomp.initSymbol();var a=$jscomp.global.Symbol.asyncIterator;a||(a=$jscomp.global.Symbol.asyncIterator=$jscomp.global.Symbol("Symbol.asyncIterator"));$jscomp.initSymbolAsyncIterator=function(){}};$jscomp.iteratorPrototype=function(a){$jscomp.initSymbolIterator();a={next:a};a[$jscomp.global.Symbol.iterator]=function(){return this};return a};$jscomp.createTemplateTagFirstArg=function(a){return a.raw=a};
-$jscomp.createTemplateTagFirstArgWithRaw=function(a,b){a.raw=b;return a};$jscomp.makeIterator=function(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):$jscomp.arrayIterator(a)};$jscomp.arrayFromIterator=function(a){for(var b,c=[];!(b=a.next()).done;)c.push(b.value);return c};$jscomp.arrayFromIterable=function(a){return a instanceof Array?a:$jscomp.arrayFromIterator($jscomp.makeIterator(a))};
-$jscomp.objectCreate=$jscomp.ASSUME_ES5||"function"==typeof Object.create?Object.create:function(a){var b=function(){};b.prototype=a;return new b};$jscomp.underscoreProtoCanBeSet=function(){var a={a:!0},b={};try{return b.__proto__=a,b.a}catch(c){}return!1};$jscomp.setPrototypeOf="function"==typeof Object.setPrototypeOf?Object.setPrototypeOf:$jscomp.underscoreProtoCanBeSet()?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+" is not extensible");return a}:null;
-$jscomp.inherits=function(a,b){a.prototype=$jscomp.objectCreate(b.prototype);a.prototype.constructor=a;if($jscomp.setPrototypeOf){var c=$jscomp.setPrototypeOf;c(a,b)}else for(c in b)if("prototype"!=c)if(Object.defineProperties){var d=Object.getOwnPropertyDescriptor(b,c);d&&Object.defineProperty(a,c,d)}else a[c]=b[c];a.superClass_=b.prototype};$jscomp.owns=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)};
-$jscomp.assign="function"==typeof Object.assign?Object.assign:function(a,b){for(var c=1;c<arguments.length;c++){var d=arguments[c];if(d)for(var e in d)$jscomp.owns(d,e)&&(a[e]=d[e])}return a};$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");
-var $jscomp$lookupPolyfilledValue=function(a,b){var c=$jscomp.propertyToPolyfillSymbol[b];if(null==c)return a[b];c=a[c];return void 0!==c?c:a[b]};$jscomp.polyfill=function(a,b,c,d){b&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(a,b,c,d):$jscomp.polyfillUnisolated(a,b,c,d))};
-$jscomp.polyfillUnisolated=function(a,b,c,d){c=$jscomp.global;a=a.split(".");for(d=0;d<a.length-1;d++){var e=a[d];e in c||(c[e]={});c=c[e]}a=a[a.length-1];d=c[a];b=b(d);b!=d&&null!=b&&$jscomp.defineProperty(c,a,{configurable:!0,writable:!0,value:b})};
-$jscomp.polyfillIsolated=function(a,b,c,d){var e=a.split(".");a=1===e.length;d=e[0];d=!a&&d in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var f=0;f<e.length-1;f++){var h=e[f];h in d||(d[h]={});d=d[h]}e=e[e.length-1];c=$jscomp.IS_SYMBOL_NATIVE&&"es6"===c?d[e]:null;b=b(c);null!=b&&(a?$jscomp.defineProperty($jscomp.polyfills,e,{configurable:!0,writable:!0,value:b}):b!==c&&($jscomp.propertyToPolyfillSymbol[e]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(e):$jscomp.POLYFILL_PREFIX+e,e=$jscomp.propertyToPolyfillSymbol[e],
-$jscomp.defineProperty(d,e,{configurable:!0,writable:!0,value:b})))};$jscomp.polyfill("Object.assign",function(a){return a||$jscomp.assign},"es6","es3");$jscomp.findInternal=function(a,b,c){a instanceof String&&(a=String(a));for(var d=a.length,e=0;e<d;e++){var f=a[e];if(b.call(c,f,e,a))return{i:e,v:f}}return{i:-1,v:void 0}};$jscomp.polyfill("Array.prototype.findIndex",function(a){return a?a:function(a,c){return $jscomp.findInternal(this,a,c).i}},"es6","es3");
-$jscomp.polyfill("Object.is",function(a){return a?a:function(a,c){return a===c?0!==a||1/a===1/c:a!==a&&c!==c}},"es6","es3");$jscomp.polyfill("Array.prototype.includes",function(a){return a?a:function(a,c){var b=this;b instanceof String&&(b=String(b));var e=b.length;c=c||0;for(0>c&&(c=Math.max(c+e,0));c<e;c++){var f=b[c];if(f===a||Object.is(f,a))return!0}return!1}},"es7","es3");
-$jscomp.checkStringArgs=function(a,b,c){if(null==a)throw new TypeError("The 'this' value for String.prototype."+c+" must not be null or undefined");if(b instanceof RegExp)throw new TypeError("First argument to String.prototype."+c+" must not be a regular expression");return a+""};$jscomp.polyfill("String.prototype.includes",function(a){return a?a:function(a,c){return-1!==$jscomp.checkStringArgs(this,a,"includes").indexOf(a,c||0)}},"es6","es3");
-$jscomp.iteratorFromArray=function(a,b){$jscomp.initSymbolIterator();a instanceof String&&(a+="");var c=0,d={next:function(){if(c<a.length){var e=c++;return{value:b(e,a[e]),done:!1}}d.next=function(){return{done:!0,value:void 0}};return d.next()}};d[Symbol.iterator]=function(){return d};return d};$jscomp.polyfill("Array.prototype.keys",function(a){return a?a:function(){return $jscomp.iteratorFromArray(this,function(a){return a})}},"es6","es3");
-$jscomp.polyfill("Array.prototype.find",function(a){return a?a:function(a,c){return $jscomp.findInternal(this,a,c).v}},"es6","es3");
-var hljs=function(){function a(b){Object.freeze(b);var c="function"===typeof b;Object.getOwnPropertyNames(b).forEach(function(d){!b.hasOwnProperty(d)||null===b[d]||"object"!==typeof b[d]&&"function"!==typeof b[d]||c&&("caller"===d||"callee"===d||"arguments"===d)||Object.isFrozen(b[d])||a(b[d])});return b}function b(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function c(a){var b={},c=Array.prototype.slice.call(arguments,1),d;for(d in a)b[d]=a[d];c.forEach(function(a){for(var c in a)b[c]=
-a[c]});return b}function d(a){return a&&a.source||a}function e(a){for(var b=[],c=0;c<arguments.length;++c)b[c-0]=arguments[c];return b.map(function(a){return d(a)}).join("")}function f(a,b){for(var c=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,e=0,f="",t=0;t<a.length;t++){var h=e+=1,y=d(a[t]);0<t&&(f+=b);for(f+="(";0<y.length;){var g=c.exec(y);if(null==g){f+=y;break}f+=y.substring(0,g.index);y=y.substring(g.index+g[0].length);"\\"===g[0][0]&&g[1]?f+="\\"+String(Number(g[1])+h):(f+=g[0],"("===
-g[0]&&e++)}f+=")"}return f}function h(a){function b(b,c){return new RegExp(d(b),"m"+(a.case_insensitive?"i":"")+(c?"g":""))}function c(a){var b=new g;a.contains.forEach(function(a){return b.addRule(a.begin,{rule:a,type:"begin"})});a.terminator_end&&b.addRule(a.terminator_end,{type:"end"});a.illegal&&b.addRule(a.illegal,{type:"illegal"});return b}function e(a,b){var c=a.input[a.index+a[0].length];"."!==a.input[a.index-1]&&"."!==c||b.ignoreMatch()}function t(f,h){if(!f.compiled){f.compiled=!0;f.__beforeBegin=
-null;f.keywords=f.keywords||f.beginKeywords;var g=null;"object"===typeof f.keywords&&(g=f.keywords.$pattern,delete f.keywords.$pattern);f.keywords&&(f.keywords=l(f.keywords,a.case_insensitive));if(f.lexemes&&g)throw Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");f.keywordPatternRe=b(f.lexemes||g||/\w+/,!0);h&&(f.beginKeywords&&(f.begin="\\b("+f.beginKeywords.split(" ").join("|")+")(?=\\b|\\s)",f.__beforeBegin=e),f.begin||(f.begin=/\B|\b/),f.beginRe=
-b(f.begin),f.endSameAsBegin&&(f.end=f.begin),f.end||f.endsWithParent||(f.end=/\B|\b/),f.end&&(f.endRe=b(f.end)),f.terminator_end=d(f.end)||"",f.endsWithParent&&h.terminator_end&&(f.terminator_end+=(f.end?"|":"")+h.terminator_end));f.illegal&&(f.illegalRe=b(f.illegal));null==f.relevance&&(f.relevance=1);f.contains||(f.contains=[]);f.contains=[].concat.apply([],$jscomp.arrayFromIterable(f.contains.map(function(a){return k("self"===a?f:a)})));f.contains.forEach(function(a){t(a,f)});f.starts&&t(f.starts,
-h);f.matcher=c(f)}}var h=function(){this.matchIndexes={};this.regexes=[];this.matchAt=1;this.position=0};h.prototype.addRule=function(a,b){b.position=this.position++;this.matchIndexes[this.matchAt]=b;this.regexes.push([b,a]);this.matchAt+=(new RegExp(a.toString()+"|")).exec("").length-1+1};h.prototype.compile=function(){0===this.regexes.length&&(this.exec=function(){return null});var a=this.regexes.map(function(a){return a[1]});this.matcherRe=b(f(a,"|"),!0);this.lastIndex=0};h.prototype.exec=function(a){this.matcherRe.lastIndex=
-this.lastIndex;a=this.matcherRe.exec(a);if(!a)return null;var b=a.findIndex(function(a,b){return 0<b&&void 0!==a}),c=this.matchIndexes[b];a.splice(0,b);return Object.assign(a,c)};var g=function(){this.rules=[];this.multiRegexes=[];this.regexIndex=this.lastIndex=this.count=0};g.prototype.getMatcher=function(a){if(this.multiRegexes[a])return this.multiRegexes[a];var b=new h;this.rules.slice(a).forEach(function(a){var c=$jscomp.makeIterator(a);a=c.next().value;c=c.next().value;return b.addRule(a,c)});
-b.compile();return this.multiRegexes[a]=b};g.prototype.considerAll=function(){this.regexIndex=0};g.prototype.addRule=function(a,b){this.rules.push([a,b]);"begin"===b.type&&this.count++};g.prototype.exec=function(a){var b=this.getMatcher(this.regexIndex);b.lastIndex=this.lastIndex;if(a=b.exec(a))this.regexIndex+=a.position+1,this.regexIndex===this.count&&(this.regexIndex=0);return a};if(a.contains&&a.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language.  See documentation.");
-t(a)}function g(a){return a?a.endsWithParent||g(a.starts):!1}function k(a){a.variants&&!a.cached_variants&&(a.cached_variants=a.variants.map(function(b){return c(a,{variants:null},b)}));return a.cached_variants?a.cached_variants:g(a)?c(a,{starts:a.starts?c(a.starts):null}):Object.isFrozen(a)?c(a):a}function l(a,b){function c(a,c){b&&(c=c.toLowerCase());c.split(" ").forEach(function(b){var c=b.split("|");b=c[0];var e=c[1];c=e?Number(e):I.includes(c[0].toLowerCase())?0:1;d[b]=[a,c]})}var d={};"string"===
-typeof a?c("keyword",a):Object.keys(a).forEach(function(b){c(b,a[b])});return d}var m=function(a){void 0===a.data&&(a.data={});this.data=a.data};m.prototype.ignoreMatch=function(){this.ignore=!0};var p=Object.freeze({__proto__:null,escapeHTML:b,inherit:c,nodeStream:function(a){var b=[];(function R(a,c){for(a=a.firstChild;a;a=a.nextSibling)3===a.nodeType?c+=a.nodeValue.length:1===a.nodeType&&(b.push({event:"start",offset:c,node:a}),c=R(a,c),a.nodeName.toLowerCase().match(/br|hr|img|input/)||b.push({event:"stop",
-offset:c,node:a}));return c})(a,0);return b},mergeStreams:function(a,c,d){function e(){return a.length&&c.length?a[0].offset!==c[0].offset?a[0].offset<c[0].offset?a:c:"start"===c[0].event?a:c:a.length?a:c}function f(a){l+="<"+a.nodeName.toLowerCase()+[].map.call(a.attributes,function(a){return" "+a.nodeName+'="'+b(a.value).replace(/"/g,"&quot;")+'"'}).join("")+">"}function h(a){l+="</"+a.nodeName.toLowerCase()+">"}function g(a){("start"===a.event?f:h)(a.node)}for(var t=0,l="",k=[];a.length||c.length;){var m=
-e();l+=b(d.substring(t,m[0].offset));t=m[0].offset;if(m===a){k.reverse().forEach(h);do g(m.splice(0,1)[0]),m=e();while(m===a&&m.length&&m[0].offset===t);k.reverse().forEach(f)}else"start"===m[0].event?k.push(m[0].node):k.pop(),g(m.splice(0,1)[0])}return l+b(d.substr(t))}}),q=function(a,b){this.buffer="";this.classPrefix=b.classPrefix;a.walk(this)};q.prototype.addText=function(a){this.buffer+=b(a)};q.prototype.openNode=function(a){if(a.kind){var b=a.kind;a.sublanguage||(b=""+this.classPrefix+b);this.span(b)}};
-q.prototype.closeNode=function(a){a.kind&&(this.buffer+="</span>")};q.prototype.span=function(a){this.buffer+='<span class="'+a+'">'};q.prototype.value=function(){return this.buffer};var r=function(){this.rootNode={children:[]};this.stack=[this.rootNode]};r.prototype.add=function(a){this.top.children.push(a)};r.prototype.openNode=function(a){a={kind:a,children:[]};this.add(a);this.stack.push(a)};r.prototype.closeNode=function(){if(1<this.stack.length)return this.stack.pop()};r.prototype.closeAllNodes=
-function(){for(;this.closeNode(););};r.prototype.toJSON=function(){return JSON.stringify(this.rootNode,null,4)};r.prototype.walk=function(a){return this.constructor._walk(a,this.rootNode)};r._walk=function(a,b){var c=this;"string"===typeof b?a.addText(b):b.children&&(a.openNode(b),b.children.forEach(function(b){return c._walk(a,b)}),a.closeNode(b));return a};r._collapse=function(a){a.children&&(a.children.every(function(a){return"string"===typeof a})?(a.text=a.children.join(""),delete a.children):
-a.children.forEach(function(a){"string"!==typeof a&&r._collapse(a)}))};$jscomp.global.Object.defineProperties(r.prototype,{top:{configurable:!0,enumerable:!0,get:function(){return this.stack[this.stack.length-1]}},root:{configurable:!0,enumerable:!0,get:function(){return this.rootNode}}});var z=function(a){var b=r.call(this)||this;b.options=a;return b};$jscomp.inherits(z,r);z.prototype.addKeyword=function(a,b){""!==a&&(this.openNode(b),this.addText(a),this.closeNode())};z.prototype.addText=function(a){""!==
-a&&this.add(a)};z.prototype.addSublanguage=function(a,b){a=a.root;a.kind=b;a.sublanguage=!0;this.add(a)};z.prototype.toHTML=function(){return(new q(this,this.options)).value()};z.prototype.finalize=function(){return!0};var x={begin:"\\\\[\\s\\S]",relevance:0},S={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[x]},T={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[x]},C={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},
-A=function(a,b,d){a=c({className:"comment",begin:a,end:b,contains:[]},d||{});a.contains.push(C);a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0});return a},U=A("//","$"),V=A("/\\*","\\*/"),W=A("#","$"),G=Object.freeze({__proto__:null,IDENT_RE:"[a-zA-Z]\\w*",UNDERSCORE_IDENT_RE:"[a-zA-Z_]\\w*",NUMBER_RE:"\\b\\d+(\\.\\d+)?",C_NUMBER_RE:"(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",BINARY_NUMBER_RE:"\\b(0b[01]+)",RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
-SHEBANG:function(a){a=void 0===a?{}:a;var b=/^#![ ]*\//;a.binary&&(a.begin=e(b,/.*\b/,a.binary,/\b.*/));return c({className:"meta",begin:b,end:/$/,relevance:0,"on:begin":function(a,b){0!==a.index&&b.ignoreMatch()}},a)},BACKSLASH_ESCAPE:x,APOS_STRING_MODE:S,QUOTE_STRING_MODE:T,PHRASAL_WORDS_MODE:C,COMMENT:A,C_LINE_COMMENT_MODE:U,C_BLOCK_COMMENT_MODE:V,HASH_COMMENT_MODE:W,NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?",relevance:0},C_NUMBER_MODE:{className:"number",begin:"(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",
-relevance:0},BINARY_NUMBER_MODE:{className:"number",begin:"\\b(0b[01]+)",relevance:0},CSS_NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[x,{begin:/\[/,end:/\]/,relevance:0,contains:[x]}]}]},TITLE_MODE:{className:"title",begin:"[a-zA-Z]\\w*",relevance:0},UNDERSCORE_TITLE_MODE:{className:"title",
-begin:"[a-zA-Z_]\\w*",relevance:0},METHOD_GUARD:{begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:function(a){return Object.assign(a,{"on:begin":function(a,b){b.data._beginMatch=a[1]},"on:end":function(a,b){b.data._beginMatch!==a[1]&&b.ignoreMatch()}})}}),I="of and for in not or if then".split(" "),K=p.nodeStream,X=p.mergeStreams;$jscomp.initSymbol();var L=Symbol("nomatch");return function(d){function e(a){var b=a.className+" ";b+=a.parentNode?a.parentNode.className:"";var c=w.languageDetectRe.exec(b);
-return c?(b=t(c[1]),b||(console.warn("Could not find the language '{}', did you forget to load/include a language module?".replace("{}",c[1])),console.warn("Falling back to no-highlight mode for this block.",a)),b?c[1]:"no-highlight"):b.split(/\s+/).find(function(a){return w.noHighlightRe.test(a)||t(a)})}function f(a,b,c,d){a={code:b,language:a};B("before:highlight",a);c=a.result?a.result:g(a.language,a.code,c,d);c.code=a.code;B("after:highlight",c);return c}function g(a,c,d,e){function f(){if(null!=
-n.subLanguage){if(""!==u){var a="string"===typeof n.subLanguage;if(a&&!E[n.subLanguage])v.addText(u);else{var b=a?g(n.subLanguage,u,!0,r[n.subLanguage]):k(u,n.subLanguage.length?n.subLanguage:null);0<n.relevance&&(x+=b.relevance);a&&(r[n.subLanguage]=b.top);v.addSublanguage(b.emitter,b.language)}}}else if(n.keywords){var c=0;n.keywordPatternRe.lastIndex=0;a=n.keywordPatternRe.exec(u);for(b="";a;){b+=u.substring(c,a.index);c=n;var d=a;d=q.case_insensitive?d[0].toLowerCase():d[0];(c=Object.prototype.hasOwnProperty.call(c.keywords,
-d)&&c.keywords[d])?(d=$jscomp.makeIterator(c),c=d.next().value,d=d.next().value,v.addText(b),b="",x+=d,v.addKeyword(a[0],c)):b+=a[0];c=n.keywordPatternRe.lastIndex;a=n.keywordPatternRe.exec(u)}b+=u.substr(c);v.addText(b)}else v.addText(u);u=""}function l(a){a.className&&v.openNode(a.className);return n=Object.create(a,{parent:{value:n}})}function M(a,b,c){var d;if(d=(d=(d=a.endRe)&&d.exec(c))&&0===d.index){if(a["on:end"]){var e=new m(a);a["on:end"](b,e);e.ignore&&(d=!1)}if(d){for(;a.endsParent&&a.parent;)a=
-a.parent;return a}}if(a.endsWithParent)return M(a.parent,b,c)}function N(b,c){var e=c&&c[0];u+=b;if(null==e)return f(),0;if("begin"===H.type&&"end"===c.type&&H.index===c.index&&""===e){u+=p.slice(c.index,c.index+1);if(!F)throw c=Error("0 width match regex"),c.languageName=a,c.badRule=H.rule,c;return 1}H=c;if("begin"===c.type){a:{e=c[0];b=c.rule;for(var g=new m(b),h=$jscomp.makeIterator([b.__beforeBegin,b["on:begin"]]),k=h.next();!k.done;k=h.next())if(k=k.value)if(k(c,g),g.ignore){0===n.matcher.regexIndex?
-(u+=e[0],c=1):(A=!0,c=0);break a}b&&b.endSameAsBegin&&(b.endRe=new RegExp(e.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m"));b.skip?u+=e:(b.excludeBegin&&(u+=e),f(),b.returnBegin||b.excludeBegin||(u=e));l(b);c=b.returnBegin?0:e.length}return c}if("illegal"===c.type&&!d)throw c=Error('Illegal lexeme "'+e+'" for mode "'+(n.className||"<unnamed>")+'"'),c.mode=n,c;if("end"===c.type){b=c[0];g=p.substr(c.index);if(g=M(n,c,g)){h=n;h.skip?u+=b:(h.returnEnd||h.excludeEnd||(u+=b),f(),h.excludeEnd&&(u=b));do n.className&&
-v.closeNode(),n.skip||n.subLanguage||(x+=n.relevance),n=n.parent;while(n!==g.parent);g.starts&&(g.endSameAsBegin&&(g.starts.endRe=g.endRe),l(g.starts));b=h.returnEnd?0:b.length}else b=L;if(b!==L)return b}if("illegal"===c.type&&""===e)return 1;if(1E5<z&&z>3*c.index)throw Error("potential infinite loop, way more iterations than matches");u+=e;return e.length}var p=c,H={},q=t(a);if(!q)throw console.error("Could not find the language '{}', did you forget to load/include a language module?".replace("{}",
-a)),Error('Unknown language: "'+a+'"');h(q);c="";var n=e||q,r={},v=new w.__emitter(w);(function(){for(var a=[],b=n;b!==q;b=b.parent)b.className&&a.unshift(b.className);a.forEach(function(a){return v.openNode(a)})})();var u="",x=0,z=e=0,A=!1;try{for(n.matcher.considerAll();;){z++;A?A=!1:(n.matcher.lastIndex=e,n.matcher.considerAll());var y=n.matcher.exec(p);if(!y)break;var B=p.substring(e,y.index),C=N(B,y);e=y.index+C}N(p.substr(e));v.closeAllNodes();v.finalize();c=v.toHTML();return{relevance:x,value:c,
-language:a,illegal:!1,emitter:v,top:n}}catch(D){if(D.message&&D.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:D.message,context:p.slice(e-100,e+100),mode:D.mode},sofar:c,relevance:0,value:b(p),emitter:v};if(F)return{relevance:0,value:b(p),emitter:v,language:a,top:n,errorRaised:D};throw D;}}function l(a){var c={relevance:0,emitter:new w.__emitter(w),value:b(a),illegal:!1,top:O};c.emitter.addText(a);return c}function k(a,b){b=b||w.languages||Object.keys(E);var c=l(a),d=c;b.filter(t).filter(A).forEach(function(b){var e=
-g(b,a,!1);e.language=b;e.relevance>d.relevance&&(d=e);e.relevance>c.relevance&&(d=c,c=e)});d.language&&(c.second_best=d);return c}function p(a){return w.tabReplace||w.useBR?a.replace(I,function(a){return"\n"===a?w.useBR?"<br>":a:w.tabReplace?a.replace(/\t/g,w.tabReplace):a}):a}function q(a){var b=e(a);if(!w.noHighlightRe.test(b)){B("before:highlightBlock",{block:a,language:b});if(w.useBR){var c=document.createElement("div");c.innerHTML=a.innerHTML.replace(/\n/g,"").replace(/<br[ /]*>/g,"\n")}else c=
-a;var d=c.textContent,g=b?f(b,d,!0):k(d);c=K(c);if(c.length){var h=document.createElement("div");h.innerHTML=g.value;g.value=X(c,K(h),d)}g.value=p(g.value);B("after:highlightBlock",{block:a,result:g});a.innerHTML=g.value;d=a.className;b=b?J[b]:g.language;c=[d.trim()];d.match(/\bhljs\b/)||c.push("hljs");d.includes(b)||c.push(b);b=c.join(" ").trim();a.className=b;a.result={language:g.language,re:g.relevance};g.second_best&&(a.second_best={language:g.second_best.language,re:g.second_best.relevance})}}
-function r(){if(!r.called){r.called=!0;var a=document.querySelectorAll("pre code");C.forEach.call(a,q)}}function t(a){a=(a||"").toLowerCase();return E[a]||E[J[a]]}function x(a,b){var c=b.languageName;"string"===typeof a&&(a=[a]);a.forEach(function(a){return J[a]=c})}function A(a){return(a=t(a))&&!a.disableAutodetect}function B(a,b){P.forEach(function(c){if(c[a])c[a](b)})}var C=[],E={},J={},P=[],F=!0,I=/(^(<[^>]+>|\t|)+|\n)/gm,w={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,
-classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:z},O={disableAutodetect:!0,name:"Plain text"};Object.assign(d,{highlight:f,highlightAuto:k,fixMarkup:p,highlightBlock:q,configure:function(a){w=c(w,a)},initHighlighting:r,initHighlightingOnLoad:function(){window.addEventListener("DOMContentLoaded",r,!1)},registerLanguage:function(a,b){var c=null;try{c=b(d)}catch(v){console.error("Language definition for '{}' could not be registered.".replace("{}",a));if(F)console.error(v);else throw v;
-c=O}c.name||(c.name=a);E[a]=c;c.rawDefinition=b.bind(null,d);c.aliases&&x(c.aliases,{languageName:a})},listLanguages:function(){return Object.keys(E)},getLanguage:t,registerAliases:x,requireLanguage:function(a){var b=t(a);if(b)return b;throw Error("The '{}' language is required, but not loaded.".replace("{}",a));},autoDetection:A,inherit:c,addPlugin:function(a){P.push(a)}});d.debugMode=function(){F=!1};d.safeMode=function(){F=!0};d.versionString="10.0.0";for(var Q in G)"object"===typeof G[Q]&&a(G[Q]);
-Object.assign(d,G);return d}({})}();"object"===typeof exports&&"undefined"!==typeof module&&(module.exports=hljs);
-hljs.registerLanguage("1c",function(){return function(a){var b=a.inherit(a.NUMBER_MODE),c={className:"string",begin:'"|\\|',end:'"|$',contains:[{begin:'""'}]},d={begin:"'",end:"'",excludeBegin:!0,excludeEnd:!0,contains:[{className:"number",begin:"\\d{4}([\\.\\\\/:-]?\\d{2}){0,5}"}]},e=a.inherit(a.C_LINE_COMMENT_MODE),f={className:"meta",begin:"#|&",end:"$",keywords:{$pattern:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+","meta-keyword":"\u0434\u0430\u043b\u0435\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c\u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0434\u043b\u044f \u0435\u0441\u043b\u0438 \u0438 \u0438\u0437 \u0438\u043b\u0438 \u0438\u043d\u0430\u0447\u0435 \u0438\u043d\u0430\u0447\u0435\u0435\u0441\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043a\u043e\u043d\u0435\u0446\u0435\u0441\u043b\u0438 \u043a\u043e\u043d\u0435\u0446\u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u043a\u043e\u043d\u0435\u0446\u0446\u0438\u043a\u043b\u0430 \u043d\u0435 \u043d\u043e\u0432\u044b\u0439 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043f\u0435\u0440\u0435\u043c \u043f\u043e \u043f\u043e\u043a\u0430 \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u043f\u0440\u0435\u0440\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0442\u043e\u0433\u0434\u0430 \u0446\u0438\u043a\u043b \u044d\u043a\u0441\u043f\u043e\u0440\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c\u0438\u0437\u0444\u0430\u0439\u043b\u0430 \u0432\u0435\u0431\u043a\u043b\u0438\u0435\u043d\u0442 \u0432\u043c\u0435\u0441\u0442\u043e \u0432\u043d\u0435\u0448\u043d\u0435\u0435\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442 \u043a\u043e\u043d\u0435\u0446\u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043a\u043b\u0438\u0435\u043d\u0442 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435\u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435\u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435\u0431\u0435\u0437\u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430 \u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435\u0431\u0435\u0437\u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434 \u043f\u043e\u0441\u043b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u043e\u043b\u0441\u0442\u044b\u0439\u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0431\u044b\u0447\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u043e\u043b\u0441\u0442\u044b\u0439\u043a\u043b\u0438\u0435\u043d\u0442\u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u043e\u043d\u043a\u0438\u0439\u043a\u043b\u0438\u0435\u043d\u0442 "},
-contains:[e]};a={className:"function",variants:[{begin:"\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430|\u0444\u0443\u043d\u043a\u0446\u0438\u044f",end:"\\)",keywords:"\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f"},{begin:"\u043a\u043e\u043d\u0435\u0446\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b|\u043a\u043e\u043d\u0435\u0446\u0444\u0443\u043d\u043a\u0446\u0438\u0438",keywords:"\u043a\u043e\u043d\u0435\u0446\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b \u043a\u043e\u043d\u0435\u0446\u0444\u0443\u043d\u043a\u0446\u0438\u0438"}],
-contains:[{begin:"\\(",end:"\\)",endsParent:!0,contains:[{className:"params",begin:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+",end:",",excludeEnd:!0,endsWithParent:!0,keywords:{$pattern:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+",keyword:"\u0437\u043d\u0430\u0447",literal:"null \u0438\u0441\u0442\u0438\u043d\u0430 \u043b\u043e\u0436\u044c \u043d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e"},
-contains:[b,c,d]},e]},a.inherit(a.TITLE_MODE,{begin:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+"})]};return{name:"1C:Enterprise",case_insensitive:!0,keywords:{$pattern:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+",keyword:"\u0434\u0430\u043b\u0435\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c\u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0434\u043b\u044f \u0435\u0441\u043b\u0438 \u0438 \u0438\u0437 \u0438\u043b\u0438 \u0438\u043d\u0430\u0447\u0435 \u0438\u043d\u0430\u0447\u0435\u0435\u0441\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043a\u043e\u043d\u0435\u0446\u0435\u0441\u043b\u0438 \u043a\u043e\u043d\u0435\u0446\u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u043a\u043e\u043d\u0435\u0446\u0446\u0438\u043a\u043b\u0430 \u043d\u0435 \u043d\u043e\u0432\u044b\u0439 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043f\u0435\u0440\u0435\u043c \u043f\u043e \u043f\u043e\u043a\u0430 \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u043f\u0440\u0435\u0440\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0442\u043e\u0433\u0434\u0430 \u0446\u0438\u043a\u043b \u044d\u043a\u0441\u043f\u043e\u0440\u0442 ",
+/*
+  Highlight.js 10.6.0 (d24895f4)
+  License: BSD-3-Clause
+  Copyright (c) 2006-2020, Ivan Sagalaev
+*/
+var hljs=function(){"use strict";function e(t){
+return t instanceof Map?t.clear=t.delete=t.set=()=>{
+throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{
+throw Error("set is read-only")
+}),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{var i=t[n]
+;"object"!=typeof i||Object.isFrozen(i)||e(i)})),t}var t=e,n=e;t.default=n
+;class i{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data}
+ignoreMatch(){this.ignore=!0}}function r(e){
+return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;")
+}function s(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t]
+;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const a=e=>!!e.kind
+;class l{constructor(e,t){
+this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){
+this.buffer+=r(e)}openNode(e){if(!a(e))return;let t=e.kind
+;e.sublanguage||(t=`${this.classPrefix}${t}`),this.span(t)}closeNode(e){
+a(e)&&(this.buffer+="</span>")}value(){return this.buffer}span(e){
+this.buffer+=`<span class="${e}">`}}class o{constructor(){this.rootNode={
+children:[]},this.stack=[this.rootNode]}get top(){
+return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
+this.top.children.push(e)}openNode(e){const t={kind:e,children:[]}
+;this.add(t),this.stack.push(t)}closeNode(){
+if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
+for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
+walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){
+return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),
+t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){
+"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
+o._collapse(e)})))}}class c extends o{constructor(e){super(),this.options=e}
+addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())}
+addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root
+;n.kind=t,n.sublanguage=!0,this.add(n)}toHTML(){
+return new l(this,this.options).value()}finalize(){return!0}}function g(e){
+return e?"string"==typeof e?e:e.source:null}
+const u=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,d="[a-zA-Z]\\w*",h="[a-zA-Z_]\\w*",f="\\b\\d+(\\.\\d+)?",p="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",m="\\b(0b[01]+)",b={
+begin:"\\\\[\\s\\S]",relevance:0},E={className:"string",begin:"'",end:"'",
+illegal:"\\n",contains:[b]},x={className:"string",begin:'"',end:'"',
+illegal:"\\n",contains:[b]},v={
+begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
+},w=(e,t,n={})=>{const i=s({className:"comment",begin:e,end:t,contains:[]},n)
+;return i.contains.push(v),i.contains.push({className:"doctag",
+begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),i
+},y=w("//","$"),N=w("/\\*","\\*/"),R=w("#","$");var _=Object.freeze({
+__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:d,UNDERSCORE_IDENT_RE:h,
+NUMBER_RE:f,C_NUMBER_RE:p,BINARY_NUMBER_RE:m,
+RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
+SHEBANG:(e={})=>{const t=/^#![ ]*\//
+;return e.binary&&(e.begin=((...e)=>e.map((e=>g(e))).join(""))(t,/.*\b/,e.binary,/\b.*/)),
+s({className:"meta",begin:t,end:/$/,relevance:0,"on:begin":(e,t)=>{
+0!==e.index&&t.ignoreMatch()}},e)},BACKSLASH_ESCAPE:b,APOS_STRING_MODE:E,
+QUOTE_STRING_MODE:x,PHRASAL_WORDS_MODE:v,COMMENT:w,C_LINE_COMMENT_MODE:y,
+C_BLOCK_COMMENT_MODE:N,HASH_COMMENT_MODE:R,NUMBER_MODE:{className:"number",
+begin:f,relevance:0},C_NUMBER_MODE:{className:"number",begin:p,relevance:0},
+BINARY_NUMBER_MODE:{className:"number",begin:m,relevance:0},CSS_NUMBER_MODE:{
+className:"number",
+begin:f+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",
+relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",
+begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[b,{begin:/\[/,end:/\]/,
+relevance:0,contains:[b]}]}]},TITLE_MODE:{className:"title",begin:d,relevance:0
+},UNDERSCORE_TITLE_MODE:{className:"title",begin:h,relevance:0},METHOD_GUARD:{
+begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{
+"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{
+t.data._beginMatch!==e[1]&&t.ignoreMatch()}})});function k(e,t){
+"."===e.input[e.index-1]&&t.ignoreMatch()}function O(e,t){
+t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
+e.__beforeBegin=k,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
+void 0===e.relevance&&(e.relevance=0))}function M(e,t){
+Array.isArray(e.illegal)&&(e.illegal=((...e)=>"("+e.map((e=>g(e))).join("|")+")")(...e.illegal))
+}function A(e,t){if(e.match){
+if(e.begin||e.end)throw Error("begin & end are not supported with match")
+;e.begin=e.match,delete e.match}}function L(e,t){
+void 0===e.relevance&&(e.relevance=1)}
+const j=["of","and","for","in","not","or","if","then","parent","list","value"]
+;function B(e,t,n="keyword"){const i={}
+;return"string"==typeof e?r(n,e.split(" ")):Array.isArray(e)?r(n,e):Object.keys(e).forEach((n=>{
+Object.assign(i,B(e[n],t,n))})),i;function r(e,n){
+t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|")
+;i[n[0]]=[e,I(n[0],n[1])]}))}}function I(e,t){
+return t?Number(t):(e=>j.includes(e.toLowerCase()))(e)?0:1}
+function T(e,{plugins:t}){function n(t,n){
+return RegExp(g(t),"m"+(e.case_insensitive?"i":"")+(n?"g":""))}class i{
+constructor(){
+this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
+addRule(e,t){
+t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),
+this.matchAt+=(e=>RegExp(e.toString()+"|").exec("").length-1)(e)+1}compile(){
+0===this.regexes.length&&(this.exec=()=>null)
+;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(((e,t="|")=>{let n=0
+;return e.map((e=>{n+=1;const t=n;let i=g(e),r="";for(;i.length>0;){
+const e=u.exec(i);if(!e){r+=i;break}
+r+=i.substring(0,e.index),i=i.substring(e.index+e[0].length),
+"\\"===e[0][0]&&e[1]?r+="\\"+(Number(e[1])+t):(r+=e[0],"("===e[0]&&n++)}return r
+})).map((e=>`(${e})`)).join(t)})(e),!0),this.lastIndex=0}exec(e){
+this.matcherRe.lastIndex=this.lastIndex;const t=this.matcherRe.exec(e)
+;if(!t)return null
+;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n]
+;return t.splice(0,n),Object.assign(t,i)}}class r{constructor(){
+this.rules=[],this.multiRegexes=[],
+this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
+if(this.multiRegexes[e])return this.multiRegexes[e];const t=new i
+;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),
+t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){
+return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){
+this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){
+const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex
+;let n=t.exec(e)
+;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{
+const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}
+return n&&(this.regexIndex+=n.position+1,
+this.regexIndex===this.count&&this.considerAll()),n}}
+if(e.compilerExtensions||(e.compilerExtensions=[]),
+e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language.  See documentation.")
+;return e.classNameAliases=s(e.classNameAliases||{}),function t(i,a){const l=i
+;if(i.compiled)return l
+;[A].forEach((e=>e(i,a))),e.compilerExtensions.forEach((e=>e(i,a))),
+i.__beforeBegin=null,[O,M,L].forEach((e=>e(i,a))),i.compiled=!0;let o=null
+;if("object"==typeof i.keywords&&(o=i.keywords.$pattern,
+delete i.keywords.$pattern),
+i.keywords&&(i.keywords=B(i.keywords,e.case_insensitive)),
+i.lexemes&&o)throw Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ")
+;return o=o||i.lexemes||/\w+/,
+l.keywordPatternRe=n(o,!0),a&&(i.begin||(i.begin=/\B|\b/),
+l.beginRe=n(i.begin),i.endSameAsBegin&&(i.end=i.begin),
+i.end||i.endsWithParent||(i.end=/\B|\b/),
+i.end&&(l.endRe=n(i.end)),l.terminatorEnd=g(i.end)||"",
+i.endsWithParent&&a.terminatorEnd&&(l.terminatorEnd+=(i.end?"|":"")+a.terminatorEnd)),
+i.illegal&&(l.illegalRe=n(i.illegal)),
+i.contains||(i.contains=[]),i.contains=[].concat(...i.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>s(e,{
+variants:null},t)))),e.cachedVariants?e.cachedVariants:S(e)?s(e,{
+starts:e.starts?s(e.starts):null
+}):Object.isFrozen(e)?s(e):e))("self"===e?i:e)))),i.contains.forEach((e=>{t(e,l)
+})),i.starts&&t(i.starts,a),l.matcher=(e=>{const t=new r
+;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"
+}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"
+}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(l),l}(e)}function S(e){
+return!!e&&(e.endsWithParent||S(e.starts))}function P(e){const t={
+props:["language","code","autodetect"],data:()=>({detectedLanguage:"",
+unknownLanguage:!1}),computed:{className(){
+return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){
+if(!this.autoDetect&&!e.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),
+this.unknownLanguage=!0,r(this.code);let t={}
+;return this.autoDetect?(t=e.highlightAuto(this.code),
+this.detectedLanguage=t.language):(t=e.highlight(this.language,this.code,this.ignoreIllegals),
+this.detectedLanguage=this.language),t.value},autoDetect(){
+return!(this.language&&(e=this.autodetect,!e&&""!==e));var e},
+ignoreIllegals:()=>!0},render(e){return e("pre",{},[e("code",{
+class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{
+Component:t,VuePlugin:{install(e){e.component("highlightjs",t)}}}}const D={
+"after:highlightElement":({el:e,result:t,text:n})=>{const i=H(e)
+;if(!i.length)return;const s=document.createElement("div")
+;s.innerHTML=t.value,t.value=((e,t,n)=>{let i=0,s="";const a=[];function l(){
+return e.length&&t.length?e[0].offset!==t[0].offset?e[0].offset<t[0].offset?e:t:"start"===t[0].event?e:t:e.length?e:t
+}function o(e){s+="<"+C(e)+[].map.call(e.attributes,(function(e){
+return" "+e.nodeName+'="'+r(e.value)+'"'})).join("")+">"}function c(e){
+s+="</"+C(e)+">"}function g(e){("start"===e.event?o:c)(e.node)}
+for(;e.length||t.length;){let t=l()
+;if(s+=r(n.substring(i,t[0].offset)),i=t[0].offset,t===e){a.reverse().forEach(c)
+;do{g(t.splice(0,1)[0]),t=l()}while(t===e&&t.length&&t[0].offset===i)
+;a.reverse().forEach(o)
+}else"start"===t[0].event?a.push(t[0].node):a.pop(),g(t.splice(0,1)[0])}
+return s+r(n.substr(i))})(i,H(s),n)}};function C(e){
+return e.nodeName.toLowerCase()}function H(e){const t=[];return function e(n,i){
+for(let r=n.firstChild;r;r=r.nextSibling)3===r.nodeType?i+=r.nodeValue.length:1===r.nodeType&&(t.push({
+event:"start",offset:i,node:r}),i=e(r,i),C(r).match(/br|hr|img|input/)||t.push({
+event:"stop",offset:i,node:r}));return i}(e,0),t}const $=e=>{console.error(e)
+},U=(e,...t)=>{console.log("WARN: "+e,...t)},z=(e,t)=>{
+console.log(`Deprecated as of ${e}. ${t}`)},K=r,G=s,V=Symbol("nomatch")
+;return(e=>{const n=Object.create(null),r=Object.create(null),s=[];let a=!0
+;const l=/(^(<[^>]+>|\t|)+|\n)/gm,o="Could not find the language '{}', did you forget to load/include a language module?",g={
+disableAutodetect:!0,name:"Plain text",contains:[]};let u={
+noHighlightRe:/^(no-?highlight)$/i,
+languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
+tabReplace:null,useBR:!1,languages:null,__emitter:c};function d(e){
+return u.noHighlightRe.test(e)}function h(e,t,n,i){const r={code:t,language:e}
+;M("before:highlight",r);const s=r.result?r.result:f(r.language,r.code,n,i)
+;return s.code=r.code,M("after:highlight",s),s}function f(e,t,r,l){const c=t
+;function g(e,t){const n=w.case_insensitive?t[0].toLowerCase():t[0]
+;return Object.prototype.hasOwnProperty.call(e.keywords,n)&&e.keywords[n]}
+function d(){null!=_.subLanguage?(()=>{if(""===M)return;let e=null
+;if("string"==typeof _.subLanguage){
+if(!n[_.subLanguage])return void O.addText(M)
+;e=f(_.subLanguage,M,!0,k[_.subLanguage]),k[_.subLanguage]=e.top
+}else e=p(M,_.subLanguage.length?_.subLanguage:null)
+;_.relevance>0&&(A+=e.relevance),O.addSublanguage(e.emitter,e.language)
+})():(()=>{if(!_.keywords)return void O.addText(M);let e=0
+;_.keywordPatternRe.lastIndex=0;let t=_.keywordPatternRe.exec(M),n="";for(;t;){
+n+=M.substring(e,t.index);const i=g(_,t);if(i){const[e,r]=i
+;O.addText(n),n="",A+=r;const s=w.classNameAliases[e]||e;O.addKeyword(t[0],s)
+}else n+=t[0];e=_.keywordPatternRe.lastIndex,t=_.keywordPatternRe.exec(M)}
+n+=M.substr(e),O.addText(n)})(),M=""}function h(e){
+return e.className&&O.openNode(w.classNameAliases[e.className]||e.className),
+_=Object.create(e,{parent:{value:_}}),_}function m(e,t,n){let r=((e,t)=>{
+const n=e&&e.exec(t);return n&&0===n.index})(e.endRe,n);if(r){if(e["on:end"]){
+const n=new i(e);e["on:end"](t,n),n.ignore&&(r=!1)}if(r){
+for(;e.endsParent&&e.parent;)e=e.parent;return e}}
+if(e.endsWithParent)return m(e.parent,t,n)}function b(e){
+return 0===_.matcher.regexIndex?(M+=e[0],1):(B=!0,0)}function E(e){
+const t=e[0],n=c.substr(e.index),i=m(_,e,n);if(!i)return V;const r=_
+;r.skip?M+=t:(r.returnEnd||r.excludeEnd||(M+=t),d(),r.excludeEnd&&(M=t));do{
+_.className&&O.closeNode(),_.skip||_.subLanguage||(A+=_.relevance),_=_.parent
+}while(_!==i.parent)
+;return i.starts&&(i.endSameAsBegin&&(i.starts.endRe=i.endRe),
+h(i.starts)),r.returnEnd?0:t.length}let x={};function v(t,n){const s=n&&n[0]
+;if(M+=t,null==s)return d(),0
+;if("begin"===x.type&&"end"===n.type&&x.index===n.index&&""===s){
+if(M+=c.slice(n.index,n.index+1),!a){const t=Error("0 width match regex")
+;throw t.languageName=e,t.badRule=x.rule,t}return 1}
+if(x=n,"begin"===n.type)return function(e){
+const t=e[0],n=e.rule,r=new i(n),s=[n.__beforeBegin,n["on:begin"]]
+;for(const n of s)if(n&&(n(e,r),r.ignore))return b(t)
+;return n&&n.endSameAsBegin&&(n.endRe=RegExp(t.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")),
+n.skip?M+=t:(n.excludeBegin&&(M+=t),
+d(),n.returnBegin||n.excludeBegin||(M=t)),h(n),n.returnBegin?0:t.length}(n)
+;if("illegal"===n.type&&!r){
+const e=Error('Illegal lexeme "'+s+'" for mode "'+(_.className||"<unnamed>")+'"')
+;throw e.mode=_,e}if("end"===n.type){const e=E(n);if(e!==V)return e}
+if("illegal"===n.type&&""===s)return 1
+;if(j>1e5&&j>3*n.index)throw Error("potential infinite loop, way more iterations than matches")
+;return M+=s,s.length}const w=R(e)
+;if(!w)throw $(o.replace("{}",e)),Error('Unknown language: "'+e+'"')
+;const y=T(w,{plugins:s});let N="",_=l||y;const k={},O=new u.__emitter(u);(()=>{
+const e=[];for(let t=_;t!==w;t=t.parent)t.className&&e.unshift(t.className)
+;e.forEach((e=>O.openNode(e)))})();let M="",A=0,L=0,j=0,B=!1;try{
+for(_.matcher.considerAll();;){
+j++,B?B=!1:_.matcher.considerAll(),_.matcher.lastIndex=L
+;const e=_.matcher.exec(c);if(!e)break;const t=v(c.substring(L,e.index),e)
+;L=e.index+t}return v(c.substr(L)),O.closeAllNodes(),O.finalize(),N=O.toHTML(),{
+relevance:Math.floor(A),value:N,language:e,illegal:!1,emitter:O,top:_}}catch(t){
+if(t.message&&t.message.includes("Illegal"))return{illegal:!0,illegalBy:{
+msg:t.message,context:c.slice(L-100,L+100),mode:t.mode},sofar:N,relevance:0,
+value:K(c),emitter:O};if(a)return{illegal:!1,relevance:0,value:K(c),emitter:O,
+language:e,top:_,errorRaised:t};throw t}}function p(e,t){
+t=t||u.languages||Object.keys(n);const i=(e=>{const t={relevance:0,
+emitter:new u.__emitter(u),value:K(e),illegal:!1,top:g}
+;return t.emitter.addText(e),t})(e),r=t.filter(R).filter(O).map((t=>f(t,e,!1)))
+;r.unshift(i);const s=r.sort(((e,t)=>{
+if(e.relevance!==t.relevance)return t.relevance-e.relevance
+;if(e.language&&t.language){if(R(e.language).supersetOf===t.language)return 1
+;if(R(t.language).supersetOf===e.language)return-1}return 0})),[a,l]=s,o=a
+;return o.second_best=l,o}const m={"before:highlightElement":({el:e})=>{
+u.useBR&&(e.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/<br[ /]*>/g,"\n"))
+},"after:highlightElement":({result:e})=>{
+u.useBR&&(e.value=e.value.replace(/\n/g,"<br>"))}},b=/^(<[^>]+>|\t)+/gm,E={
+"after:highlightElement":({result:e})=>{
+u.tabReplace&&(e.value=e.value.replace(b,(e=>e.replace(/\t/g,u.tabReplace))))}}
+;function x(e){let t=null;const n=(e=>{let t=e.className+" "
+;t+=e.parentNode?e.parentNode.className:"";const n=u.languageDetectRe.exec(t)
+;if(n){const t=R(n[1])
+;return t||(U(o.replace("{}",n[1])),U("Falling back to no-highlight mode for this block.",e)),
+t?n[1]:"no-highlight"}return t.split(/\s+/).find((e=>d(e)||R(e)))})(e)
+;if(d(n))return;M("before:highlightElement",{el:e,language:n}),t=e
+;const i=t.textContent,s=n?h(n,i,!0):p(i);M("after:highlightElement",{el:e,
+result:s,text:i}),e.innerHTML=s.value,((e,t,n)=>{const i=t?r[t]:n
+;e.classList.add("hljs"),i&&e.classList.add(i)})(e,n,s.language),e.result={
+language:s.language,re:s.relevance,relavance:s.relevance
+},s.second_best&&(e.second_best={language:s.second_best.language,
+re:s.second_best.relevance,relavance:s.second_best.relevance})}const v=()=>{
+v.called||(v.called=!0,
+z("10.6.0","initHighlighting() is deprecated.  Use highlightAll() instead."),
+document.querySelectorAll("pre code").forEach(x))};let w=!1,y=!1;function N(){
+y?document.querySelectorAll("pre code").forEach(x):w=!0}function R(e){
+return e=(e||"").toLowerCase(),n[e]||n[r[e]]}function k(e,{languageName:t}){
+"string"==typeof e&&(e=[e]),e.forEach((e=>{r[e.toLowerCase()]=t}))}
+function O(e){const t=R(e);return t&&!t.disableAutodetect}function M(e,t){
+const n=e;s.forEach((e=>{e[n]&&e[n](t)}))}
+"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{
+y=!0,w&&N()}),!1),Object.assign(e,{highlight:h,highlightAuto:p,highlightAll:N,
+fixMarkup:e=>{
+return z("10.2.0","fixMarkup will be removed entirely in v11.0"),z("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),
+t=e,
+u.tabReplace||u.useBR?t.replace(l,(e=>"\n"===e?u.useBR?"<br>":e:u.tabReplace?e.replace(/\t/g,u.tabReplace):e)):t
+;var t},highlightElement:x,
+highlightBlock:e=>(z("10.7.0","highlightBlock will be removed entirely in v12.0"),
+z("10.7.0","Please use highlightElement now."),x(e)),configure:e=>{
+e.useBR&&(z("10.3.0","'useBR' will be removed entirely in v11.0"),
+z("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),
+u=G(u,e)},initHighlighting:v,initHighlightingOnLoad:()=>{
+z("10.6.0","initHighlightingOnLoad() is deprecated.  Use highlightAll() instead."),
+w=!0},registerLanguage:(t,i)=>{let r=null;try{r=i(e)}catch(e){
+if($("Language definition for '{}' could not be registered.".replace("{}",t)),
+!a)throw e;$(e),r=g}
+r.name||(r.name=t),n[t]=r,r.rawDefinition=i.bind(null,e),r.aliases&&k(r.aliases,{
+languageName:t})},unregisterLanguage:e=>{delete n[e]
+;for(const t of Object.keys(r))r[t]===e&&delete r[t]},
+listLanguages:()=>Object.keys(n),getLanguage:R,registerAliases:k,
+requireLanguage:e=>{
+z("10.4.0","requireLanguage will be removed entirely in v11."),
+z("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844")
+;const t=R(e);if(t)return t
+;throw Error("The '{}' language is required, but not loaded.".replace("{}",e))},
+autoDetection:O,inherit:G,addPlugin:e=>{(e=>{
+e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{
+e["before:highlightBlock"](Object.assign({block:t.el},t))
+}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{
+e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),s.push(e)},
+vuePlugin:P(e).VuePlugin}),e.debugMode=()=>{a=!1},e.safeMode=()=>{a=!0
+},e.versionString="10.6.0";for(const e in _)"object"==typeof _[e]&&t(_[e])
+;return Object.assign(e,_),e.addPlugin(m),e.addPlugin(D),e.addPlugin(E),e})({})
+}();"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);
+hljs.registerLanguage("1c",(()=>{"use strict";return s=>{
+var x="[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+",n="\u0434\u0430\u043b\u0435\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c\u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0434\u043b\u044f \u0435\u0441\u043b\u0438 \u0438 \u0438\u0437 \u0438\u043b\u0438 \u0438\u043d\u0430\u0447\u0435 \u0438\u043d\u0430\u0447\u0435\u0435\u0441\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043a\u043e\u043d\u0435\u0446\u0435\u0441\u043b\u0438 \u043a\u043e\u043d\u0435\u0446\u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u043a\u043e\u043d\u0435\u0446\u0446\u0438\u043a\u043b\u0430 \u043d\u0435 \u043d\u043e\u0432\u044b\u0439 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043f\u0435\u0440\u0435\u043c \u043f\u043e \u043f\u043e\u043a\u0430 \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u043f\u0440\u0435\u0440\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0442\u043e\u0433\u0434\u0430 \u0446\u0438\u043a\u043b \u044d\u043a\u0441\u043f\u043e\u0440\u0442 ",e="null \u0438\u0441\u0442\u0438\u043d\u0430 \u043b\u043e\u0436\u044c \u043d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e",o=s.inherit(s.NUMBER_MODE),t={
+className:"string",begin:'"|\\|',end:'"|$',contains:[{begin:'""'}]},a={
+begin:"'",end:"'",excludeBegin:!0,excludeEnd:!0,contains:[{className:"number",
+begin:"\\d{4}([\\.\\\\/:-]?\\d{2}){0,5}"}]},m=s.inherit(s.C_LINE_COMMENT_MODE)
+;return{name:"1C:Enterprise",case_insensitive:!0,keywords:{$pattern:x,keyword:n,
 built_in:"\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u044c\u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u044c\u0441\u0442\u0440\u043e\u043a \u0441\u0438\u043c\u0432\u043e\u043b\u0442\u0430\u0431\u0443\u043b\u044f\u0446\u0438\u0438 ansitooem oemtoansi \u0432\u0432\u0435\u0441\u0442\u0438\u0432\u0438\u0434\u0441\u0443\u0431\u043a\u043e\u043d\u0442\u043e \u0432\u0432\u0435\u0441\u0442\u0438\u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435 \u0432\u0432\u0435\u0441\u0442\u0438\u043f\u0435\u0440\u0438\u043e\u0434 \u0432\u0432\u0435\u0441\u0442\u0438\u043f\u043b\u0430\u043d\u0441\u0447\u0435\u0442\u043e\u0432 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439\u043f\u043b\u0430\u043d\u0441\u0447\u0435\u0442\u043e\u0432 \u0434\u0430\u0442\u0430\u0433\u043e\u0434 \u0434\u0430\u0442\u0430\u043c\u0435\u0441\u044f\u0446 \u0434\u0430\u0442\u0430\u0447\u0438\u0441\u043b\u043e \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a\u0441\u0438\u0441\u0442\u0435\u043c\u044b \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0432\u0441\u0442\u0440\u043e\u043a\u0443 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0438\u0437\u0441\u0442\u0440\u043e\u043a\u0438 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0438\u0431 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043a\u043e\u0434\u0441\u0438\u043c\u0432 \u043a\u043e\u043d\u0433\u043e\u0434\u0430 \u043a\u043e\u043d\u0435\u0446\u043f\u0435\u0440\u0438\u043e\u0434\u0430\u0431\u0438 \u043a\u043e\u043d\u0435\u0446\u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043d\u043d\u043e\u0433\u043e\u043f\u0435\u0440\u0438\u043e\u0434\u0430\u0431\u0438 \u043a\u043e\u043d\u0435\u0446\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e\u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0430 \u043a\u043e\u043d\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430 \u043a\u043e\u043d\u043c\u0435\u0441\u044f\u0446\u0430 \u043a\u043e\u043d\u043d\u0435\u0434\u0435\u043b\u0438 \u043b\u043e\u0433 \u043b\u043e\u043310 \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435\u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e\u0441\u0443\u0431\u043a\u043e\u043d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043d\u0430\u0431\u043e\u0440\u0430\u043f\u0440\u0430\u0432 \u043d\u0430\u0437\u043d\u0430\u0447\u0438\u0442\u044c\u0432\u0438\u0434 \u043d\u0430\u0437\u043d\u0430\u0447\u0438\u0442\u044c\u0441\u0447\u0435\u0442 \u043d\u0430\u0439\u0442\u0438\u0441\u0441\u044b\u043b\u043a\u0438 \u043d\u0430\u0447\u0430\u043b\u043e\u043f\u0435\u0440\u0438\u043e\u0434\u0430\u0431\u0438 \u043d\u0430\u0447\u0430\u043b\u043e\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e\u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0430 \u043d\u0430\u0447\u0433\u043e\u0434\u0430 \u043d\u0430\u0447\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430 \u043d\u0430\u0447\u043c\u0435\u0441\u044f\u0446\u0430 \u043d\u0430\u0447\u043d\u0435\u0434\u0435\u043b\u0438 \u043d\u043e\u043c\u0435\u0440\u0434\u043d\u044f\u0433\u043e\u0434\u0430 \u043d\u043e\u043c\u0435\u0440\u0434\u043d\u044f\u043d\u0435\u0434\u0435\u043b\u0438 \u043d\u043e\u043c\u0435\u0440\u043d\u0435\u0434\u0435\u043b\u0438\u0433\u043e\u0434\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439\u0436\u0443\u0440\u043d\u0430\u043b\u0440\u0430\u0441\u0447\u0435\u0442\u043e\u0432 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439\u043f\u043b\u0430\u043d\u0441\u0447\u0435\u0442\u043e\u0432 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439\u044f\u0437\u044b\u043a \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c\u043e\u043a\u043d\u043e\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u043f\u0435\u0440\u0438\u043e\u0434\u0441\u0442\u0440 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0432\u0440\u0435\u043c\u044f\u0442\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0434\u0430\u0442\u0443\u0442\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0442\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043e\u0442\u0431\u043e\u0440\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043f\u043e\u0437\u0438\u0446\u0438\u044e\u0442\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043f\u0443\u0441\u0442\u043e\u0435\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0442\u0430 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u0430\u0432\u0442\u043e\u043d\u0443\u043c\u0435\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u043f\u0438\u0441\u044c \u043f\u0443\u0441\u0442\u043e\u0435\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0440\u0430\u0437\u043c \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c\u043f\u043e\u0437\u0438\u0446\u0438\u044e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u0442\u044c\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u043d\u0430 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u0442\u044c\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u043f\u043e \u0441\u0438\u043c\u0432 \u0441\u043e\u0437\u0434\u0430\u0442\u044c\u043e\u0431\u044a\u0435\u043a\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u0432\u043e\u0437\u0432\u0440\u0430\u0442\u0430 \u0441\u0442\u0440\u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e\u0441\u0442\u0440\u043e\u043a \u0441\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u043f\u043e\u0437\u0438\u0446\u0438\u044e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0441\u0447\u0435\u0442\u043f\u043e\u043a\u043e\u0434\u0443 \u0442\u0435\u043a\u0443\u0449\u0435\u0435\u0432\u0440\u0435\u043c\u044f \u0442\u0438\u043f\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0442\u0438\u043f\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u0441\u0442\u0440 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0442\u0430\u043d\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0442\u0430\u043f\u043e \u0444\u0438\u043a\u0441\u0448\u0430\u0431\u043b\u043e\u043d \u0448\u0430\u0431\u043b\u043e\u043d acos asin atan base64\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 base64\u0441\u0442\u0440\u043e\u043a\u0430 cos exp log log10 pow sin sqrt tan xml\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 xml\u0441\u0442\u0440\u043e\u043a\u0430 xml\u0442\u0438\u043f xml\u0442\u0438\u043f\u0437\u043d\u0447 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0435\u043e\u043a\u043d\u043e \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439\u0440\u0435\u0436\u0438\u043c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439\u0440\u0435\u0436\u0438\u043c\u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u044f\u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u043b\u0435\u0432\u043e \u0432\u0432\u0435\u0441\u0442\u0438\u0434\u0430\u0442\u0443 \u0432\u0432\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0432\u0432\u0435\u0441\u0442\u0438\u0441\u0442\u0440\u043e\u043a\u0443 \u0432\u0432\u0435\u0441\u0442\u0438\u0447\u0438\u0441\u043b\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u0447\u0442\u0435\u043d\u0438\u044fxml \u0432\u043e\u043f\u0440\u043e\u0441 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0432\u0440\u0435\u0433 \u0432\u044b\u0433\u0440\u0443\u0437\u0438\u0442\u044c\u0436\u0443\u0440\u043d\u0430\u043b\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443\u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443\u043f\u0440\u0430\u0432\u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0432\u044b\u0447\u0438\u0441\u043b\u0438\u0442\u044c \u0433\u043e\u0434 \u0434\u0430\u043d\u043d\u044b\u0435\u0444\u043e\u0440\u043c\u044b\u0432\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0430 \u0434\u0435\u043d\u044c \u0434\u0435\u043d\u044c\u0433\u043e\u0434\u0430 \u0434\u0435\u043d\u044c\u043d\u0435\u0434\u0435\u043b\u0438 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c\u043c\u0435\u0441\u044f\u0446 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0434\u0430\u043d\u043d\u044b\u0435\u0434\u043b\u044f\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0440\u0430\u0431\u043e\u0442\u0443\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c\u0440\u0430\u0431\u043e\u0442\u0443\u0441\u0438\u0441\u0442\u0435\u043c\u044b \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c\u0432\u043d\u0435\u0448\u043d\u044e\u044e\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443 \u0437\u0430\u043a\u0440\u044b\u0442\u044c\u0441\u043f\u0440\u0430\u0432\u043a\u0443 \u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044cjson \u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044cxml \u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0434\u0430\u0442\u0443json \u0437\u0430\u043f\u0438\u0441\u044c\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0437\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u0441\u0432\u043e\u0439\u0441\u0442\u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442\u044c\u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c\u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0437\u0430\u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0432\u0434\u0430\u043d\u043d\u044b\u0435\u0444\u043e\u0440\u043c\u044b \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0432\u0441\u0442\u0440\u043e\u043a\u0443\u0432\u043d\u0443\u0442\u0440 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0432\u0444\u0430\u0439\u043b \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0438\u0437\u0441\u0442\u0440\u043e\u043a\u0438\u0432\u043d\u0443\u0442\u0440 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0438\u0437\u0444\u0430\u0439\u043b\u0430 \u0438\u0437xml\u0442\u0438\u043f\u0430 \u0438\u043c\u043f\u043e\u0440\u0442\u043c\u043e\u0434\u0435\u043b\u0438xdto \u0438\u043c\u044f\u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0430 \u0438\u043c\u044f\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u043f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435\u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u043e\u0431\u043e\u0448\u0438\u0431\u043a\u0435 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438\u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445\u0444\u0430\u0439\u043b\u043e\u0432 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u0442\u0440\u043e\u043a\u0443 \u043a\u043e\u0434\u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u043a\u043e\u0434\u0441\u0438\u043c\u0432\u043e\u043b\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043a\u043e\u043d\u0435\u0446\u0433\u043e\u0434\u0430 \u043a\u043e\u043d\u0435\u0446\u0434\u043d\u044f \u043a\u043e\u043d\u0435\u0446\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430 \u043a\u043e\u043d\u0435\u0446\u043c\u0435\u0441\u044f\u0446\u0430 \u043a\u043e\u043d\u0435\u0446\u043c\u0438\u043d\u0443\u0442\u044b \u043a\u043e\u043d\u0435\u0446\u043d\u0435\u0434\u0435\u043b\u0438 \u043a\u043e\u043d\u0435\u0446\u0447\u0430\u0441\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0431\u0430\u0437\u044b\u0434\u0430\u043d\u043d\u044b\u0445\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0430\u0434\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u0441\u043a\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0430 \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0434\u0430\u043d\u043d\u044b\u0435\u0444\u043e\u0440\u043c\u044b \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0444\u0430\u0439\u043b \u043a\u0440\u0430\u0442\u043a\u043e\u0435\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043e\u0448\u0438\u0431\u043a\u0438 \u043b\u0435\u0432 \u043c\u0430\u043a\u0441 \u043c\u0435\u0441\u0442\u043d\u043e\u0435\u0432\u0440\u0435\u043c\u044f \u043c\u0435\u0441\u044f\u0446 \u043c\u0438\u043d \u043c\u0438\u043d\u0443\u0442\u0430 \u043c\u043e\u043d\u043e\u043f\u043e\u043b\u044c\u043d\u044b\u0439\u0440\u0435\u0436\u0438\u043c \u043d\u0430\u0439\u0442\u0438 \u043d\u0430\u0439\u0442\u0438\u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435\u0441\u0438\u043c\u0432\u043e\u043b\u044bxml \u043d\u0430\u0439\u0442\u0438\u043e\u043a\u043d\u043e\u043f\u043e\u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0441\u0441\u044b\u043b\u043a\u0435 \u043d\u0430\u0439\u0442\u0438\u043f\u043e\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0435\u043d\u0430\u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043d\u0430\u0439\u0442\u0438\u043f\u043e\u0441\u0441\u044b\u043b\u043a\u0430\u043c \u043d\u0430\u0439\u0442\u0438\u0444\u0430\u0439\u043b\u044b \u043d\u0430\u0447\u0430\u043b\u043e\u0433\u043e\u0434\u0430 \u043d\u0430\u0447\u0430\u043b\u043e\u0434\u043d\u044f \u043d\u0430\u0447\u0430\u043b\u043e\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430 \u043d\u0430\u0447\u0430\u043b\u043e\u043c\u0435\u0441\u044f\u0446\u0430 \u043d\u0430\u0447\u0430\u043b\u043e\u043c\u0438\u043d\u0443\u0442\u044b \u043d\u0430\u0447\u0430\u043b\u043e\u043d\u0435\u0434\u0435\u043b\u0438 \u043d\u0430\u0447\u0430\u043b\u043e\u0447\u0430\u0441\u0430 \u043d\u0430\u0447\u0430\u0442\u044c\u0437\u0430\u043f\u0440\u043e\u0441\u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u044f\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0430\u0447\u0430\u0442\u044c\u0437\u0430\u043f\u0443\u0441\u043a\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0430\u0447\u0430\u0442\u044c\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u0444\u0430\u0439\u043b\u0430 \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435\u0444\u0430\u0439\u043b\u0430 \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u0432\u043d\u0435\u0448\u043d\u0435\u0439\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f\u0440\u0430\u0431\u043e\u0442\u044b\u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0435\u0439 \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f\u0440\u0430\u0431\u043e\u0442\u044b\u0441\u0444\u0430\u0439\u043b\u0430\u043c\u0438 \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u043e\u0438\u0441\u043a\u0444\u0430\u0439\u043b\u043e\u0432 \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435\u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0430\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445\u0444\u0430\u0439\u043b\u043e\u0432 \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435\u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0430\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435\u0440\u0430\u0431\u043e\u0447\u0435\u0433\u043e\u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0430\u0434\u0430\u043d\u043d\u044b\u0445\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435\u0444\u0430\u0439\u043b\u043e\u0432 \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435\u0444\u0430\u0439\u043b\u0430 \u043d\u0430\u0447\u0430\u0442\u044c\u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435\u0444\u0430\u0439\u043b\u043e\u0432 \u043d\u0430\u0447\u0430\u0442\u044c\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u0434\u0432\u043e\u0438\u0447\u043d\u044b\u0445\u0434\u0430\u043d\u043d\u044b\u0445\u0438\u0437\u0444\u0430\u0439\u043b\u0430 \u043d\u0430\u0447\u0430\u0442\u044c\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0430 \u043d\u0430\u0447\u0430\u0442\u044c\u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044e \u043d\u0430\u0447\u0430\u0442\u044c\u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u0444\u0430\u0439\u043b\u043e\u0432 \u043d\u0430\u0447\u0430\u0442\u044c\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443\u0432\u043d\u0435\u0448\u043d\u0435\u0439\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u043d\u0430\u0447\u0430\u0442\u044c\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f\u0440\u0430\u0431\u043e\u0442\u044b\u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0435\u0439 \u043d\u0430\u0447\u0430\u0442\u044c\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f\u0440\u0430\u0431\u043e\u0442\u044b\u0441\u0444\u0430\u0439\u043b\u0430\u043c\u0438 \u043d\u0435\u0434\u0435\u043b\u044f\u0433\u043e\u0434\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u044c\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043d\u043e\u043c\u0435\u0440\u0441\u0435\u0430\u043d\u0441\u0430\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u043d\u043e\u043c\u0435\u0440\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u043d\u0440\u0435\u0433 \u043d\u0441\u0442\u0440 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c\u043d\u0443\u043c\u0435\u0440\u0430\u0446\u0438\u044e\u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c\u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u043f\u0440\u0435\u0440\u044b\u0432\u0430\u043d\u0438\u044f\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0438\u0442\u044c\u0444\u0430\u0439\u043b\u044b \u043e\u043a\u0440 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435\u043e\u0448\u0438\u0431\u043a\u0438 \u043e\u043f\u043e\u0432\u0435\u0441\u0442\u0438\u0442\u044c \u043e\u043f\u043e\u0432\u0435\u0441\u0442\u0438\u0442\u044c\u043e\u0431\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0437\u0430\u043f\u0440\u043e\u0441\u0430\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u044f \u043e\u0442\u043a\u0440\u044b\u0442\u044c\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043e\u0442\u043a\u0440\u044b\u0442\u044c\u0438\u043d\u0434\u0435\u043a\u0441\u0441\u043f\u0440\u0430\u0432\u043a\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u044c\u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435\u0441\u043f\u0440\u0430\u0432\u043a\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u044c\u0441\u043f\u0440\u0430\u0432\u043a\u0443 \u043e\u0442\u043a\u0440\u044b\u0442\u044c\u0444\u043e\u0440\u043c\u0443 \u043e\u0442\u043a\u0440\u044b\u0442\u044c\u0444\u043e\u0440\u043c\u0443\u043c\u043e\u0434\u0430\u043b\u044c\u043d\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c\u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044e \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c\u0436\u0443\u0440\u043d\u0430\u043b\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0435\u0440\u0435\u0439\u0442\u0438\u043f\u043e\u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0441\u0441\u044b\u043b\u043a\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c\u0444\u0430\u0439\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0432\u043d\u0435\u0448\u043d\u044e\u044e\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0437\u0430\u043f\u0440\u043e\u0441\u0430\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435\u0440\u0430\u0431\u043e\u0442\u044b\u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0435\u0439 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435\u0440\u0430\u0431\u043e\u0442\u044b\u0441\u0444\u0430\u0439\u043b\u0430\u043c\u0438 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0435\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043e\u0448\u0438\u0431\u043a\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0432\u0432\u043e\u0434\u0434\u0430\u0442\u044b \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0432\u0432\u043e\u0434\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0432\u0432\u043e\u0434\u0441\u0442\u0440\u043e\u043a\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0432\u0432\u043e\u0434\u0447\u0438\u0441\u043b\u0430 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0432\u043e\u043f\u0440\u043e\u0441 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e\u043e\u0431\u043e\u0448\u0438\u0431\u043a\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u043d\u0430\u043a\u0430\u0440\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u043d\u043e\u0435\u0438\u043c\u044f\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044ccom\u043e\u0431\u044a\u0435\u043a\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044cxml\u0442\u0438\u043f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0430\u0434\u0440\u0435\u0441\u043f\u043e\u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0443\u0441\u0435\u0430\u043d\u0441\u043e\u0432 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0432\u0440\u0435\u043c\u044f\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f\u0441\u043f\u044f\u0449\u0435\u0433\u043e\u0441\u0435\u0430\u043d\u0441\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0432\u0440\u0435\u043c\u044f\u0437\u0430\u0441\u044b\u043f\u0430\u043d\u0438\u044f\u043f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0433\u043e\u0441\u0435\u0430\u043d\u0441\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0432\u0440\u0435\u043c\u044f\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0434\u0430\u043d\u043d\u044b\u0435\u0432\u044b\u0431\u043e\u0440\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435\u043a\u043e\u0434\u044b\u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435\u0447\u0430\u0441\u043e\u0432\u044b\u0435\u043f\u043e\u044f\u0441\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a\u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043e\u0442\u0431\u043e\u0440\u0430\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0438\u0437\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0438\u043c\u044f\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e\u0444\u0430\u0439\u043b\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0438\u043c\u044f\u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e\u044d\u043a\u0440\u0430\u043d\u043e\u0432\u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0441\u043e\u0431\u044b\u0442\u0438\u044f\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043a\u0440\u0430\u0442\u043a\u0438\u0439\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043c\u0430\u043a\u0435\u0442\u043e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043c\u0430\u0441\u043a\u0443\u0432\u0441\u0435\u0444\u0430\u0439\u043b\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043c\u0430\u0441\u043a\u0443\u0432\u0441\u0435\u0444\u0430\u0439\u043b\u044b\u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043c\u0430\u0441\u043a\u0443\u0432\u0441\u0435\u0444\u0430\u0439\u043b\u044b\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043f\u043e\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0443\u044e\u0434\u043b\u0438\u043d\u0443\u043f\u0430\u0440\u043e\u043b\u0435\u0439\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u0443\u044e\u0441\u0441\u044b\u043b\u043a\u0443 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u0443\u044e\u0441\u0441\u044b\u043b\u043a\u0443\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438\u0431\u0430\u0437\u044b\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445\u0434\u0430\u043d\u043d\u044b\u0445\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043e\u0431\u0449\u0438\u0439\u043c\u0430\u043a\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043e\u0431\u0449\u0443\u044e\u0444\u043e\u0440\u043c\u0443 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043e\u043a\u043d\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u0443\u044e\u043e\u0442\u043c\u0435\u0442\u043a\u0443\u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0433\u043e\u0440\u0435\u0436\u0438\u043c\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0445\u043e\u043f\u0446\u0438\u0439\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043f\u043e\u043b\u043d\u043e\u0435\u0438\u043c\u044f\u043f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0433\u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f\u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445\u0441\u0441\u044b\u043b\u043e\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443\u0441\u043b\u043e\u0436\u043d\u043e\u0441\u0442\u0438\u043f\u0430\u0440\u043e\u043b\u0435\u0439\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u044c\u043f\u0443\u0442\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u044c\u043f\u0443\u0442\u0438\u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u044c\u043f\u0443\u0442\u0438\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0441\u0435\u0430\u043d\u0441\u044b\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435\u043e\u0431\u044a\u0435\u043a\u0442\u0430\u0438\u0444\u043e\u0440\u043c\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0441\u043e\u0441\u0442\u0430\u0432\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430odata \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f\u0431\u0430\u0437\u044b\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0442\u0435\u043a\u0443\u0449\u0438\u0439\u0441\u0435\u0430\u043d\u0441\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0444\u0430\u0439\u043b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0444\u0430\u0439\u043b\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0444\u043e\u0440\u043c\u0443 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u0443\u044e\u043e\u043f\u0446\u0438\u044e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u0443\u044e\u043e\u043f\u0446\u0438\u044e\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0447\u0430\u0441\u043e\u0432\u043e\u0439\u043f\u043e\u044f\u0441\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438\u043e\u0441 \u043f\u043e\u043c\u0435\u0441\u0442\u0438\u0442\u044c\u0432\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0435\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u043f\u043e\u043c\u0435\u0441\u0442\u0438\u0442\u044c\u0444\u0430\u0439\u043b \u043f\u043e\u043c\u0435\u0441\u0442\u0438\u0442\u044c\u0444\u0430\u0439\u043b\u044b \u043f\u0440\u0430\u0432 \u043f\u0440\u0430\u0432\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0435\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043a\u043e\u0434\u0430\u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0435\u0440\u0438\u043e\u0434\u0430 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0440\u0430\u0432\u0430 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u0441\u043e\u0431\u044b\u0442\u0438\u044f\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u0447\u0430\u0441\u043e\u0432\u043e\u0433\u043e\u043f\u043e\u044f\u0441\u0430 \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u043f\u0440\u0435\u043a\u0440\u0430\u0442\u0438\u0442\u044c\u0440\u0430\u0431\u043e\u0442\u0443\u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043f\u0440\u0438\u0432\u0438\u043b\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439\u0440\u0435\u0436\u0438\u043c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c\u0432\u044b\u0437\u043e\u0432 \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044cjson \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044cxml \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044c\u0434\u0430\u0442\u0443json \u043f\u0443\u0441\u0442\u0430\u044f\u0441\u0442\u0440\u043e\u043a\u0430 \u0440\u0430\u0431\u043e\u0447\u0438\u0439\u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0434\u0430\u043d\u043d\u044b\u0445\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0434\u0430\u043d\u043d\u044b\u0435\u0434\u043b\u044f\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c\u0444\u0430\u0439\u043b \u0440\u0430\u0437\u043e\u0440\u0432\u0430\u0442\u044c\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435\u0441\u0432\u043d\u0435\u0448\u043d\u0438\u043c\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0441\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u0442\u0440\u043e\u043a\u0443 \u0440\u043e\u043b\u044c\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441\u0435\u043a\u0443\u043d\u0434\u0430 \u0441\u0438\u0433\u043d\u0430\u043b \u0441\u0438\u043c\u0432\u043e\u043b \u0441\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0436\u0443\u0440\u043d\u0430\u043b\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0441\u043c\u0435\u0449\u0435\u043d\u0438\u0435\u043b\u0435\u0442\u043d\u0435\u0433\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043c\u0435\u0449\u0435\u043d\u0438\u0435\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043e\u0435\u0434\u0438\u043d\u0438\u0442\u044c\u0431\u0443\u0444\u0435\u0440\u044b\u0434\u0432\u043e\u0438\u0447\u043d\u044b\u0445\u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u043e\u0437\u0434\u0430\u0442\u044c\u043a\u0430\u0442\u0430\u043b\u043e\u0433 \u0441\u043e\u0437\u0434\u0430\u0442\u044c\u0444\u0430\u0431\u0440\u0438\u043a\u0443xdto \u0441\u043e\u043a\u0440\u043b \u0441\u043e\u043a\u0440\u043b\u043f \u0441\u043e\u043a\u0440\u043f \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0441\u0440\u0435\u0434 \u0441\u0442\u0440\u0434\u043b\u0438\u043d\u0430 \u0441\u0442\u0440\u0437\u0430\u043a\u0430\u043d\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f\u043d\u0430 \u0441\u0442\u0440\u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u0441\u0442\u0440\u043d\u0430\u0439\u0442\u0438 \u0441\u0442\u0440\u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f\u0441 \u0441\u0442\u0440\u043e\u043a\u0430 \u0441\u0442\u0440\u043e\u043a\u0430\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u0441\u0442\u0440\u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0441\u0442\u0440\u043e\u043a\u0443 \u0441\u0442\u0440\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u0442\u0440\u0441\u043e\u0435\u0434\u0438\u043d\u0438\u0442\u044c \u0441\u0442\u0440\u0441\u0440\u0430\u0432\u043d\u0438\u0442\u044c \u0441\u0442\u0440\u0447\u0438\u0441\u043b\u043e\u0432\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0439 \u0441\u0442\u0440\u0447\u0438\u0441\u043b\u043e\u0441\u0442\u0440\u043e\u043a \u0441\u0442\u0440\u0448\u0430\u0431\u043b\u043e\u043d \u0442\u0435\u043a\u0443\u0449\u0430\u044f\u0434\u0430\u0442\u0430 \u0442\u0435\u043a\u0443\u0449\u0430\u044f\u0434\u0430\u0442\u0430\u0441\u0435\u0430\u043d\u0441\u0430 \u0442\u0435\u043a\u0443\u0449\u0430\u044f\u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u0430\u044f\u0434\u0430\u0442\u0430 \u0442\u0435\u043a\u0443\u0449\u0430\u044f\u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u0430\u044f\u0434\u0430\u0442\u0430\u0432\u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445 \u0442\u0435\u043a\u0443\u0449\u0438\u0439\u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0442\u0435\u043a\u0443\u0449\u0438\u0439\u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e\u0448\u0440\u0438\u0444\u0442\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0442\u0435\u043a\u0443\u0449\u0438\u0439\u043a\u043e\u0434\u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0442\u0435\u043a\u0443\u0449\u0438\u0439\u0440\u0435\u0436\u0438\u043c\u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0442\u0435\u043a\u0443\u0449\u0438\u0439\u044f\u0437\u044b\u043a \u0442\u0435\u043a\u0443\u0449\u0438\u0439\u044f\u0437\u044b\u043a\u0441\u0438\u0441\u0442\u0435\u043c\u044b \u0442\u0438\u043f \u0442\u0438\u043f\u0437\u043d\u0447 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f\u0430\u043a\u0442\u0438\u0432\u043d\u0430 \u0442\u0440\u0435\u0433 \u0443\u0434\u0430\u043b\u0438\u0442\u044c\u0434\u0430\u043d\u043d\u044b\u0435\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c\u0438\u0437\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430 \u0443\u0434\u0430\u043b\u0438\u0442\u044c\u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c\u0444\u0430\u0439\u043b\u044b \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u043e\u0435\u0432\u0440\u0435\u043c\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439\u0440\u0435\u0436\u0438\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439\u0440\u0435\u0436\u0438\u043c\u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u044f\u0434\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0443\u0441\u0435\u0430\u043d\u0441\u043e\u0432 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0432\u043d\u0435\u0448\u043d\u044e\u044e\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0432\u0440\u0435\u043c\u044f\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f\u0441\u043f\u044f\u0449\u0435\u0433\u043e\u0441\u0435\u0430\u043d\u0441\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0432\u0440\u0435\u043c\u044f\u0437\u0430\u0441\u044b\u043f\u0430\u043d\u0438\u044f\u043f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0433\u043e\u0441\u0435\u0430\u043d\u0441\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0432\u0440\u0435\u043c\u044f\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a\u0441\u0438\u0441\u0442\u0435\u043c\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0441\u043e\u0431\u044b\u0442\u0438\u044f\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u043a\u0440\u0430\u0442\u043a\u0438\u0439\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0443\u044e\u0434\u043b\u0438\u043d\u0443\u043f\u0430\u0440\u043e\u043b\u0435\u0439\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u043c\u043e\u043d\u043e\u043f\u043e\u043b\u044c\u043d\u044b\u0439\u0440\u0435\u0436\u0438\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445\u0434\u0430\u043d\u043d\u044b\u0445\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0433\u043e\u0440\u0435\u0436\u0438\u043c\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0445\u043e\u043f\u0446\u0438\u0439\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u043f\u0440\u0438\u0432\u0438\u043b\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439\u0440\u0435\u0436\u0438\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443\u0441\u043b\u043e\u0436\u043d\u043e\u0441\u0442\u0438\u043f\u0430\u0440\u043e\u043b\u0435\u0439\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435\u0440\u0430\u0431\u043e\u0442\u044b\u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0435\u0439 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435\u0440\u0430\u0431\u043e\u0442\u044b\u0441\u0444\u0430\u0439\u043b\u0430\u043c\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435\u0441\u0432\u043d\u0435\u0448\u043d\u0438\u043c\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c\u0434\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435\u043e\u0431\u044a\u0435\u043a\u0442\u0430\u0438\u0444\u043e\u0440\u043c\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0441\u043e\u0441\u0442\u0430\u0432\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430odata \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0447\u0430\u0441\u043e\u0432\u043e\u0439\u043f\u043e\u044f\u0441\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c\u0447\u0430\u0441\u043e\u0432\u043e\u0439\u043f\u043e\u044f\u0441\u0441\u0435\u0430\u043d\u0441\u0430 \u0444\u043e\u0440\u043c\u0430\u0442 \u0446\u0435\u043b \u0447\u0430\u0441 \u0447\u0430\u0441\u043e\u0432\u043e\u0439\u043f\u043e\u044f\u0441 \u0447\u0430\u0441\u043e\u0432\u043e\u0439\u043f\u043e\u044f\u0441\u0441\u0435\u0430\u043d\u0441\u0430 \u0447\u0438\u0441\u043b\u043e \u0447\u0438\u0441\u043b\u043e\u043f\u0440\u043e\u043f\u0438\u0441\u044c\u044e \u044d\u0442\u043e\u0430\u0434\u0440\u0435\u0441\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430 ws\u0441\u0441\u044b\u043b\u043a\u0438 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u043a\u0430\u0440\u0442\u0438\u043d\u043e\u043a \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u043c\u0430\u043a\u0435\u0442\u043e\u0432\u043e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u044f\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u0441\u0442\u0438\u043b\u0435\u0439 \u0431\u0438\u0437\u043d\u0435\u0441\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u044b \u0432\u043d\u0435\u0448\u043d\u0438\u0435\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u043d\u0435\u0448\u043d\u0438\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0432\u043d\u0435\u0448\u043d\u0438\u0435\u043e\u0442\u0447\u0435\u0442\u044b \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0435\u043f\u043e\u043a\u0443\u043f\u043a\u0438 \u0433\u043b\u0430\u0432\u043d\u044b\u0439\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0433\u043b\u0430\u0432\u043d\u044b\u0439\u0441\u0442\u0438\u043b\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0435\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u0436\u0443\u0440\u043d\u0430\u043b\u044b\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u043e\u0431\u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0440\u0430\u0431\u043e\u0447\u0435\u0439\u0434\u0430\u0442\u044b \u0438\u0441\u0442\u043e\u0440\u0438\u044f\u0440\u0430\u0431\u043e\u0442\u044b\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u044b \u043a\u0440\u0438\u0442\u0435\u0440\u0438\u0438\u043e\u0442\u0431\u043e\u0440\u0430 \u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0440\u0435\u043a\u043b\u0430\u043c\u044b \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0430\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0445\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 \u043e\u0442\u0447\u0435\u0442\u044b \u043f\u0430\u043d\u0435\u043b\u044c\u0437\u0430\u0434\u0430\u0447\u043e\u0441 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u0441\u0435\u0430\u043d\u0441\u0430 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u043f\u043b\u0430\u043d\u044b\u0432\u0438\u0434\u043e\u0432\u0440\u0430\u0441\u0447\u0435\u0442\u0430 \u043f\u043b\u0430\u043d\u044b\u0432\u0438\u0434\u043e\u0432\u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a \u043f\u043b\u0430\u043d\u044b\u043e\u0431\u043c\u0435\u043d\u0430 \u043f\u043b\u0430\u043d\u044b\u0441\u0447\u0435\u0442\u043e\u0432 \u043f\u043e\u043b\u043d\u043e\u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0439\u043f\u043e\u0438\u0441\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0431\u0430\u0437\u044b \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445\u043f\u043e\u043a\u0443\u043f\u043e\u043a \u0440\u0430\u0431\u043e\u0447\u0430\u044f\u0434\u0430\u0442\u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u043d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0440\u0430\u0441\u0447\u0435\u0442\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0441\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442\u043d\u044b\u0435\u0437\u0430\u0434\u0430\u043d\u0438\u044f \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440xdto \u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u0433\u0435\u043e\u043f\u043e\u0437\u0438\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0438 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043c\u0443\u043b\u044c\u0442\u0438\u043c\u0435\u0434\u0438\u0430 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0440\u0435\u043a\u043b\u0430\u043c\u044b \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043f\u043e\u0447\u0442\u044b \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0438\u0438 \u0444\u0430\u0431\u0440\u0438\u043a\u0430xdto \u0444\u0430\u0439\u043b\u043e\u0432\u044b\u0435\u043f\u043e\u0442\u043e\u043a\u0438 \u0444\u043e\u043d\u043e\u0432\u044b\u0435\u0437\u0430\u0434\u0430\u043d\u0438\u044f \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u0432\u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\u0434\u0430\u043d\u043d\u044b\u0445\u0444\u043e\u0440\u043c \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u043e\u0431\u0449\u0438\u0445\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\u0434\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u0441\u043a\u0438\u0445\u0441\u043f\u0438\u0441\u043a\u043e\u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0445\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a ",
 class:"web\u0446\u0432\u0435\u0442\u0430 windows\u0446\u0432\u0435\u0442\u0430 windows\u0448\u0440\u0438\u0444\u0442\u044b \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u043a\u0430\u0440\u0442\u0438\u043d\u043e\u043a \u0440\u0430\u043c\u043a\u0438\u0441\u0442\u0438\u043b\u044f \u0441\u0438\u043c\u0432\u043e\u043b\u044b \u0446\u0432\u0435\u0442\u0430\u0441\u0442\u0438\u043b\u044f \u0448\u0440\u0438\u0444\u0442\u044b\u0441\u0442\u0438\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435\u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435\u0434\u0430\u043d\u043d\u044b\u0445\u0444\u043e\u0440\u043c\u044b\u0432\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0430\u0432\u0442\u043e\u043d\u0443\u043c\u0435\u0440\u0430\u0446\u0438\u044f\u0432\u0444\u043e\u0440\u043c\u0435 \u0430\u0432\u0442\u043e\u0440\u0430\u0437\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435\u0441\u0435\u0440\u0438\u0439 \u0430\u043d\u0438\u043c\u0430\u0446\u0438\u044f\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0432\u044b\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u043d\u0438\u044f\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0438\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u043e\u0432 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f\u0432\u044b\u0441\u043e\u0442\u043e\u0439\u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u0430\u044f\u043f\u0440\u043e\u043a\u0440\u0443\u0442\u043a\u0430\u0444\u043e\u0440\u043c\u044b \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u0432\u0438\u0434\u0433\u0440\u0443\u043f\u043f\u044b\u0444\u043e\u0440\u043c\u044b \u0432\u0438\u0434\u0434\u0435\u043a\u043e\u0440\u0430\u0446\u0438\u0438\u0444\u043e\u0440\u043c\u044b \u0432\u0438\u0434\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0444\u043e\u0440\u043c\u044b \u0432\u0438\u0434\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u0438\u0434\u043a\u043d\u043e\u043f\u043a\u0438\u0444\u043e\u0440\u043c\u044b \u0432\u0438\u0434\u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f \u0432\u0438\u0434\u043f\u043e\u0434\u043f\u0438\u0441\u0435\u0439\u043a\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0435 \u0432\u0438\u0434\u043f\u043e\u043b\u044f\u0444\u043e\u0440\u043c\u044b \u0432\u0438\u0434\u0444\u043b\u0430\u0436\u043a\u0430 \u0432\u043b\u0438\u044f\u043d\u0438\u0435\u0440\u0430\u0437\u043c\u0435\u0440\u0430\u043d\u0430\u043f\u0443\u0437\u044b\u0440\u0435\u043a\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0430\u043a\u043e\u043b\u043e\u043d\u043e\u043a \u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0430\u043f\u043e\u0434\u0447\u0438\u043d\u0435\u043d\u043d\u044b\u0445\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0444\u043e\u0440\u043c\u044b \u0433\u0440\u0443\u043f\u043f\u044b\u0438\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435\u043f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439\u0440\u0435\u0436\u0438\u043c\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f\u043f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u043c\u0435\u0436\u0434\u0443\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u043c\u0438\u0444\u043e\u0440\u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0432\u044b\u0432\u043e\u0434\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043f\u043e\u043b\u043e\u0441\u044b\u043f\u0440\u043e\u043a\u0440\u0443\u0442\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0435\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0442\u043e\u0447\u043a\u0438\u0431\u0438\u0440\u0436\u0435\u0432\u043e\u0439\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0438\u0441\u0442\u043e\u0440\u0438\u044f\u0432\u044b\u0431\u043e\u0440\u0430\u043f\u0440\u0438\u0432\u0432\u043e\u0434\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439\u043e\u0441\u0438\u0442\u043e\u0447\u0435\u043a\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u0440\u0430\u0437\u043c\u0435\u0440\u0430\u043f\u0443\u0437\u044b\u0440\u044c\u043a\u0430\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f\u0433\u0440\u0443\u043f\u043f\u044b\u043a\u043e\u043c\u0430\u043d\u0434 \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c\u0441\u0435\u0440\u0438\u0439 \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u0435\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0434\u0435\u0440\u0435\u0432\u0430 \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u0435\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0441\u043f\u0438\u0441\u043a\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435\u0442\u0435\u043a\u0441\u0442\u0430\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0440\u0438\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0434\u0435\u043d\u0434\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u043e\u0440\u0438\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043e\u0440\u0438\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u043c\u0435\u0442\u043e\u043a\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043e\u0440\u0438\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u043c\u0435\u0442\u043e\u043a\u0441\u0432\u043e\u0434\u043d\u043e\u0439\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043e\u0440\u0438\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0444\u043e\u0440\u043c\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0432\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0435 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0432\u043b\u0435\u0433\u0435\u043d\u0434\u0435\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0433\u0440\u0443\u043f\u043f\u044b\u043a\u043d\u043e\u043f\u043e\u043a \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0430\u0448\u043a\u0430\u043b\u044b\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439\u0441\u0432\u043e\u0434\u043d\u043e\u0439\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0430\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b\u0433\u0430\u043d\u0442\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043a\u043d\u043e\u043f\u043a\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043a\u043d\u043e\u043f\u043a\u0438\u0432\u044b\u0431\u043e\u0440\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043e\u0431\u0441\u0443\u0436\u0434\u0435\u043d\u0438\u0439\u0444\u043e\u0440\u043c\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043e\u0431\u044b\u0447\u043d\u043e\u0439\u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439\u043f\u0443\u0437\u044b\u0440\u044c\u043a\u043e\u0432\u043e\u0439\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043f\u0430\u043d\u0435\u043b\u0438\u043f\u043e\u0438\u0441\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f\u043f\u0440\u0438\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0438\u043f\u043e\u043b\u043e\u0441\u044b\u0440\u0435\u0433\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0444\u043e\u0440\u043c\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0442\u0430\u0431\u043b\u0438\u0446\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0442\u0435\u043a\u0441\u0442\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b\u0433\u0430\u043d\u0442\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f\u043e\u0431\u044b\u0447\u043d\u043e\u0439\u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0444\u0438\u0433\u0443\u0440\u044b\u043a\u043d\u043e\u043f\u043a\u0438 \u043f\u0430\u043b\u0438\u0442\u0440\u0430\u0446\u0432\u0435\u0442\u043e\u0432\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435\u043e\u0431\u044b\u0447\u043d\u043e\u0439\u0433\u0440\u0443\u043f\u043f\u044b \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430\u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0430\u0434\u0435\u043d\u0434\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430\u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0430\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b\u0433\u0430\u043d\u0442\u0430 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430\u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0430\u0441\u0432\u043e\u0434\u043d\u043e\u0439\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043f\u043e\u0438\u0441\u043a\u0432\u0442\u0430\u0431\u043b\u0438\u0446\u0435\u043f\u0440\u0438\u0432\u0432\u043e\u0434\u0435 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0430\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0444\u043e\u0440\u043c\u044b \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438\u043a\u043d\u043e\u043f\u043a\u0438\u0444\u043e\u0440\u043c\u044b \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043a\u043e\u043c\u0430\u043d\u0434\u043d\u043e\u0439\u043f\u0430\u043d\u0435\u043b\u0438\u0444\u043e\u0440\u043c\u044b \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043a\u043e\u043c\u0430\u043d\u0434\u043d\u043e\u0439\u043f\u0430\u043d\u0435\u043b\u0438\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0444\u043e\u0440\u043c\u044b \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043e\u043f\u043e\u0440\u043d\u043e\u0439\u0442\u043e\u0447\u043a\u0438\u043e\u0442\u0440\u0438\u0441\u043e\u0432\u043a\u0438 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043f\u043e\u0434\u043f\u0438\u0441\u0435\u0439\u043a\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0435 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043f\u043e\u0434\u043f\u0438\u0441\u0435\u0439\u0448\u043a\u0430\u043b\u044b\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439\u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f\u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0441\u0442\u0440\u043e\u043a\u0438\u043f\u043e\u0438\u0441\u043a\u0430 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u0435\u043a\u0441\u0442\u0430\u0441\u043e\u0435\u0434\u0438\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439\u043b\u0438\u043d\u0438\u0438 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f\u043f\u043e\u0438\u0441\u043a\u043e\u043c \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0448\u043a\u0430\u043b\u044b\u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u043e\u0440\u044f\u0434\u043e\u043a\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0442\u043e\u0447\u0435\u043a\u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0439\u0433\u0438\u0441\u0442\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u043f\u043e\u0440\u044f\u0434\u043e\u043a\u0441\u0435\u0440\u0438\u0439\u0432\u043b\u0435\u0433\u0435\u043d\u0434\u0435\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0440\u0430\u0437\u043c\u0435\u0440\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0430\u0448\u043a\u0430\u043b\u044b\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0440\u0430\u0441\u0442\u044f\u0433\u0438\u0432\u0430\u043d\u0438\u0435\u043f\u043e\u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u0438\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b\u0433\u0430\u043d\u0442\u0430 \u0440\u0435\u0436\u0438\u043c\u0430\u0432\u0442\u043e\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0440\u0435\u0436\u0438\u043c\u0432\u0432\u043e\u0434\u0430\u0441\u0442\u0440\u043e\u043a\u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0440\u0435\u0436\u0438\u043c\u0432\u044b\u0431\u043e\u0440\u0430\u043d\u0435\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u043e\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u044f\u0434\u0430\u0442\u044b \u0440\u0435\u0436\u0438\u043c\u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u044f\u0441\u0442\u0440\u043e\u043a\u0438\u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0440\u0435\u0436\u0438\u043c\u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u044f\u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0440\u0435\u0436\u0438\u043c\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u0440\u0430\u0437\u043c\u0435\u0440\u0430 \u0440\u0435\u0436\u0438\u043c\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u0441\u0432\u044f\u0437\u0430\u043d\u043d\u043e\u0433\u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0440\u0435\u0436\u0438\u043c\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u0434\u0438\u0430\u043b\u043e\u0433\u0430\u043f\u0435\u0447\u0430\u0442\u0438 \u0440\u0435\u0436\u0438\u043c\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0440\u0435\u0436\u0438\u043c\u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u0440\u0435\u0436\u0438\u043c\u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e\u043e\u043a\u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0436\u0438\u043c\u043e\u0442\u043a\u0440\u044b\u0442\u0438\u044f\u043e\u043a\u043d\u0430\u0444\u043e\u0440\u043c\u044b \u0440\u0435\u0436\u0438\u043c\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0440\u0435\u0436\u0438\u043c\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0440\u0435\u0436\u0438\u043c\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439\u0441\u0435\u0440\u0438\u0438 \u0440\u0435\u0436\u0438\u043c\u043e\u0442\u0440\u0438\u0441\u043e\u0432\u043a\u0438\u0441\u0435\u0442\u043a\u0438\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0440\u0435\u0436\u0438\u043c\u043f\u043e\u043b\u0443\u043f\u0440\u043e\u0437\u0440\u0430\u0447\u043d\u043e\u0441\u0442\u0438\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0440\u0435\u0436\u0438\u043c\u043f\u0440\u043e\u0431\u0435\u043b\u043e\u0432\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0440\u0435\u0436\u0438\u043c\u0440\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u044f\u043d\u0430\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u0440\u0435\u0436\u0438\u043c\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043a\u043e\u043b\u043e\u043d\u043a\u0438 \u0440\u0435\u0436\u0438\u043c\u0441\u0433\u043b\u0430\u0436\u0438\u0432\u0430\u043d\u0438\u044f\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0440\u0435\u0436\u0438\u043c\u0441\u0433\u043b\u0430\u0436\u0438\u0432\u0430\u043d\u0438\u044f\u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0440\u0435\u0436\u0438\u043c\u0441\u043f\u0438\u0441\u043a\u0430\u0437\u0430\u0434\u0430\u0447 \u0441\u043a\u0432\u043e\u0437\u043d\u043e\u0435\u0432\u044b\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435\u0434\u0430\u043d\u043d\u044b\u0445\u0444\u043e\u0440\u043c\u044b\u0432\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0441\u043f\u043e\u0441\u043e\u0431\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f\u0442\u0435\u043a\u0441\u0442\u0430\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0430\u0448\u043a\u0430\u043b\u044b\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0432\u0430\u044e\u0449\u0435\u0433\u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u0433\u0440\u0443\u043f\u043f\u0430\u043a\u043e\u043c\u0430\u043d\u0434 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0435\u043e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u044f\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0441\u0442\u0438\u043b\u044c\u0441\u0442\u0440\u0435\u043b\u043a\u0438 \u0442\u0438\u043f\u0430\u043f\u043f\u0440\u043e\u043a\u0441\u0438\u043c\u0430\u0446\u0438\u0438\u043b\u0438\u043d\u0438\u0438\u0442\u0440\u0435\u043d\u0434\u0430\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0442\u0438\u043f\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0442\u0438\u043f\u0435\u0434\u0438\u043d\u0438\u0446\u044b\u0448\u043a\u0430\u043b\u044b\u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0442\u0438\u043f\u0438\u043c\u043f\u043e\u0440\u0442\u0430\u0441\u0435\u0440\u0438\u0439\u0441\u043b\u043e\u044f\u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0442\u0438\u043f\u043b\u0438\u043d\u0438\u0438\u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0442\u0438\u043f\u043b\u0438\u043d\u0438\u0438\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0442\u0438\u043f\u043c\u0430\u0440\u043a\u0435\u0440\u0430\u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0442\u0438\u043f\u043c\u0430\u0440\u043a\u0435\u0440\u0430\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0442\u0438\u043f\u043e\u0431\u043b\u0430\u0441\u0442\u0438\u043e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u044f \u0442\u0438\u043f\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u0434\u0430\u043d\u043d\u044b\u0445\u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0442\u0438\u043f\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0441\u0435\u0440\u0438\u0438\u0441\u043b\u043e\u044f\u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0442\u0438\u043f\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0442\u043e\u0447\u0435\u0447\u043d\u043e\u0433\u043e\u043e\u0431\u044a\u0435\u043a\u0442\u0430\u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0442\u0438\u043f\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0448\u043a\u0430\u043b\u044b\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u043b\u0435\u0433\u0435\u043d\u0434\u044b\u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0442\u0438\u043f\u043f\u043e\u0438\u0441\u043a\u0430\u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432\u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0442\u0438\u043f\u043f\u0440\u043e\u0435\u043a\u0446\u0438\u0438\u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0442\u0438\u043f\u0440\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u044f\u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u0439 \u0442\u0438\u043f\u0440\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u044f\u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u043e\u0432\u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u0439 \u0442\u0438\u043f\u0440\u0430\u043c\u043a\u0438\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0442\u0438\u043f\u0441\u0432\u043e\u0434\u043d\u043e\u0439\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0442\u0438\u043f\u0441\u0432\u044f\u0437\u0438\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b\u0433\u0430\u043d\u0442\u0430 \u0442\u0438\u043f\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439\u043f\u043e\u0441\u0435\u0440\u0438\u044f\u043c\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0442\u0438\u043f\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f\u0442\u043e\u0447\u0435\u043a\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0442\u0438\u043f\u0441\u043e\u0435\u0434\u0438\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439\u043b\u0438\u043d\u0438\u0438 \u0442\u0438\u043f\u0441\u0442\u043e\u0440\u043e\u043d\u044b\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0442\u0438\u043f\u0444\u043e\u0440\u043c\u044b\u043e\u0442\u0447\u0435\u0442\u0430 \u0442\u0438\u043f\u0448\u043a\u0430\u043b\u044b\u0440\u0430\u0434\u0430\u0440\u043d\u043e\u0439\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0444\u0430\u043a\u0442\u043e\u0440\u043b\u0438\u043d\u0438\u0438\u0442\u0440\u0435\u043d\u0434\u0430\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b \u0444\u0438\u0433\u0443\u0440\u0430\u043a\u043d\u043e\u043f\u043a\u0438 \u0444\u0438\u0433\u0443\u0440\u044b\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439\u0441\u0445\u0435\u043c\u044b \u0444\u0438\u043a\u0441\u0430\u0446\u0438\u044f\u0432\u0442\u0430\u0431\u043b\u0438\u0446\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u0434\u043d\u044f\u0448\u043a\u0430\u043b\u044b\u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0444\u043e\u0440\u043c\u0430\u0442\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438 \u0448\u0438\u0440\u0438\u043d\u0430\u043f\u043e\u0434\u0447\u0438\u043d\u0435\u043d\u043d\u044b\u0445\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0444\u043e\u0440\u043c\u044b \u0432\u0438\u0434\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438 \u0432\u0438\u0434\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f \u0432\u0438\u0434\u043f\u0435\u0440\u0438\u043e\u0434\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0440\u0430\u0441\u0447\u0435\u0442\u0430 \u0432\u0438\u0434\u0441\u0447\u0435\u0442\u0430 \u0432\u0438\u0434\u0442\u043e\u0447\u043a\u0438\u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430\u0431\u0438\u0437\u043d\u0435\u0441\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0430\u0433\u0440\u0435\u0433\u0430\u0442\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u043d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0433\u0440\u0443\u043f\u043f\u0438\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0440\u0435\u0436\u0438\u043c\u0430\u043f\u0440\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0441\u0440\u0435\u0437\u0430 \u043f\u0435\u0440\u0438\u043e\u0434\u0438\u0447\u043d\u043e\u0441\u0442\u044c\u0430\u0433\u0440\u0435\u0433\u0430\u0442\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u043d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f \u0440\u0435\u0436\u0438\u043c\u0430\u0432\u0442\u043e\u0432\u0440\u0435\u043c\u044f \u0440\u0435\u0436\u0438\u043c\u0437\u0430\u043f\u0438\u0441\u0438\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0440\u0435\u0436\u0438\u043c\u043f\u0440\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0430\u0432\u0442\u043e\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439\u043d\u043e\u043c\u0435\u0440\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0430\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u043e\u0440\u0438\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0438\u0442\u043e\u0433\u043e\u0432\u043a\u043e\u043b\u043e\u043d\u043e\u043a\u0441\u0432\u043e\u0434\u043d\u043e\u0439\u0442\u0430\u0431\u043b\u0438\u0446\u044b \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0438\u0442\u043e\u0433\u043e\u0432\u0441\u0442\u0440\u043e\u043a\u0441\u0432\u043e\u0434\u043d\u043e\u0439\u0442\u0430\u0431\u043b\u0438\u0446\u044b \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u0435\u043a\u0441\u0442\u0430\u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0430\u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0441\u043f\u043e\u0441\u043e\u0431\u0447\u0442\u0435\u043d\u0438\u044f\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u0434\u0432\u0443\u0441\u0442\u043e\u0440\u043e\u043d\u043d\u0435\u0439\u043f\u0435\u0447\u0430\u0442\u0438 \u0442\u0438\u043f\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f\u043e\u0431\u043b\u0430\u0441\u0442\u0438\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u043a\u0443\u0440\u0441\u043e\u0440\u043e\u0432\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u043b\u0438\u043d\u0438\u0438\u0440\u0438\u0441\u0443\u043d\u043a\u0430\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u043b\u0438\u043d\u0438\u0438\u044f\u0447\u0435\u0439\u043a\u0438\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f\u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u044f\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u043b\u0438\u043d\u0438\u0439\u0441\u0432\u043e\u0434\u043d\u043e\u0439\u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0442\u0438\u043f\u0440\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u044f\u0442\u0435\u043a\u0441\u0442\u0430\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u0440\u0438\u0441\u0443\u043d\u043a\u0430\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u0441\u043c\u0435\u0449\u0435\u043d\u0438\u044f\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u0443\u0437\u043e\u0440\u0430\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u0444\u0430\u0439\u043b\u0430\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c\u043f\u0435\u0447\u0430\u0442\u0438 \u0447\u0435\u0440\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0435\u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f\u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u0438\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u043f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0449\u0438\u043a\u0430 \u0442\u0438\u043f\u0444\u0430\u0439\u043b\u0430\u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u043e\u0431\u0445\u043e\u0434\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u0438\u043f\u0437\u0430\u043f\u0438\u0441\u0438\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0432\u0438\u0434\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f\u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438\u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044f\u043e\u0442\u0447\u0435\u0442\u0430 \u0442\u0438\u043f\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0439 \u0442\u0438\u043f\u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f\u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044f\u043e\u0442\u0447\u0435\u0442\u0430 \u0442\u0438\u043f\u0440\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u044f\u0438\u0442\u043e\u0433\u043e\u0432 \u0434\u043e\u0441\u0442\u0443\u043f\u043a\u0444\u0430\u0439\u043b\u0443 \u0440\u0435\u0436\u0438\u043c\u0434\u0438\u0430\u043b\u043e\u0433\u0430\u0432\u044b\u0431\u043e\u0440\u0430\u0444\u0430\u0439\u043b\u0430 \u0440\u0435\u0436\u0438\u043c\u043e\u0442\u043a\u0440\u044b\u0442\u0438\u044f\u0444\u0430\u0439\u043b\u0430 \u0442\u0438\u043f\u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f\u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044f\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0432\u0438\u0434\u0434\u0430\u043d\u043d\u044b\u0445\u0430\u043d\u0430\u043b\u0438\u0437\u0430 \u043c\u0435\u0442\u043e\u0434\u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0442\u0438\u043f\u0435\u0434\u0438\u043d\u0438\u0446\u044b\u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0430\u0432\u0440\u0435\u043c\u0435\u043d\u0438\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f\u0442\u0430\u0431\u043b\u0438\u0446\u044b\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u0447\u0438\u0441\u043b\u043e\u0432\u044b\u0445\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u0434\u0430\u043d\u043d\u044b\u0445\u043f\u043e\u0438\u0441\u043a\u0430\u0430\u0441\u0441\u043e\u0446\u0438\u0430\u0446\u0438\u0439 \u0442\u0438\u043f\u043a\u043e\u043b\u043e\u043d\u043a\u0438\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445\u0434\u0435\u0440\u0435\u0432\u043e\u0440\u0435\u0448\u0435\u043d\u0438\u0439 \u0442\u0438\u043f\u043a\u043e\u043b\u043e\u043d\u043a\u0438\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445\u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0442\u0438\u043f\u043a\u043e\u043b\u043e\u043d\u043a\u0438\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445\u043e\u0431\u0449\u0430\u044f\u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430 \u0442\u0438\u043f\u043a\u043e\u043b\u043e\u043d\u043a\u0438\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445\u043f\u043e\u0438\u0441\u043a\u0430\u0441\u0441\u043e\u0446\u0438\u0430\u0446\u0438\u0439 \u0442\u0438\u043f\u043a\u043e\u043b\u043e\u043d\u043a\u0438\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445\u043f\u043e\u0438\u0441\u043a\u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0435\u0439 \u0442\u0438\u043f\u043a\u043e\u043b\u043e\u043d\u043a\u0438\u043c\u043e\u0434\u0435\u043b\u0438\u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u0442\u0438\u043f\u043c\u0435\u0440\u044b\u0440\u0430\u0441\u0441\u0442\u043e\u044f\u043d\u0438\u044f\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u043e\u0442\u0441\u0435\u0447\u0435\u043d\u0438\u044f\u043f\u0440\u0430\u0432\u0438\u043b\u0430\u0441\u0441\u043e\u0446\u0438\u0430\u0446\u0438\u0438 \u0442\u0438\u043f\u043f\u043e\u043b\u044f\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0438\u0437\u0430\u0446\u0438\u0438\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0443\u043f\u043e\u0440\u044f\u0434\u043e\u0447\u0438\u0432\u0430\u043d\u0438\u044f\u043f\u0440\u0430\u0432\u0438\u043b\u0430\u0441\u0441\u043e\u0446\u0438\u0430\u0446\u0438\u0438\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0443\u043f\u043e\u0440\u044f\u0434\u043e\u0447\u0438\u0432\u0430\u043d\u0438\u044f\u0448\u0430\u0431\u043b\u043e\u043d\u043e\u0432\u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0435\u0439\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0443\u043f\u0440\u043e\u0449\u0435\u043d\u0438\u044f\u0434\u0435\u0440\u0435\u0432\u0430\u0440\u0435\u0448\u0435\u043d\u0438\u0439 ws\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \u0432\u0430\u0440\u0438\u0430\u043d\u0442xpathxs \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0437\u0430\u043f\u0438\u0441\u0438\u0434\u0430\u0442\u044bjson \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043f\u0440\u043e\u0441\u0442\u043e\u0433\u043e\u0442\u0438\u043f\u0430xs \u0432\u0438\u0434\u0433\u0440\u0443\u043f\u043f\u044b\u043c\u043e\u0434\u0435\u043b\u0438xs \u0432\u0438\u0434\u0444\u0430\u0441\u0435\u0442\u0430xdto \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435\u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044fdom \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u043e\u0441\u0442\u044c\u043f\u0440\u043e\u0441\u0442\u043e\u0433\u043e\u0442\u0438\u043f\u0430xs \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u043e\u0441\u0442\u044c\u0441\u043e\u0441\u0442\u0430\u0432\u043d\u043e\u0433\u043e\u0442\u0438\u043f\u0430xs \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u043e\u0441\u0442\u044c\u0441\u0445\u0435\u043c\u044bxs \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043d\u044b\u0435\u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438xs \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u0433\u0440\u0443\u043f\u043f\u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438xs \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430xs \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u0438\u0434\u0435\u043d\u0442\u0438\u0447\u043d\u043e\u0441\u0442\u0438xs \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0438\u043c\u0435\u043dxs \u043c\u0435\u0442\u043e\u0434\u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u044fxs \u043c\u043e\u0434\u0435\u043b\u044c\u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043exs \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0442\u0438\u043f\u0430xml \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435\u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438xs \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u043f\u0440\u043e\u0431\u0435\u043b\u044c\u043d\u044b\u0445\u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432xs \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043exs \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044fxs \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u043e\u0442\u0431\u043e\u0440\u0430\u0443\u0437\u043b\u043e\u0432dom \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0441\u0442\u0440\u043e\u043ajson \u043f\u043e\u0437\u0438\u0446\u0438\u044f\u0432\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0435dom \u043f\u0440\u043e\u0431\u0435\u043b\u044c\u043d\u044b\u0435\u0441\u0438\u043c\u0432\u043e\u043b\u044bxml \u0442\u0438\u043f\u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430xml \u0442\u0438\u043f\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044fjson \u0442\u0438\u043f\u043a\u0430\u043d\u043e\u043d\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043exml \u0442\u0438\u043f\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044bxs \u0442\u0438\u043f\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438xml \u0442\u0438\u043f\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430domxpath \u0442\u0438\u043f\u0443\u0437\u043b\u0430dom \u0442\u0438\u043f\u0443\u0437\u043b\u0430xml \u0444\u043e\u0440\u043c\u0430xml \u0444\u043e\u0440\u043c\u0430\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044fxs \u0444\u043e\u0440\u043c\u0430\u0442\u0434\u0430\u0442\u044bjson \u044d\u043a\u0440\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432json \u0432\u0438\u0434\u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438\u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u0445\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0438\u0442\u043e\u0433\u043e\u0432\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043f\u043e\u043b\u0435\u0439\u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043f\u043e\u043b\u044f\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u043e\u0432\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u043e\u0433\u043e\u043e\u0441\u0442\u0430\u0442\u043a\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0432\u044b\u0432\u043e\u0434\u0430\u0442\u0435\u043a\u0441\u0442\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0433\u0440\u0443\u043f\u043f\u044b\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u043e\u0442\u0431\u043e\u0440\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f\u043f\u0435\u0440\u0438\u043e\u0434\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0430\u043f\u043e\u043b\u0435\u0439\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u043c\u0430\u043a\u0435\u0442\u0430\u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u043c\u0430\u043a\u0435\u0442\u0430\u043e\u0431\u043b\u0430\u0441\u0442\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u043e\u0441\u0442\u0430\u0442\u043a\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u043f\u0435\u0440\u0438\u043e\u0434\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0440\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u044f\u0442\u0435\u043a\u0441\u0442\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u0441\u0432\u044f\u0437\u0438\u043d\u0430\u0431\u043e\u0440\u043e\u0432\u0434\u0430\u043d\u043d\u044b\u0445\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043b\u0435\u0433\u0435\u043d\u0434\u044b\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u043e\u0442\u0431\u043e\u0440\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0435\u0436\u0438\u043c\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0435\u0436\u0438\u043c\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u043f\u043e\u0441\u043e\u0431\u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0435\u0436\u0438\u043c\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0430\u0432\u0442\u043e\u043f\u043e\u0437\u0438\u0446\u0438\u044f\u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432\u0432\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0435\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0444\u0438\u043a\u0441\u0430\u0446\u0438\u044f\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0443\u0441\u043b\u043e\u0432\u043d\u043e\u0433\u043e\u043e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u044f\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u0430\u0436\u043d\u043e\u0441\u0442\u044c\u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0433\u043e\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0442\u0435\u043a\u0441\u0442\u0430\u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0433\u043e\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0441\u043f\u043e\u0441\u043e\u0431\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0433\u043e\u0432\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441\u043f\u043e\u0441\u043e\u0431\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043d\u0435ascii\u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432\u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0433\u043e\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0442\u0438\u043f\u0442\u0435\u043a\u0441\u0442\u0430\u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0433\u043e\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043f\u043e\u0447\u0442\u044b \u0441\u0442\u0430\u0442\u0443\u0441\u0440\u0430\u0437\u0431\u043e\u0440\u0430\u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0433\u043e\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0440\u0435\u0436\u0438\u043c\u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438\u0437\u0430\u043f\u0438\u0441\u0438\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0441\u0442\u0430\u0442\u0443\u0441\u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438\u0437\u0430\u043f\u0438\u0441\u0438\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0440\u043e\u0432\u0435\u043d\u044c\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430\u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0432\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0438 \u0440\u0435\u0436\u0438\u043c\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0432\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0438 \u0440\u0435\u0436\u0438\u043c\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438\u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0438 \u0442\u0438\u043f\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430\u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0432\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0438 \u043a\u043e\u0434\u0438\u0440\u043e\u0432\u043a\u0430\u0438\u043c\u0435\u043d\u0444\u0430\u0439\u043b\u043e\u0432\u0432zip\u0444\u0430\u0439\u043b\u0435 \u043c\u0435\u0442\u043e\u0434\u0441\u0436\u0430\u0442\u0438\u044fzip \u043c\u0435\u0442\u043e\u0434\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044fzip \u0440\u0435\u0436\u0438\u043c\u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\u043f\u0443\u0442\u0435\u0439\u0444\u0430\u0439\u043b\u043e\u0432zip \u0440\u0435\u0436\u0438\u043c\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438\u043f\u043e\u0434\u043a\u0430\u0442\u0430\u043b\u043e\u0433\u043e\u0432zip \u0440\u0435\u0436\u0438\u043c\u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f\u043f\u0443\u0442\u0435\u0439zip \u0443\u0440\u043e\u0432\u0435\u043d\u044c\u0441\u0436\u0430\u0442\u0438\u044fzip \u0437\u0432\u0443\u043a\u043e\u0432\u043e\u0435\u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430\u043a\u0441\u0442\u0440\u043e\u043a\u0435 \u043f\u043e\u0437\u0438\u0446\u0438\u044f\u0432\u043f\u043e\u0442\u043e\u043a\u0435 \u043f\u043e\u0440\u044f\u0434\u043e\u043a\u0431\u0430\u0439\u0442\u043e\u0432 \u0440\u0435\u0436\u0438\u043c\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0435\u0436\u0438\u043c\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u043e\u0439\u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445\u043f\u043e\u043a\u0443\u043f\u043e\u043a \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u0444\u043e\u043d\u043e\u0432\u043e\u0433\u043e\u0437\u0430\u0434\u0430\u043d\u0438\u044f \u0442\u0438\u043f\u043f\u043e\u0434\u043f\u0438\u0441\u0447\u0438\u043a\u0430\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0445\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u0437\u0430\u0449\u0438\u0449\u0435\u043d\u043d\u043e\u0433\u043e\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044fftp \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u043e\u0440\u044f\u0434\u043a\u0430\u0441\u0445\u0435\u043c\u044b\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u0438\u043f\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f\u043f\u0435\u0440\u0438\u043e\u0434\u0430\u043c\u0438\u0441\u0445\u0435\u043c\u044b\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u0438\u043f\u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c\u043d\u043e\u0439\u0442\u043e\u0447\u043a\u0438\u0441\u0445\u0435\u043c\u044b\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u0438\u043f\u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f\u0441\u0445\u0435\u043c\u044b\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u0438\u043f\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0439\u0442\u0430\u0431\u043b\u0438\u0446\u044b\u0441\u0445\u0435\u043c\u044b\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u0438\u043f\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f\u0441\u0445\u0435\u043c\u044b\u0437\u0430\u043f\u0440\u043e\u0441\u0430 http\u043c\u0435\u0442\u043e\u0434 \u0430\u0432\u0442\u043e\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043e\u0431\u0449\u0435\u0433\u043e\u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430 \u0430\u0432\u0442\u043e\u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043d\u043e\u043c\u0435\u0440\u0430\u0437\u0430\u0434\u0430\u0447\u0438 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u043e\u0433\u043e\u044f\u0437\u044b\u043a\u0430 \u0432\u0438\u0434\u0438\u0435\u0440\u0430\u0440\u0445\u0438\u0438 \u0432\u0438\u0434\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u043d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f \u0432\u0438\u0434\u0442\u0430\u0431\u043b\u0438\u0446\u044b\u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u044c\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0439\u043f\u0440\u0438\u043f\u0440\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0438 \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435\u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0435\u0439 \u0438\u043d\u0434\u0435\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0431\u0430\u0437\u044b\u043f\u043b\u0430\u043d\u0430\u0432\u0438\u0434\u043e\u0432\u0440\u0430\u0441\u0447\u0435\u0442\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e\u0432\u044b\u0431\u043e\u0440\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043e\u0431\u0449\u0435\u0433\u043e\u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043f\u043e\u0434\u0447\u0438\u043d\u0435\u043d\u0438\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043f\u043e\u043b\u043d\u043e\u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0433\u043e\u043f\u043e\u0438\u0441\u043a\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0440\u0430\u0437\u0434\u0435\u043b\u044f\u0435\u043c\u044b\u0445\u0434\u0430\u043d\u043d\u044b\u0445\u043e\u0431\u0449\u0435\u0433\u043e\u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445\u0434\u0430\u043d\u043d\u044b\u0445 \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0435\u043f\u0440\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0435\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u0432\u0438\u0434\u0430\u0440\u0430\u0441\u0447\u0435\u0442\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0435\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u0432\u0438\u0434\u0430\u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0438 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0435\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u0437\u0430\u0434\u0430\u0447\u0438 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0435\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u043b\u0430\u043d\u0430\u043e\u0431\u043c\u0435\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0435\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0435\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u0441\u0447\u0435\u0442\u0430 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435\u0433\u0440\u0430\u043d\u0438\u0446\u044b\u043f\u0440\u0438\u043f\u0440\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0438 \u043f\u0435\u0440\u0438\u043e\u0434\u0438\u0447\u043d\u043e\u0441\u0442\u044c\u043d\u043e\u043c\u0435\u0440\u0430\u0431\u0438\u0437\u043d\u0435\u0441\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043f\u0435\u0440\u0438\u043e\u0434\u0438\u0447\u043d\u043e\u0441\u0442\u044c\u043d\u043e\u043c\u0435\u0440\u0430\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u043f\u0435\u0440\u0438\u043e\u0434\u0438\u0447\u043d\u043e\u0441\u0442\u044c\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0440\u0430\u0441\u0447\u0435\u0442\u0430 \u043f\u0435\u0440\u0438\u043e\u0434\u0438\u0447\u043d\u043e\u0441\u0442\u044c\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0441\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043f\u043e\u043b\u043d\u043e\u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0439\u043f\u043e\u0438\u0441\u043a\u043f\u0440\u0438\u0432\u0432\u043e\u0434\u0435\u043f\u043e\u0441\u0442\u0440\u043e\u043a\u0435 \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u043d\u043e\u0441\u0442\u044c\u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043f\u0440\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438\u043e\u0431\u0449\u0435\u0433\u043e\u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u0434\u0430\u043d\u043d\u044b\u0445\u043e\u0431\u0449\u0435\u0433\u043e\u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0439\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438\u043e\u0431\u0449\u0435\u0433\u043e\u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430 \u0440\u0435\u0436\u0438\u043c\u0430\u0432\u0442\u043e\u043d\u0443\u043c\u0435\u0440\u0430\u0446\u0438\u0438\u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0440\u0435\u0436\u0438\u043c\u0437\u0430\u043f\u0438\u0441\u0438\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430 \u0440\u0435\u0436\u0438\u043c\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u043e\u0434\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0440\u0435\u0436\u0438\u043c\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0445\u0432\u044b\u0437\u043e\u0432\u043e\u0432\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0439\u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u044b\u0438\u0432\u043d\u0435\u0448\u043d\u0438\u0445\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0440\u0435\u0436\u0438\u043c\u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0433\u043e\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u0441\u0435\u0430\u043d\u0441\u043e\u0432 \u0440\u0435\u0436\u0438\u043c\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f\u0434\u0430\u043d\u043d\u044b\u0445\u0432\u044b\u0431\u043e\u0440\u0430\u043f\u0440\u0438\u0432\u0432\u043e\u0434\u0435\u043f\u043e\u0441\u0442\u0440\u043e\u043a\u0435 \u0440\u0435\u0436\u0438\u043c\u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u0438 \u0440\u0435\u0436\u0438\u043c\u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u0438\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u0440\u0435\u0436\u0438\u043c\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u043e\u0439\u0434\u0430\u043d\u043d\u044b\u0445\u043f\u043e\u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0441\u0435\u0440\u0438\u0438\u043a\u043e\u0434\u043e\u0432\u043f\u043b\u0430\u043d\u0430\u0432\u0438\u0434\u043e\u0432\u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a \u0441\u0435\u0440\u0438\u0438\u043a\u043e\u0434\u043e\u0432\u043f\u043b\u0430\u043d\u0430\u0441\u0447\u0435\u0442\u043e\u0432 \u0441\u0435\u0440\u0438\u0438\u043a\u043e\u0434\u043e\u0432\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u043f\u0440\u0438\u0432\u0432\u043e\u0434\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0432\u044b\u0431\u043e\u0440\u0430 \u0441\u043f\u043e\u0441\u043e\u0431\u043f\u043e\u0438\u0441\u043a\u0430\u0441\u0442\u0440\u043e\u043a\u0438\u043f\u0440\u0438\u0432\u0432\u043e\u0434\u0435\u043f\u043e\u0441\u0442\u0440\u043e\u043a\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0442\u0438\u043f\u0434\u0430\u043d\u043d\u044b\u0445\u0442\u0430\u0431\u043b\u0438\u0446\u044b\u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0438\u043f\u043a\u043e\u0434\u0430\u043f\u043b\u0430\u043d\u0430\u0432\u0438\u0434\u043e\u0432\u0440\u0430\u0441\u0447\u0435\u0442\u0430 \u0442\u0438\u043f\u043a\u043e\u0434\u0430\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0430 \u0442\u0438\u043f\u043c\u0430\u043a\u0435\u0442\u0430 \u0442\u0438\u043f\u043d\u043e\u043c\u0435\u0440\u0430\u0431\u0438\u0437\u043d\u0435\u0441\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u0442\u0438\u043f\u043d\u043e\u043c\u0435\u0440\u0430\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0442\u0438\u043f\u043d\u043e\u043c\u0435\u0440\u0430\u0437\u0430\u0434\u0430\u0447\u0438 \u0442\u0438\u043f\u0444\u043e\u0440\u043c\u044b \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0439 \u0432\u0430\u0436\u043d\u043e\u0441\u0442\u044c\u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b\u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0430\u0444\u043e\u0440\u043c\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e\u0448\u0440\u0438\u0444\u0442\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e\u043f\u0435\u0440\u0438\u043e\u0434\u0430 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0439\u0434\u0430\u0442\u044b\u043d\u0430\u0447\u0430\u043b\u0430 \u0432\u0438\u0434\u0433\u0440\u0430\u043d\u0438\u0446\u044b \u0432\u0438\u0434\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438 \u0432\u0438\u0434\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u043f\u043e\u043b\u043d\u043e\u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0433\u043e\u043f\u043e\u0438\u0441\u043a\u0430 \u0432\u0438\u0434\u0440\u0430\u043c\u043a\u0438 \u0432\u0438\u0434\u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u0432\u0438\u0434\u0446\u0432\u0435\u0442\u0430 \u0432\u0438\u0434\u0447\u0438\u0441\u043b\u043e\u0432\u043e\u0433\u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432\u0438\u0434\u0448\u0440\u0438\u0444\u0442\u0430 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u0430\u044f\u0434\u043b\u0438\u043d\u0430 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439\u0437\u043d\u0430\u043a \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435byteordermark \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0445\u043f\u043e\u043b\u043d\u043e\u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0433\u043e\u043f\u043e\u0438\u0441\u043a\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0439\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043a\u043b\u0430\u0432\u0438\u0448\u0430 \u043a\u043e\u0434\u0432\u043e\u0437\u0432\u0440\u0430\u0442\u0430\u0434\u0438\u0430\u043b\u043e\u0433\u0430 \u043a\u043e\u0434\u0438\u0440\u043e\u0432\u043a\u0430xbase \u043a\u043e\u0434\u0438\u0440\u043e\u0432\u043a\u0430\u0442\u0435\u043a\u0441\u0442\u0430 \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u043e\u0438\u0441\u043a\u0430 \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u043a\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445\u0434\u0430\u043d\u043d\u044b\u0445 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435\u043f\u0440\u0438\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043f\u0430\u043d\u0435\u043b\u0438\u0440\u0430\u0437\u0434\u0435\u043b\u043e\u0432 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0440\u0435\u0436\u0438\u043c\u0434\u0438\u0430\u043b\u043e\u0433\u0430\u0432\u043e\u043f\u0440\u043e\u0441 \u0440\u0435\u0436\u0438\u043c\u0437\u0430\u043f\u0443\u0441\u043a\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0436\u0438\u043c\u043e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u044f \u0440\u0435\u0436\u0438\u043c\u043e\u0442\u043a\u0440\u044b\u0442\u0438\u044f\u0444\u043e\u0440\u043c\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0436\u0438\u043c\u043f\u043e\u043b\u043d\u043e\u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0433\u043e\u043f\u043e\u0438\u0441\u043a\u0430 \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438\u0431\u0430\u0437\u044b\u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u043f\u043e\u0441\u043e\u0431\u0432\u044b\u0431\u043e\u0440\u0430\u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430windows \u0441\u043f\u043e\u0441\u043e\u0431\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u0441\u0442\u0440\u043e\u043a\u0438 \u0441\u0442\u0430\u0442\u0443\u0441\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0442\u0438\u043f\u0432\u043d\u0435\u0448\u043d\u0435\u0439\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0442\u0438\u043f\u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u044b \u0442\u0438\u043f\u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f\u043a\u043b\u0430\u0432\u0438\u0448\u0438enter \u0442\u0438\u043f\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438\u043e\u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438\u0431\u0430\u0437\u044b\u0434\u0430\u043d\u043d\u044b\u0445 \u0443\u0440\u043e\u0432\u0435\u043d\u044c\u0438\u0437\u043e\u043b\u044f\u0446\u0438\u0438\u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0439 \u0445\u0435\u0448\u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0447\u0430\u0441\u0442\u0438\u0434\u0430\u0442\u044b",
 type:"com\u043e\u0431\u044a\u0435\u043a\u0442 ftp\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 http\u0437\u0430\u043f\u0440\u043e\u0441 http\u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0442\u0432\u0435\u0442 http\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 ws\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f ws\u043f\u0440\u043e\u043a\u0441\u0438 xbase \u0430\u043d\u0430\u043b\u0438\u0437\u0434\u0430\u043d\u043d\u044b\u0445 \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u044fxs \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0444\u0435\u0440\u0434\u0432\u043e\u0438\u0447\u043d\u044b\u0445\u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435xs \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0433\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440\u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0445\u0447\u0438\u0441\u0435\u043b \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u0430\u044f\u0441\u0445\u0435\u043c\u0430 \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u0438\u0435\u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u0430\u044f\u0441\u0445\u0435\u043c\u0430 \u0433\u0440\u0443\u043f\u043f\u0430\u043c\u043e\u0434\u0435\u043b\u0438xs \u0434\u0430\u043d\u043d\u044b\u0435\u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u0432\u043e\u0438\u0447\u043d\u044b\u0435\u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0435\u043d\u0434\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0430 \u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430\u0433\u0430\u043d\u0442\u0430 \u0434\u0438\u0430\u043b\u043e\u0433\u0432\u044b\u0431\u043e\u0440\u0430\u0444\u0430\u0439\u043b\u0430 \u0434\u0438\u0430\u043b\u043e\u0433\u0432\u044b\u0431\u043e\u0440\u0430\u0446\u0432\u0435\u0442\u0430 \u0434\u0438\u0430\u043b\u043e\u0433\u0432\u044b\u0431\u043e\u0440\u0430\u0448\u0440\u0438\u0444\u0442\u0430 \u0434\u0438\u0430\u043b\u043e\u0433\u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u044f\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442\u043d\u043e\u0433\u043e\u0437\u0430\u0434\u0430\u043d\u0438\u044f \u0434\u0438\u0430\u043b\u043e\u0433\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e\u043f\u0435\u0440\u0438\u043e\u0434\u0430 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442dom \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442html \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044fxs \u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u0435\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0438\u0441\u044cdom \u0437\u0430\u043f\u0438\u0441\u044cfastinfoset \u0437\u0430\u043f\u0438\u0441\u044chtml \u0437\u0430\u043f\u0438\u0441\u044cjson \u0437\u0430\u043f\u0438\u0441\u044cxml \u0437\u0430\u043f\u0438\u0441\u044czip\u0444\u0430\u0439\u043b\u0430 \u0437\u0430\u043f\u0438\u0441\u044c\u0434\u0430\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u044c\u0442\u0435\u043a\u0441\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c\u0443\u0437\u043b\u043e\u0432dom \u0437\u0430\u043f\u0440\u043e\u0441 \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u043d\u043e\u0435\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435openssl \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043f\u043e\u043b\u0435\u0439\u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0435\u0442\u0435\u043a\u0441\u0442\u0430 \u0438\u043c\u043f\u043e\u0440\u0442xs \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043f\u043e\u0447\u0442\u0430 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0435\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439\u043f\u0440\u043e\u0444\u0438\u043b\u044c \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043f\u0440\u043e\u043a\u0441\u0438 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0434\u043b\u044f\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044fxs \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430xs \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u0441\u043e\u0431\u044b\u0442\u0438\u044f\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0442\u0435\u0440\u0430\u0442\u043e\u0440\u0443\u0437\u043b\u043e\u0432dom \u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0430 \u043a\u0432\u0430\u043b\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b\u0434\u0430\u0442\u044b \u043a\u0432\u0430\u043b\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b\u0434\u0432\u043e\u0438\u0447\u043d\u044b\u0445\u0434\u0430\u043d\u043d\u044b\u0445 \u043a\u0432\u0430\u043b\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b\u0441\u0442\u0440\u043e\u043a\u0438 \u043a\u0432\u0430\u043b\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b\u0447\u0438\u0441\u043b\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u0449\u0438\u043a\u043c\u0430\u043a\u0435\u0442\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u0449\u0438\u043a\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u043e\u0440\u043c\u0430\u043a\u0435\u0442\u0430\u043e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u044f\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u043e\u0440\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u043e\u0440\u0444\u043e\u0440\u043c\u0430\u0442\u043d\u043e\u0439\u0441\u0442\u0440\u043e\u043a\u0438 \u043b\u0438\u043d\u0438\u044f \u043c\u0430\u043a\u0435\u0442\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043c\u0430\u043a\u0435\u0442\u043e\u0431\u043b\u0430\u0441\u0442\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043c\u0430\u043a\u0435\u0442\u043e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u044f\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043c\u0430\u0441\u043a\u0430xs \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0438 \u043d\u0430\u0431\u043e\u0440\u0441\u0445\u0435\u043cxml \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438json \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u043a\u0430\u0440\u0442\u0438\u043d\u043e\u043a \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043e\u0431\u0445\u043e\u0434\u0434\u0435\u0440\u0435\u0432\u0430dom \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435\u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430xs \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435\u043d\u043e\u0442\u0430\u0446\u0438\u0438xs \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430xs \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u0441\u043e\u0431\u044b\u0442\u0438\u044f\u0434\u043e\u0441\u0442\u0443\u043f\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\u0441\u043e\u0431\u044b\u0442\u0438\u044f\u043e\u0442\u043a\u0430\u0437\u0432\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438\u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435\u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u043e\u0433\u043e\u0444\u0430\u0439\u043b\u0430 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435\u0442\u0438\u043f\u043e\u0432 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u0433\u0440\u0443\u043f\u043f\u044b\u0430\u0442\u0440\u0438\u0431\u0443\u0442\u043e\u0432xs \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u0433\u0440\u0443\u043f\u043f\u044b\u043c\u043e\u0434\u0435\u043b\u0438xs \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u0438\u0434\u0435\u043d\u0442\u0438\u0447\u043d\u043e\u0441\u0442\u0438xs \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043f\u0440\u043e\u0441\u0442\u043e\u0433\u043e\u0442\u0438\u043f\u0430xs \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u0441\u043e\u0441\u0442\u0430\u0432\u043d\u043e\u0433\u043e\u0442\u0438\u043f\u0430xs \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u0442\u0438\u043f\u0430\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430dom \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044fxpathxs \u043e\u0442\u0431\u043e\u0440\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u0430\u043a\u0435\u0442\u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0445\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0432\u044b\u0431\u043e\u0440\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u0437\u0430\u043f\u0438\u0441\u0438json \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u0437\u0430\u043f\u0438\u0441\u0438xml \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u0447\u0442\u0435\u043d\u0438\u044fxml \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435xs \u043f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0449\u0438\u043a \u043f\u043e\u043b\u0435\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u043b\u0435\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044cdom \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044c\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044c\u043e\u0442\u0447\u0435\u0442\u0430 \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044c\u043e\u0442\u0447\u0435\u0442\u0430\u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044c\u0441\u0445\u0435\u043cxml \u043f\u043e\u0442\u043e\u043a \u043f\u043e\u0442\u043e\u043a\u0432\u043f\u0430\u043c\u044f\u0442\u0438 \u043f\u043e\u0447\u0442\u0430 \u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0435\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435xsl \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043a\u043a\u0430\u043d\u043e\u043d\u0438\u0447\u0435\u0441\u043a\u043e\u043c\u0443xml \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440\u0432\u044b\u0432\u043e\u0434\u0430\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445\u0432\u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u044e\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440\u0432\u044b\u0432\u043e\u0434\u0430\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445\u0432\u0442\u0430\u0431\u043b\u0438\u0447\u043d\u044b\u0439\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0440\u0430\u0437\u044b\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0438\u043c\u0435\u043ddom \u0440\u0430\u043c\u043a\u0430 \u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442\u043d\u043e\u0433\u043e\u0437\u0430\u0434\u0430\u043d\u0438\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0435\u0438\u043c\u044fxml \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0447\u0442\u0435\u043d\u0438\u044f\u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u0432\u043e\u0434\u043d\u0430\u044f\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u0441\u0432\u044f\u0437\u044c\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u0432\u044b\u0431\u043e\u0440\u0430 \u0441\u0432\u044f\u0437\u044c\u043f\u043e\u0442\u0438\u043f\u0443 \u0441\u0432\u044f\u0437\u044c\u043f\u043e\u0442\u0438\u043f\u0443\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440xdto \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043a\u043b\u0438\u0435\u043d\u0442\u0430windows \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043a\u043b\u0438\u0435\u043d\u0442\u0430\u0444\u0430\u0439\u043b \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043a\u0440\u0438\u043f\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044b\u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u044e\u0449\u0438\u0445\u0446\u0435\u043d\u0442\u0440\u043e\u0432windows \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044b\u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u044e\u0449\u0438\u0445\u0446\u0435\u043d\u0442\u0440\u043e\u0432\u0444\u0430\u0439\u043b \u0441\u0436\u0430\u0442\u0438\u0435\u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0430\u044f\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044e \u0441\u043e\u0447\u0435\u0442\u0430\u043d\u0438\u0435\u043a\u043b\u0430\u0432\u0438\u0448 \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u0435\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u0434\u0430\u0442\u0430\u043d\u0430\u0447\u0430\u043b\u0430 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439\u043f\u0435\u0440\u0438\u043e\u0434 \u0441\u0445\u0435\u043c\u0430xml \u0441\u0445\u0435\u043c\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0430\u0431\u043b\u0438\u0447\u043d\u044b\u0439\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0439\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u0438\u043f\u0434\u0430\u043d\u043d\u044b\u0445xml \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439\u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0444\u0430\u0431\u0440\u0438\u043a\u0430xdto \u0444\u0430\u0439\u043b \u0444\u0430\u0439\u043b\u043e\u0432\u044b\u0439\u043f\u043e\u0442\u043e\u043a \u0444\u0430\u0441\u0435\u0442\u0434\u043b\u0438\u043d\u044bxs \u0444\u0430\u0441\u0435\u0442\u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430\u0440\u0430\u0437\u0440\u044f\u0434\u043e\u0432\u0434\u0440\u043e\u0431\u043d\u043e\u0439\u0447\u0430\u0441\u0442\u0438xs \u0444\u0430\u0441\u0435\u0442\u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0433\u043e\u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0449\u0435\u0433\u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044fxs \u0444\u0430\u0441\u0435\u0442\u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0433\u043e\u0438\u0441\u043a\u043b\u044e\u0447\u0430\u044e\u0449\u0435\u0433\u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044fxs \u0444\u0430\u0441\u0435\u0442\u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0439\u0434\u043b\u0438\u043d\u044bxs \u0444\u0430\u0441\u0435\u0442\u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0433\u043e\u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0449\u0435\u0433\u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044fxs \u0444\u0430\u0441\u0435\u0442\u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0433\u043e\u0438\u0441\u043a\u043b\u044e\u0447\u0430\u044e\u0449\u0435\u0433\u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044fxs \u0444\u0430\u0441\u0435\u0442\u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0439\u0434\u043b\u0438\u043d\u044bxs \u0444\u0430\u0441\u0435\u0442\u043e\u0431\u0440\u0430\u0437\u0446\u0430xs \u0444\u0430\u0441\u0435\u0442\u043e\u0431\u0449\u0435\u0433\u043e\u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430\u0440\u0430\u0437\u0440\u044f\u0434\u043e\u0432xs \u0444\u0430\u0441\u0435\u0442\u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044fxs \u0444\u0430\u0441\u0435\u0442\u043f\u0440\u043e\u0431\u0435\u043b\u044c\u043d\u044b\u0445\u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432xs \u0444\u0438\u043b\u044c\u0442\u0440\u0443\u0437\u043b\u043e\u0432dom \u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f\u0441\u0442\u0440\u043e\u043a\u0430 \u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u0444\u0440\u0430\u0433\u043c\u0435\u043d\u0442xs \u0445\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u0434\u0430\u043d\u043d\u044b\u0445 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0446\u0432\u0435\u0442 \u0447\u0442\u0435\u043d\u0438\u0435fastinfoset \u0447\u0442\u0435\u043d\u0438\u0435html \u0447\u0442\u0435\u043d\u0438\u0435json \u0447\u0442\u0435\u043d\u0438\u0435xml \u0447\u0442\u0435\u043d\u0438\u0435zip\u0444\u0430\u0439\u043b\u0430 \u0447\u0442\u0435\u043d\u0438\u0435\u0434\u0430\u043d\u043d\u044b\u0445 \u0447\u0442\u0435\u043d\u0438\u0435\u0442\u0435\u043a\u0441\u0442\u0430 \u0447\u0442\u0435\u043d\u0438\u0435\u0443\u0437\u043b\u043e\u0432dom \u0448\u0440\u0438\u0444\u0442 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438\u0434\u0430\u043d\u043d\u044b\u0445 comsafearray \u0434\u0435\u0440\u0435\u0432\u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043c\u0430\u0441\u0441\u0438\u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435 \u0441\u043f\u0438\u0441\u043e\u043a\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435\u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435 \u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439\u043c\u0430\u0441\u0441\u0438\u0432 ",
-literal:"null \u0438\u0441\u0442\u0438\u043d\u0430 \u043b\u043e\u0436\u044c \u043d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e"},contains:[f,a,e,{className:"symbol",begin:"~",end:";|:",excludeEnd:!0},b,c,d]}}}());
-hljs.registerLanguage("abnf",function(){return function(a){return{name:"Augmented Backus-Naur Form",illegal:"[!@#$^&',?+~`|:]",keywords:"ALPHA BIT CHAR CR CRLF CTL DIGIT DQUOTE HEXDIG HTAB LF LWSP OCTET SP VCHAR WSP",contains:[{className:"attribute",begin:"^[a-zA-Z][a-zA-Z0-9-]*(?=\\s*=)"},a.COMMENT(";","$"),{className:"symbol",begin:/%b[0-1]+(-[0-1]+|(\.[0-1]+)+){0,1}/},{className:"symbol",begin:/%d[0-9]+(-[0-9]+|(\.[0-9]+)+){0,1}/},{className:"symbol",begin:/%x[0-9A-F]+(-[0-9A-F]+|(\.[0-9A-F]+)+){0,1}/},
-{className:"symbol",begin:/%[si]/},a.QUOTE_STRING_MODE,a.NUMBER_MODE]}}}());
-hljs.registerLanguage("accesslog",function(){return function(a){a="GET POST HEAD PUT DELETE CONNECT OPTIONS PATCH TRACE".split(" ");return{name:"Apache Access Log",contains:[{className:"number",begin:"^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b",relevance:5},{className:"number",begin:"\\b\\d+\\b",relevance:0},{className:"string",begin:'"('+a.join("|")+")",end:'"',keywords:a.join(" "),illegal:"\\n",relevance:5,contains:[{begin:"HTTP/[12]\\.\\d",relevance:5}]},{className:"string",begin:/\[\d[^\]\n]{8,}\]/,
-illegal:"\\n",relevance:1},{className:"string",begin:/\[/,end:/\]/,illegal:"\\n",relevance:0},{className:"string",begin:'"Mozilla/\\d\\.\\d \\(',end:'"',illegal:"\\n",relevance:3},{className:"string",begin:'"',end:'"',illegal:"\\n",relevance:0}]}}}());
-hljs.registerLanguage("actionscript",function(){return function(a){return{name:"ActionScript",aliases:["as"],keywords:{keyword:"as break case catch class const continue default delete do dynamic each else extends final finally for function get if implements import in include instanceof interface internal is namespace native new override package private protected public return set static super switch this throw try typeof use var void while with",literal:"true false null undefined"},contains:[a.APOS_STRING_MODE,
-a.QUOTE_STRING_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.C_NUMBER_MODE,{className:"class",beginKeywords:"package",end:"{",contains:[a.TITLE_MODE]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,contains:[{beginKeywords:"extends implements"},a.TITLE_MODE]},{className:"meta",beginKeywords:"import include",end:";",keywords:{"meta-keyword":"import include"}},{className:"function",beginKeywords:"function",end:"[{;]",excludeEnd:!0,illegal:"\\S",contains:[a.TITLE_MODE,
-{className:"params",begin:"\\(",end:"\\)",contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"rest_arg",begin:"[.]{3}",end:"[a-zA-Z_$][a-zA-Z0-9_$]*",relevance:10}]},{begin:":\\s*([*]|[a-zA-Z_$][a-zA-Z0-9_$]*)"}]},a.METHOD_GUARD],illegal:/#/}}}());
-hljs.registerLanguage("ada",function(){return function(a){a=a.COMMENT("--","$");var b={begin:"\\s+:\\s+",end:"\\s*(:=|;|\\)|=>|$)",illegal:"[]{}%#'\"",contains:[{beginKeywords:"loop for declare others",endsParent:!0},{className:"keyword",beginKeywords:"not null constant access function procedure in out aliased exception"},{className:"type",begin:"[A-Za-z](_?[A-Za-z0-9.])*",endsParent:!0,relevance:0}]};return{name:"Ada",case_insensitive:!0,keywords:{keyword:"abort else new return abs elsif not reverse abstract end accept entry select access exception of separate aliased exit or some all others subtype and for out synchronized array function overriding at tagged generic package task begin goto pragma terminate body private then if procedure type case in protected constant interface is raise use declare range delay limited record when delta loop rem while digits renames with do mod requeue xor",
-literal:"True False"},contains:[a,{className:"string",begin:/"/,end:/"/,contains:[{begin:/""/,relevance:0}]},{className:"string",begin:/'.'/},{className:"number",begin:"\\b(\\d(_|\\d)*#\\w+(\\.\\w+)?#([eE][-+]?\\d(_|\\d)*)?|\\d(_|\\d)*(\\.\\d(_|\\d)*)?([eE][-+]?\\d(_|\\d)*)?)",relevance:0},{className:"symbol",begin:"'[A-Za-z](_?[A-Za-z0-9.])*"},{className:"title",begin:"(\\bwith\\s+)?(\\bprivate\\s+)?\\bpackage\\s+(\\bbody\\s+)?",end:"(is|$)",keywords:"package body",excludeBegin:!0,excludeEnd:!0,
-illegal:"[]{}%#'\""},{begin:"(\\b(with|overriding)\\s+)?\\b(function|procedure)\\s+",end:"(\\bis|\\bwith|\\brenames|\\)\\s*;)",keywords:"overriding function procedure with is renames return",returnBegin:!0,contains:[a,{className:"title",begin:"(\\bwith\\s+)?\\b(function|procedure)\\s+",end:"(\\(|\\s+|$)",excludeBegin:!0,excludeEnd:!0,illegal:"[]{}%#'\""},b,{className:"type",begin:"\\breturn\\s+",end:"(\\s+|;|$)",keywords:"return",excludeBegin:!0,excludeEnd:!0,endsParent:!0,illegal:"[]{}%#'\""}]},
-{className:"type",begin:"\\b(sub)?type\\s+",end:"\\s+",keywords:"type",excludeBegin:!0,illegal:"[]{}%#'\""},b]}}}());
-hljs.registerLanguage("angelscript",function(){return function(a){var b={className:"built_in",begin:"\\b(void|bool|int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|string|ref|array|double|float|auto|dictionary)"},c={className:"symbol",begin:"[a-zA-Z0-9_]+@"},d={className:"keyword",begin:"<",end:">",contains:[b,c]};b.contains=[d];c.contains=[d];return{name:"AngelScript",aliases:["asc"],keywords:"for in|0 break continue while do|0 return if else case switch namespace is cast or and xor not get|0 in inout|10 out override set|0 private public const default|0 final shared external mixin|10 enum typedef funcdef this super import from interface abstract|0 try catch protected explicit property",
-illegal:"(^using\\s+[A-Za-z0-9_\\.]+;$|\\bfunctions*[^\\(])",contains:[{className:"string",begin:"'",end:"'",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE],relevance:0},{className:"string",begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE],relevance:0},{className:"string",begin:'"""',end:'"""'},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{beginKeywords:"interface namespace",end:"{",illegal:"[;.\\-]",contains:[{className:"symbol",begin:"[a-zA-Z0-9_]+"}]},{beginKeywords:"class",end:"{",illegal:"[;.\\-]",
-contains:[{className:"symbol",begin:"[a-zA-Z0-9_]+",contains:[{begin:"[:,]\\s*",contains:[{className:"symbol",begin:"[a-zA-Z0-9_]+"}]}]}]},b,c,{className:"literal",begin:"\\b(null|true|false)"},{className:"number",begin:"(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?f?|\\.\\d+f?)([eE][-+]?\\d+f?)?)"}]}}}());
-hljs.registerLanguage("apache",function(){return function(a){var b={className:"number",begin:"\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?"};return{name:"Apache config",aliases:["apacheconf"],case_insensitive:!0,contains:[a.HASH_COMMENT_MODE,{className:"section",begin:"</?",end:">",contains:[b,{className:"number",begin:":\\d{1,5}"},a.inherit(a.QUOTE_STRING_MODE,{relevance:0})]},{className:"attribute",begin:/\w+/,relevance:0,keywords:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},
-starts:{end:/$/,relevance:0,keywords:{literal:"on off all deny allow"},contains:[{className:"meta",begin:"\\s\\[",end:"\\]$"},{className:"variable",begin:"[\\$%]\\{",end:"\\}",contains:["self",{className:"number",begin:"[\\$%]\\d+"}]},b,{className:"number",begin:"\\d+"},a.QUOTE_STRING_MODE]}}],illegal:/\S/}}}());
-hljs.registerLanguage("applescript",function(){return function(a){var b=a.inherit(a.QUOTE_STRING_MODE,{illegal:""}),c={className:"params",begin:"\\(",end:"\\)",contains:["self",a.C_NUMBER_MODE,b]},d=a.COMMENT("--","$"),e=a.COMMENT("\\(\\*","\\*\\)",{contains:["self",d]});return{name:"AppleScript",aliases:["osascript"],keywords:{keyword:"about above after against and around as at back before beginning behind below beneath beside between but by considering contain contains continue copy div does eighth else end equal equals error every exit fifth first for fourth from front get given global if ignoring in into is it its last local me middle mod my ninth not of on onto or over prop property put ref reference repeat returning script second set seventh since sixth some tell tenth that the|0 then third through thru timeout times to transaction try until where while whose with without",
-literal:"AppleScript false linefeed return pi quote result space tab true",built_in:"alias application boolean class constant date file integer list number real record string text activate beep count delay launch log offset read round run say summarize write character characters contents day frontmost id item length month name paragraph paragraphs rest reverse running time version weekday word words year"},contains:[b,a.C_NUMBER_MODE,{className:"built_in",begin:"\\b(clipboard info|the clipboard|info for|list (disks|folder)|mount volume|path to|(close|open for) access|(get|set) eof|current date|do shell script|get volume settings|random number|set volume|system attribute|system info|time to GMT|(load|run|store) script|scripting components|ASCII (character|number)|localized string|choose (application|color|file|file name|folder|from list|remote application|URL)|display (alert|dialog))\\b|^\\s*return\\b"},
-{className:"literal",begin:"\\b(text item delimiters|current application|missing value)\\b"},{className:"keyword",begin:"\\b(apart from|aside from|instead of|out of|greater than|isn't|(doesn't|does not) (equal|come before|come after|contain)|(greater|less) than( or equal)?|(starts?|ends|begins?) with|contained by|comes (before|after)|a (ref|reference)|POSIX file|POSIX path|(date|time) string|quoted form)\\b"},{beginKeywords:"on",illegal:"[${=;\\n]",contains:[a.UNDERSCORE_TITLE_MODE,c]}].concat([d,
-e,a.HASH_COMMENT_MODE]),illegal:"//|->|=>|\\[\\["}}}());
-hljs.registerLanguage("arcade",function(){return function(a){var b={keyword:"if for while var new function do return void else break",literal:"BackSlash DoubleQuote false ForwardSlash Infinity NaN NewLine null PI SingleQuote Tab TextFormatting true undefined",built_in:"Abs Acos Angle Attachments Area AreaGeodetic Asin Atan Atan2 Average Bearing Boolean Buffer BufferGeodetic Ceil Centroid Clip Console Constrain Contains Cos Count Crosses Cut Date DateAdd DateDiff Day Decode DefaultValue Dictionary Difference Disjoint Distance DistanceGeodetic Distinct DomainCode DomainName Equals Exp Extent Feature FeatureSet FeatureSetByAssociation FeatureSetById FeatureSetByPortalItem FeatureSetByRelationshipName FeatureSetByTitle FeatureSetByUrl Filter First Floor Geometry GroupBy Guid HasKey Hour IIf IndexOf Intersection Intersects IsEmpty IsNan IsSelfIntersecting Length LengthGeodetic Log Max Mean Millisecond Min Minute Month MultiPartToSinglePart Multipoint NextSequenceValue Now Number OrderBy Overlaps Point Polygon Polyline Portal Pow Random Relate Reverse RingIsClockWise Round Second SetGeometry Sin Sort Sqrt Stdev Sum SymmetricDifference Tan Text Timestamp Today ToLocal Top Touches ToUTC TrackCurrentTime TrackGeometryWindow TrackIndex TrackStartTime TrackWindow TypeOf Union UrlEncode Variance Weekday When Within Year "},c=
-{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},d={className:"subst",begin:"\\$\\{",end:"\\}",keywords:b,contains:[]},e={className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,d]};d.contains=[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,e,c,a.REGEXP_MODE];d=d.contains.concat([a.C_BLOCK_COMMENT_MODE,a.C_LINE_COMMENT_MODE]);return{name:"ArcGIS Arcade",aliases:["arcade"],keywords:b,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,
-e,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"symbol",begin:"\\$[datastore|feature|layer|map|measure|sourcefeature|sourcelayer|targetfeature|targetlayer|value|view]+"},c,{begin:/[{,]\s*/,relevance:0,contains:[{begin:"[A-Za-z_][0-9A-Za-z_]*\\s*:",returnBegin:!0,relevance:0,contains:[{className:"attr",begin:"[A-Za-z_][0-9A-Za-z_]*",relevance:0}]}]},{begin:"("+a.RE_STARTERS_RE+"|\\b(return)\\b)\\s*",keywords:"return",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",
-begin:"(\\(.*?\\)|[A-Za-z_][0-9A-Za-z_]*)\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:"[A-Za-z_][0-9A-Za-z_]*"},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:d}]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_][0-9A-Za-z_]*"}),{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:d}],illegal:/\[|%/},{begin:/\$[(.]/}],
-illegal:/#(?!!)/}}}());
-hljs.registerLanguage("c-like",function(){return function(a){var b={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},c={className:"string",variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",end:"'",illegal:"."},a.END_SAME_AS_BEGIN({begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},d={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],
-relevance:0},e={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"},contains:[{begin:/\\\n/,relevance:0},a.inherit(c,{className:"meta-string"}),{className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},f={className:"title",begin:"(?:[a-zA-Z_]\\w*::)?"+a.IDENT_RE,relevance:0},h="(?:[a-zA-Z_]\\w*::)?"+a.IDENT_RE+"\\s*\\(",g={keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",
+literal:e},contains:[{className:"meta",begin:"#|&",end:"$",keywords:{$pattern:x,
+"meta-keyword":n+"\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c\u0438\u0437\u0444\u0430\u0439\u043b\u0430 \u0432\u0435\u0431\u043a\u043b\u0438\u0435\u043d\u0442 \u0432\u043c\u0435\u0441\u0442\u043e \u0432\u043d\u0435\u0448\u043d\u0435\u0435\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442 \u043a\u043e\u043d\u0435\u0446\u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043a\u043b\u0438\u0435\u043d\u0442 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435\u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435\u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435\u0431\u0435\u0437\u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430 \u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435\u0431\u0435\u0437\u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434 \u043f\u043e\u0441\u043b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u043e\u043b\u0441\u0442\u044b\u0439\u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0431\u044b\u0447\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u043e\u043b\u0441\u0442\u044b\u0439\u043a\u043b\u0438\u0435\u043d\u0442\u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u043e\u043d\u043a\u0438\u0439\u043a\u043b\u0438\u0435\u043d\u0442 "
+},contains:[m]},{className:"function",variants:[{
+begin:"\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430|\u0444\u0443\u043d\u043a\u0446\u0438\u044f",
+end:"\\)",
+keywords:"\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f"
+},{
+begin:"\u043a\u043e\u043d\u0435\u0446\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b|\u043a\u043e\u043d\u0435\u0446\u0444\u0443\u043d\u043a\u0446\u0438\u0438",
+keywords:"\u043a\u043e\u043d\u0435\u0446\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b \u043a\u043e\u043d\u0435\u0446\u0444\u0443\u043d\u043a\u0446\u0438\u0438"
+}],contains:[{begin:"\\(",end:"\\)",endsParent:!0,contains:[{className:"params",
+begin:x,end:",",excludeEnd:!0,endsWithParent:!0,keywords:{$pattern:x,
+keyword:"\u0437\u043d\u0430\u0447",literal:e},contains:[o,t,a]},m]
+},s.inherit(s.TITLE_MODE,{begin:x})]},m,{className:"symbol",begin:"~",end:";|:",
+excludeEnd:!0},o,t,a]}}})());
+hljs.registerLanguage("abnf",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(a=e)?"string"==typeof a?a:a.source:null;var a
+})).join("")}return a=>{const s={ruleDeclaration:/^[a-zA-Z][a-zA-Z0-9-]*/,
+unexpectedChars:/[!@#$^&',?+~`|:]/},n=a.COMMENT(/;/,/$/),r={
+className:"attribute",begin:e(s.ruleDeclaration,/(?=\s*=)/)};return{
+name:"Augmented Backus-Naur Form",illegal:s.unexpectedChars,
+keywords:["ALPHA","BIT","CHAR","CR","CRLF","CTL","DIGIT","DQUOTE","HEXDIG","HTAB","LF","LWSP","OCTET","SP","VCHAR","WSP"],
+contains:[r,n,{className:"symbol",begin:/%b[0-1]+(-[0-1]+|(\.[0-1]+)+){0,1}/},{
+className:"symbol",begin:/%d[0-9]+(-[0-9]+|(\.[0-9]+)+){0,1}/},{
+className:"symbol",begin:/%x[0-9A-F]+(-[0-9A-F]+|(\.[0-9A-F]+)+){0,1}/},{
+className:"symbol",begin:/%[si]/},a.QUOTE_STRING_MODE,a.NUMBER_MODE]}}})());
+hljs.registerLanguage("accesslog",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function n(...n){
+return n.map((n=>e(n))).join("")}function a(...n){
+return"("+n.map((n=>e(n))).join("|")+")"}return e=>{
+const l=["GET","POST","HEAD","PUT","DELETE","CONNECT","OPTIONS","PATCH","TRACE"]
+;return{name:"Apache Access Log",contains:[{className:"number",
+begin:/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d{1,5})?\b/,relevance:5},{
+className:"number",begin:/\b\d+\b/,relevance:0},{className:"string",
+begin:n(/"/,a(...l)),end:/"/,keywords:l,illegal:/\n/,relevance:5,contains:[{
+begin:/HTTP\/[12]\.\d'/,relevance:5}]},{className:"string",
+begin:/\[\d[^\]\n]{8,}\]/,illegal:/\n/,relevance:1},{className:"string",
+begin:/\[/,end:/\]/,illegal:/\n/,relevance:0},{className:"string",
+begin:/"Mozilla\/\d\.\d \(/,end:/"/,illegal:/\n/,relevance:3},{
+className:"string",begin:/"/,end:/"/,illegal:/\n/,relevance:0}]}}})());
+hljs.registerLanguage("actionscript",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>({name:"ActionScript",aliases:["as"],keywords:{
+keyword:"as break case catch class const continue default delete do dynamic each else extends final finally for function get if implements import in include instanceof interface internal is namespace native new override package private protected public return set static super switch this throw try typeof use var void while with",
+literal:"true false null undefined"},
+contains:[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE,n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,n.C_NUMBER_MODE,{
+className:"class",beginKeywords:"package",end:/\{/,contains:[n.TITLE_MODE]},{
+className:"class",beginKeywords:"class interface",end:/\{/,excludeEnd:!0,
+contains:[{beginKeywords:"extends implements"},n.TITLE_MODE]},{className:"meta",
+beginKeywords:"import include",end:/;/,keywords:{"meta-keyword":"import include"
+}},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,
+illegal:/\S/,contains:[n.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,
+contains:[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE,n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,{
+className:"rest_arg",begin:/[.]{3}/,end:/[a-zA-Z_$][a-zA-Z0-9_$]*/,relevance:10
+}]},{begin:e(/:\s*/,/([*]|[a-zA-Z_$][a-zA-Z0-9_$]*)/)}]},n.METHOD_GUARD],
+illegal:/#/})})());
+hljs.registerLanguage("ada",(()=>{"use strict";return e=>{
+const n="[A-Za-z](_?[A-Za-z0-9.])*",s="[]\\{\\}%#'\"",a=e.COMMENT("--","$"),r={
+begin:"\\s+:\\s+",end:"\\s*(:=|;|\\)|=>|$)",illegal:s,contains:[{
+beginKeywords:"loop for declare others",endsParent:!0},{className:"keyword",
+beginKeywords:"not null constant access function procedure in out aliased exception"
+},{className:"type",begin:n,endsParent:!0,relevance:0}]};return{name:"Ada",
+case_insensitive:!0,keywords:{
+keyword:"abort else new return abs elsif not reverse abstract end accept entry select access exception of separate aliased exit or some all others subtype and for out synchronized array function overriding at tagged generic package task begin goto pragma terminate body private then if procedure type case in protected constant interface is raise use declare range delay limited record when delta loop rem while digits renames with do mod requeue xor",
+literal:"True False"},contains:[a,{className:"string",begin:/"/,end:/"/,
+contains:[{begin:/""/,relevance:0}]},{className:"string",begin:/'.'/},{
+className:"number",
+begin:"\\b(\\d(_|\\d)*#\\w+(\\.\\w+)?#([eE][-+]?\\d(_|\\d)*)?|\\d(_|\\d)*(\\.\\d(_|\\d)*)?([eE][-+]?\\d(_|\\d)*)?)",
+relevance:0},{className:"symbol",begin:"'"+n},{className:"title",
+begin:"(\\bwith\\s+)?(\\bprivate\\s+)?\\bpackage\\s+(\\bbody\\s+)?",
+end:"(is|$)",keywords:"package body",excludeBegin:!0,excludeEnd:!0,illegal:s},{
+begin:"(\\b(with|overriding)\\s+)?\\b(function|procedure)\\s+",
+end:"(\\bis|\\bwith|\\brenames|\\)\\s*;)",
+keywords:"overriding function procedure with is renames return",returnBegin:!0,
+contains:[a,{className:"title",
+begin:"(\\bwith\\s+)?\\b(function|procedure)\\s+",end:"(\\(|\\s+|$)",
+excludeBegin:!0,excludeEnd:!0,illegal:s},r,{className:"type",
+begin:"\\breturn\\s+",end:"(\\s+|;|$)",keywords:"return",excludeBegin:!0,
+excludeEnd:!0,endsParent:!0,illegal:s}]},{className:"type",
+begin:"\\b(sub)?type\\s+",end:"\\s+",keywords:"type",excludeBegin:!0,illegal:s
+},r]}}})());
+hljs.registerLanguage("angelscript",(()=>{"use strict";return e=>{var n={
+className:"built_in",
+begin:"\\b(void|bool|int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|string|ref|array|double|float|auto|dictionary)"
+},a={className:"symbol",begin:"[a-zA-Z0-9_]+@"},i={className:"keyword",
+begin:"<",end:">",contains:[n,a]};return n.contains=[i],a.contains=[i],{
+name:"AngelScript",aliases:["asc"],
+keywords:"for in|0 break continue while do|0 return if else case switch namespace is cast or and xor not get|0 in inout|10 out override set|0 private public const default|0 final shared external mixin|10 enum typedef funcdef this super import from interface abstract|0 try catch protected explicit property",
+illegal:"(^using\\s+[A-Za-z0-9_\\.]+;$|\\bfunction\\s*[^\\(])",contains:[{
+className:"string",begin:"'",end:"'",illegal:"\\n",
+contains:[e.BACKSLASH_ESCAPE],relevance:0},{className:"string",begin:'"""',
+end:'"""'},{className:"string",begin:'"',end:'"',illegal:"\\n",
+contains:[e.BACKSLASH_ESCAPE],relevance:0
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"string",
+begin:"^\\s*\\[",end:"\\]"},{beginKeywords:"interface namespace",end:/\{/,
+illegal:"[;.\\-]",contains:[{className:"symbol",begin:"[a-zA-Z0-9_]+"}]},{
+beginKeywords:"class",end:/\{/,illegal:"[;.\\-]",contains:[{className:"symbol",
+begin:"[a-zA-Z0-9_]+",contains:[{begin:"[:,]\\s*",contains:[{className:"symbol",
+begin:"[a-zA-Z0-9_]+"}]}]}]},n,a,{className:"literal",
+begin:"\\b(null|true|false)"},{className:"number",relevance:0,
+begin:"(-?)(\\b0[xXbBoOdD][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?f?|\\.\\d+f?)([eE][-+]?\\d+f?)?)"
+}]}}})());
+hljs.registerLanguage("apache",(()=>{"use strict";return e=>{const n={
+className:"number",begin:/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d{1,5})?/}
+;return{name:"Apache config",aliases:["apacheconf"],case_insensitive:!0,
+contains:[e.HASH_COMMENT_MODE,{className:"section",begin:/<\/?/,end:/>/,
+contains:[n,{className:"number",begin:/:\d{1,5}/
+},e.inherit(e.QUOTE_STRING_MODE,{relevance:0})]},{className:"attribute",
+begin:/\w+/,relevance:0,keywords:{
+nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"
+},starts:{end:/$/,relevance:0,keywords:{literal:"on off all deny allow"},
+contains:[{className:"meta",begin:/\s\[/,end:/\]$/},{className:"variable",
+begin:/[\$%]\{/,end:/\}/,contains:["self",{className:"number",begin:/[$%]\d+/}]
+},n,{className:"number",begin:/\d+/},e.QUOTE_STRING_MODE]}}],illegal:/\S/}}
+})());
+hljs.registerLanguage("applescript",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function t(...t){
+return t.map((t=>e(t))).join("")}function n(...t){
+return"("+t.map((t=>e(t))).join("|")+")"}return e=>{
+const r=e.inherit(e.QUOTE_STRING_MODE,{illegal:null}),i={className:"params",
+begin:/\(/,end:/\)/,contains:["self",e.C_NUMBER_MODE,r]
+},o=e.COMMENT(/--/,/$/),a=[o,e.COMMENT(/\(\*/,/\*\)/,{contains:["self",o]
+}),e.HASH_COMMENT_MODE];return{name:"AppleScript",aliases:["osascript"],
+keywords:{
+keyword:"about above after against and around as at back before beginning behind below beneath beside between but by considering contain contains continue copy div does eighth else end equal equals error every exit fifth first for fourth from front get given global if ignoring in into is it its last local me middle mod my ninth not of on onto or over prop property put ref reference repeat returning script second set seventh since sixth some tell tenth that the|0 then third through thru timeout times to transaction try until where while whose with without",
+literal:"AppleScript false linefeed return pi quote result space tab true",
+built_in:"alias application boolean class constant date file integer list number real record string text activate beep count delay launch log offset read round run say summarize write character characters contents day frontmost id item length month name paragraph paragraphs rest reverse running time version weekday word words year"
+},contains:[r,e.C_NUMBER_MODE,{className:"built_in",
+begin:t(/\b/,n(/clipboard info/,/the clipboard/,/info for/,/list (disks|folder)/,/mount volume/,/path to/,/(close|open for) access/,/(get|set) eof/,/current date/,/do shell script/,/get volume settings/,/random number/,/set volume/,/system attribute/,/system info/,/time to GMT/,/(load|run|store) script/,/scripting components/,/ASCII (character|number)/,/localized string/,/choose (application|color|file|file name|folder|from list|remote application|URL)/,/display (alert|dialog)/),/\b/)
+},{className:"built_in",begin:/^\s*return\b/},{className:"literal",
+begin:/\b(text item delimiters|current application|missing value)\b/},{
+className:"keyword",
+begin:t(/\b/,n(/apart from/,/aside from/,/instead of/,/out of/,/greater than/,/isn't|(doesn't|does not) (equal|come before|come after|contain)/,/(greater|less) than( or equal)?/,/(starts?|ends|begins?) with/,/contained by/,/comes (before|after)/,/a (ref|reference)/,/POSIX (file|path)/,/(date|time) string/,/quoted form/),/\b/)
+},{beginKeywords:"on",illegal:/[${=;\n]/,contains:[e.UNDERSCORE_TITLE_MODE,i]
+},...a],illegal:/\/\/|->|=>|\[\[/}}})());
+hljs.registerLanguage("arcade",(()=>{"use strict";return e=>{
+const n="[A-Za-z_][0-9A-Za-z_]*",a={
+keyword:"if for while var new function do return void else break",
+literal:"BackSlash DoubleQuote false ForwardSlash Infinity NaN NewLine null PI SingleQuote Tab TextFormatting true undefined",
+built_in:"Abs Acos Angle Attachments Area AreaGeodetic Asin Atan Atan2 Average Bearing Boolean Buffer BufferGeodetic Ceil Centroid Clip Console Constrain Contains Cos Count Crosses Cut Date DateAdd DateDiff Day Decode DefaultValue Dictionary Difference Disjoint Distance DistanceGeodetic Distinct DomainCode DomainName Equals Exp Extent Feature FeatureSet FeatureSetByAssociation FeatureSetById FeatureSetByPortalItem FeatureSetByRelationshipName FeatureSetByTitle FeatureSetByUrl Filter First Floor Geometry GroupBy Guid HasKey Hour IIf IndexOf Intersection Intersects IsEmpty IsNan IsSelfIntersecting Length LengthGeodetic Log Max Mean Millisecond Min Minute Month MultiPartToSinglePart Multipoint NextSequenceValue Now Number OrderBy Overlaps Point Polygon Polyline Portal Pow Random Relate Reverse RingIsClockWise Round Second SetGeometry Sin Sort Sqrt Stdev Sum SymmetricDifference Tan Text Timestamp Today ToLocal Top Touches ToUTC TrackCurrentTime TrackGeometryWindow TrackIndex TrackStartTime TrackWindow TypeOf Union UrlEncode Variance Weekday When Within Year "
+},t={className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{
+begin:"\\b(0[oO][0-7]+)"},{begin:e.C_NUMBER_RE}],relevance:0},i={
+className:"subst",begin:"\\$\\{",end:"\\}",keywords:a,contains:[]},r={
+className:"string",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE,i]}
+;i.contains=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,r,t,e.REGEXP_MODE]
+;const s=i.contains.concat([e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE])
+;return{name:"ArcGIS Arcade",aliases:["arcade"],keywords:a,
+contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{
+className:"symbol",
+begin:"\\$[datastore|feature|layer|map|measure|sourcefeature|sourcelayer|targetfeature|targetlayer|value|view]+"
+},t,{begin:/[{,]\s*/,relevance:0,contains:[{begin:n+"\\s*:",returnBegin:!0,
+relevance:0,contains:[{className:"attr",begin:n,relevance:0}]}]},{
+begin:"("+e.RE_STARTERS_RE+"|\\b(return)\\b)\\s*",keywords:"return",
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.REGEXP_MODE,{
+className:"function",begin:"(\\(.*?\\)|"+n+")\\s*=>",returnBegin:!0,
+end:"\\s*=>",contains:[{className:"params",variants:[{begin:n},{begin:/\(\s*\)/
+},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:a,contains:s}]}]
+}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,
+excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:n}),{className:"params",
+begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:s}],illegal:/\[|%/},{
+begin:/\$[(.]/}],illegal:/#(?!!)/}}})());
+hljs.registerLanguage("arduino",(()=>{"use strict";function e(e){
+return((...e)=>e.map((e=>(e=>e?"string"==typeof e?e:e.source:null)(e))).join(""))("(",e,")?")
+}return t=>{const r=(t=>{const r=t.COMMENT("//","$",{contains:[{begin:/\\\n/}]
+}),n="[a-zA-Z_]\\w*::",i="(decltype\\(auto\\)|"+e(n)+"[a-zA-Z_]\\w*"+e("<[^<>]+>")+")",a={
+className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},s={className:"string",
+variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",
+contains:[t.BACKSLASH_ESCAPE]},{
+begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
+end:"'",illegal:"."},t.END_SAME_AS_BEGIN({
+begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={
+className:"number",variants:[{begin:"\\b(0b[01']+)"},{
+begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"
+},{
+begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
+}],relevance:0},l={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
+"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
+},contains:[{begin:/\\\n/,relevance:0},t.inherit(s,{className:"meta-string"}),{
+className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"
+},r,t.C_BLOCK_COMMENT_MODE]},c={className:"title",begin:e(n)+t.IDENT_RE,
+relevance:0},d=e(n)+t.IDENT_RE+"\\s*\\(",u={
+keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",
 built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary",
-literal:"true false nullptr NULL"},k=[b,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,c],l={variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],keywords:g,contains:k.concat([{begin:/\(/,end:/\)/,keywords:g,contains:k.concat(["self"]),relevance:0}]),relevance:0};return{aliases:"c cc h c++ h++ hpp hh hxx cxx".split(" "),keywords:g,disableAutodetect:!0,illegal:"</",contains:[].concat(l,{className:"function",begin:"((decltype\\(auto\\)|(?:[a-zA-Z_]\\w*::)?[a-zA-Z_]\\w*(?:<.*?>)?)[\\*&\\s]+)+"+
-h,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:g,illegal:/[^\w\s\*&:<>]/,contains:[{begin:"decltype\\(auto\\)",keywords:g,relevance:0},{begin:h,returnBegin:!0,contains:[f],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:g,relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,d,b,{begin:/\(/,end:/\)/,keywords:g,relevance:0,contains:["self",a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,d,b]}]},b,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,e]},k,[e,{begin:"\\b(deque|list|queue|priority_queue|pair|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",
-end:">",keywords:g,contains:["self",b]},{begin:a.IDENT_RE+"::",keywords:g},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin:/</,end:/>/,contains:["self"]},a.TITLE_MODE]}]),exports:{preprocessor:e,strings:c,keywords:g}}}}());hljs.registerLanguage("cpp",function(){return function(a){a=a.getLanguage("c-like").rawDefinition();a.disableAutodetect=!1;a.name="C++";a.aliases="cc c++ h++ hpp hh hxx cxx".split(" ");return a}}());
-hljs.registerLanguage("arduino",function(){return function(a){a=a.requireLanguage("cpp").rawDefinition();var b=a.keywords;b.keyword+=" boolean byte word String";b.literal+=" DIGITAL_MESSAGE FIRMATA_STRING ANALOG_MESSAGE REPORT_DIGITAL REPORT_ANALOG INPUT_PULLUP SET_PIN_MODE INTERNAL2V56 SYSTEM_RESET LED_BUILTIN INTERNAL1V1 SYSEX_START INTERNAL EXTERNAL DEFAULT OUTPUT INPUT HIGH LOW";b.built_in+=" setup loop KeyboardController MouseController SoftwareSerial EthernetServer EthernetClient LiquidCrystal RobotControl GSMVoiceCall EthernetUDP EsploraTFT HttpClient RobotMotor WiFiClient GSMScanner FileSystem Scheduler GSMServer YunClient YunServer IPAddress GSMClient GSMModem Keyboard Ethernet Console GSMBand Esplora Stepper Process WiFiUDP GSM_SMS Mailbox USBHost Firmata PImage Client Server GSMPIN FileIO Bridge Serial EEPROM Stream Mouse Audio Servo File Task GPRS WiFi Wire TFT GSM SPI SD runShellCommandAsynchronously analogWriteResolution retrieveCallingNumber printFirmwareVersion analogReadResolution sendDigitalPortPair noListenOnLocalhost readJoystickButton setFirmwareVersion readJoystickSwitch scrollDisplayRight getVoiceCallStatus scrollDisplayLeft writeMicroseconds delayMicroseconds beginTransmission getSignalStrength runAsynchronously getAsynchronously listenOnLocalhost getCurrentCarrier readAccelerometer messageAvailable sendDigitalPorts lineFollowConfig countryNameWrite runShellCommand readStringUntil rewindDirectory readTemperature setClockDivider readLightSensor endTransmission analogReference detachInterrupt countryNameRead attachInterrupt encryptionType readBytesUntil robotNameWrite readMicrophone robotNameRead cityNameWrite userNameWrite readJoystickY readJoystickX mouseReleased openNextFile scanNetworks noInterrupts digitalWrite beginSpeaker mousePressed isActionDone mouseDragged displayLogos noAutoscroll addParameter remoteNumber getModifiers keyboardRead userNameRead waitContinue processInput parseCommand printVersion readNetworks writeMessage blinkVersion cityNameRead readMessage setDataMode parsePacket isListening setBitOrder beginPacket isDirectory motorsWrite drawCompass digitalRead clearScreen serialEvent rightToLeft setTextSize leftToRight requestFrom keyReleased compassRead analogWrite interrupts WiFiServer disconnect playMelody parseFloat autoscroll getPINUsed setPINUsed setTimeout sendAnalog readSlider analogRead beginWrite createChar motorsStop keyPressed tempoWrite readButton subnetMask debugPrint macAddress writeGreen randomSeed attachGPRS readString sendString remotePort releaseAll mouseMoved background getXChange getYChange answerCall getResult voiceCall endPacket constrain getSocket writeJSON getButton available connected findUntil readBytes exitValue readGreen writeBlue startLoop IPAddress isPressed sendSysex pauseMode gatewayIP setCursor getOemKey tuneWrite noDisplay loadImage switchPIN onRequest onReceive changePIN playFile noBuffer parseInt overflow checkPIN knobRead beginTFT bitClear updateIR bitWrite position writeRGB highByte writeRed setSpeed readBlue noStroke remoteIP transfer shutdown hangCall beginSMS endWrite attached maintain noCursor checkReg checkPUK shiftOut isValid shiftIn pulseIn connect println localIP pinMode getIMEI display noBlink process getBand running beginSD drawBMP lowByte setBand release bitRead prepare pointTo readRed setMode noFill remove listen stroke detach attach noTone exists buffer height bitSet circle config cursor random IRread setDNS endSMS getKey micros millis begin print write ready flush width isPIN blink clear press mkdir rmdir close point yield image BSSID click delay read text move peek beep rect line open seek fill size turn stop home find step tone sqrt RSSI SSID end bit tan cos sin pow map abs max min get run put";
-a.name="Arduino";return a}}());
-hljs.registerLanguage("armasm",function(){return function(a){var b={variants:[a.COMMENT("^[ \\t]*(?=#)","$",{relevance:0,excludeBegin:!0}),a.COMMENT("[;@]","$",{relevance:0}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]};return{name:"ARM Assembly",case_insensitive:!0,aliases:["arm"],keywords:{$pattern:"\\.?"+a.IDENT_RE,meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .arm .thumb .code16 .code32 .force_thumb .thumb_func .ltorg ALIAS ALIGN ARM AREA ASSERT ATTR CN CODE CODE16 CODE32 COMMON CP DATA DCB DCD DCDU DCDO DCFD DCFDU DCI DCQ DCQU DCW DCWU DN ELIF ELSE END ENDFUNC ENDIF ENDP ENTRY EQU EXPORT EXPORTAS EXTERN FIELD FILL FUNCTION GBLA GBLL GBLS GET GLOBAL IF IMPORT INCBIN INCLUDE INFO KEEP LCLA LCLL LCLS LTORG MACRO MAP MEND MEXIT NOFP OPT PRESERVE8 PROC QN READONLY RELOC REQUIRE REQUIRE8 RLIST FN ROUT SETA SETL SETS SN SPACE SUBT THUMB THUMBX TTL WHILE WEND ",built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 pc lr sp ip sl sb fp a1 a2 a3 a4 v1 v2 v3 v4 v5 v6 v7 v8 f0 f1 f2 f3 f4 f5 f6 f7 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15 q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 q12 q13 q14 q15 cpsr_c cpsr_x cpsr_s cpsr_f cpsr_cx cpsr_cxs cpsr_xs cpsr_xsf cpsr_sf cpsr_cxsf spsr_c spsr_x spsr_s spsr_f spsr_cx spsr_cxs spsr_xs spsr_xsf spsr_sf spsr_cxsf s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15 d16 d17 d18 d19 d20 d21 d22 d23 d24 d25 d26 d27 d28 d29 d30 d31 {PC} {VAR} {TRUE} {FALSE} {OPT} {CONFIG} {ENDIAN} {CODESIZE} {CPU} {FPU} {ARCHITECTURE} {PCSTOREOFFSET} {ARMASM_VERSION} {INTER} {ROPI} {RWPI} {SWST} {NOSWST} . @"},
-contains:[{className:"keyword",begin:"\\b(adc|(qd?|sh?|u[qh]?)?add(8|16)?|usada?8|(q|sh?|u[qh]?)?(as|sa)x|and|adrl?|sbc|rs[bc]|asr|b[lx]?|blx|bxj|cbn?z|tb[bh]|bic|bfc|bfi|[su]bfx|bkpt|cdp2?|clz|clrex|cmp|cmn|cpsi[ed]|cps|setend|dbg|dmb|dsb|eor|isb|it[te]{0,3}|lsl|lsr|ror|rrx|ldm(([id][ab])|f[ds])?|ldr((s|ex)?[bhd])?|movt?|mvn|mra|mar|mul|[us]mull|smul[bwt][bt]|smu[as]d|smmul|smmla|mla|umlaal|smlal?([wbt][bt]|d)|mls|smlsl?[ds]|smc|svc|sev|mia([bt]{2}|ph)?|mrr?c2?|mcrr2?|mrs|msr|orr|orn|pkh(tb|bt)|rbit|rev(16|sh)?|sel|[su]sat(16)?|nop|pop|push|rfe([id][ab])?|stm([id][ab])?|str(ex)?[bhd]?|(qd?)?sub|(sh?|q|u[qh]?)?sub(8|16)|[su]xt(a?h|a?b(16)?)|srs([id][ab])?|swpb?|swi|smi|tst|teq|wfe|wfi|yield)(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo)?[sptrx]?(?=\\s)"},
-b,a.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"[^\\\\]'",relevance:0},{className:"title",begin:"\\|",end:"\\|",illegal:"\\n",relevance:0},{className:"number",variants:[{begin:"[#$=]?0x[0-9a-f]+"},{begin:"[#$=]?0b[01]+"},{begin:"[#$=]\\d+"},{begin:"\\b\\d+"}],relevance:0},{className:"symbol",variants:[{begin:"^[ \\t]*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{begin:"^[a-z_\\.\\$][a-z0-9_\\.\\$]+"},{begin:"[=#]\\w+"}],relevance:0}]}}}());
-hljs.registerLanguage("xml",function(){return function(a){var b={className:"symbol",begin:"&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;"},c={begin:"\\s",contains:[{className:"meta-keyword",begin:"#?[a-z_][a-z1-9_-]+",illegal:"\\n"}]},d=a.inherit(c,{begin:"\\(",end:"\\)"}),e=a.inherit(a.APOS_STRING_MODE,{className:"meta-string"}),f=a.inherit(a.QUOTE_STRING_MODE,{className:"meta-string"}),h={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",relevance:0},{begin:/=\s*/,
-relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[b]},{begin:/'/,end:/'/,contains:[b]},{begin:/[^\s"'=<>`]+/}]}]}]};return{name:"HTML, XML",aliases:"html xhtml rss atom xjb xsd xsl plist wsf svg".split(" "),case_insensitive:!0,contains:[{className:"meta",begin:"<![a-z]",end:">",relevance:10,contains:[c,f,e,d,{begin:"\\[",end:"\\]",contains:[{className:"meta",begin:"<![a-z]",end:">",contains:[c,d,f,e]}]}]},a.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",
-end:"\\]\\]>",relevance:10},b,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:"<style(?=\\s|>)",end:">",keywords:{name:"style"},contains:[h],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>)",end:">",keywords:{name:"script"},contains:[h],starts:{end:"\x3c/script>",returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},
-h]}]}}}());
-hljs.registerLanguage("asciidoc",function(){return function(a){return{name:"AsciiDoc",aliases:["adoc"],contains:[a.COMMENT("^/{4,}\\n","\\n/{4,}$",{relevance:10}),a.COMMENT("^//","$",{relevance:0}),{className:"title",begin:"^\\.\\w.*$"},{begin:"^[=\\*]{4,}\\n",end:"\\n^[=\\*]{4,}$",relevance:10},{className:"section",relevance:10,variants:[{begin:"^(={1,5}) .+?( \\1)?$"},{begin:"^[^\\[\\]\\n]+?\\n[=\\-~\\^\\+]{2,}$"}]},{className:"meta",begin:"^:.+?:",end:"\\s",excludeEnd:!0,relevance:10},{className:"meta",
-begin:"^\\[.+?\\]$",relevance:0},{className:"quote",begin:"^_{4,}\\n",end:"\\n_{4,}$",relevance:10},{className:"code",begin:"^[\\-\\.]{4,}\\n",end:"\\n[\\-\\.]{4,}$",relevance:10},{begin:"^\\+{4,}\\n",end:"\\n\\+{4,}$",contains:[{begin:"<",end:">",subLanguage:"xml",relevance:0}],relevance:10},{className:"bullet",begin:"^(\\*+|\\-+|\\.+|[^\\n]+?::)\\s+"},{className:"symbol",begin:"^(NOTE|TIP|IMPORTANT|WARNING|CAUTION):\\s+",relevance:10},{className:"strong",begin:"\\B\\*(?![\\*\\s])",end:"(\\n{2}|\\*)",
-contains:[{begin:"\\\\*\\w",relevance:0}]},{className:"emphasis",begin:"\\B'(?!['\\s])",end:"(\\n{2}|')",contains:[{begin:"\\\\'\\w",relevance:0}],relevance:0},{className:"emphasis",begin:"_(?![_\\s])",end:"(\\n{2}|_)",relevance:0},{className:"string",variants:[{begin:"``.+?''"},{begin:"`.+?'"}]},{className:"code",begin:"(`.+?`|\\+.+?\\+)",relevance:0},{className:"code",begin:"^[ \\t]",end:"$",relevance:0},{begin:"^'{3,}[ \\t]*$",relevance:10},{begin:"(link:)?(http|https|ftp|file|irc|image:?):\\S+\\[.*?\\]",
-returnBegin:!0,contains:[{begin:"(link|image:?):",relevance:0},{className:"link",begin:"\\w",end:"[^\\[]+",relevance:0},{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0,relevance:0}],relevance:10}]}}}());
-hljs.registerLanguage("aspectj",function(){return function(a){return{name:"AspectJ",keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance",illegal:/<\/|#/,
-contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"aspect",end:/[{;=]/,excludeEnd:!0,illegal:/[:;"\[\]]/,contains:[{beginKeywords:"extends implements pertypewithin perthis pertarget percflowbelow percflow issingleton"},a.UNDERSCORE_TITLE_MODE,{begin:/\([^\)]*/,end:/[)]+/,keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance get set args call",
-excludeEnd:!1}]},{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,relevance:0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"pointcut after before around throwing returning",end:/[)]/,excludeEnd:!1,illegal:/["\[\]]/,contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.UNDERSCORE_TITLE_MODE]}]},{begin:/[:]/,returnBegin:!0,end:/[{;]/,relevance:0,excludeEnd:!1,keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance",
-illegal:/["\[\]]/,contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance get set args call",
-relevance:0},a.QUOTE_STRING_MODE]},{beginKeywords:"new throw",relevance:0},{className:"function",begin:/\w+ +\w+(\.)?\w+\s*\([^\)]*\)\s*((throws)[\w\s,]+)?[\{;]/,returnBegin:!0,end:/[{;=]/,keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance",
-excludeEnd:!0,contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,relevance:0,keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance",
-contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}}}());
-hljs.registerLanguage("autohotkey",function(){return function(a){var b={begin:"`[\\s\\S]"};return{name:"AutoHotkey",case_insensitive:!0,aliases:["ahk"],keywords:{keyword:"Break Continue Critical Exit ExitApp Gosub Goto New OnExit Pause return SetBatchLines SetTimer Suspend Thread Throw Until ahk_id ahk_class ahk_pid ahk_exe ahk_group",literal:"true false NOT AND OR",built_in:"ComSpec Clipboard ClipboardAll ErrorLevel"},contains:[b,a.inherit(a.QUOTE_STRING_MODE,{contains:[b]}),a.COMMENT(";","$",{relevance:0}),
-a.C_BLOCK_COMMENT_MODE,{className:"number",begin:a.NUMBER_RE,relevance:0},{className:"variable",begin:"%[a-zA-Z0-9#_$@]+%"},{className:"built_in",begin:"^\\s*\\w+\\s*(,|%)"},{className:"title",variants:[{begin:'^[^\\n";]+::(?!=)'},{begin:'^[^\\n";]+:(?!=)',relevance:0}]},{className:"meta",begin:"^\\s*#\\w+",end:"$",relevance:0},{className:"built_in",begin:"A_[a-zA-Z0-9]+"},{begin:",\\s*,"}]}}}());
-hljs.registerLanguage("autoit",function(){return function(a){var b={variants:[a.COMMENT(";","$",{relevance:0}),a.COMMENT("#cs","#ce"),a.COMMENT("#comments-start","#comments-end")]},c={begin:"\\$[A-z0-9_]+"},d={className:"string",variants:[{begin:/"/,end:/"/,contains:[{begin:/""/,relevance:0}]},{begin:/'/,end:/'/,contains:[{begin:/''/,relevance:0}]}]},e={variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]};return{name:"AutoIt",case_insensitive:!0,illegal:/\/\*/,keywords:{keyword:"ByRef Case Const ContinueCase ContinueLoop Default Dim Do Else ElseIf EndFunc EndIf EndSelect EndSwitch EndWith Enum Exit ExitLoop For Func Global If In Local Next ReDim Return Select Static Step Switch Then To Until Volatile WEnd While With",
-built_in:"Abs ACos AdlibRegister AdlibUnRegister Asc AscW ASin Assign ATan AutoItSetOption AutoItWinGetTitle AutoItWinSetTitle Beep Binary BinaryLen BinaryMid BinaryToString BitAND BitNOT BitOR BitRotate BitShift BitXOR BlockInput Break Call CDTray Ceiling Chr ChrW ClipGet ClipPut ConsoleRead ConsoleWrite ConsoleWriteError ControlClick ControlCommand ControlDisable ControlEnable ControlFocus ControlGetFocus ControlGetHandle ControlGetPos ControlGetText ControlHide ControlListView ControlMove ControlSend ControlSetText ControlShow ControlTreeView Cos Dec DirCopy DirCreate DirGetSize DirMove DirRemove DllCall DllCallAddress DllCallbackFree DllCallbackGetPtr DllCallbackRegister DllClose DllOpen DllStructCreate DllStructGetData DllStructGetPtr DllStructGetSize DllStructSetData DriveGetDrive DriveGetFileSystem DriveGetLabel DriveGetSerial DriveGetType DriveMapAdd DriveMapDel DriveMapGet DriveSetLabel DriveSpaceFree DriveSpaceTotal DriveStatus EnvGet EnvSet EnvUpdate Eval Execute Exp FileChangeDir FileClose FileCopy FileCreateNTFSLink FileCreateShortcut FileDelete FileExists FileFindFirstFile FileFindNextFile FileFlush FileGetAttrib FileGetEncoding FileGetLongName FileGetPos FileGetShortcut FileGetShortName FileGetSize FileGetTime FileGetVersion FileInstall FileMove FileOpen FileOpenDialog FileRead FileReadLine FileReadToArray FileRecycle FileRecycleEmpty FileSaveDialog FileSelectFolder FileSetAttrib FileSetEnd FileSetPos FileSetTime FileWrite FileWriteLine Floor FtpSetProxy FuncName GUICreate GUICtrlCreateAvi GUICtrlCreateButton GUICtrlCreateCheckbox GUICtrlCreateCombo GUICtrlCreateContextMenu GUICtrlCreateDate GUICtrlCreateDummy GUICtrlCreateEdit GUICtrlCreateGraphic GUICtrlCreateGroup GUICtrlCreateIcon GUICtrlCreateInput GUICtrlCreateLabel GUICtrlCreateList GUICtrlCreateListView GUICtrlCreateListViewItem GUICtrlCreateMenu GUICtrlCreateMenuItem GUICtrlCreateMonthCal GUICtrlCreateObj GUICtrlCreatePic GUICtrlCreateProgress GUICtrlCreateRadio GUICtrlCreateSlider GUICtrlCreateTab GUICtrlCreateTabItem GUICtrlCreateTreeView GUICtrlCreateTreeViewItem GUICtrlCreateUpdown GUICtrlDelete GUICtrlGetHandle GUICtrlGetState GUICtrlRead GUICtrlRecvMsg GUICtrlRegisterListViewSort GUICtrlSendMsg GUICtrlSendToDummy GUICtrlSetBkColor GUICtrlSetColor GUICtrlSetCursor GUICtrlSetData GUICtrlSetDefBkColor GUICtrlSetDefColor GUICtrlSetFont GUICtrlSetGraphic GUICtrlSetImage GUICtrlSetLimit GUICtrlSetOnEvent GUICtrlSetPos GUICtrlSetResizing GUICtrlSetState GUICtrlSetStyle GUICtrlSetTip GUIDelete GUIGetCursorInfo GUIGetMsg GUIGetStyle GUIRegisterMsg GUISetAccelerators GUISetBkColor GUISetCoord GUISetCursor GUISetFont GUISetHelp GUISetIcon GUISetOnEvent GUISetState GUISetStyle GUIStartGroup GUISwitch Hex HotKeySet HttpSetProxy HttpSetUserAgent HWnd InetClose InetGet InetGetInfo InetGetSize InetRead IniDelete IniRead IniReadSection IniReadSectionNames IniRenameSection IniWrite IniWriteSection InputBox Int IsAdmin IsArray IsBinary IsBool IsDeclared IsDllStruct IsFloat IsFunc IsHWnd IsInt IsKeyword IsNumber IsObj IsPtr IsString Log MemGetStats Mod MouseClick MouseClickDrag MouseDown MouseGetCursor MouseGetPos MouseMove MouseUp MouseWheel MsgBox Number ObjCreate ObjCreateInterface ObjEvent ObjGet ObjName OnAutoItExitRegister OnAutoItExitUnRegister Ping PixelChecksum PixelGetColor PixelSearch ProcessClose ProcessExists ProcessGetStats ProcessList ProcessSetPriority ProcessWait ProcessWaitClose ProgressOff ProgressOn ProgressSet Ptr Random RegDelete RegEnumKey RegEnumVal RegRead RegWrite Round Run RunAs RunAsWait RunWait Send SendKeepActive SetError SetExtended ShellExecute ShellExecuteWait Shutdown Sin Sleep SoundPlay SoundSetWaveVolume SplashImageOn SplashOff SplashTextOn Sqrt SRandom StatusbarGetText StderrRead StdinWrite StdioClose StdoutRead String StringAddCR StringCompare StringFormat StringFromASCIIArray StringInStr StringIsAlNum StringIsAlpha StringIsASCII StringIsDigit StringIsFloat StringIsInt StringIsLower StringIsSpace StringIsUpper StringIsXDigit StringLeft StringLen StringLower StringMid StringRegExp StringRegExpReplace StringReplace StringReverse StringRight StringSplit StringStripCR StringStripWS StringToASCIIArray StringToBinary StringTrimLeft StringTrimRight StringUpper Tan TCPAccept TCPCloseSocket TCPConnect TCPListen TCPNameToIP TCPRecv TCPSend TCPShutdown, UDPShutdown TCPStartup, UDPStartup TimerDiff TimerInit ToolTip TrayCreateItem TrayCreateMenu TrayGetMsg TrayItemDelete TrayItemGetHandle TrayItemGetState TrayItemGetText TrayItemSetOnEvent TrayItemSetState TrayItemSetText TraySetClick TraySetIcon TraySetOnEvent TraySetPauseIcon TraySetState TraySetToolTip TrayTip UBound UDPBind UDPCloseSocket UDPOpen UDPRecv UDPSend VarGetType WinActivate WinActive WinClose WinExists WinFlash WinGetCaretPos WinGetClassList WinGetClientSize WinGetHandle WinGetPos WinGetProcess WinGetState WinGetText WinGetTitle WinKill WinList WinMenuSelectItem WinMinimizeAll WinMinimizeAllUndo WinMove WinSetOnTop WinSetState WinSetTitle WinSetTrans WinWait",
-literal:"True False And Null Not Or"},contains:[b,c,d,e,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"comments include include-once NoTrayIcon OnAutoItStartRegister pragma compile RequireAdmin"},contains:[{begin:/\\\n/,relevance:0},{beginKeywords:"include",keywords:{"meta-keyword":"include"},end:"$",contains:[d,{className:"meta-string",variants:[{begin:"<",end:">"},{begin:/"/,end:/"/,contains:[{begin:/""/,relevance:0}]},{begin:/'/,end:/'/,contains:[{begin:/''/,relevance:0}]}]}]},d,
-b]},{className:"symbol",begin:"@[A-z0-9_]+"},{className:"function",beginKeywords:"Func",end:"$",illegal:"\\$|\\[|%",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",contains:[c,d,e]}]}]}}}());
-hljs.registerLanguage("avrasm",function(){return function(a){return{name:"AVR Assembly",case_insensitive:!0,keywords:{$pattern:"\\.?"+a.IDENT_RE,keyword:"adc add adiw and andi asr bclr bld brbc brbs brcc brcs break breq brge brhc brhs brid brie brlo brlt brmi brne brpl brsh brtc brts brvc brvs bset bst call cbi cbr clc clh cli cln clr cls clt clv clz com cp cpc cpi cpse dec eicall eijmp elpm eor fmul fmuls fmulsu icall ijmp in inc jmp ld ldd ldi lds lpm lsl lsr mov movw mul muls mulsu neg nop or ori out pop push rcall ret reti rjmp rol ror sbc sbr sbrc sbrs sec seh sbi sbci sbic sbis sbiw sei sen ser ses set sev sez sleep spm st std sts sub subi swap tst wdr",built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 r16 r17 r18 r19 r20 r21 r22 r23 r24 r25 r26 r27 r28 r29 r30 r31 x|0 xh xl y|0 yh yl z|0 zh zl ucsr1c udr1 ucsr1a ucsr1b ubrr1l ubrr1h ucsr0c ubrr0h tccr3c tccr3a tccr3b tcnt3h tcnt3l ocr3ah ocr3al ocr3bh ocr3bl ocr3ch ocr3cl icr3h icr3l etimsk etifr tccr1c ocr1ch ocr1cl twcr twdr twar twsr twbr osccal xmcra xmcrb eicra spmcsr spmcr portg ddrg ping portf ddrf sreg sph spl xdiv rampz eicrb eimsk gimsk gicr eifr gifr timsk tifr mcucr mcucsr tccr0 tcnt0 ocr0 assr tccr1a tccr1b tcnt1h tcnt1l ocr1ah ocr1al ocr1bh ocr1bl icr1h icr1l tccr2 tcnt2 ocr2 ocdr wdtcr sfior eearh eearl eedr eecr porta ddra pina portb ddrb pinb portc ddrc pinc portd ddrd pind spdr spsr spcr udr0 ucsr0a ucsr0b ubrr0l acsr admux adcsr adch adcl porte ddre pine pinf",
-meta:".byte .cseg .db .def .device .dseg .dw .endmacro .equ .eseg .exit .include .list .listmac .macro .nolist .org .set"},contains:[a.C_BLOCK_COMMENT_MODE,a.COMMENT(";","$",{relevance:0}),a.C_NUMBER_MODE,a.BINARY_NUMBER_MODE,{className:"number",begin:"\\b(\\$[a-zA-Z0-9]+|0o[0-7]+)"},a.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"[^\\\\]'",illegal:"[^\\\\][^']"},{className:"symbol",begin:"^[A-Za-z0-9_.$]+:"},{className:"meta",begin:"#",end:"$"},{className:"subst",begin:"@[0-9]+"}]}}}());
-hljs.registerLanguage("awk",function(){return function(a){return{name:"Awk",keywords:{keyword:"BEGIN END if else while do for in break continue delete next nextfile function func exit|10"},contains:[{className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},{className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,relevance:10},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,
-relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},a.REGEXP_MODE,a.HASH_COMMENT_MODE,a.NUMBER_MODE]}}}());
-hljs.registerLanguage("axapta",function(){return function(a){return{name:"Dynamics 365",keywords:"false int abstract private char boolean static null if for true while long throw finally protected final return void enum else break new catch byte super case short default double public try this switch continue reverse firstfast firstonly forupdate nofetch sum avg minof maxof count order group by asc desc index hint like dispaly edit client server ttsbegin ttscommit str real date container anytype common div mod",contains:[a.C_LINE_COMMENT_MODE,
-a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{className:"meta",begin:"#",end:"$"},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:":",contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]}]}}}());
-hljs.registerLanguage("bash",function(){return function(a){var b={};Object.assign(b,{className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{/,end:/\}/,contains:[{begin:/:-/,contains:[b]}]}]});var c={className:"subst",begin:/\$\(/,end:/\)/,contains:[a.BACKSLASH_ESCAPE]},d={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,b,c]};c.contains.push(d);c={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},a.NUMBER_MODE,b]};var e=a.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",
-relevance:10}),f={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b-?[a-z\._]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",
-_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[e,a.SHEBANG(),f,c,a.HASH_COMMENT_MODE,d,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},b]}}}());
-hljs.registerLanguage("basic",function(){return function(a){return{name:"BASIC",case_insensitive:!0,illegal:"^.",keywords:{$pattern:"[a-zA-Z][a-zA-Z0-9_$%!#]*",keyword:"ABS ASC AND ATN AUTO|0 BEEP BLOAD|10 BSAVE|10 CALL CALLS CDBL CHAIN CHDIR CHR$|10 CINT CIRCLE CLEAR CLOSE CLS COLOR COM COMMON CONT COS CSNG CSRLIN CVD CVI CVS DATA DATE$ DEFDBL DEFINT DEFSNG DEFSTR DEF|0 SEG USR DELETE DIM DRAW EDIT END ENVIRON ENVIRON$ EOF EQV ERASE ERDEV ERDEV$ ERL ERR ERROR EXP FIELD FILES FIX FOR|0 FRE GET GOSUB|10 GOTO HEX$ IF THEN ELSE|0 INKEY$ INP INPUT INPUT# INPUT$ INSTR IMP INT IOCTL IOCTL$ KEY ON OFF LIST KILL LEFT$ LEN LET LINE LLIST LOAD LOC LOCATE LOF LOG LPRINT USING LSET MERGE MID$ MKDIR MKD$ MKI$ MKS$ MOD NAME NEW NEXT NOISE NOT OCT$ ON OR PEN PLAY STRIG OPEN OPTION BASE OUT PAINT PALETTE PCOPY PEEK PMAP POINT POKE POS PRINT PRINT] PSET PRESET PUT RANDOMIZE READ REM RENUM RESET|0 RESTORE RESUME RETURN|0 RIGHT$ RMDIR RND RSET RUN SAVE SCREEN SGN SHELL SIN SOUND SPACE$ SPC SQR STEP STICK STOP STR$ STRING$ SWAP SYSTEM TAB TAN TIME$ TIMER TROFF TRON TO USR VAL VARPTR VARPTR$ VIEW WAIT WHILE WEND WIDTH WINDOW WRITE XOR"},contains:[a.QUOTE_STRING_MODE,
-a.COMMENT("REM","$",{relevance:10}),a.COMMENT("'","$",{relevance:0}),{className:"symbol",begin:"^[0-9]+ ",relevance:10},{className:"number",begin:"\\b([0-9]+[0-9edED.]*[#!]?)",relevance:0},{className:"number",begin:"(&[hH][0-9a-fA-F]{1,4})"},{className:"number",begin:"(&[oO][0-7]{1,6})"}]}}}());
-hljs.registerLanguage("bnf",function(){return function(a){return{name:"Backus\u2013Naur Form",contains:[{className:"attribute",begin:/</,end:/>/},{begin:/::=/,end:/$/,contains:[{begin:/</,end:/>/},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]}}}());
-hljs.registerLanguage("brainfuck",function(){return function(a){var b={className:"literal",begin:"[\\+\\-]",relevance:0};return{name:"Brainfuck",aliases:["bf"],contains:[a.COMMENT("[^\\[\\]\\.,\\+\\-<> \r\n]","[\\[\\]\\.,\\+\\-<> \r\n]",{returnEnd:!0,relevance:0}),{className:"title",begin:"[\\[\\]]",relevance:0},{className:"string",begin:"[\\.,]",relevance:0},{begin:/(?:\+\+|\-\-)/,contains:[b]},b]}}}());
-hljs.registerLanguage("c",function(){return function(a){a=a.getLanguage("c-like").rawDefinition();a.name="C";a.aliases=["c","h"];return a}}());
-hljs.registerLanguage("cal",function(){return function(a){var b=[a.C_LINE_COMMENT_MODE,a.COMMENT(/\{/,/\}/,{relevance:0}),a.COMMENT(/\(\*/,/\*\)/,{relevance:10})],c={className:"string",begin:/'/,end:/'/,contains:[{begin:/''/}]},d={className:"string",begin:/(#\d+)+/};b={className:"function",beginKeywords:"procedure",end:/[:;]/,keywords:"procedure|10",contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:"div mod in and or not xor asserterror begin case do downto else end exit for if of repeat then to until while with var",
-contains:[c,d]}].concat(b)};return{name:"C/AL",case_insensitive:!0,keywords:{keyword:"div mod in and or not xor asserterror begin case do downto else end exit for if of repeat then to until while with var",literal:"false true"},illegal:/\/\*/,contains:[c,d,{className:"number",begin:"\\b\\d+(\\.\\d+)?(DT|D|T)",relevance:0},{className:"string",begin:'"',end:'"'},a.NUMBER_MODE,{className:"class",begin:"OBJECT (Table|Form|Report|Dataport|Codeunit|XMLport|MenuSuite|Page|Query) (\\d+) ([^\\r\\n]+)",returnBegin:!0,
-contains:[a.TITLE_MODE,b]},b]}}}());
-hljs.registerLanguage("capnproto",function(){return function(a){return{name:"Cap\u2019n Proto",aliases:["capnp"],keywords:{keyword:"struct enum interface union group import using const annotation extends in of on as with from fixed",built_in:"Void Bool Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 Float32 Float64 Text Data AnyPointer AnyStruct Capability List",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.HASH_COMMENT_MODE,{className:"meta",begin:/@0x[\w\d]{16};/,illegal:/\n/},
-{className:"symbol",begin:/@\d+\b/},{className:"class",beginKeywords:"struct enum",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]},{className:"class",beginKeywords:"interface",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]}]}}}());
-hljs.registerLanguage("ceylon",function(){return function(a){var b={className:"subst",excludeBegin:!0,excludeEnd:!0,begin:/``/,end:/``/,keywords:"assembly module package import alias class interface object given value assign void function new of extends satisfies abstracts in out return break continue throw assert dynamic if else switch case for while try catch finally then let this outer super is exists nonempty",relevance:10},c=[{className:"string",begin:'"""',end:'"""',relevance:10},{className:"string",
-begin:'"',end:'"',contains:[b]},{className:"string",begin:"'",end:"'"},{className:"number",begin:"#[0-9a-fA-F_]+|\\$[01_]+|[0-9_]+(?:\\.[0-9_](?:[eE][+-]?\\d+)?)?[kMGTPmunpf]?",relevance:0}];b.contains=c;return{name:"Ceylon",keywords:{keyword:"assembly module package import alias class interface object given value assign void function new of extends satisfies abstracts in out return break continue throw assert dynamic if else switch case for while try catch finally then let this outer super is exists nonempty shared abstract formal default actual variable late native deprecated final sealed annotation suppressWarnings small",
-meta:"doc by license see throws tagged"},illegal:"\\$[^01]|#[^0-9a-fA-F]",contains:[a.C_LINE_COMMENT_MODE,a.COMMENT("/\\*","\\*/",{contains:["self"]}),{className:"meta",begin:'@[a-z]\\w*(?:\\:"[^"]*")?'}].concat(c)}}}());
-hljs.registerLanguage("clean",function(){return function(a){return{name:"Clean",aliases:["clean","icl","dcl"],keywords:{keyword:"if let in with where case of class instance otherwise implementation definition system module from import qualified as special code inline foreign export ccall stdcall generic derive infix infixl infixr",built_in:"Int Real Char Bool",literal:"True False"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{begin:"->|<-[|:]?|#!?|>>=|\\{\\||\\|\\}|:==|=:|<>"}]}}}());
-hljs.registerLanguage("clojure",function(){return function(a){var b={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},c=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),d=a.COMMENT(";","$",{relevance:0}),e={className:"literal",begin:/\b(true|false|nil)\b/},f={begin:"[\\[\\{]",end:"[\\]\\}]"},h={className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},g=a.COMMENT("\\^\\{","\\}"),k={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},
-l={begin:"\\(",end:"\\)"},m={endsWithParent:!0,relevance:0},p={keywords:{$pattern:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*","builtin-name":"def defonce defprotocol defstruct defmulti defmethod defn- defn defmacro deftype defrecord cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy first rest cons cast coll last butlast sigs reify second ffirst fnext nfirst nnext meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},
-className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:m},q=[l,c,h,g,d,k,f,b,e,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}],r={beginKeywords:"def defonce defprotocol defstruct defmulti defmethod defn- defn defmacro deftype defrecord",lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",end:'(\\[|\\#|\\d|"|:|\\{|\\)|\\(|$)',contains:[{className:"title",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0,
-excludeEnd:!0,endsParent:!0}].concat(q)};l.contains=[a.COMMENT("comment",""),r,p,m];m.contains=q;f.contains=q;g.contains=[f];return{name:"Clojure",aliases:["clj"],illegal:/\S/,contains:[l,c,h,g,d,k,f,b,e]}}}());hljs.registerLanguage("clojure-repl",function(){return function(a){return{name:"Clojure REPL",contains:[{className:"meta",begin:/^([\w.-]+|\s*#_)?=>/,starts:{end:/$/,subLanguage:"clojure"}}]}}}());
-hljs.registerLanguage("cmake",function(){return function(a){return{name:"CMake",aliases:["cmake.in"],case_insensitive:!0,keywords:{keyword:"break cmake_host_system_information cmake_minimum_required cmake_parse_arguments cmake_policy configure_file continue elseif else endforeach endfunction endif endmacro endwhile execute_process file find_file find_library find_package find_path find_program foreach function get_cmake_property get_directory_property get_filename_component get_property if include include_guard list macro mark_as_advanced math message option return separate_arguments set_directory_properties set_property set site_name string unset variable_watch while add_compile_definitions add_compile_options add_custom_command add_custom_target add_definitions add_dependencies add_executable add_library add_link_options add_subdirectory add_test aux_source_directory build_command create_test_sourcelist define_property enable_language enable_testing export fltk_wrap_ui get_source_file_property get_target_property get_test_property include_directories include_external_msproject include_regular_expression install link_directories link_libraries load_cache project qt_wrap_cpp qt_wrap_ui remove_definitions set_source_files_properties set_target_properties set_tests_properties source_group target_compile_definitions target_compile_features target_compile_options target_include_directories target_link_directories target_link_libraries target_link_options target_sources try_compile try_run ctest_build ctest_configure ctest_coverage ctest_empty_binary_directory ctest_memcheck ctest_read_custom_files ctest_run_script ctest_sleep ctest_start ctest_submit ctest_test ctest_update ctest_upload build_name exec_program export_library_dependencies install_files install_programs install_targets load_command make_directory output_required_files remove subdir_depends subdirs use_mangled_mesa utility_source variable_requires write_file qt5_use_modules qt5_use_package qt5_wrap_cpp on off true false and or not command policy target test exists is_newer_than is_directory is_symlink is_absolute matches less greater equal less_equal greater_equal strless strgreater strequal strless_equal strgreater_equal version_less version_greater version_equal version_less_equal version_greater_equal in_list defined"},
-contains:[{className:"variable",begin:"\\${",end:"}"},a.HASH_COMMENT_MODE,a.QUOTE_STRING_MODE,a.NUMBER_MODE]}}}());
-hljs.registerLanguage("coffeescript",function(){var a="as in of if for while finally var new function do return void else break catch instanceof with throw case default try switch continue typeof delete let yield const class debugger async await static import from export extends".split(" "),b="true false null undefined NaN Infinity".split(" "),c=[].concat("setInterval setTimeout clearInterval clearTimeout require exports eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape".split(" "),"arguments this super console window document localStorage module global".split(" "),
-"Intl DataView Number Math Date String RegExp Object Function Boolean Error Symbol Set Map WeakSet WeakMap Proxy Reflect JSON Promise Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Float32Array Array Uint8Array Uint8ClampedArray ArrayBuffer".split(" "),"EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError".split(" "));return function(d){var e={keyword:a.concat("then unless until loop by when and or is isnt not".split(" ")).filter(function(a){return function(b){return!a.includes(b)}}(["var",
-"const","let","function","static"])).join(" "),literal:b.concat(["yes","no","on","off"]).join(" "),built_in:c.concat(["npm","print"]).join(" ")},f={className:"subst",begin:/#\{/,end:/}/,keywords:e},h=[d.BINARY_NUMBER_MODE,d.inherit(d.C_NUMBER_MODE,{starts:{end:"(\\s*/)?",relevance:0}}),{className:"string",variants:[{begin:/'''/,end:/'''/,contains:[d.BACKSLASH_ESCAPE]},{begin:/'/,end:/'/,contains:[d.BACKSLASH_ESCAPE]},{begin:/"""/,end:/"""/,contains:[d.BACKSLASH_ESCAPE,f]},{begin:/"/,end:/"/,contains:[d.BACKSLASH_ESCAPE,
-f]}]},{className:"regexp",variants:[{begin:"///",end:"///",contains:[f,d.HASH_COMMENT_MODE]},{begin:"//[gim]{0,3}(?=\\W)",relevance:0},{begin:/\/(?![ *]).*?(?![\\]).\/[gim]{0,3}(?=\W)/}]},{begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},{subLanguage:"javascript",excludeBegin:!0,excludeEnd:!0,variants:[{begin:"```",end:"```"},{begin:"`",end:"`"}]}];f.contains=h;f=d.inherit(d.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"});var g={className:"params",begin:"\\([^\\(]",returnBegin:!0,contains:[{begin:/\(/,end:/\)/,
-keywords:e,contains:["self"].concat(h)}]};return{name:"CoffeeScript",aliases:["coffee","cson","iced"],keywords:e,illegal:/\/\*/,contains:h.concat([d.COMMENT("###","###"),d.HASH_COMMENT_MODE,{className:"function",begin:"^\\s*[A-Za-z$_][0-9A-Za-z$_]*\\s*=\\s*(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[f,g]},{begin:/[:\(,=]\s*/,relevance:0,contains:[{className:"function",begin:"(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[g]}]},{className:"class",beginKeywords:"class",
-end:"$",illegal:/[:="\[\]]/,contains:[{beginKeywords:"extends",endsWithParent:!0,illegal:/[:="\[\]]/,contains:[f]},f]},{begin:"[A-Za-z$_][0-9A-Za-z$_]*:",end:":",returnBegin:!0,returnEnd:!0,relevance:0}])}}}());
-hljs.registerLanguage("coq",function(){return function(a){return{name:"Coq",keywords:{keyword:"_|0 as at cofix else end exists exists2 fix for forall fun if IF in let match mod Prop return Set then Type using where with Abort About Add Admit Admitted All Arguments Assumptions Axiom Back BackTo Backtrack Bind Blacklist Canonical Cd Check Class Classes Close Coercion Coercions CoFixpoint CoInductive Collection Combined Compute Conjecture Conjectures Constant constr Constraint Constructors Context Corollary CreateHintDb Cut Declare Defined Definition Delimit Dependencies Dependent Derive Drop eauto End Equality Eval Example Existential Existentials Existing Export exporting Extern Extract Extraction Fact Field Fields File Fixpoint Focus for From Function Functional Generalizable Global Goal Grab Grammar Graph Guarded Heap Hint HintDb Hints Hypotheses Hypothesis ident Identity If Immediate Implicit Import Include Inductive Infix Info Initial Inline Inspect Instance Instances Intro Intros Inversion Inversion_clear Language Left Lemma Let Libraries Library Load LoadPath Local Locate Ltac ML Mode Module Modules Monomorphic Morphism Next NoInline Notation Obligation Obligations Opaque Open Optimize Options Parameter Parameters Parametric Path Paths pattern Polymorphic Preterm Print Printing Program Projections Proof Proposition Pwd Qed Quit Rec Record Recursive Redirect Relation Remark Remove Require Reserved Reset Resolve Restart Rewrite Right Ring Rings Save Scheme Scope Scopes Script Search SearchAbout SearchHead SearchPattern SearchRewrite Section Separate Set Setoid Show Solve Sorted Step Strategies Strategy Structure SubClass Table Tables Tactic Term Test Theorem Time Timeout Transparent Type Typeclasses Types Undelimit Undo Unfocus Unfocused Unfold Universe Universes Unset Unshelve using Variable Variables Variant Verbose Visibility where with",built_in:"abstract absurd admit after apply as assert assumption at auto autorewrite autounfold before bottom btauto by case case_eq cbn cbv change classical_left classical_right clear clearbody cofix compare compute congruence constr_eq constructor contradict contradiction cut cutrewrite cycle decide decompose dependent destruct destruction dintuition discriminate discrR do double dtauto eapply eassumption eauto ecase econstructor edestruct ediscriminate eelim eexact eexists einduction einjection eleft elim elimtype enough equality erewrite eright esimplify_eq esplit evar exact exactly_once exfalso exists f_equal fail field field_simplify field_simplify_eq first firstorder fix fold fourier functional generalize generalizing gfail give_up has_evar hnf idtac in induction injection instantiate intro intro_pattern intros intuition inversion inversion_clear is_evar is_var lapply lazy left lia lra move native_compute nia nsatz omega once pattern pose progress proof psatz quote record red refine reflexivity remember rename repeat replace revert revgoals rewrite rewrite_strat right ring ring_simplify rtauto set setoid_reflexivity setoid_replace setoid_rewrite setoid_symmetry setoid_transitivity shelve shelve_unifiable simpl simple simplify_eq solve specialize split split_Rabs split_Rmult stepl stepr subst sum swap symmetry tactic tauto time timeout top transitivity trivial try tryif unfold unify until using vm_compute with"},
-contains:[a.QUOTE_STRING_MODE,a.COMMENT("\\(\\*","\\*\\)"),a.C_NUMBER_MODE,{className:"type",excludeBegin:!0,begin:"\\|\\s*",end:"\\w+"},{begin:/[-=]>/}]}}}());
-hljs.registerLanguage("cos",function(){return function(a){return{name:"Cach\u00e9 Object Script",case_insensitive:!0,aliases:["cos","cls"],keywords:"property parameter class classmethod clientmethod extends as break catch close continue do d|0 else elseif for goto halt hang h|0 if job j|0 kill k|0 lock l|0 merge new open quit q|0 read r|0 return set s|0 tcommit throw trollback try tstart use view while write w|0 xecute x|0 zkill znspace zn ztrap zwrite zw zzdump zzwrite print zbreak zinsert zload zprint zremove zsave zzprint mv mvcall mvcrt mvdim mvprint zquit zsync ascii",contains:[{className:"number",
-begin:"\\b(\\d+(\\.\\d*)?|\\.\\d+)",relevance:0},{className:"string",variants:[{begin:'"',end:'"',contains:[{begin:'""',relevance:0}]}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"comment",begin:/;/,end:"$",relevance:0},{className:"built_in",begin:/(?:\$\$?|\.\.)\^?[a-zA-Z]+/},{className:"built_in",begin:/\$\$\$[a-zA-Z]+/},{className:"built_in",begin:/%[a-z]+(?:\.[a-z]+)*/},{className:"symbol",begin:/\^%?[a-zA-Z][\w]*/},{className:"keyword",begin:/##class|##super|#define|#dim/},{begin:/&sql\(/,
-end:/\)/,excludeBegin:!0,excludeEnd:!0,subLanguage:"sql"},{begin:/&(js|jscript|javascript)</,end:/>/,excludeBegin:!0,excludeEnd:!0,subLanguage:"javascript"},{begin:/&html<\s*</,end:/>\s*>/,subLanguage:"xml"}]}}}());
-hljs.registerLanguage("crmsh",function(){return function(a){return{name:"crmsh",aliases:["crm","pcmk"],case_insensitive:!0,keywords:{keyword:"params meta operations op rule attributes utilization read write deny defined not_defined in_range date spec in ref reference attribute type xpath version and or lt gt tag lte gte eq ne \\ number string",literal:"Master Started Slave Stopped start promote demote stop monitor true false"},contains:[a.HASH_COMMENT_MODE,{beginKeywords:"node",starts:{end:"\\s*([\\w_-]+:)?",
-starts:{className:"title",end:"\\s*[\\$\\w_][\\w_-]*"}}},{beginKeywords:"primitive rsc_template",starts:{className:"title",end:"\\s*[\\$\\w_][\\w_-]*",starts:{end:"\\s*@?[\\w_][\\w_\\.:-]*"}}},{begin:"\\b(group|clone|ms|master|location|colocation|order|fencing_topology|rsc_ticket|acl_target|acl_group|user|role|tag|xml)\\s+",keywords:"group clone ms master location colocation order fencing_topology rsc_ticket acl_target acl_group user role tag xml",starts:{className:"title",end:"[\\$\\w_][\\w_-]*"}},
-{beginKeywords:"property rsc_defaults op_defaults",starts:{className:"title",end:"\\s*([\\w_-]+:)?"}},a.QUOTE_STRING_MODE,{className:"meta",begin:"(ocf|systemd|service|lsb):[\\w_:-]+",relevance:0},{className:"number",begin:"\\b\\d+(\\.\\d+)?(ms|s|h|m)?",relevance:0},{className:"literal",begin:"[-]?(infinity|inf)",relevance:0},{className:"attr",begin:/([A-Za-z\$_#][\w_-]+)=/,relevance:0},{className:"tag",begin:"</?",end:"/?>",relevance:0}]}}}());
-hljs.registerLanguage("crystal",function(){return function(a){function b(a,b){a=[{begin:a,end:b}];return a[0].contains=a}var c={$pattern:"[a-zA-Z_]\\w*[!?=]?",keyword:"abstract alias annotation as as? asm begin break case class def do else elsif end ensure enum extend for fun if include instance_sizeof is_a? lib macro module next nil? of out pointerof private protected rescue responds_to? return require select self sizeof struct super then type typeof union uninitialized unless until verbatim when while with yield __DIR__ __END_LINE__ __FILE__ __LINE__",
-literal:"false nil true"},d={className:"subst",begin:"#{",end:"}",keywords:c},e={className:"template-variable",variants:[{begin:"\\{\\{",end:"\\}\\}"},{begin:"\\{%",end:"%\\}"}],keywords:c},f={className:"string",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[Qwi]?\\(",end:"\\)",contains:b("\\(","\\)")},{begin:"%[Qwi]?\\[",end:"\\]",contains:b("\\[","\\]")},{begin:"%[Qwi]?{",end:"}",contains:b("{","}")},{begin:"%[Qwi]?<",end:">",contains:b("<",
-">")},{begin:"%[Qwi]?\\|",end:"\\|"},{begin:/<<-\w+$/,end:/^\s*\w+$/}],relevance:0},h={className:"string",variants:[{begin:"%q\\(",end:"\\)",contains:b("\\(","\\)")},{begin:"%q\\[",end:"\\]",contains:b("\\[","\\]")},{begin:"%q{",end:"}",contains:b("{","}")},{begin:"%q<",end:">",contains:b("<",">")},{begin:"%q\\|",end:"\\|"},{begin:/<<-'\w+'$/,end:/^\s*\w+$/}],relevance:0},g={begin:"(?!%})("+a.RE_STARTERS_RE+"|\\n|\\b(case|if|select|unless|until|when|while)\\b)\\s*",keywords:"case if select unless until when while",
-contains:[{className:"regexp",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:"//[a-z]*",relevance:0},{begin:"/(?!\\/)",end:"/[a-z]*"}]}],relevance:0},k={className:"regexp",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:"%r\\(",end:"\\)",contains:b("\\(","\\)")},{begin:"%r\\[",end:"\\]",contains:b("\\[","\\]")},{begin:"%r{",end:"}",contains:b("{","}")},{begin:"%r<",end:">",contains:b("<",">")},{begin:"%r\\|",end:"\\|"}],relevance:0},l={className:"meta",begin:"@\\[",end:"\\]",contains:[a.inherit(a.QUOTE_STRING_MODE,
-{className:"meta-string"})]};a=[e,f,h,k,g,l,a.HASH_COMMENT_MODE,{className:"class",beginKeywords:"class module struct",end:"$|;",illegal:/=/,contains:[a.HASH_COMMENT_MODE,a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<"}]},{className:"class",beginKeywords:"lib enum union",end:"$|;",illegal:/=/,contains:[a.HASH_COMMENT_MODE,a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"})],relevance:10},{beginKeywords:"annotation",end:"$|;",illegal:/=/,contains:[a.HASH_COMMENT_MODE,
-a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"})],relevance:10},{className:"function",beginKeywords:"def",end:/\B\b/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|[=!]~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~|]|//|//=|&[-+*]=?|&\\*\\*|\\[\\][=?]?",endsParent:!0})]},{className:"function",beginKeywords:"fun macro",end:/\B\b/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|[=!]~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~|]|//|//=|&[-+*]=?|&\\*\\*|\\[\\][=?]?",
-endsParent:!0})],relevance:5},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":",contains:[f,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|[=!]~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~|]|//|//=|&[-+*]=?|&\\*\\*|\\[\\][=?]?"}],relevance:0},{className:"number",variants:[{begin:"\\b0b([01_]+)(_*[ui](8|16|32|64|128))?"},{begin:"\\b0o([0-7_]+)(_*[ui](8|16|32|64|128))?"},{begin:"\\b0x([A-Fa-f0-9_]+)(_*[ui](8|16|32|64|128))?"},{begin:"\\b([1-9][0-9_]*[0-9]|[0-9])(\\.[0-9][0-9_]*)?([eE]_*[-+]?[0-9_]*)?(_*f(32|64))?(?!_)"},
-{begin:"\\b([1-9][0-9_]*|0)(_*[ui](8|16|32|64|128))?"}],relevance:0}];d.contains=a;e.contains=a.slice(1);return{name:"Crystal",aliases:["cr"],keywords:c,contains:a}}}());
-hljs.registerLanguage("csharp",function(){return function(a){var b={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let nameof on orderby partial remove select set value var when where yield",literal:"null false true"},
-c=a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),d={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},e={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},f=a.inherit(e,{illegal:/\n/}),h={className:"subst",begin:"{",end:"}",keywords:b},g=a.inherit(h,{illegal:/\n/}),k={className:"string",begin:/\$"/,
-end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},a.BACKSLASH_ESCAPE,g]},l={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},h]},m=a.inherit(l,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},g]});h.contains=[l,k,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,d,a.C_BLOCK_COMMENT_MODE];g.contains=[m,k,f,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,d,a.inherit(a.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];e={variants:[l,k,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]};
-f=a.IDENT_RE+"(<"+a.IDENT_RE+"(\\s*,\\s*"+a.IDENT_RE+")*>)?(\\[\\])?";h={begin:"@"+a.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],keywords:b,illegal:/::/,contains:[a.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:"</?",end:">"}]}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},
-e,d,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"},c,{begin:"<",end:">",keywords:"in out"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",
-begin:"("+f+"\\s+)+"+a.IDENT_RE+"\\s*\\(",returnBegin:!0,end:/\s*[{;=]/,excludeEnd:!0,keywords:b,contains:[{begin:a.IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,relevance:0,contains:[e,d,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},h]}}}());
-hljs.registerLanguage("csp",function(){return function(a){return{name:"CSP",case_insensitive:!1,keywords:{$pattern:"[a-zA-Z][a-zA-Z0-9_-]*",keyword:"base-uri child-src connect-src default-src font-src form-action frame-ancestors frame-src img-src media-src object-src plugin-types report-uri sandbox script-src style-src"},contains:[{className:"string",begin:"'",end:"'"},{className:"attribute",begin:"^Content",end:":",excludeEnd:!0}]}}}());
-hljs.registerLanguage("css",function(){return function(a){return{name:"CSS",case_insensitive:!0,illegal:/[=\/|'\$]/,contains:[a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9_\-\+\(\)"'.]+/},{begin:"@(page|font-face)",lexemes:"@[a-z-]+",keywords:"@page @font-face"},
-{begin:"@",end:"[{;]",illegal:/:/,returnBegin:!0,contains:[{className:"keyword",begin:/@\-?\w[\w]*(\-\w+)*/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:"and or not only",contains:[{begin:/[a-z-]+:/,className:"attribute"},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[a.C_BLOCK_COMMENT_MODE,{begin:/(?:[A-Z_\.\-]+|--[a-zA-Z0-9_-]+)\s*:/,returnBegin:!0,
-end:";",endsWithParent:!0,contains:[{className:"attribute",begin:/\S/,end:":",excludeEnd:!0,starts:{endsWithParent:!0,excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE]}]},a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]}]}]}}}());
-hljs.registerLanguage("d",function(){return function(a){var b={$pattern:a.UNDERSCORE_IDENT_RE,keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",
-built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"},c=a.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{name:"D",keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,{className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},{className:"string",begin:'"',contains:[{begin:"\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",
-relevance:0}],end:'"[cwd]?'},{className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},{className:"number",begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(i|[fF]i|Li))",
-relevance:0},{className:"number",begin:"\\b((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(L|u|U|Lu|LU|uL|UL)?",relevance:0},{className:"string",begin:"'(\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};|.)",end:"'",illegal:"."},{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}}}());
-hljs.registerLanguage("markdown",function(){return function(a){a={begin:"<",end:">",subLanguage:"xml",relevance:0};var b={begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},c={className:"strong",contains:[],variants:[{begin:/_{2}/,end:/_{2}/},
-{begin:/\*{2}/,end:/\*{2}/}]},d={className:"emphasis",contains:[],variants:[{begin:/\*(?!\*)/,end:/\*/},{begin:/_(?!_)/,end:/_/,relevance:0}]};c.contains.push(d);d.contains.push(c);var e=[a,b];c.contains=c.contains.concat(e);d.contains=d.contains.concat(e);e=e.concat(c,d);return{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:e},{begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",contains:e}]}]},a,
-{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",end:"\\s+",excludeEnd:!0},c,d,{className:"quote",begin:"^>\\s+",contains:e,end:"$"},{className:"code",variants:[{begin:"(`{3,})(.|\\n)*?\\1`*[ ]*"},{begin:"(~{3,})(.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},b,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",
-begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}}());
-hljs.registerLanguage("dart",function(){return function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"}]},c={className:"subst",variants:[{begin:"\\${",end:"}"}],keywords:"true false null this is new super"};b={className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',end:'"""'},{begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"},{begin:"'''",end:"'''",contains:[a.BACKSLASH_ESCAPE,b,c]},{begin:'"""',end:'"""',contains:[a.BACKSLASH_ESCAPE,b,c]},{begin:"'",
-end:"'",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b,c]},{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b,c]}]};c.contains=[a.C_NUMBER_MODE,b];return{name:"Dart",keywords:{keyword:"abstract as assert async await break case catch class const continue covariant default deferred do dynamic else enum export extends extension external factory false final finally for Function get hide if implements import in inferface is library mixin new null on operator part rethrow return set show static super switch sync this throw true try typedef var void while with yield",
-built_in:"Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double dynamic int num print Element ElementList document querySelector querySelectorAll window"},contains:[b,a.COMMENT("/\\*\\*","\\*/",{subLanguage:"markdown",relevance:0}),a.COMMENT("///+\\s*","$",{contains:[{subLanguage:"markdown",begin:".",end:"$",relevance:0}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",
-beginKeywords:"class interface",end:"{",excludeEnd:!0,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}}}());
-hljs.registerLanguage("delphi",function(){return function(a){var b=[a.C_LINE_COMMENT_MODE,a.COMMENT(/\{/,/\}/,{relevance:0}),a.COMMENT(/\(\*/,/\*\)/,{relevance:10})],c={className:"meta",variants:[{begin:/\{\$/,end:/\}/},{begin:/\(\*\$/,end:/\*\)/}]},d={className:"string",begin:/'/,end:/'/,contains:[{begin:/''/}]},e={className:"string",begin:/(#\d+)+/},f={begin:a.IDENT_RE+"\\s*=\\s*class\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE]},h={className:"function",beginKeywords:"function constructor destructor procedure",
-end:/[:;]/,keywords:"function constructor|10 destructor|10 procedure|10",contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:"exports register file shl array record property for mod while set ally label uses raise not stored class safecall var interface or private static exit index inherited to else stdcall override shr asm far resourcestring finalization packed virtual out and protected library do xorwrite goto near function end div overload object unit begin string on inline repeat until destructor write message program with read initialization except default nil if case cdecl in downto threadvar of try pascal const external constructor type public then implementation finally published procedure absolute reintroduce operator as is abstract alias assembler bitpacked break continue cppdecl cvar enumerator experimental platform deprecated unimplemented dynamic export far16 forward generic helper implements interrupt iochecks local name nodefault noreturn nostackframe oldfpccall otherwise saveregisters softfloat specialize strict unaligned varargs ",
-contains:[d,e,c].concat(b)},c].concat(b)};return{name:"Delphi",aliases:"dpr dfm pas pascal freepascal lazarus lpr lfm".split(" "),case_insensitive:!0,keywords:"exports register file shl array record property for mod while set ally label uses raise not stored class safecall var interface or private static exit index inherited to else stdcall override shr asm far resourcestring finalization packed virtual out and protected library do xorwrite goto near function end div overload object unit begin string on inline repeat until destructor write message program with read initialization except default nil if case cdecl in downto threadvar of try pascal const external constructor type public then implementation finally published procedure absolute reintroduce operator as is abstract alias assembler bitpacked break continue cppdecl cvar enumerator experimental platform deprecated unimplemented dynamic export far16 forward generic helper implements interrupt iochecks local name nodefault noreturn nostackframe oldfpccall otherwise saveregisters softfloat specialize strict unaligned varargs ",
-illegal:/"|\$[G-Zg-z]|\/\*|<\/|\|/,contains:[d,e,a.NUMBER_MODE,{className:"number",relevance:0,variants:[{begin:"\\$[0-9A-Fa-f]+"},{begin:"&[0-7]+"},{begin:"%[01]+"}]},f,h,c].concat(b)}}}());
-hljs.registerLanguage("diff",function(){return function(a){return{name:"Diff",aliases:["patch"],contains:[{className:"meta",relevance:10,variants:[{begin:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{begin:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{begin:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{className:"comment",variants:[{begin:/Index: /,end:/$/},{begin:/={3,}/,end:/$/},{begin:/^\-{3}/,end:/$/},{begin:/^\*{3} /,end:/$/},{begin:/^\+{3}/,end:/$/},{begin:/^\*{15}$/}]},{className:"addition",begin:"^\\+",end:"$"},{className:"deletion",
-begin:"^\\-",end:"$"},{className:"addition",begin:"^\\!",end:"$"}]}}}());
-hljs.registerLanguage("django",function(){return function(a){var b={begin:/\|[A-Za-z]+:?/,keywords:{name:"truncatewords removetags linebreaksbr yesno get_digit timesince random striptags filesizeformat escape linebreaks length_is ljust rjust cut urlize fix_ampersands title floatformat capfirst pprint divisibleby add make_list unordered_list urlencode timeuntil urlizetrunc wordcount stringformat linenumbers slice date dictsort dictsortreversed default_if_none pluralize lower join center default truncatewords_html upper length phone2numeric wordwrap time addslashes slugify first escapejs force_escape iriencode last safe safeseq truncatechars localize unlocalize localtime utc timezone"},contains:[a.QUOTE_STRING_MODE,
-a.APOS_STRING_MODE]};return{name:"Django",aliases:["jinja"],case_insensitive:!0,subLanguage:"xml",contains:[a.COMMENT(/\{%\s*comment\s*%}/,/\{%\s*endcomment\s*%}/),a.COMMENT(/\{#/,/#}/),{className:"template-tag",begin:/\{%/,end:/%}/,contains:[{className:"name",begin:/\w+/,keywords:{name:"comment endcomment load templatetag ifchanged endifchanged if endif firstof for endfor ifnotequal endifnotequal widthratio extends include spaceless endspaceless regroup ifequal endifequal ssi now with cycle url filter endfilter debug block endblock else autoescape endautoescape csrf_token empty elif endwith static trans blocktrans endblocktrans get_static_prefix get_media_prefix plural get_current_language language get_available_languages get_current_language_bidi get_language_info get_language_info_list localize endlocalize localtime endlocaltime timezone endtimezone get_current_timezone verbatim"},
-starts:{endsWithParent:!0,keywords:"in by as",contains:[b],relevance:0}}]},{className:"template-variable",begin:/\{\{/,end:/}}/,contains:[b]}]}}}());
-hljs.registerLanguage("dns",function(){return function(a){return{name:"DNS Zone",aliases:["bind","zone"],keywords:{keyword:"IN A AAAA AFSDB APL CAA CDNSKEY CDS CERT CNAME DHCID DLV DNAME DNSKEY DS HIP IPSECKEY KEY KX LOC MX NAPTR NS NSEC NSEC3 NSEC3PARAM PTR RRSIG RP SIG SOA SRV SSHFP TA TKEY TLSA TSIG TXT"},contains:[a.COMMENT(";","$",{relevance:0}),{className:"meta",begin:/^\$(TTL|GENERATE|INCLUDE|ORIGIN)\b/},{className:"number",begin:"((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))\\b"},
-{className:"number",begin:"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\b"},a.inherit(a.NUMBER_MODE,{begin:/\b\d+[dhwm]?/})]}}}());
-hljs.registerLanguage("dockerfile",function(){return function(a){return{name:"Dockerfile",aliases:["docker"],case_insensitive:!0,keywords:"from maintainer expose env arg user onbuild stopsignal",contains:[a.HASH_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.NUMBER_MODE,{beginKeywords:"run cmd entrypoint volume add copy workdir label healthcheck shell",starts:{end:/[^\\]$/,subLanguage:"bash"}}],illegal:"</"}}}());
-hljs.registerLanguage("dos",function(){return function(a){var b=a.COMMENT(/^\s*@?rem\b/,/$/,{relevance:10});return{name:"Batch file (DOS)",aliases:["bat","cmd"],case_insensitive:!0,illegal:/\/\*/,keywords:{keyword:"if else goto for in do call exit not exist errorlevel defined equ neq lss leq gtr geq",built_in:"prn nul lpt3 lpt2 lpt1 con com4 com3 com2 com1 aux shift cd dir echo setlocal endlocal set pause copy append assoc at attrib break cacls cd chcp chdir chkdsk chkntfs cls cmd color comp compact convert date dir diskcomp diskcopy doskey erase fs find findstr format ftype graftabl help keyb label md mkdir mode more move path pause print popd pushd promt rd recover rem rename replace restore rmdir shift sort start subst time title tree type ver verify vol ping net ipconfig taskkill xcopy ren del"},
-contains:[{className:"variable",begin:/%%[^ ]|%[^ ]+?%|![^ ]+?!/},{className:"function",begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)",end:"goto:eof",contains:[a.inherit(a.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),b]},{className:"number",begin:"\\b\\d+",relevance:0},b]}}}());
-hljs.registerLanguage("dsconfig",function(){return function(a){return{keywords:"dsconfig",contains:[{className:"keyword",begin:"^dsconfig",end:"\\s",excludeEnd:!0,relevance:10},{className:"built_in",begin:"(list|create|get|set|delete)-(\\w+)",end:"\\s",excludeEnd:!0,illegal:"!@#$%^&*()",relevance:10},{className:"built_in",begin:"--(\\w+)",end:"\\s",excludeEnd:!0},{className:"string",begin:/"/,end:/"/},{className:"string",begin:/'/,end:/'/},{className:"string",begin:"[\\w-?]+:\\w+",end:"\\W",relevance:0},
-{className:"string",begin:"\\w+-?\\w+",end:"\\W",relevance:0},a.HASH_COMMENT_MODE]}}}());
-hljs.registerLanguage("dts",function(){return function(a){var b={className:"string",variants:[a.inherit(a.QUOTE_STRING_MODE,{begin:'((u8?|U)|L)?"'}),{begin:'(u8?|U)?R"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},c={className:"number",variants:[{begin:"\\b(\\d+(\\.\\d*)?|\\.\\d+)(u|U|l|L|ul|UL|f|F)"},{begin:a.C_NUMBER_RE}],relevance:0},d={className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef ifdef ifndef"},contains:[{begin:/\\\n/,
-relevance:0},{beginKeywords:"include",end:"$",keywords:{"meta-keyword":"include"},contains:[a.inherit(b,{className:"meta-string"}),{className:"meta-string",begin:"<",end:">",illegal:"\\n"}]},b,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},e={className:"variable",begin:"\\&[a-z\\d_]*\\b"},f={className:"meta-keyword",begin:"/[a-z][a-z\\d-]*/"},h={className:"symbol",begin:"^\\s*[a-zA-Z_][a-zA-Z\\d_]*:"},g={className:"params",begin:"<",end:">",contains:[c,e]},k={className:"class",begin:/[a-zA-Z_][a-zA-Z\d_@]*\s{/,
-end:/[{;=]/,returnBegin:!0,excludeEnd:!0};return{name:"Device Tree",keywords:"",contains:[{className:"class",begin:"/\\s*{",end:"};",relevance:10,contains:[e,f,h,k,g,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,b]},e,f,h,k,g,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,b,d,{begin:a.IDENT_RE+"::",keywords:""}]}}}());
-hljs.registerLanguage("dust",function(){return function(a){return{name:"Dust",aliases:["dst"],case_insensitive:!0,subLanguage:"xml",contains:[{className:"template-tag",begin:/\{[#\/]/,end:/\}/,illegal:/;/,contains:[{className:"name",begin:/[a-zA-Z\.-]+/,starts:{endsWithParent:!0,relevance:0,contains:[a.QUOTE_STRING_MODE]}}]},{className:"template-variable",begin:/\{/,end:/\}/,illegal:/;/,keywords:"if eq ne lt lte gt gte select default math sep"}]}}}());
-hljs.registerLanguage("ebnf",function(){return function(a){var b=a.COMMENT(/\(\*/,/\*\)/);return{name:"Extended Backus-Naur Form",illegal:/\S/,contains:[b,{className:"attribute",begin:/^[ ]*[a-zA-Z][a-zA-Z-_]*([\s-_]+[a-zA-Z][a-zA-Z]*)*/},{begin:/=/,end:/[.;]/,contains:[b,{className:"meta",begin:/\?.*\?/},{className:"string",variants:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{begin:"`",end:"`"}]}]}]}}}());
-hljs.registerLanguage("elixir",function(){return function(a){var b={$pattern:"[a-zA-Z_][a-zA-Z0-9_.]*(\\!|\\?)?",keyword:"and false then defined module in return redo retry end for true self when next until do begin unless nil break not case cond alias while ensure or include use alias fn quote require import with|0"},c={className:"subst",begin:"#\\{",end:"}",keywords:b},d={className:"number",begin:"(\\b0o[0-7_]+)|(\\b0b[01_]+)|(\\b0x[0-9a-fA-F_]+)|(-?\\b[1-9][0-9_]*(.[0-9_]+([eE][-+]?[0-9]+)?)?)",
-relevance:0},e={className:"string",begin:"~[a-z](?=[/|([{<\"'])",contains:[{endsParent:!0,contains:[{contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/\//,end:/\//},{begin:/\|/,end:/\|/},{begin:/\(/,end:/\)/},{begin:/\[/,end:/\]/},{begin:/\{/,end:/\}/},{begin:/</,end:/>/}]}]}]},f={className:"string",contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:/"""/,end:/"""/},{begin:/'''/,end:/'''/},{begin:/~S"""/,end:/"""/,contains:[]},{begin:/~S"/,end:/"/,contains:[]},
-{begin:/~S'''/,end:/'''/,contains:[]},{begin:/~S'/,end:/'/,contains:[]},{begin:/'/,end:/'/},{begin:/"/,end:/"/}]},h={className:"function",beginKeywords:"def defp defmacro",end:/\B\b/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_][a-zA-Z0-9_.]*(\\!|\\?)?",endsParent:!0})]},g=a.inherit(h,{className:"class",beginKeywords:"defimpl defmodule defprotocol defrecord",end:/\bdo\b|$|;/});a=[f,{className:"string",begin:"~[A-Z](?=[/|([{<\"'])",contains:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/\//,
-end:/\//},{begin:/\|/,end:/\|/},{begin:/\(/,end:/\)/},{begin:/\[/,end:/\]/},{begin:/\{/,end:/\}/},{begin:/</,end:/>/}]},e,a.HASH_COMMENT_MODE,g,h,{begin:"::"},{className:"symbol",begin:":(?![\\s:])",contains:[f,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],relevance:0},{className:"symbol",begin:"[a-zA-Z_][a-zA-Z0-9_.]*(\\!|\\?)?:(?!:)",relevance:0},d,{className:"variable",begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{begin:"->"},{begin:"("+a.RE_STARTERS_RE+
-")\\s*",contains:[a.HASH_COMMENT_MODE,{begin:/\/: (?=\d+\s*[,\]])/,relevance:0,contains:[d]},{className:"regexp",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}],relevance:0}];c.contains=a;return{name:"Elixir",keywords:b,contains:a}}}());
-hljs.registerLanguage("elm",function(){return function(a){var b={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},c={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},d={begin:"\\(",end:"\\)",illegal:'"',contains:[{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},b]};return{name:"Elm",keywords:"let in if then else case of where module import exposing type alias as infix infixl infixr port effect command subscription",contains:[{beginKeywords:"port effect module",
-end:"exposing",keywords:"port effect module where command subscription exposing",contains:[d,b],illegal:"\\W\\.|;"},{begin:"import",end:"$",keywords:"import as exposing",contains:[d,b],illegal:"\\W\\.|;"},{begin:"type",end:"$",keywords:"type alias",contains:[c,d,{begin:"{",end:"}",contains:d.contains},b]},{beginKeywords:"infix infixl infixr",end:"$",contains:[a.C_NUMBER_MODE,b]},{begin:"port",end:"$",keywords:"port",contains:[b]},{className:"string",begin:"'\\\\?.",end:"'",illegal:"."},a.QUOTE_STRING_MODE,
-a.C_NUMBER_MODE,c,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),b,{begin:"->|<-"}],illegal:/;/}}}());
-hljs.registerLanguage("ruby",function(){return function(a){var b={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},c={className:"doctag",begin:"@[A-Za-z]+"},d={begin:"#<",end:">"};c=[a.COMMENT("#","$",{contains:[c]}),a.COMMENT("^\\=begin","^\\=end",{contains:[c],relevance:10}),
-a.COMMENT("^__END__","\\n$")];var e={className:"subst",begin:"#\\{",end:"}",keywords:b},f={className:"string",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},
-{begin:/<<[-~]?'?(\w+)(?:.|\n)*?\n\s*\1\b/,returnBegin:!0,contains:[{begin:/<<[-~]?'?/},a.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,contains:[a.BACKSLASH_ESCAPE,e]})]}]},h={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:b};a=[f,d,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+a.IDENT_RE+"::)?"+a.IDENT_RE}]}].concat(c)},{className:"function",beginKeywords:"def",
-end:"$|;",contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}),h].concat(c)},{begin:a.IDENT_RE+"::"},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[f,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",
-relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:b},{begin:"("+a.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[d,{className:"regexp",contains:[a.BACKSLASH_ESCAPE,e],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(c),relevance:0}].concat(c);e.contains=a;h.contains=a;return{name:"Ruby",aliases:["rb","gemspec",
-"podspec","thor","irb"],keywords:b,illegal:/\/\*/,contains:c.concat([{begin:/^\s*=>/,starts:{end:"$",contains:a}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:a}}]).concat(a)}}}());hljs.registerLanguage("erb",function(){return function(a){return{name:"ERB",subLanguage:"xml",contains:[a.COMMENT("<%#","%>"),{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0}]}}}());
-hljs.registerLanguage("erlang-repl",function(){return function(a){return{name:"Erlang REPL",keywords:{built_in:"spawn spawn_link self",keyword:"after and andalso|10 band begin bnot bor bsl bsr bxor case catch cond div end fun if let not of or orelse|10 query receive rem try when xor"},contains:[{className:"meta",begin:"^[0-9]+> ",relevance:10},a.COMMENT("%","$"),{className:"number",begin:"\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)",relevance:0},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{begin:"\\?(::)?([A-Z]\\w*(::)?)+"},
-{begin:"->"},{begin:"ok"},{begin:"!"},{begin:"(\\b[a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*)|(\\b[a-z'][a-zA-Z0-9_']*)",relevance:0},{begin:"[A-Z][a-zA-Z0-9_']*",relevance:0}]}}}());
-hljs.registerLanguage("erlang",function(){return function(a){var b={keyword:"after and andalso|10 band begin bnot bor bsl bzr bxor case catch cond div end fun if let not of orelse|10 query receive rem try when xor",literal:"false true"},c=a.COMMENT("%","$"),d={className:"number",begin:"\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)",relevance:0},e={begin:"fun\\s+[a-z'][a-zA-Z0-9_']*/\\d+"},f={begin:"([a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*|[a-z'][a-zA-Z0-9_']*)\\(",end:"\\)",returnBegin:!0,
-relevance:0,contains:[{begin:"([a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*|[a-z'][a-zA-Z0-9_']*)",relevance:0},{begin:"\\(",end:"\\)",endsWithParent:!0,returnEnd:!0,relevance:0}]},h={begin:"{",end:"}",relevance:0},g={begin:"\\b_([A-Z][A-Za-z0-9_]*)?",relevance:0},k={begin:"[A-Z][a-zA-Z0-9_]*",relevance:0},l={begin:"#"+a.UNDERSCORE_IDENT_RE,relevance:0,returnBegin:!0,contains:[{begin:"#"+a.UNDERSCORE_IDENT_RE,relevance:0},{begin:"{",end:"}",relevance:0}]},m={beginKeywords:"fun receive if try case",end:"end",
-keywords:b};m.contains=[c,e,a.inherit(a.APOS_STRING_MODE,{className:""}),m,f,a.QUOTE_STRING_MODE,d,h,g,k,l];e=[c,e,m,f,a.QUOTE_STRING_MODE,d,h,g,k,l];f.contains[1].contains=e;h.contains=e;l.contains[1].contains=e;f={className:"params",begin:"\\(",end:"\\)",contains:e};return{name:"Erlang",aliases:["erl"],keywords:b,illegal:"(</|\\*=|\\+=|-=|/\\*|\\*/|\\(\\*|\\*\\))",contains:[{className:"function",begin:"^[a-z'][a-zA-Z0-9_']*\\s*\\(",end:"->",returnBegin:!0,illegal:"\\(|#|//|/\\*|\\\\|:|;",contains:[f,
-a.inherit(a.TITLE_MODE,{begin:"[a-z'][a-zA-Z0-9_']*"})],starts:{end:";|\\.",keywords:b,contains:e}},c,{begin:"^-",end:"\\.",relevance:0,excludeEnd:!0,returnBegin:!0,keywords:{$pattern:"-"+a.IDENT_RE,keyword:"-module -record -undef -export -ifdef -ifndef -author -copyright -doc -vsn -import -include -include_lib -compile -define -else -endif -file -behaviour -behavior -spec"},contains:[f]},d,a.QUOTE_STRING_MODE,l,g,k,h,{begin:/\.$/}]}}}());
-hljs.registerLanguage("excel",function(){return function(a){return{name:"Excel formulae",aliases:["xlsx","xls"],case_insensitive:!0,keywords:{$pattern:/[a-zA-Z][\w\.]*/,built_in:"ABS ACCRINT ACCRINTM ACOS ACOSH ACOT ACOTH AGGREGATE ADDRESS AMORDEGRC AMORLINC AND ARABIC AREAS ASC ASIN ASINH ATAN ATAN2 ATANH AVEDEV AVERAGE AVERAGEA AVERAGEIF AVERAGEIFS BAHTTEXT BASE BESSELI BESSELJ BESSELK BESSELY BETADIST BETA.DIST BETAINV BETA.INV BIN2DEC BIN2HEX BIN2OCT BINOMDIST BINOM.DIST BINOM.DIST.RANGE BINOM.INV BITAND BITLSHIFT BITOR BITRSHIFT BITXOR CALL CEILING CEILING.MATH CEILING.PRECISE CELL CHAR CHIDIST CHIINV CHITEST CHISQ.DIST CHISQ.DIST.RT CHISQ.INV CHISQ.INV.RT CHISQ.TEST CHOOSE CLEAN CODE COLUMN COLUMNS COMBIN COMBINA COMPLEX CONCAT CONCATENATE CONFIDENCE CONFIDENCE.NORM CONFIDENCE.T CONVERT CORREL COS COSH COT COTH COUNT COUNTA COUNTBLANK COUNTIF COUNTIFS COUPDAYBS COUPDAYS COUPDAYSNC COUPNCD COUPNUM COUPPCD COVAR COVARIANCE.P COVARIANCE.S CRITBINOM CSC CSCH CUBEKPIMEMBER CUBEMEMBER CUBEMEMBERPROPERTY CUBERANKEDMEMBER CUBESET CUBESETCOUNT CUBEVALUE CUMIPMT CUMPRINC DATE DATEDIF DATEVALUE DAVERAGE DAY DAYS DAYS360 DB DBCS DCOUNT DCOUNTA DDB DEC2BIN DEC2HEX DEC2OCT DECIMAL DEGREES DELTA DEVSQ DGET DISC DMAX DMIN DOLLAR DOLLARDE DOLLARFR DPRODUCT DSTDEV DSTDEVP DSUM DURATION DVAR DVARP EDATE EFFECT ENCODEURL EOMONTH ERF ERF.PRECISE ERFC ERFC.PRECISE ERROR.TYPE EUROCONVERT EVEN EXACT EXP EXPON.DIST EXPONDIST FACT FACTDOUBLE FALSE|0 F.DIST FDIST F.DIST.RT FILTERXML FIND FINDB F.INV F.INV.RT FINV FISHER FISHERINV FIXED FLOOR FLOOR.MATH FLOOR.PRECISE FORECAST FORECAST.ETS FORECAST.ETS.CONFINT FORECAST.ETS.SEASONALITY FORECAST.ETS.STAT FORECAST.LINEAR FORMULATEXT FREQUENCY F.TEST FTEST FV FVSCHEDULE GAMMA GAMMA.DIST GAMMADIST GAMMA.INV GAMMAINV GAMMALN GAMMALN.PRECISE GAUSS GCD GEOMEAN GESTEP GETPIVOTDATA GROWTH HARMEAN HEX2BIN HEX2DEC HEX2OCT HLOOKUP HOUR HYPERLINK HYPGEOM.DIST HYPGEOMDIST IF IFERROR IFNA IFS IMABS IMAGINARY IMARGUMENT IMCONJUGATE IMCOS IMCOSH IMCOT IMCSC IMCSCH IMDIV IMEXP IMLN IMLOG10 IMLOG2 IMPOWER IMPRODUCT IMREAL IMSEC IMSECH IMSIN IMSINH IMSQRT IMSUB IMSUM IMTAN INDEX INDIRECT INFO INT INTERCEPT INTRATE IPMT IRR ISBLANK ISERR ISERROR ISEVEN ISFORMULA ISLOGICAL ISNA ISNONTEXT ISNUMBER ISODD ISREF ISTEXT ISO.CEILING ISOWEEKNUM ISPMT JIS KURT LARGE LCM LEFT LEFTB LEN LENB LINEST LN LOG LOG10 LOGEST LOGINV LOGNORM.DIST LOGNORMDIST LOGNORM.INV LOOKUP LOWER MATCH MAX MAXA MAXIFS MDETERM MDURATION MEDIAN MID MIDBs MIN MINIFS MINA MINUTE MINVERSE MIRR MMULT MOD MODE MODE.MULT MODE.SNGL MONTH MROUND MULTINOMIAL MUNIT N NA NEGBINOM.DIST NEGBINOMDIST NETWORKDAYS NETWORKDAYS.INTL NOMINAL NORM.DIST NORMDIST NORMINV NORM.INV NORM.S.DIST NORMSDIST NORM.S.INV NORMSINV NOT NOW NPER NPV NUMBERVALUE OCT2BIN OCT2DEC OCT2HEX ODD ODDFPRICE ODDFYIELD ODDLPRICE ODDLYIELD OFFSET OR PDURATION PEARSON PERCENTILE.EXC PERCENTILE.INC PERCENTILE PERCENTRANK.EXC PERCENTRANK.INC PERCENTRANK PERMUT PERMUTATIONA PHI PHONETIC PI PMT POISSON.DIST POISSON POWER PPMT PRICE PRICEDISC PRICEMAT PROB PRODUCT PROPER PV QUARTILE QUARTILE.EXC QUARTILE.INC QUOTIENT RADIANS RAND RANDBETWEEN RANK.AVG RANK.EQ RANK RATE RECEIVED REGISTER.ID REPLACE REPLACEB REPT RIGHT RIGHTB ROMAN ROUND ROUNDDOWN ROUNDUP ROW ROWS RRI RSQ RTD SEARCH SEARCHB SEC SECH SECOND SERIESSUM SHEET SHEETS SIGN SIN SINH SKEW SKEW.P SLN SLOPE SMALL SQL.REQUEST SQRT SQRTPI STANDARDIZE STDEV STDEV.P STDEV.S STDEVA STDEVP STDEVPA STEYX SUBSTITUTE SUBTOTAL SUM SUMIF SUMIFS SUMPRODUCT SUMSQ SUMX2MY2 SUMX2PY2 SUMXMY2 SWITCH SYD T TAN TANH TBILLEQ TBILLPRICE TBILLYIELD T.DIST T.DIST.2T T.DIST.RT TDIST TEXT TEXTJOIN TIME TIMEVALUE T.INV T.INV.2T TINV TODAY TRANSPOSE TREND TRIM TRIMMEAN TRUE|0 TRUNC T.TEST TTEST TYPE UNICHAR UNICODE UPPER VALUE VAR VAR.P VAR.S VARA VARP VARPA VDB VLOOKUP WEBSERVICE WEEKDAY WEEKNUM WEIBULL WEIBULL.DIST WORKDAY WORKDAY.INTL XIRR XNPV XOR YEAR YEARFRAC YIELD YIELDDISC YIELDMAT Z.TEST ZTEST"},
-contains:[{begin:/^=/,end:/[^=]/,returnEnd:!0,illegal:/=/,relevance:10},{className:"symbol",begin:/\b[A-Z]{1,2}\d+\b/,end:/[^\d]/,excludeEnd:!0,relevance:0},{className:"symbol",begin:/[A-Z]{0,2}\d*:[A-Z]{0,2}\d*/,relevance:0},a.BACKSLASH_ESCAPE,a.QUOTE_STRING_MODE,{className:"number",begin:a.NUMBER_RE+"(%)?",relevance:0},a.COMMENT(/\bN\(/,/\)/,{excludeBegin:!0,excludeEnd:!0,illegal:/\n/})]}}}());
-hljs.registerLanguage("fix",function(){return function(a){return{name:"FIX",contains:[{begin:/[^\u2401\u0001]+/,end:/[\u2401\u0001]/,excludeEnd:!0,returnBegin:!0,returnEnd:!1,contains:[{begin:/([^\u2401\u0001=]+)/,end:/=([^\u2401\u0001=]+)/,returnEnd:!0,returnBegin:!1,className:"attr"},{begin:/=/,end:/([\u2401\u0001])/,excludeEnd:!0,excludeBegin:!0,className:"string"}]}],case_insensitive:!0}}}());
-hljs.registerLanguage("flix",function(){return function(a){return{name:"Flix",keywords:{literal:"true false",keyword:"case class def else enum if impl import in lat rel index let match namespace switch type yield with"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",begin:/'(.|\\[xXuU][a-zA-Z0-9]+)'/},{className:"string",variants:[{begin:'"',end:'"'}]},{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[{className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/}]},
-a.C_NUMBER_MODE]}}}());
-hljs.registerLanguage("fortran",function(){return function(a){var b={variants:[a.COMMENT("!","$",{relevance:0}),a.COMMENT("^C","$",{relevance:0})]};return{name:"Fortran",case_insensitive:!0,aliases:["f90","f95"],keywords:{literal:".False. .True.",keyword:"kind do concurrent local shared while private call intrinsic where elsewhere type endtype endmodule endselect endinterface end enddo endif if forall endforall only contains default return stop then block endblock endassociate public subroutine|10 function program .and. .or. .not. .le. .eq. .ge. .gt. .lt. goto save else use module select case access blank direct exist file fmt form formatted iostat name named nextrec number opened rec recl sequential status unformatted unit continue format pause cycle exit c_null_char c_alert c_backspace c_form_feed flush wait decimal round iomsg synchronous nopass non_overridable pass protected volatile abstract extends import non_intrinsic value deferred generic final enumerator class associate bind enum c_int c_short c_long c_long_long c_signed_char c_size_t c_int8_t c_int16_t c_int32_t c_int64_t c_int_least8_t c_int_least16_t c_int_least32_t c_int_least64_t c_int_fast8_t c_int_fast16_t c_int_fast32_t c_int_fast64_t c_intmax_t C_intptr_t c_float c_double c_long_double c_float_complex c_double_complex c_long_double_complex c_bool c_char c_null_ptr c_null_funptr c_new_line c_carriage_return c_horizontal_tab c_vertical_tab iso_c_binding c_loc c_funloc c_associated  c_f_pointer c_ptr c_funptr iso_fortran_env character_storage_size error_unit file_storage_size input_unit iostat_end iostat_eor numeric_storage_size output_unit c_f_procpointer ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode newunit contiguous recursive pad position action delim readwrite eor advance nml interface procedure namelist include sequence elemental pure impure integer real character complex logical codimension dimension allocatable|10 parameter external implicit|10 none double precision assign intent optional pointer target in out common equivalence data",built_in:"alog alog10 amax0 amax1 amin0 amin1 amod cabs ccos cexp clog csin csqrt dabs dacos dasin datan datan2 dcos dcosh ddim dexp dint dlog dlog10 dmax1 dmin1 dmod dnint dsign dsin dsinh dsqrt dtan dtanh float iabs idim idint idnint ifix isign max0 max1 min0 min1 sngl algama cdabs cdcos cdexp cdlog cdsin cdsqrt cqabs cqcos cqexp cqlog cqsin cqsqrt dcmplx dconjg derf derfc dfloat dgamma dimag dlgama iqint qabs qacos qasin qatan qatan2 qcmplx qconjg qcos qcosh qdim qerf qerfc qexp qgamma qimag qlgama qlog qlog10 qmax1 qmin1 qmod qnint qsign qsin qsinh qsqrt qtan qtanh abs acos aimag aint anint asin atan atan2 char cmplx conjg cos cosh exp ichar index int log log10 max min nint sign sin sinh sqrt tan tanh print write dim lge lgt lle llt mod nullify allocate deallocate adjustl adjustr all allocated any associated bit_size btest ceiling count cshift date_and_time digits dot_product eoshift epsilon exponent floor fraction huge iand ibclr ibits ibset ieor ior ishft ishftc lbound len_trim matmul maxexponent maxloc maxval merge minexponent minloc minval modulo mvbits nearest pack present product radix random_number random_seed range repeat reshape rrspacing scale scan selected_int_kind selected_real_kind set_exponent shape size spacing spread sum system_clock tiny transpose trim ubound unpack verify achar iachar transfer dble entry dprod cpu_time command_argument_count get_command get_command_argument get_environment_variable is_iostat_end ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode is_iostat_eor move_alloc new_line selected_char_kind same_type_as extends_type_of acosh asinh atanh bessel_j0 bessel_j1 bessel_jn bessel_y0 bessel_y1 bessel_yn erf erfc erfc_scaled gamma log_gamma hypot norm2 atomic_define atomic_ref execute_command_line leadz trailz storage_size merge_bits bge bgt ble blt dshiftl dshiftr findloc iall iany iparity image_index lcobound ucobound maskl maskr num_images parity popcnt poppar shifta shiftl shiftr this_image sync change team co_broadcast co_max co_min co_sum co_reduce"},
-illegal:/\/\*/,contains:[{className:"string",relevance:0,variants:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},{className:"function",beginKeywords:"subroutine function program",illegal:"[${=\\n]",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)"}]},{begin:/^C\s*=(?!=)/,relevance:0},b,{className:"number",begin:"(?=\\b|\\+|\\-|\\.)(?:\\.|\\d+\\.?)\\d*([de][+-]?\\d+)?(_[a-z_\\d]+)?",relevance:0}]}}}());
-hljs.registerLanguage("fsharp",function(){return function(a){var b={begin:"<",end:">",contains:[a.inherit(a.TITLE_MODE,{begin:/'[a-zA-Z0-9_]+/})]};return{name:"F#",aliases:["fs"],keywords:"abstract and as assert base begin class default delegate do done downcast downto elif else end exception extern false finally for fun function global if in inherit inline interface internal lazy let match member module mutable namespace new null of open or override private public rec return sig static struct then to true try type upcast use val void when while with yield",
-illegal:/\/\*/,contains:[{className:"keyword",begin:/\b(yield|return|let|do)!/},{className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},{className:"string",begin:'"""',end:'"""'},a.COMMENT("\\(\\*","\\*\\)"),{className:"class",beginKeywords:"type",end:"\\(|=|$",excludeEnd:!0,contains:[a.UNDERSCORE_TITLE_MODE,b]},{className:"meta",begin:"\\[<",end:">\\]",relevance:10},{className:"symbol",begin:"\\B('[A-Za-z])\\b",contains:[a.BACKSLASH_ESCAPE]},a.C_LINE_COMMENT_MODE,a.inherit(a.QUOTE_STRING_MODE,
-{illegal:null}),a.C_NUMBER_MODE]}}}());
-hljs.registerLanguage("gams",function(){return function(a){var b={keyword:"abort acronym acronyms alias all and assign binary card diag display else eq file files for free ge gt if integer le loop lt maximizing minimizing model models ne negative no not option options or ord positive prod put putpage puttl repeat sameas semicont semiint smax smin solve sos1 sos2 sum system table then until using while xor yes",literal:"eps inf na",built_in:"abs arccos arcsin arctan arctan2 Beta betaReg binomial ceil centropy cos cosh cvPower div div0 eDist entropy errorf execSeed exp fact floor frac gamma gammaReg log logBeta logGamma log10 log2 mapVal max min mod ncpCM ncpF ncpVUpow ncpVUsin normal pi poly power randBinomial randLinear randTriangle round rPower sigmoid sign signPower sin sinh slexp sllog10 slrec sqexp sqlog10 sqr sqrec sqrt tan tanh trunc uniform uniformInt vcPower bool_and bool_eqv bool_imp bool_not bool_or bool_xor ifThen rel_eq rel_ge rel_gt rel_le rel_lt rel_ne gday gdow ghour gleap gmillisec gminute gmonth gsecond gyear jdate jnow jstart jtime errorLevel execError gamsRelease gamsVersion handleCollect handleDelete handleStatus handleSubmit heapFree heapLimit heapSize jobHandle jobKill jobStatus jobTerminate licenseLevel licenseStatus maxExecError sleep timeClose timeComp timeElapsed timeExec timeStart"},c=
-{className:"symbol",variants:[{begin:/=[lgenxc]=/},{begin:/\$/}]},d={className:"comment",variants:[{begin:"'",end:"'"},{begin:'"',end:'"'}],illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},e={begin:"/",end:"/",keywords:b,contains:[d,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_NUMBER_MODE]};d={begin:/[a-z][a-z0-9_]*(\([a-z0-9_, ]*\))?[ \t]+/,excludeBegin:!0,end:"$",endsWithParent:!0,contains:[d,e,{className:"comment",begin:/([ ]*[a-z0-9&#*=?@>\\<:\-,()$\[\]_.{}!+%^]+)+/,
-relevance:0}]};return{name:"GAMS",aliases:["gms"],case_insensitive:!0,keywords:b,contains:[a.COMMENT(/^\$ontext/,/^\$offtext/),{className:"meta",begin:"^\\$[a-z0-9]+",end:"$",returnBegin:!0,contains:[{className:"meta-keyword",begin:"^\\$[a-z0-9]+"}]},a.COMMENT("^\\*","$"),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{beginKeywords:"set sets parameter parameters variable variables scalar scalars equation equations",end:";",contains:[a.COMMENT("^\\*","$"),a.C_LINE_COMMENT_MODE,
-a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,e,d]},{beginKeywords:"table",end:";",returnBegin:!0,contains:[{beginKeywords:"table",end:"$",contains:[d]},a.COMMENT("^\\*","$"),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_NUMBER_MODE]},{className:"function",begin:/^[a-z][a-z0-9_,\-+' ()$]+\.{2}/,returnBegin:!0,contains:[{className:"title",begin:/^[a-z0-9_]+/},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0},c]},a.C_NUMBER_MODE,
-c]}}}());
-hljs.registerLanguage("gauss",function(){return function(a){var b={keyword:"bool break call callexe checkinterrupt clear clearg closeall cls comlog compile continue create debug declare delete disable dlibrary dllcall do dos ed edit else elseif enable end endfor endif endp endo errorlog errorlogat expr external fn for format goto gosub graph if keyword let lib library line load loadarray loadexe loadf loadk loadm loadp loads loadx local locate loopnextindex lprint lpwidth lshow matrix msym ndpclex new open output outwidth plot plotsym pop prcsn print printdos proc push retp return rndcon rndmod rndmult rndseed run save saveall screen scroll setarray show sparse stop string struct system trace trap threadfor threadendfor threadbegin threadjoin threadstat threadend until use while winprint ne ge le gt lt and xor or not eq eqv",built_in:"abs acf aconcat aeye amax amean AmericanBinomCall AmericanBinomCall_Greeks AmericanBinomCall_ImpVol AmericanBinomPut AmericanBinomPut_Greeks AmericanBinomPut_ImpVol AmericanBSCall AmericanBSCall_Greeks AmericanBSCall_ImpVol AmericanBSPut AmericanBSPut_Greeks AmericanBSPut_ImpVol amin amult annotationGetDefaults annotationSetBkd annotationSetFont annotationSetLineColor annotationSetLineStyle annotationSetLineThickness annualTradingDays arccos arcsin areshape arrayalloc arrayindex arrayinit arraytomat asciiload asclabel astd astds asum atan atan2 atranspose axmargin balance band bandchol bandcholsol bandltsol bandrv bandsolpd bar base10 begwind besselj bessely beta box boxcox cdfBeta cdfBetaInv cdfBinomial cdfBinomialInv cdfBvn cdfBvn2 cdfBvn2e cdfCauchy cdfCauchyInv cdfChic cdfChii cdfChinc cdfChincInv cdfExp cdfExpInv cdfFc cdfFnc cdfFncInv cdfGam cdfGenPareto cdfHyperGeo cdfLaplace cdfLaplaceInv cdfLogistic cdfLogisticInv cdfmControlCreate cdfMvn cdfMvn2e cdfMvnce cdfMvne cdfMvt2e cdfMvtce cdfMvte cdfN cdfN2 cdfNc cdfNegBinomial cdfNegBinomialInv cdfNi cdfPoisson cdfPoissonInv cdfRayleigh cdfRayleighInv cdfTc cdfTci cdfTnc cdfTvn cdfWeibull cdfWeibullInv cdir ceil ChangeDir chdir chiBarSquare chol choldn cholsol cholup chrs close code cols colsf combinate combinated complex con cond conj cons ConScore contour conv convertsatostr convertstrtosa corrm corrms corrvc corrx corrxs cos cosh counts countwts crossprd crout croutp csrcol csrlin csvReadM csvReadSA cumprodc cumsumc curve cvtos datacreate datacreatecomplex datalist dataload dataloop dataopen datasave date datestr datestring datestrymd dayinyr dayofweek dbAddDatabase dbClose dbCommit dbCreateQuery dbExecQuery dbGetConnectOptions dbGetDatabaseName dbGetDriverName dbGetDrivers dbGetHostName dbGetLastErrorNum dbGetLastErrorText dbGetNumericalPrecPolicy dbGetPassword dbGetPort dbGetTableHeaders dbGetTables dbGetUserName dbHasFeature dbIsDriverAvailable dbIsOpen dbIsOpenError dbOpen dbQueryBindValue dbQueryClear dbQueryCols dbQueryExecPrepared dbQueryFetchAllM dbQueryFetchAllSA dbQueryFetchOneM dbQueryFetchOneSA dbQueryFinish dbQueryGetBoundValue dbQueryGetBoundValues dbQueryGetField dbQueryGetLastErrorNum dbQueryGetLastErrorText dbQueryGetLastInsertID dbQueryGetLastQuery dbQueryGetPosition dbQueryIsActive dbQueryIsForwardOnly dbQueryIsNull dbQueryIsSelect dbQueryIsValid dbQueryPrepare dbQueryRows dbQuerySeek dbQuerySeekFirst dbQuerySeekLast dbQuerySeekNext dbQuerySeekPrevious dbQuerySetForwardOnly dbRemoveDatabase dbRollback dbSetConnectOptions dbSetDatabaseName dbSetHostName dbSetNumericalPrecPolicy dbSetPort dbSetUserName dbTransaction DeleteFile delif delrows denseToSp denseToSpRE denToZero design det detl dfft dffti diag diagrv digamma doswin DOSWinCloseall DOSWinOpen dotfeq dotfeqmt dotfge dotfgemt dotfgt dotfgtmt dotfle dotflemt dotflt dotfltmt dotfne dotfnemt draw drop dsCreate dstat dstatmt dstatmtControlCreate dtdate dtday dttime dttodtv dttostr dttoutc dtvnormal dtvtodt dtvtoutc dummy dummybr dummydn eig eigh eighv eigv elapsedTradingDays endwind envget eof eqSolve eqSolvemt eqSolvemtControlCreate eqSolvemtOutCreate eqSolveset erf erfc erfccplx erfcplx error etdays ethsec etstr EuropeanBinomCall EuropeanBinomCall_Greeks EuropeanBinomCall_ImpVol EuropeanBinomPut EuropeanBinomPut_Greeks EuropeanBinomPut_ImpVol EuropeanBSCall EuropeanBSCall_Greeks EuropeanBSCall_ImpVol EuropeanBSPut EuropeanBSPut_Greeks EuropeanBSPut_ImpVol exctsmpl exec execbg exp extern eye fcheckerr fclearerr feq feqmt fflush fft ffti fftm fftmi fftn fge fgemt fgets fgetsa fgetsat fgetst fgt fgtmt fileinfo filesa fle flemt floor flt fltmt fmod fne fnemt fonts fopen formatcv formatnv fputs fputst fseek fstrerror ftell ftocv ftos ftostrC gamma gammacplx gammaii gausset gdaAppend gdaCreate gdaDStat gdaDStatMat gdaGetIndex gdaGetName gdaGetNames gdaGetOrders gdaGetType gdaGetTypes gdaGetVarInfo gdaIsCplx gdaLoad gdaPack gdaRead gdaReadByIndex gdaReadSome gdaReadSparse gdaReadStruct gdaReportVarInfo gdaSave gdaUpdate gdaUpdateAndPack gdaVars gdaWrite gdaWrite32 gdaWriteSome getarray getdims getf getGAUSShome getmatrix getmatrix4D getname getnamef getNextTradingDay getNextWeekDay getnr getorders getpath getPreviousTradingDay getPreviousWeekDay getRow getscalar3D getscalar4D getTrRow getwind glm gradcplx gradMT gradMTm gradMTT gradMTTm gradp graphprt graphset hasimag header headermt hess hessMT hessMTg hessMTgw hessMTm hessMTmw hessMTT hessMTTg hessMTTgw hessMTTm hessMTw hessp hist histf histp hsec imag indcv indexcat indices indices2 indicesf indicesfn indnv indsav integrate1d integrateControlCreate intgrat2 intgrat3 inthp1 inthp2 inthp3 inthp4 inthpControlCreate intquad1 intquad2 intquad3 intrleav intrleavsa intrsect intsimp inv invpd invswp iscplx iscplxf isden isinfnanmiss ismiss key keyav keyw lag lag1 lagn lapEighb lapEighi lapEighvb lapEighvi lapgEig lapgEigh lapgEighv lapgEigv lapgSchur lapgSvdcst lapgSvds lapgSvdst lapSvdcusv lapSvds lapSvdusv ldlp ldlsol linSolve listwise ln lncdfbvn lncdfbvn2 lncdfmvn lncdfn lncdfn2 lncdfnc lnfact lngammacplx lnpdfmvn lnpdfmvt lnpdfn lnpdft loadd loadstruct loadwind loess loessmt loessmtControlCreate log loglog logx logy lower lowmat lowmat1 ltrisol lu lusol machEpsilon make makevars makewind margin matalloc matinit mattoarray maxbytes maxc maxindc maxv maxvec mbesselei mbesselei0 mbesselei1 mbesseli mbesseli0 mbesseli1 meanc median mergeby mergevar minc minindc minv miss missex missrv moment momentd movingave movingaveExpwgt movingaveWgt nextindex nextn nextnevn nextwind ntos null null1 numCombinations ols olsmt olsmtControlCreate olsqr olsqr2 olsqrmt ones optn optnevn orth outtyp pacf packedToSp packr parse pause pdfCauchy pdfChi pdfExp pdfGenPareto pdfHyperGeo pdfLaplace pdfLogistic pdfn pdfPoisson pdfRayleigh pdfWeibull pi pinv pinvmt plotAddArrow plotAddBar plotAddBox plotAddHist plotAddHistF plotAddHistP plotAddPolar plotAddScatter plotAddShape plotAddTextbox plotAddTS plotAddXY plotArea plotBar plotBox plotClearLayout plotContour plotCustomLayout plotGetDefaults plotHist plotHistF plotHistP plotLayout plotLogLog plotLogX plotLogY plotOpenWindow plotPolar plotSave plotScatter plotSetAxesPen plotSetBar plotSetBarFill plotSetBarStacked plotSetBkdColor plotSetFill plotSetGrid plotSetLegend plotSetLineColor plotSetLineStyle plotSetLineSymbol plotSetLineThickness plotSetNewWindow plotSetTitle plotSetWhichYAxis plotSetXAxisShow plotSetXLabel plotSetXRange plotSetXTicInterval plotSetXTicLabel plotSetYAxisShow plotSetYLabel plotSetYRange plotSetZAxisShow plotSetZLabel plotSurface plotTS plotXY polar polychar polyeval polygamma polyint polymake polymat polymroot polymult polyroot pqgwin previousindex princomp printfm printfmt prodc psi putarray putf putvals pvCreate pvGetIndex pvGetParNames pvGetParVector pvLength pvList pvPack pvPacki pvPackm pvPackmi pvPacks pvPacksi pvPacksm pvPacksmi pvPutParVector pvTest pvUnpack QNewton QNewtonmt QNewtonmtControlCreate QNewtonmtOutCreate QNewtonSet QProg QProgmt QProgmtInCreate qqr qqre qqrep qr qre qrep qrsol qrtsol qtyr qtyre qtyrep quantile quantiled qyr qyre qyrep qz rank rankindx readr real reclassify reclassifyCuts recode recserar recsercp recserrc rerun rescale reshape rets rev rfft rffti rfftip rfftn rfftnp rfftp rndBernoulli rndBeta rndBinomial rndCauchy rndChiSquare rndCon rndCreateState rndExp rndGamma rndGeo rndGumbel rndHyperGeo rndi rndKMbeta rndKMgam rndKMi rndKMn rndKMnb rndKMp rndKMu rndKMvm rndLaplace rndLCbeta rndLCgam rndLCi rndLCn rndLCnb rndLCp rndLCu rndLCvm rndLogNorm rndMTu rndMVn rndMVt rndn rndnb rndNegBinomial rndp rndPoisson rndRayleigh rndStateSkip rndu rndvm rndWeibull rndWishart rotater round rows rowsf rref sampleData satostrC saved saveStruct savewind scale scale3d scalerr scalinfnanmiss scalmiss schtoc schur searchsourcepath seekr select selif seqa seqm setdif setdifsa setvars setvwrmode setwind shell shiftr sin singleindex sinh sleep solpd sortc sortcc sortd sorthc sorthcc sortind sortindc sortmc sortr sortrc spBiconjGradSol spChol spConjGradSol spCreate spDenseSubmat spDiagRvMat spEigv spEye spLDL spline spLU spNumNZE spOnes spreadSheetReadM spreadSheetReadSA spreadSheetWrite spScale spSubmat spToDense spTrTDense spTScalar spZeros sqpSolve sqpSolveMT sqpSolveMTControlCreate sqpSolveMTlagrangeCreate sqpSolveMToutCreate sqpSolveSet sqrt statements stdc stdsc stocv stof strcombine strindx strlen strput strrindx strsect strsplit strsplitPad strtodt strtof strtofcplx strtriml strtrimr strtrunc strtruncl strtruncpad strtruncr submat subscat substute subvec sumc sumr surface svd svd1 svd2 svdcusv svds svdusv sysstate tab tan tanh tempname time timedt timestr timeutc title tkf2eps tkf2ps tocart todaydt toeplitz token topolar trapchk trigamma trimr trunc type typecv typef union unionsa uniqindx uniqindxsa unique uniquesa upmat upmat1 upper utctodt utctodtv utrisol vals varCovMS varCovXS varget vargetl varmall varmares varput varputl vartypef vcm vcms vcx vcxs vec vech vecr vector vget view viewxyz vlist vnamecv volume vput vread vtypecv wait waitc walkindex where window writer xlabel xlsGetSheetCount xlsGetSheetSize xlsGetSheetTypes xlsMakeRange xlsReadM xlsReadSA xlsWrite xlsWriteM xlsWriteSA xpnd xtics xy xyz ylabel ytics zeros zeta zlabel ztics cdfEmpirical dot h5create h5open h5read h5readAttribute h5write h5writeAttribute ldl plotAddErrorBar plotAddSurface plotCDFEmpirical plotSetColormap plotSetContourLabels plotSetLegendFont plotSetTextInterpreter plotSetXTicCount plotSetYTicCount plotSetZLevels powerm strjoin sylvester strtrim",
-literal:"DB_AFTER_LAST_ROW DB_ALL_TABLES DB_BATCH_OPERATIONS DB_BEFORE_FIRST_ROW DB_BLOB DB_EVENT_NOTIFICATIONS DB_FINISH_QUERY DB_HIGH_PRECISION DB_LAST_INSERT_ID DB_LOW_PRECISION_DOUBLE DB_LOW_PRECISION_INT32 DB_LOW_PRECISION_INT64 DB_LOW_PRECISION_NUMBERS DB_MULTIPLE_RESULT_SETS DB_NAMED_PLACEHOLDERS DB_POSITIONAL_PLACEHOLDERS DB_PREPARED_QUERIES DB_QUERY_SIZE DB_SIMPLE_LOCKING DB_SYSTEM_TABLES DB_TABLES DB_TRANSACTIONS DB_UNICODE DB_VIEWS __STDIN __STDOUT __STDERR __FILE_DIR"},c=a.COMMENT("@",
-"@"),d={className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"define definecs|10 undef ifdef ifndef iflight ifdllcall ifmac ifos2win ifunix else endif lineson linesoff srcfile srcline"},contains:[{begin:/\\\n/,relevance:0},{beginKeywords:"include",end:"$",keywords:{"meta-keyword":"include"},contains:[{className:"meta-string",begin:'"',end:'"',illegal:"\\n"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c]},e={begin:/\bstruct\s+/,end:/\s/,keywords:"struct",contains:[{className:"type",begin:a.UNDERSCORE_IDENT_RE,
-relevance:0}]},f=[{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,endsWithParent:!0,relevance:0,contains:[{className:"literal",begin:/\.\.\./},a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,c,e]}],h={className:"title",begin:a.UNDERSCORE_IDENT_RE,relevance:0},g=function(b,d,e){b=a.inherit({className:"function",beginKeywords:b,end:d,excludeEnd:!0,contains:[].concat(f)},e||{});b.contains.push(h);b.contains.push(a.C_NUMBER_MODE);b.contains.push(a.C_BLOCK_COMMENT_MODE);b.contains.push(c);
-return b},k={className:"built_in",begin:"\\b("+b.built_in.split(" ").join("|")+")\\b"},l={className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE],relevance:0},m={begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,keywords:b,relevance:0,contains:[{beginKeywords:b.keyword},k,{className:"built_in",begin:a.UNDERSCORE_IDENT_RE,relevance:0}]};k={begin:/\(/,end:/\)/,relevance:0,keywords:{built_in:b.built_in,literal:b.literal},contains:[a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,c,k,m,l,"self"]};
-m.contains.push(k);return{name:"GAUSS",aliases:["gss"],case_insensitive:!0,keywords:b,illegal:/(\{[%#]|[%#]\}| <- )/,contains:[a.C_NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,l,d,{className:"keyword",begin:/\bexternal (matrix|string|array|sparse matrix|struct|proc|keyword|fn)/},g("proc keyword",";"),g("fn","="),{beginKeywords:"for threadfor",end:/;/,relevance:0,contains:[a.C_BLOCK_COMMENT_MODE,c,k]},{variants:[{begin:a.UNDERSCORE_IDENT_RE+"\\."+a.UNDERSCORE_IDENT_RE},{begin:a.UNDERSCORE_IDENT_RE+
-"\\s*="}],relevance:0},m,e]}}}());
-hljs.registerLanguage("gcode",function(){return function(a){a=[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.COMMENT(/\(/,/\)/),a.inherit(a.C_NUMBER_MODE,{begin:"([-+]?([0-9]*\\.?[0-9]+\\.?))|"+a.C_NUMBER_RE}),a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"name",begin:"([G])([0-9]+\\.?[0-9]?)"},{className:"name",begin:"([M])([0-9]+\\.?[0-9]?)"},{className:"attr",begin:"(VC|VS|#)",end:"(\\d+)"},{className:"attr",begin:"(VZOFX|VZOFY|VZOFZ)"},
-{className:"built_in",begin:"(ATAN|ABS|ACOS|ASIN|SIN|COS|EXP|FIX|FUP|ROUND|LN|TAN)(\\[)",end:"([-+]?([0-9]*\\.?[0-9]+\\.?))(\\])"},{className:"symbol",variants:[{begin:"N",end:"\\d+",illegal:"\\W"}]}];return{name:"G-code (ISO 6983)",aliases:["nc"],case_insensitive:!0,keywords:{$pattern:"[A-Z_][A-Z0-9_.]*",keyword:"IF DO WHILE ENDWHILE CALL ENDIF SUB ENDSUB GOTO REPEAT ENDREPEAT EQ LT GT NE GE LE OR XOR"},contains:[{className:"meta",begin:"\\%"},{className:"meta",begin:"([O])([0-9]+)"}].concat(a)}}}());
-hljs.registerLanguage("gherkin",function(){return function(a){return{name:"Gherkin",aliases:["feature"],keywords:"Feature Background Ability Business Need Scenario Scenarios Scenario Outline Scenario Template Examples Given And Then But When",contains:[{className:"symbol",begin:"\\*",relevance:0},{className:"meta",begin:"@[^@\\s]+"},{begin:"\\|",end:"\\|\\w*$",contains:[{className:"string",begin:"[^|]+"}]},{className:"variable",begin:"<",end:">"},a.HASH_COMMENT_MODE,{className:"string",begin:'"""',
-end:'"""'},a.QUOTE_STRING_MODE]}}}());
-hljs.registerLanguage("glsl",function(){return function(a){return{name:"GLSL",keywords:{keyword:"break continue discard do else for if return while switch case default attribute binding buffer ccw centroid centroid varying coherent column_major const cw depth_any depth_greater depth_less depth_unchanged early_fragment_tests equal_spacing flat fractional_even_spacing fractional_odd_spacing highp in index inout invariant invocations isolines layout line_strip lines lines_adjacency local_size_x local_size_y local_size_z location lowp max_vertices mediump noperspective offset origin_upper_left out packed patch pixel_center_integer point_mode points precise precision quads r11f_g11f_b10f r16 r16_snorm r16f r16i r16ui r32f r32i r32ui r8 r8_snorm r8i r8ui readonly restrict rg16 rg16_snorm rg16f rg16i rg16ui rg32f rg32i rg32ui rg8 rg8_snorm rg8i rg8ui rgb10_a2 rgb10_a2ui rgba16 rgba16_snorm rgba16f rgba16i rgba16ui rgba32f rgba32i rgba32ui rgba8 rgba8_snorm rgba8i rgba8ui row_major sample shared smooth std140 std430 stream triangle_strip triangles triangles_adjacency uniform varying vertices volatile writeonly",type:"atomic_uint bool bvec2 bvec3 bvec4 dmat2 dmat2x2 dmat2x3 dmat2x4 dmat3 dmat3x2 dmat3x3 dmat3x4 dmat4 dmat4x2 dmat4x3 dmat4x4 double dvec2 dvec3 dvec4 float iimage1D iimage1DArray iimage2D iimage2DArray iimage2DMS iimage2DMSArray iimage2DRect iimage3D iimageBuffer iimageCube iimageCubeArray image1D image1DArray image2D image2DArray image2DMS image2DMSArray image2DRect image3D imageBuffer imageCube imageCubeArray int isampler1D isampler1DArray isampler2D isampler2DArray isampler2DMS isampler2DMSArray isampler2DRect isampler3D isamplerBuffer isamplerCube isamplerCubeArray ivec2 ivec3 ivec4 mat2 mat2x2 mat2x3 mat2x4 mat3 mat3x2 mat3x3 mat3x4 mat4 mat4x2 mat4x3 mat4x4 sampler1D sampler1DArray sampler1DArrayShadow sampler1DShadow sampler2D sampler2DArray sampler2DArrayShadow sampler2DMS sampler2DMSArray sampler2DRect sampler2DRectShadow sampler2DShadow sampler3D samplerBuffer samplerCube samplerCubeArray samplerCubeArrayShadow samplerCubeShadow image1D uimage1DArray uimage2D uimage2DArray uimage2DMS uimage2DMSArray uimage2DRect uimage3D uimageBuffer uimageCube uimageCubeArray uint usampler1D usampler1DArray usampler2D usampler2DArray usampler2DMS usampler2DMSArray usampler2DRect usampler3D samplerBuffer usamplerCube usamplerCubeArray uvec2 uvec3 uvec4 vec2 vec3 vec4 void",
+literal:"true false nullptr NULL"},m=[l,a,r,t.C_BLOCK_COMMENT_MODE,o,s],p={
+variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{
+beginKeywords:"new throw return else",end:/;/}],keywords:u,contains:m.concat([{
+begin:/\(/,end:/\)/,keywords:u,contains:m.concat(["self"]),relevance:0}]),
+relevance:0},g={className:"function",begin:"("+i+"[\\*&\\s]+)+"+d,
+returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:u,illegal:/[^\w\s\*&:<>.]/,
+contains:[{begin:"decltype\\(auto\\)",keywords:u,relevance:0},{begin:d,
+returnBegin:!0,contains:[c],relevance:0},{begin:/::/,relevance:0},{begin:/:/,
+endsWithParent:!0,contains:[s,o]},{className:"params",begin:/\(/,end:/\)/,
+keywords:u,relevance:0,contains:[r,t.C_BLOCK_COMMENT_MODE,s,o,a,{begin:/\(/,
+end:/\)/,keywords:u,relevance:0,contains:["self",r,t.C_BLOCK_COMMENT_MODE,s,o,a]
+}]},a,r,t.C_BLOCK_COMMENT_MODE,l]};return{name:"C++",
+aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"</",
+contains:[].concat(p,g,m,[l,{
+begin:"\\b(deque|list|queue|priority_queue|pair|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",
+end:">",keywords:u,contains:["self",a]},{begin:t.IDENT_RE+"::",keywords:u},{
+className:"class",beginKeywords:"enum class struct union",end:/[{;:<>=]/,
+contains:[{beginKeywords:"final class struct"},t.TITLE_MODE]}]),exports:{
+preprocessor:l,strings:s,keywords:u}}})(t),n=r.keywords
+;return n.keyword+=" boolean byte word String",
+n.literal+=" DIGITAL_MESSAGE FIRMATA_STRING ANALOG_MESSAGE REPORT_DIGITAL REPORT_ANALOG INPUT_PULLUP SET_PIN_MODE INTERNAL2V56 SYSTEM_RESET LED_BUILTIN INTERNAL1V1 SYSEX_START INTERNAL EXTERNAL DEFAULT OUTPUT INPUT HIGH LOW",
+n.built_in+=" setup loop KeyboardController MouseController SoftwareSerial EthernetServer EthernetClient LiquidCrystal RobotControl GSMVoiceCall EthernetUDP EsploraTFT HttpClient RobotMotor WiFiClient GSMScanner FileSystem Scheduler GSMServer YunClient YunServer IPAddress GSMClient GSMModem Keyboard Ethernet Console GSMBand Esplora Stepper Process WiFiUDP GSM_SMS Mailbox USBHost Firmata PImage Client Server GSMPIN FileIO Bridge Serial EEPROM Stream Mouse Audio Servo File Task GPRS WiFi Wire TFT GSM SPI SD runShellCommandAsynchronously analogWriteResolution retrieveCallingNumber printFirmwareVersion analogReadResolution sendDigitalPortPair noListenOnLocalhost readJoystickButton setFirmwareVersion readJoystickSwitch scrollDisplayRight getVoiceCallStatus scrollDisplayLeft writeMicroseconds delayMicroseconds beginTransmission getSignalStrength runAsynchronously getAsynchronously listenOnLocalhost getCurrentCarrier readAccelerometer messageAvailable sendDigitalPorts lineFollowConfig countryNameWrite runShellCommand readStringUntil rewindDirectory readTemperature setClockDivider readLightSensor endTransmission analogReference detachInterrupt countryNameRead attachInterrupt encryptionType readBytesUntil robotNameWrite readMicrophone robotNameRead cityNameWrite userNameWrite readJoystickY readJoystickX mouseReleased openNextFile scanNetworks noInterrupts digitalWrite beginSpeaker mousePressed isActionDone mouseDragged displayLogos noAutoscroll addParameter remoteNumber getModifiers keyboardRead userNameRead waitContinue processInput parseCommand printVersion readNetworks writeMessage blinkVersion cityNameRead readMessage setDataMode parsePacket isListening setBitOrder beginPacket isDirectory motorsWrite drawCompass digitalRead clearScreen serialEvent rightToLeft setTextSize leftToRight requestFrom keyReleased compassRead analogWrite interrupts WiFiServer disconnect playMelody parseFloat autoscroll getPINUsed setPINUsed setTimeout sendAnalog readSlider analogRead beginWrite createChar motorsStop keyPressed tempoWrite readButton subnetMask debugPrint macAddress writeGreen randomSeed attachGPRS readString sendString remotePort releaseAll mouseMoved background getXChange getYChange answerCall getResult voiceCall endPacket constrain getSocket writeJSON getButton available connected findUntil readBytes exitValue readGreen writeBlue startLoop IPAddress isPressed sendSysex pauseMode gatewayIP setCursor getOemKey tuneWrite noDisplay loadImage switchPIN onRequest onReceive changePIN playFile noBuffer parseInt overflow checkPIN knobRead beginTFT bitClear updateIR bitWrite position writeRGB highByte writeRed setSpeed readBlue noStroke remoteIP transfer shutdown hangCall beginSMS endWrite attached maintain noCursor checkReg checkPUK shiftOut isValid shiftIn pulseIn connect println localIP pinMode getIMEI display noBlink process getBand running beginSD drawBMP lowByte setBand release bitRead prepare pointTo readRed setMode noFill remove listen stroke detach attach noTone exists buffer height bitSet circle config cursor random IRread setDNS endSMS getKey micros millis begin print write ready flush width isPIN blink clear press mkdir rmdir close point yield image BSSID click delay read text move peek beep rect line open seek fill size turn stop home find step tone sqrt RSSI SSID end bit tan cos sin pow map abs max min get run put",
+r.name="Arduino",r.aliases=["ino"],r.supersetOf="cpp",r}})());
+hljs.registerLanguage("armasm",(()=>{"use strict";return s=>{const e={
+variants:[s.COMMENT("^[ \\t]*(?=#)","$",{relevance:0,excludeBegin:!0
+}),s.COMMENT("[;@]","$",{relevance:0
+}),s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE]};return{name:"ARM Assembly",
+case_insensitive:!0,aliases:["arm"],keywords:{$pattern:"\\.?"+s.IDENT_RE,
+meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .arm .thumb .code16 .code32 .force_thumb .thumb_func .ltorg ALIAS ALIGN ARM AREA ASSERT ATTR CN CODE CODE16 CODE32 COMMON CP DATA DCB DCD DCDU DCDO DCFD DCFDU DCI DCQ DCQU DCW DCWU DN ELIF ELSE END ENDFUNC ENDIF ENDP ENTRY EQU EXPORT EXPORTAS EXTERN FIELD FILL FUNCTION GBLA GBLL GBLS GET GLOBAL IF IMPORT INCBIN INCLUDE INFO KEEP LCLA LCLL LCLS LTORG MACRO MAP MEND MEXIT NOFP OPT PRESERVE8 PROC QN READONLY RELOC REQUIRE REQUIRE8 RLIST FN ROUT SETA SETL SETS SN SPACE SUBT THUMB THUMBX TTL WHILE WEND ",
+built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 pc lr sp ip sl sb fp a1 a2 a3 a4 v1 v2 v3 v4 v5 v6 v7 v8 f0 f1 f2 f3 f4 f5 f6 f7 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15 q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 q12 q13 q14 q15 cpsr_c cpsr_x cpsr_s cpsr_f cpsr_cx cpsr_cxs cpsr_xs cpsr_xsf cpsr_sf cpsr_cxsf spsr_c spsr_x spsr_s spsr_f spsr_cx spsr_cxs spsr_xs spsr_xsf spsr_sf spsr_cxsf s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15 d16 d17 d18 d19 d20 d21 d22 d23 d24 d25 d26 d27 d28 d29 d30 d31 {PC} {VAR} {TRUE} {FALSE} {OPT} {CONFIG} {ENDIAN} {CODESIZE} {CPU} {FPU} {ARCHITECTURE} {PCSTOREOFFSET} {ARMASM_VERSION} {INTER} {ROPI} {RWPI} {SWST} {NOSWST} . @"
+},contains:[{className:"keyword",
+begin:"\\b(adc|(qd?|sh?|u[qh]?)?add(8|16)?|usada?8|(q|sh?|u[qh]?)?(as|sa)x|and|adrl?|sbc|rs[bc]|asr|b[lx]?|blx|bxj|cbn?z|tb[bh]|bic|bfc|bfi|[su]bfx|bkpt|cdp2?|clz|clrex|cmp|cmn|cpsi[ed]|cps|setend|dbg|dmb|dsb|eor|isb|it[te]{0,3}|lsl|lsr|ror|rrx|ldm(([id][ab])|f[ds])?|ldr((s|ex)?[bhd])?|movt?|mvn|mra|mar|mul|[us]mull|smul[bwt][bt]|smu[as]d|smmul|smmla|mla|umlaal|smlal?([wbt][bt]|d)|mls|smlsl?[ds]|smc|svc|sev|mia([bt]{2}|ph)?|mrr?c2?|mcrr2?|mrs|msr|orr|orn|pkh(tb|bt)|rbit|rev(16|sh)?|sel|[su]sat(16)?|nop|pop|push|rfe([id][ab])?|stm([id][ab])?|str(ex)?[bhd]?|(qd?)?sub|(sh?|q|u[qh]?)?sub(8|16)|[su]xt(a?h|a?b(16)?)|srs([id][ab])?|swpb?|swi|smi|tst|teq|wfe|wfi|yield)(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo)?[sptrx]?(?=\\s)"
+},e,s.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"[^\\\\]'",relevance:0
+},{className:"title",begin:"\\|",end:"\\|",illegal:"\\n",relevance:0},{
+className:"number",variants:[{begin:"[#$=]?0x[0-9a-f]+"},{begin:"[#$=]?0b[01]+"
+},{begin:"[#$=]\\d+"},{begin:"\\b\\d+"}],relevance:0},{className:"symbol",
+variants:[{begin:"^[ \\t]*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{
+begin:"^[a-z_\\.\\$][a-z0-9_\\.\\$]+"},{begin:"[=#]\\w+"}],relevance:0}]}}})());
+hljs.registerLanguage("xml",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function n(e){return a("(?=",e,")")}
+function a(...n){return n.map((n=>e(n))).join("")}function s(...n){
+return"("+n.map((n=>e(n))).join("|")+")"}return e=>{
+const t=a(/[A-Z_]/,a("(",/[A-Z0-9_.-]*:/,")?"),/[A-Z0-9_.-]*/),i={
+className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},r={begin:/\s/,
+contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]
+},c=e.inherit(r,{begin:/\(/,end:/\)/}),l=e.inherit(e.APOS_STRING_MODE,{
+className:"meta-string"}),g=e.inherit(e.QUOTE_STRING_MODE,{
+className:"meta-string"}),m={endsWithParent:!0,illegal:/</,relevance:0,
+contains:[{className:"attr",begin:/[A-Za-z0-9._:-]+/,relevance:0},{begin:/=\s*/,
+relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,
+end:/"/,contains:[i]},{begin:/'/,end:/'/,contains:[i]},{begin:/[^\s"'=<>`]+/}]}]
+}]};return{name:"HTML, XML",
+aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],
+case_insensitive:!0,contains:[{className:"meta",begin:/<![a-z]/,end:/>/,
+relevance:10,contains:[r,g,l,c,{begin:/\[/,end:/\]/,contains:[{className:"meta",
+begin:/<![a-z]/,end:/>/,contains:[r,c,g,l]}]}]},e.COMMENT(/<!--/,/-->/,{
+relevance:10}),{begin:/<!\[CDATA\[/,end:/\]\]>/,relevance:10},i,{
+className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",
+begin:/<style(?=\s|>)/,end:/>/,keywords:{name:"style"},contains:[m],starts:{
+end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",
+begin:/<script(?=\s|>)/,end:/>/,keywords:{name:"script"},contains:[m],starts:{
+end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{
+className:"tag",begin:/<>|<\/>/},{className:"tag",
+begin:a(/</,n(a(t,s(/\/>/,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",
+begin:t,relevance:0,starts:m}]},{className:"tag",begin:a(/<\//,n(a(t,/>/))),
+contains:[{className:"name",begin:t,relevance:0},{begin:/>/,relevance:0}]}]}}
+})());
+hljs.registerLanguage("asciidoc",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>{const a=[{className:"strong",begin:/\*{2}([^\n]+?)\*{2}/
+},{className:"strong",
+begin:e(/\*\*/,/((\*(?!\*)|\\[^\n]|[^*\n\\])+\n)+/,/(\*(?!\*)|\\[^\n]|[^*\n\\])*/,/\*\*/),
+relevance:0},{className:"strong",begin:/\B\*(\S|\S[^\n]*?\S)\*(?!\w)/},{
+className:"strong",begin:/\*[^\s]([^\n]+\n)+([^\n]+)\*/}],s=[{
+className:"emphasis",begin:/_{2}([^\n]+?)_{2}/},{className:"emphasis",
+begin:e(/__/,/((_(?!_)|\\[^\n]|[^_\n\\])+\n)+/,/(_(?!_)|\\[^\n]|[^_\n\\])*/,/__/),
+relevance:0},{className:"emphasis",begin:/\b_(\S|\S[^\n]*?\S)_(?!\w)/},{
+className:"emphasis",begin:/_[^\s]([^\n]+\n)+([^\n]+)_/},{className:"emphasis",
+begin:"\\B'(?!['\\s])",end:"(\\n{2}|')",contains:[{begin:"\\\\'\\w",relevance:0
+}],relevance:0}];return{name:"AsciiDoc",aliases:["adoc"],
+contains:[n.COMMENT("^/{4,}\\n","\\n/{4,}$",{relevance:10
+}),n.COMMENT("^//","$",{relevance:0}),{className:"title",begin:"^\\.\\w.*$"},{
+begin:"^[=\\*]{4,}\\n",end:"\\n^[=\\*]{4,}$",relevance:10},{className:"section",
+relevance:10,variants:[{begin:"^(={1,6})[ \t].+?([ \t]\\1)?$"},{
+begin:"^[^\\[\\]\\n]+?\\n[=\\-~\\^\\+]{2,}$"}]},{className:"meta",
+begin:"^:.+?:",end:"\\s",excludeEnd:!0,relevance:10},{className:"meta",
+begin:"^\\[.+?\\]$",relevance:0},{className:"quote",begin:"^_{4,}\\n",
+end:"\\n_{4,}$",relevance:10},{className:"code",begin:"^[\\-\\.]{4,}\\n",
+end:"\\n[\\-\\.]{4,}$",relevance:10},{begin:"^\\+{4,}\\n",end:"\\n\\+{4,}$",
+contains:[{begin:"<",end:">",subLanguage:"xml",relevance:0}],relevance:10},{
+className:"bullet",begin:"^(\\*+|-+|\\.+|[^\\n]+?::)\\s+"},{className:"symbol",
+begin:"^(NOTE|TIP|IMPORTANT|WARNING|CAUTION):\\s+",relevance:10},{
+begin:/\\[*_`]/},{begin:/\\\\\*{2}[^\n]*?\*{2}/},{begin:/\\\\_{2}[^\n]*_{2}/},{
+begin:/\\\\`{2}[^\n]*`{2}/},{begin:/[:;}][*_`](?![*_`])/},...a,...s,{
+className:"string",variants:[{begin:"``.+?''"},{begin:"`.+?'"}]},{
+className:"code",begin:/`{2}/,end:/(\n{2}|`{2})/},{className:"code",
+begin:"(`.+?`|\\+.+?\\+)",relevance:0},{className:"code",begin:"^[ \\t]",
+end:"$",relevance:0},{begin:"^'{3,}[ \\t]*$",relevance:10},{
+begin:"(link:)?(http|https|ftp|file|irc|image:?):\\S+?\\[[^[]*?\\]",
+returnBegin:!0,contains:[{begin:"(link|image:?):",relevance:0},{
+className:"link",begin:"\\w",end:"[^\\[]+",relevance:0},{className:"string",
+begin:"\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0,relevance:0}],relevance:10}]
+}}})());
+hljs.registerLanguage("aspectj",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>{
+const t="false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance",i="get set args call"
+;return{name:"AspectJ",keywords:t,illegal:/<\/|#/,
+contains:[n.COMMENT(/\/\*\*/,/\*\//,{relevance:0,contains:[{begin:/\w+@/,
+relevance:0},{className:"doctag",begin:/@[A-Za-z]+/}]
+}),n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,n.APOS_STRING_MODE,n.QUOTE_STRING_MODE,{
+className:"class",beginKeywords:"aspect",end:/[{;=]/,excludeEnd:!0,
+illegal:/[:;"\[\]]/,contains:[{
+beginKeywords:"extends implements pertypewithin perthis pertarget percflowbelow percflow issingleton"
+},n.UNDERSCORE_TITLE_MODE,{begin:/\([^\)]*/,end:/[)]+/,keywords:t+" "+i,
+excludeEnd:!1}]},{className:"class",beginKeywords:"class interface",end:/[{;=]/,
+excludeEnd:!0,relevance:0,keywords:"class interface",illegal:/[:"\[\]]/,
+contains:[{beginKeywords:"extends implements"},n.UNDERSCORE_TITLE_MODE]},{
+beginKeywords:"pointcut after before around throwing returning",end:/[)]/,
+excludeEnd:!1,illegal:/["\[\]]/,contains:[{
+begin:e(n.UNDERSCORE_IDENT_RE,/\s*\(/),returnBegin:!0,
+contains:[n.UNDERSCORE_TITLE_MODE]}]},{begin:/[:]/,returnBegin:!0,end:/[{;]/,
+relevance:0,excludeEnd:!1,keywords:t,illegal:/["\[\]]/,contains:[{
+begin:e(n.UNDERSCORE_IDENT_RE,/\s*\(/),keywords:t+" "+i,relevance:0
+},n.QUOTE_STRING_MODE]},{beginKeywords:"new throw",relevance:0},{
+className:"function",
+begin:/\w+ +\w+(\.\w+)?\s*\([^\)]*\)\s*((throws)[\w\s,]+)?[\{;]/,returnBegin:!0,
+end:/[{;=]/,keywords:t,excludeEnd:!0,contains:[{
+begin:e(n.UNDERSCORE_IDENT_RE,/\s*\(/),returnBegin:!0,relevance:0,
+contains:[n.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,
+relevance:0,keywords:t,
+contains:[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE,n.C_NUMBER_MODE,n.C_BLOCK_COMMENT_MODE]
+},n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE]},n.C_NUMBER_MODE,{
+className:"meta",begin:/@[A-Za-z]+/}]}}})());
+hljs.registerLanguage("autohotkey",(()=>{"use strict";return e=>{const a={
+begin:"`[\\s\\S]"};return{name:"AutoHotkey",case_insensitive:!0,aliases:["ahk"],
+keywords:{
+keyword:"Break Continue Critical Exit ExitApp Gosub Goto New OnExit Pause return SetBatchLines SetTimer Suspend Thread Throw Until ahk_id ahk_class ahk_pid ahk_exe ahk_group",
+literal:"true false NOT AND OR",
+built_in:"ComSpec Clipboard ClipboardAll ErrorLevel"},
+contains:[a,e.inherit(e.QUOTE_STRING_MODE,{contains:[a]}),e.COMMENT(";","$",{
+relevance:0}),e.C_BLOCK_COMMENT_MODE,{className:"number",begin:e.NUMBER_RE,
+relevance:0},{className:"variable",begin:"%[a-zA-Z0-9#_$@]+%"},{
+className:"built_in",begin:"^\\s*\\w+\\s*(,|%)"},{className:"title",variants:[{
+begin:'^[^\\n";]+::(?!=)'},{begin:'^[^\\n";]+:(?!=)',relevance:0}]},{
+className:"meta",begin:"^\\s*#\\w+",end:"$",relevance:0},{className:"built_in",
+begin:"A_[a-zA-Z0-9]+"},{begin:",\\s*,"}]}}})());
+hljs.registerLanguage("autoit",(()=>{"use strict";return e=>{const t={
+variants:[e.COMMENT(";","$",{relevance:0
+}),e.COMMENT("#cs","#ce"),e.COMMENT("#comments-start","#comments-end")]},r={
+begin:"\\$[A-z0-9_]+"},i={className:"string",variants:[{begin:/"/,end:/"/,
+contains:[{begin:/""/,relevance:0}]},{begin:/'/,end:/'/,contains:[{begin:/''/,
+relevance:0}]}]},n={variants:[e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE]};return{
+name:"AutoIt",case_insensitive:!0,illegal:/\/\*/,keywords:{
+keyword:"ByRef Case Const ContinueCase ContinueLoop Dim Do Else ElseIf EndFunc EndIf EndSelect EndSwitch EndWith Enum Exit ExitLoop For Func Global If In Local Next ReDim Return Select Static Step Switch Then To Until Volatile WEnd While With",
+built_in:"Abs ACos AdlibRegister AdlibUnRegister Asc AscW ASin Assign ATan AutoItSetOption AutoItWinGetTitle AutoItWinSetTitle Beep Binary BinaryLen BinaryMid BinaryToString BitAND BitNOT BitOR BitRotate BitShift BitXOR BlockInput Break Call CDTray Ceiling Chr ChrW ClipGet ClipPut ConsoleRead ConsoleWrite ConsoleWriteError ControlClick ControlCommand ControlDisable ControlEnable ControlFocus ControlGetFocus ControlGetHandle ControlGetPos ControlGetText ControlHide ControlListView ControlMove ControlSend ControlSetText ControlShow ControlTreeView Cos Dec DirCopy DirCreate DirGetSize DirMove DirRemove DllCall DllCallAddress DllCallbackFree DllCallbackGetPtr DllCallbackRegister DllClose DllOpen DllStructCreate DllStructGetData DllStructGetPtr DllStructGetSize DllStructSetData DriveGetDrive DriveGetFileSystem DriveGetLabel DriveGetSerial DriveGetType DriveMapAdd DriveMapDel DriveMapGet DriveSetLabel DriveSpaceFree DriveSpaceTotal DriveStatus EnvGet EnvSet EnvUpdate Eval Execute Exp FileChangeDir FileClose FileCopy FileCreateNTFSLink FileCreateShortcut FileDelete FileExists FileFindFirstFile FileFindNextFile FileFlush FileGetAttrib FileGetEncoding FileGetLongName FileGetPos FileGetShortcut FileGetShortName FileGetSize FileGetTime FileGetVersion FileInstall FileMove FileOpen FileOpenDialog FileRead FileReadLine FileReadToArray FileRecycle FileRecycleEmpty FileSaveDialog FileSelectFolder FileSetAttrib FileSetEnd FileSetPos FileSetTime FileWrite FileWriteLine Floor FtpSetProxy FuncName GUICreate GUICtrlCreateAvi GUICtrlCreateButton GUICtrlCreateCheckbox GUICtrlCreateCombo GUICtrlCreateContextMenu GUICtrlCreateDate GUICtrlCreateDummy GUICtrlCreateEdit GUICtrlCreateGraphic GUICtrlCreateGroup GUICtrlCreateIcon GUICtrlCreateInput GUICtrlCreateLabel GUICtrlCreateList GUICtrlCreateListView GUICtrlCreateListViewItem GUICtrlCreateMenu GUICtrlCreateMenuItem GUICtrlCreateMonthCal GUICtrlCreateObj GUICtrlCreatePic GUICtrlCreateProgress GUICtrlCreateRadio GUICtrlCreateSlider GUICtrlCreateTab GUICtrlCreateTabItem GUICtrlCreateTreeView GUICtrlCreateTreeViewItem GUICtrlCreateUpdown GUICtrlDelete GUICtrlGetHandle GUICtrlGetState GUICtrlRead GUICtrlRecvMsg GUICtrlRegisterListViewSort GUICtrlSendMsg GUICtrlSendToDummy GUICtrlSetBkColor GUICtrlSetColor GUICtrlSetCursor GUICtrlSetData GUICtrlSetDefBkColor GUICtrlSetDefColor GUICtrlSetFont GUICtrlSetGraphic GUICtrlSetImage GUICtrlSetLimit GUICtrlSetOnEvent GUICtrlSetPos GUICtrlSetResizing GUICtrlSetState GUICtrlSetStyle GUICtrlSetTip GUIDelete GUIGetCursorInfo GUIGetMsg GUIGetStyle GUIRegisterMsg GUISetAccelerators GUISetBkColor GUISetCoord GUISetCursor GUISetFont GUISetHelp GUISetIcon GUISetOnEvent GUISetState GUISetStyle GUIStartGroup GUISwitch Hex HotKeySet HttpSetProxy HttpSetUserAgent HWnd InetClose InetGet InetGetInfo InetGetSize InetRead IniDelete IniRead IniReadSection IniReadSectionNames IniRenameSection IniWrite IniWriteSection InputBox Int IsAdmin IsArray IsBinary IsBool IsDeclared IsDllStruct IsFloat IsFunc IsHWnd IsInt IsKeyword IsNumber IsObj IsPtr IsString Log MemGetStats Mod MouseClick MouseClickDrag MouseDown MouseGetCursor MouseGetPos MouseMove MouseUp MouseWheel MsgBox Number ObjCreate ObjCreateInterface ObjEvent ObjGet ObjName OnAutoItExitRegister OnAutoItExitUnRegister Ping PixelChecksum PixelGetColor PixelSearch ProcessClose ProcessExists ProcessGetStats ProcessList ProcessSetPriority ProcessWait ProcessWaitClose ProgressOff ProgressOn ProgressSet Ptr Random RegDelete RegEnumKey RegEnumVal RegRead RegWrite Round Run RunAs RunAsWait RunWait Send SendKeepActive SetError SetExtended ShellExecute ShellExecuteWait Shutdown Sin Sleep SoundPlay SoundSetWaveVolume SplashImageOn SplashOff SplashTextOn Sqrt SRandom StatusbarGetText StderrRead StdinWrite StdioClose StdoutRead String StringAddCR StringCompare StringFormat StringFromASCIIArray StringInStr StringIsAlNum StringIsAlpha StringIsASCII StringIsDigit StringIsFloat StringIsInt StringIsLower StringIsSpace StringIsUpper StringIsXDigit StringLeft StringLen StringLower StringMid StringRegExp StringRegExpReplace StringReplace StringReverse StringRight StringSplit StringStripCR StringStripWS StringToASCIIArray StringToBinary StringTrimLeft StringTrimRight StringUpper Tan TCPAccept TCPCloseSocket TCPConnect TCPListen TCPNameToIP TCPRecv TCPSend TCPShutdown, UDPShutdown TCPStartup, UDPStartup TimerDiff TimerInit ToolTip TrayCreateItem TrayCreateMenu TrayGetMsg TrayItemDelete TrayItemGetHandle TrayItemGetState TrayItemGetText TrayItemSetOnEvent TrayItemSetState TrayItemSetText TraySetClick TraySetIcon TraySetOnEvent TraySetPauseIcon TraySetState TraySetToolTip TrayTip UBound UDPBind UDPCloseSocket UDPOpen UDPRecv UDPSend VarGetType WinActivate WinActive WinClose WinExists WinFlash WinGetCaretPos WinGetClassList WinGetClientSize WinGetHandle WinGetPos WinGetProcess WinGetState WinGetText WinGetTitle WinKill WinList WinMenuSelectItem WinMinimizeAll WinMinimizeAllUndo WinMove WinSetOnTop WinSetState WinSetTitle WinSetTrans WinWait WinWaitActive WinWaitClose WinWaitNotActive",
+literal:"True False And Null Not Or Default"},contains:[t,r,i,n,{
+className:"meta",begin:"#",end:"$",keywords:{
+"meta-keyword":["EndRegion","forcedef","forceref","ignorefunc","include","include-once","NoTrayIcon","OnAutoItStartRegister","pragma","Region","RequireAdmin","Tidy_Off","Tidy_On","Tidy_Parameters"]
+},contains:[{begin:/\\\n/,relevance:0},{beginKeywords:"include",keywords:{
+"meta-keyword":"include"},end:"$",contains:[i,{className:"meta-string",
+variants:[{begin:"<",end:">"},{begin:/"/,end:/"/,contains:[{begin:/""/,
+relevance:0}]},{begin:/'/,end:/'/,contains:[{begin:/''/,relevance:0}]}]}]},i,t]
+},{className:"symbol",begin:"@[A-z0-9_]+"},{className:"function",
+beginKeywords:"Func",end:"$",illegal:"\\$|\\[|%",
+contains:[e.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",
+contains:[r,i,n]}]}]}}})());
+hljs.registerLanguage("avrasm",(()=>{"use strict";return r=>({
+name:"AVR Assembly",case_insensitive:!0,keywords:{$pattern:"\\.?"+r.IDENT_RE,
+keyword:"adc add adiw and andi asr bclr bld brbc brbs brcc brcs break breq brge brhc brhs brid brie brlo brlt brmi brne brpl brsh brtc brts brvc brvs bset bst call cbi cbr clc clh cli cln clr cls clt clv clz com cp cpc cpi cpse dec eicall eijmp elpm eor fmul fmuls fmulsu icall ijmp in inc jmp ld ldd ldi lds lpm lsl lsr mov movw mul muls mulsu neg nop or ori out pop push rcall ret reti rjmp rol ror sbc sbr sbrc sbrs sec seh sbi sbci sbic sbis sbiw sei sen ser ses set sev sez sleep spm st std sts sub subi swap tst wdr",
+built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 r16 r17 r18 r19 r20 r21 r22 r23 r24 r25 r26 r27 r28 r29 r30 r31 x|0 xh xl y|0 yh yl z|0 zh zl ucsr1c udr1 ucsr1a ucsr1b ubrr1l ubrr1h ucsr0c ubrr0h tccr3c tccr3a tccr3b tcnt3h tcnt3l ocr3ah ocr3al ocr3bh ocr3bl ocr3ch ocr3cl icr3h icr3l etimsk etifr tccr1c ocr1ch ocr1cl twcr twdr twar twsr twbr osccal xmcra xmcrb eicra spmcsr spmcr portg ddrg ping portf ddrf sreg sph spl xdiv rampz eicrb eimsk gimsk gicr eifr gifr timsk tifr mcucr mcucsr tccr0 tcnt0 ocr0 assr tccr1a tccr1b tcnt1h tcnt1l ocr1ah ocr1al ocr1bh ocr1bl icr1h icr1l tccr2 tcnt2 ocr2 ocdr wdtcr sfior eearh eearl eedr eecr porta ddra pina portb ddrb pinb portc ddrc pinc portd ddrd pind spdr spsr spcr udr0 ucsr0a ucsr0b ubrr0l acsr admux adcsr adch adcl porte ddre pine pinf",
+meta:".byte .cseg .db .def .device .dseg .dw .endmacro .equ .eseg .exit .include .list .listmac .macro .nolist .org .set"
+},contains:[r.C_BLOCK_COMMENT_MODE,r.COMMENT(";","$",{relevance:0
+}),r.C_NUMBER_MODE,r.BINARY_NUMBER_MODE,{className:"number",
+begin:"\\b(\\$[a-zA-Z0-9]+|0o[0-7]+)"},r.QUOTE_STRING_MODE,{className:"string",
+begin:"'",end:"[^\\\\]'",illegal:"[^\\\\][^']"},{className:"symbol",
+begin:"^[A-Za-z0-9_.$]+:"},{className:"meta",begin:"#",end:"$"},{
+className:"subst",begin:"@[0-9]+"}]})})());
+hljs.registerLanguage("awk",(()=>{"use strict";return e=>({name:"Awk",keywords:{
+keyword:"BEGIN END if else while do for in break continue delete next nextfile function func exit|10"
+},contains:[{className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{
+begin:/\$\{(.*?)\}/}]},{className:"string",contains:[e.BACKSLASH_ESCAPE],
+variants:[{begin:/(u|b)?r?'''/,end:/'''/,relevance:10},{begin:/(u|b)?r?"""/,
+end:/"""/,relevance:10},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{
+begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{
+begin:/(b|br)"/,end:/"/},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
+},e.REGEXP_MODE,e.HASH_COMMENT_MODE,e.NUMBER_MODE]})})());
+hljs.registerLanguage("axapta",(()=>{"use strict";return e=>({name:"X++",
+aliases:["x++"],keywords:{
+keyword:["abstract","as","asc","avg","break","breakpoint","by","byref","case","catch","changecompany","class","client","client","common","const","continue","count","crosscompany","delegate","delete_from","desc","display","div","do","edit","else","eventhandler","exists","extends","final","finally","firstfast","firstonly","firstonly1","firstonly10","firstonly100","firstonly1000","flush","for","forceliterals","forcenestedloop","forceplaceholders","forceselectorder","forupdate","from","generateonly","group","hint","if","implements","in","index","insert_recordset","interface","internal","is","join","like","maxof","minof","mod","namespace","new","next","nofetch","notexists","optimisticlock","order","outer","pessimisticlock","print","private","protected","public","readonly","repeatableread","retry","return","reverse","select","server","setting","static","sum","super","switch","this","throw","try","ttsabort","ttsbegin","ttscommit","unchecked","update_recordset","using","validtimestate","void","where","while"],
+built_in:["anytype","boolean","byte","char","container","date","double","enum","guid","int","int64","long","real","short","str","utcdatetime","var"],
+literal:["default","false","null","true"]},
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,{
+className:"meta",begin:"#",end:"$"},{className:"class",
+beginKeywords:"class interface",end:/\{/,excludeEnd:!0,illegal:":",contains:[{
+beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]}]})})());
+hljs.registerLanguage("bash",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(s=e)?"string"==typeof s?s:s.source:null;var s
+})).join("")}return s=>{const n={},t={begin:/\$\{/,end:/\}/,contains:["self",{
+begin:/:-/,contains:[n]}]};Object.assign(n,{className:"variable",variants:[{
+begin:e(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},t]});const a={
+className:"subst",begin:/\$\(/,end:/\)/,contains:[s.BACKSLASH_ESCAPE]},i={
+begin:/<<-?\s*(?=\w+)/,starts:{contains:[s.END_SAME_AS_BEGIN({begin:/(\w+)/,
+end:/(\w+)/,className:"string"})]}},c={className:"string",begin:/"/,end:/"/,
+contains:[s.BACKSLASH_ESCAPE,n,a]};a.contains.push(c);const o={begin:/\$\(\(/,
+end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},s.NUMBER_MODE,n]
+},r=s.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10
+}),l={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,
+contains:[s.inherit(s.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{
+name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z._-]+\b/,
+keyword:"if then else elif fi for while in do done case esac function",
+literal:"true false",
+built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"
+},contains:[r,s.SHEBANG(),l,o,s.HASH_COMMENT_MODE,i,c,{className:"",begin:/\\"/
+},{className:"string",begin:/'/,end:/'/},n]}}})());
+hljs.registerLanguage("basic",(()=>{"use strict";return E=>({name:"BASIC",
+case_insensitive:!0,illegal:"^.",keywords:{$pattern:"[a-zA-Z][a-zA-Z0-9_$%!#]*",
+keyword:"ABS ASC AND ATN AUTO|0 BEEP BLOAD|10 BSAVE|10 CALL CALLS CDBL CHAIN CHDIR CHR$|10 CINT CIRCLE CLEAR CLOSE CLS COLOR COM COMMON CONT COS CSNG CSRLIN CVD CVI CVS DATA DATE$ DEFDBL DEFINT DEFSNG DEFSTR DEF|0 SEG USR DELETE DIM DRAW EDIT END ENVIRON ENVIRON$ EOF EQV ERASE ERDEV ERDEV$ ERL ERR ERROR EXP FIELD FILES FIX FOR|0 FRE GET GOSUB|10 GOTO HEX$ IF THEN ELSE|0 INKEY$ INP INPUT INPUT# INPUT$ INSTR IMP INT IOCTL IOCTL$ KEY ON OFF LIST KILL LEFT$ LEN LET LINE LLIST LOAD LOC LOCATE LOF LOG LPRINT USING LSET MERGE MID$ MKDIR MKD$ MKI$ MKS$ MOD NAME NEW NEXT NOISE NOT OCT$ ON OR PEN PLAY STRIG OPEN OPTION BASE OUT PAINT PALETTE PCOPY PEEK PMAP POINT POKE POS PRINT PRINT] PSET PRESET PUT RANDOMIZE READ REM RENUM RESET|0 RESTORE RESUME RETURN|0 RIGHT$ RMDIR RND RSET RUN SAVE SCREEN SGN SHELL SIN SOUND SPACE$ SPC SQR STEP STICK STOP STR$ STRING$ SWAP SYSTEM TAB TAN TIME$ TIMER TROFF TRON TO USR VAL VARPTR VARPTR$ VIEW WAIT WHILE WEND WIDTH WINDOW WRITE XOR"
+},contains:[E.QUOTE_STRING_MODE,E.COMMENT("REM","$",{relevance:10
+}),E.COMMENT("'","$",{relevance:0}),{className:"symbol",begin:"^[0-9]+ ",
+relevance:10},{className:"number",begin:"\\b\\d+(\\.\\d+)?([edED]\\d+)?[#!]?",
+relevance:0},{className:"number",begin:"(&[hH][0-9a-fA-F]{1,4})"},{
+className:"number",begin:"(&[oO][0-7]{1,6})"}]})})());
+hljs.registerLanguage("bnf",(()=>{"use strict";return e=>({
+name:"Backus\u2013Naur Form",contains:[{className:"attribute",begin:/</,end:/>/
+},{begin:/::=/,end:/$/,contains:[{begin:/</,end:/>/
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
+}]})})());
+hljs.registerLanguage("brainfuck",(()=>{"use strict";return e=>{const n={
+className:"literal",begin:/[+-]/,relevance:0};return{name:"Brainfuck",
+aliases:["bf"],
+contains:[e.COMMENT("[^\\[\\]\\.,\\+\\-<> \r\n]","[\\[\\]\\.,\\+\\-<> \r\n]",{
+returnEnd:!0,relevance:0}),{className:"title",begin:"[\\[\\]]",relevance:0},{
+className:"string",begin:"[\\.,]",relevance:0},{begin:/(?:\+\+|--)/,contains:[n]
+},n]}}})());
+hljs.registerLanguage("c-like",(()=>{"use strict";function e(e){
+return((...e)=>e.map((e=>(e=>e?"string"==typeof e?e:e.source:null)(e))).join(""))("(",e,")?")
+}return t=>{const n=(t=>{const n=t.COMMENT("//","$",{contains:[{begin:/\\\n/}]
+}),a="[a-zA-Z_]\\w*::",r="(decltype\\(auto\\)|"+e(a)+"[a-zA-Z_]\\w*"+e("<[^<>]+>")+")",s={
+className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},i={className:"string",
+variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",
+contains:[t.BACKSLASH_ESCAPE]},{
+begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
+end:"'",illegal:"."},t.END_SAME_AS_BEGIN({
+begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},c={
+className:"number",variants:[{begin:"\\b(0b[01']+)"},{
+begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"
+},{
+begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
+}],relevance:0},o={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
+"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
+},contains:[{begin:/\\\n/,relevance:0},t.inherit(i,{className:"meta-string"}),{
+className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"
+},n,t.C_BLOCK_COMMENT_MODE]},l={className:"title",begin:e(a)+t.IDENT_RE,
+relevance:0},d=e(a)+t.IDENT_RE+"\\s*\\(",u={
+keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",
+built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary",
+literal:"true false nullptr NULL"},p=[o,s,n,t.C_BLOCK_COMMENT_MODE,c,i],m={
+variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{
+beginKeywords:"new throw return else",end:/;/}],keywords:u,contains:p.concat([{
+begin:/\(/,end:/\)/,keywords:u,contains:p.concat(["self"]),relevance:0}]),
+relevance:0},g={className:"function",begin:"("+r+"[\\*&\\s]+)+"+d,
+returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:u,illegal:/[^\w\s\*&:<>.]/,
+contains:[{begin:"decltype\\(auto\\)",keywords:u,relevance:0},{begin:d,
+returnBegin:!0,contains:[l],relevance:0},{begin:/::/,relevance:0},{begin:/:/,
+endsWithParent:!0,contains:[i,c]},{className:"params",begin:/\(/,end:/\)/,
+keywords:u,relevance:0,contains:[n,t.C_BLOCK_COMMENT_MODE,i,c,s,{begin:/\(/,
+end:/\)/,keywords:u,relevance:0,contains:["self",n,t.C_BLOCK_COMMENT_MODE,i,c,s]
+}]},s,n,t.C_BLOCK_COMMENT_MODE,o]};return{name:"C++",
+aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"</",
+contains:[].concat(m,g,p,[o,{
+begin:"\\b(deque|list|queue|priority_queue|pair|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",
+end:">",keywords:u,contains:["self",s]},{begin:t.IDENT_RE+"::",keywords:u},{
+className:"class",beginKeywords:"enum class struct union",end:/[{;:<>=]/,
+contains:[{beginKeywords:"final class struct"},t.TITLE_MODE]}]),exports:{
+preprocessor:o,strings:i,keywords:u}}})(t)
+;return n.disableAutodetect=!0,n.aliases=[],
+t.getLanguage("c")||n.aliases.push("c","h"),
+t.getLanguage("cpp")||n.aliases.push("cc","c++","h++","hpp","hh","hxx","cxx"),n}
+})());
+hljs.registerLanguage("c",(()=>{"use strict";function e(e){
+return((...e)=>e.map((e=>(e=>e?"string"==typeof e?e:e.source:null)(e))).join(""))("(",e,")?")
+}return t=>{const n=t.COMMENT("//","$",{contains:[{begin:/\\\n/}]
+}),r="[a-zA-Z_]\\w*::",a="(decltype\\(auto\\)|"+e(r)+"[a-zA-Z_]\\w*"+e("<[^<>]+>")+")",i={
+className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},s={className:"string",
+variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",
+contains:[t.BACKSLASH_ESCAPE]},{
+begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
+end:"'",illegal:"."},t.END_SAME_AS_BEGIN({
+begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={
+className:"number",variants:[{begin:"\\b(0b[01']+)"},{
+begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"
+},{
+begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
+}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
+"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
+},contains:[{begin:/\\\n/,relevance:0},t.inherit(s,{className:"meta-string"}),{
+className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"
+},n,t.C_BLOCK_COMMENT_MODE]},l={className:"title",begin:e(r)+t.IDENT_RE,
+relevance:0},d=e(r)+t.IDENT_RE+"\\s*\\(",u={
+keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",
+built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary",
+literal:"true false nullptr NULL"},m=[c,i,n,t.C_BLOCK_COMMENT_MODE,o,s],p={
+variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{
+beginKeywords:"new throw return else",end:/;/}],keywords:u,contains:m.concat([{
+begin:/\(/,end:/\)/,keywords:u,contains:m.concat(["self"]),relevance:0}]),
+relevance:0},_={className:"function",begin:"("+a+"[\\*&\\s]+)+"+d,
+returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:u,illegal:/[^\w\s\*&:<>.]/,
+contains:[{begin:"decltype\\(auto\\)",keywords:u,relevance:0},{begin:d,
+returnBegin:!0,contains:[l],relevance:0},{className:"params",begin:/\(/,
+end:/\)/,keywords:u,relevance:0,contains:[n,t.C_BLOCK_COMMENT_MODE,s,o,i,{
+begin:/\(/,end:/\)/,keywords:u,relevance:0,
+contains:["self",n,t.C_BLOCK_COMMENT_MODE,s,o,i]}]
+},i,n,t.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["c","h"],keywords:u,
+disableAutodetect:!0,illegal:"</",contains:[].concat(p,_,m,[c,{
+begin:"\\b(deque|list|queue|priority_queue|pair|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",
+end:">",keywords:u,contains:["self",i]},{begin:t.IDENT_RE+"::",keywords:u},{
+className:"class",beginKeywords:"enum class struct union",end:/[{;:<>=]/,
+contains:[{beginKeywords:"final class struct"},t.TITLE_MODE]}]),exports:{
+preprocessor:c,strings:s,keywords:u}}}})());
+hljs.registerLanguage("cal",(()=>{"use strict";return e=>{
+const n="div mod in and or not xor asserterror begin case do downto else end exit for if of repeat then to until while with var",a=[e.C_LINE_COMMENT_MODE,e.COMMENT(/\{/,/\}/,{
+relevance:0}),e.COMMENT(/\(\*/,/\*\)/,{relevance:10})],r={className:"string",
+begin:/'/,end:/'/,contains:[{begin:/''/}]},s={className:"string",begin:/(#\d+)+/
+},i={className:"function",beginKeywords:"procedure",end:/[:;]/,
+keywords:"procedure|10",contains:[e.TITLE_MODE,{className:"params",begin:/\(/,
+end:/\)/,keywords:n,contains:[r,s]}].concat(a)},t={className:"class",
+begin:"OBJECT (Table|Form|Report|Dataport|Codeunit|XMLport|MenuSuite|Page|Query) (\\d+) ([^\\r\\n]+)",
+returnBegin:!0,contains:[e.TITLE_MODE,i]};return{name:"C/AL",
+case_insensitive:!0,keywords:{keyword:n,literal:"false true"},illegal:/\/\*/,
+contains:[r,s,{className:"number",begin:"\\b\\d+(\\.\\d+)?(DT|D|T)",relevance:0
+},{className:"string",begin:'"',end:'"'},e.NUMBER_MODE,t,i]}}})());
+hljs.registerLanguage("capnproto",(()=>{"use strict";return n=>({
+name:"Cap\u2019n Proto",aliases:["capnp"],keywords:{
+keyword:"struct enum interface union group import using const annotation extends in of on as with from fixed",
+built_in:"Void Bool Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 Float32 Float64 Text Data AnyPointer AnyStruct Capability List",
+literal:"true false"},
+contains:[n.QUOTE_STRING_MODE,n.NUMBER_MODE,n.HASH_COMMENT_MODE,{
+className:"meta",begin:/@0x[\w\d]{16};/,illegal:/\n/},{className:"symbol",
+begin:/@\d+\b/},{className:"class",beginKeywords:"struct enum",end:/\{/,
+illegal:/\n/,contains:[n.inherit(n.TITLE_MODE,{starts:{endsWithParent:!0,
+excludeEnd:!0}})]},{className:"class",beginKeywords:"interface",end:/\{/,
+illegal:/\n/,contains:[n.inherit(n.TITLE_MODE,{starts:{endsWithParent:!0,
+excludeEnd:!0}})]}]})})());
+hljs.registerLanguage("ceylon",(()=>{"use strict";return e=>{
+const a="assembly module package import alias class interface object given value assign void function new of extends satisfies abstracts in out return break continue throw assert dynamic if else switch case for while try catch finally then let this outer super is exists nonempty",s={
+className:"subst",excludeBegin:!0,excludeEnd:!0,begin:/``/,end:/``/,keywords:a,
+relevance:10},n=[{className:"string",begin:'"""',end:'"""',relevance:10},{
+className:"string",begin:'"',end:'"',contains:[s]},{className:"string",
+begin:"'",end:"'"},{className:"number",
+begin:"#[0-9a-fA-F_]+|\\$[01_]+|[0-9_]+(?:\\.[0-9_](?:[eE][+-]?\\d+)?)?[kMGTPmunpf]?",
+relevance:0}];return s.contains=n,{name:"Ceylon",keywords:{
+keyword:a+" shared abstract formal default actual variable late native deprecated final sealed annotation suppressWarnings small",
+meta:"doc by license see throws tagged"},illegal:"\\$[^01]|#[^0-9a-fA-F]",
+contains:[e.C_LINE_COMMENT_MODE,e.COMMENT("/\\*","\\*/",{contains:["self"]}),{
+className:"meta",begin:'@[a-z]\\w*(?::"[^"]*")?'}].concat(n)}}})());
+hljs.registerLanguage("clean",(()=>{"use strict";return e=>({name:"Clean",
+aliases:["icl","dcl"],keywords:{
+keyword:"if let in with where case of class instance otherwise implementation definition system module from import qualified as special code inline foreign export ccall stdcall generic derive infix infixl infixr",
+built_in:"Int Real Char Bool",literal:"True False"},
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,{
+begin:"->|<-[|:]?|#!?|>>=|\\{\\||\\|\\}|:==|=:|<>"}]})})());
+hljs.registerLanguage("clojure",(()=>{"use strict";return e=>{
+const t="a-zA-Z_\\-!.?+*=<>&#'",n="["+t+"]["+t+"0-9/;:]*",r="def defonce defprotocol defstruct defmulti defmethod defn- defn defmacro deftype defrecord",a={
+$pattern:n,
+"builtin-name":r+" cond apply if-not if-let if not not= =|0 <|0 >|0 <=|0 >=|0 ==|0 +|0 /|0 *|0 -|0 rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy first rest cons cast coll last butlast sigs reify second ffirst fnext nfirst nnext meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"
+},s={begin:n,relevance:0},o={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",
+relevance:0},i=e.inherit(e.QUOTE_STRING_MODE,{illegal:null
+}),c=e.COMMENT(";","$",{relevance:0}),d={className:"literal",
+begin:/\b(true|false|nil)\b/},l={begin:"[\\[\\{]",end:"[\\]\\}]"},m={
+className:"comment",begin:"\\^"+n},p=e.COMMENT("\\^\\{","\\}"),u={
+className:"symbol",begin:"[:]{1,2}"+n},f={begin:"\\(",end:"\\)"},h={
+endsWithParent:!0,relevance:0},y={keywords:a,className:"name",begin:n,
+relevance:0,starts:h},g=[f,i,m,p,c,u,l,o,d,s],b={beginKeywords:r,lexemes:n,
+end:'(\\[|#|\\d|"|:|\\{|\\)|\\(|$)',contains:[{className:"title",begin:n,
+relevance:0,excludeEnd:!0,endsParent:!0}].concat(g)}
+;return f.contains=[e.COMMENT("comment",""),b,y,h],
+h.contains=g,l.contains=g,p.contains=[l],{name:"Clojure",aliases:["clj"],
+illegal:/\S/,contains:[f,i,m,p,c,u,l,o,d]}}})());
+hljs.registerLanguage("clojure-repl",(()=>{"use strict";return e=>({
+name:"Clojure REPL",contains:[{className:"meta",begin:/^([\w.-]+|\s*#_)?=>/,
+starts:{end:/$/,subLanguage:"clojure"}}]})})());
+hljs.registerLanguage("cmake",(()=>{"use strict";return e=>({name:"CMake",
+aliases:["cmake.in"],case_insensitive:!0,keywords:{
+keyword:"break cmake_host_system_information cmake_minimum_required cmake_parse_arguments cmake_policy configure_file continue elseif else endforeach endfunction endif endmacro endwhile execute_process file find_file find_library find_package find_path find_program foreach function get_cmake_property get_directory_property get_filename_component get_property if include include_guard list macro mark_as_advanced math message option return separate_arguments set_directory_properties set_property set site_name string unset variable_watch while add_compile_definitions add_compile_options add_custom_command add_custom_target add_definitions add_dependencies add_executable add_library add_link_options add_subdirectory add_test aux_source_directory build_command create_test_sourcelist define_property enable_language enable_testing export fltk_wrap_ui get_source_file_property get_target_property get_test_property include_directories include_external_msproject include_regular_expression install link_directories link_libraries load_cache project qt_wrap_cpp qt_wrap_ui remove_definitions set_source_files_properties set_target_properties set_tests_properties source_group target_compile_definitions target_compile_features target_compile_options target_include_directories target_link_directories target_link_libraries target_link_options target_sources try_compile try_run ctest_build ctest_configure ctest_coverage ctest_empty_binary_directory ctest_memcheck ctest_read_custom_files ctest_run_script ctest_sleep ctest_start ctest_submit ctest_test ctest_update ctest_upload build_name exec_program export_library_dependencies install_files install_programs install_targets load_command make_directory output_required_files remove subdir_depends subdirs use_mangled_mesa utility_source variable_requires write_file qt5_use_modules qt5_use_package qt5_wrap_cpp on off true false and or not command policy target test exists is_newer_than is_directory is_symlink is_absolute matches less greater equal less_equal greater_equal strless strgreater strequal strless_equal strgreater_equal version_less version_greater version_equal version_less_equal version_greater_equal in_list defined"
+},contains:[{className:"variable",begin:/\$\{/,end:/\}/
+},e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE]})})());
+hljs.registerLanguage("coffeescript",(()=>{"use strict"
+;const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"])
+;return r=>{const t={
+keyword:e.concat(["then","unless","until","loop","by","when","and","or","is","isnt","not"]).filter((i=["var","const","let","function","static"],
+e=>!i.includes(e))),literal:n.concat(["yes","no","on","off"]),
+built_in:a.concat(["npm","print"])};var i;const s="[A-Za-z$_][0-9A-Za-z$_]*",o={
+className:"subst",begin:/#\{/,end:/\}/,keywords:t
+},c=[r.BINARY_NUMBER_MODE,r.inherit(r.C_NUMBER_MODE,{starts:{end:"(\\s*/)?",
+relevance:0}}),{className:"string",variants:[{begin:/'''/,end:/'''/,
+contains:[r.BACKSLASH_ESCAPE]},{begin:/'/,end:/'/,contains:[r.BACKSLASH_ESCAPE]
+},{begin:/"""/,end:/"""/,contains:[r.BACKSLASH_ESCAPE,o]},{begin:/"/,end:/"/,
+contains:[r.BACKSLASH_ESCAPE,o]}]},{className:"regexp",variants:[{begin:"///",
+end:"///",contains:[o,r.HASH_COMMENT_MODE]},{begin:"//[gim]{0,3}(?=\\W)",
+relevance:0},{begin:/\/(?![ *]).*?(?![\\]).\/[gim]{0,3}(?=\W)/}]},{begin:"@"+s
+},{subLanguage:"javascript",excludeBegin:!0,excludeEnd:!0,variants:[{
+begin:"```",end:"```"},{begin:"`",end:"`"}]}];o.contains=c
+;const l=r.inherit(r.TITLE_MODE,{begin:s}),d="(\\(.*\\)\\s*)?\\B[-=]>",g={
+className:"params",begin:"\\([^\\(]",returnBegin:!0,contains:[{begin:/\(/,
+end:/\)/,keywords:t,contains:["self"].concat(c)}]};return{name:"CoffeeScript",
+aliases:["coffee","cson","iced"],keywords:t,illegal:/\/\*/,
+contains:c.concat([r.COMMENT("###","###"),r.HASH_COMMENT_MODE,{
+className:"function",begin:"^\\s*"+s+"\\s*=\\s*"+d,end:"[-=]>",returnBegin:!0,
+contains:[l,g]},{begin:/[:\(,=]\s*/,relevance:0,contains:[{className:"function",
+begin:d,end:"[-=]>",returnBegin:!0,contains:[g]}]},{className:"class",
+beginKeywords:"class",end:"$",illegal:/[:="\[\]]/,contains:[{
+beginKeywords:"extends",endsWithParent:!0,illegal:/[:="\[\]]/,contains:[l]},l]
+},{begin:s+":",end:":",returnBegin:!0,returnEnd:!0,relevance:0}])}}})());
+hljs.registerLanguage("coq",(()=>{"use strict";return e=>({name:"Coq",keywords:{
+keyword:"_|0 as at cofix else end exists exists2 fix for forall fun if IF in let match mod Prop return Set then Type using where with Abort About Add Admit Admitted All Arguments Assumptions Axiom Back BackTo Backtrack Bind Blacklist Canonical Cd Check Class Classes Close Coercion Coercions CoFixpoint CoInductive Collection Combined Compute Conjecture Conjectures Constant constr Constraint Constructors Context Corollary CreateHintDb Cut Declare Defined Definition Delimit Dependencies Dependent Derive Drop eauto End Equality Eval Example Existential Existentials Existing Export exporting Extern Extract Extraction Fact Field Fields File Fixpoint Focus for From Function Functional Generalizable Global Goal Grab Grammar Graph Guarded Heap Hint HintDb Hints Hypotheses Hypothesis ident Identity If Immediate Implicit Import Include Inductive Infix Info Initial Inline Inspect Instance Instances Intro Intros Inversion Inversion_clear Language Left Lemma Let Libraries Library Load LoadPath Local Locate Ltac ML Mode Module Modules Monomorphic Morphism Next NoInline Notation Obligation Obligations Opaque Open Optimize Options Parameter Parameters Parametric Path Paths pattern Polymorphic Preterm Print Printing Program Projections Proof Proposition Pwd Qed Quit Rec Record Recursive Redirect Relation Remark Remove Require Reserved Reset Resolve Restart Rewrite Right Ring Rings Save Scheme Scope Scopes Script Search SearchAbout SearchHead SearchPattern SearchRewrite Section Separate Set Setoid Show Solve Sorted Step Strategies Strategy Structure SubClass Table Tables Tactic Term Test Theorem Time Timeout Transparent Type Typeclasses Types Undelimit Undo Unfocus Unfocused Unfold Universe Universes Unset Unshelve using Variable Variables Variant Verbose Visibility where with",
+built_in:"abstract absurd admit after apply as assert assumption at auto autorewrite autounfold before bottom btauto by case case_eq cbn cbv change classical_left classical_right clear clearbody cofix compare compute congruence constr_eq constructor contradict contradiction cut cutrewrite cycle decide decompose dependent destruct destruction dintuition discriminate discrR do double dtauto eapply eassumption eauto ecase econstructor edestruct ediscriminate eelim eexact eexists einduction einjection eleft elim elimtype enough equality erewrite eright esimplify_eq esplit evar exact exactly_once exfalso exists f_equal fail field field_simplify field_simplify_eq first firstorder fix fold fourier functional generalize generalizing gfail give_up has_evar hnf idtac in induction injection instantiate intro intro_pattern intros intuition inversion inversion_clear is_evar is_var lapply lazy left lia lra move native_compute nia nsatz omega once pattern pose progress proof psatz quote record red refine reflexivity remember rename repeat replace revert revgoals rewrite rewrite_strat right ring ring_simplify rtauto set setoid_reflexivity setoid_replace setoid_rewrite setoid_symmetry setoid_transitivity shelve shelve_unifiable simpl simple simplify_eq solve specialize split split_Rabs split_Rmult stepl stepr subst sum swap symmetry tactic tauto time timeout top transitivity trivial try tryif unfold unify until using vm_compute with"
+},contains:[e.QUOTE_STRING_MODE,e.COMMENT("\\(\\*","\\*\\)"),e.C_NUMBER_MODE,{
+className:"type",excludeBegin:!0,begin:"\\|\\s*",end:"\\w+"},{begin:/[-=]>/}]})
+})());
+hljs.registerLanguage("cos",(()=>{"use strict";return e=>({
+name:"Cach\xe9 Object Script",case_insensitive:!0,aliases:["cls"],
+keywords:"property parameter class classmethod clientmethod extends as break catch close continue do d|0 else elseif for goto halt hang h|0 if job j|0 kill k|0 lock l|0 merge new open quit q|0 read r|0 return set s|0 tcommit throw trollback try tstart use view while write w|0 xecute x|0 zkill znspace zn ztrap zwrite zw zzdump zzwrite print zbreak zinsert zload zprint zremove zsave zzprint mv mvcall mvcrt mvdim mvprint zquit zsync ascii",
+contains:[{className:"number",begin:"\\b(\\d+(\\.\\d*)?|\\.\\d+)",relevance:0},{
+className:"string",variants:[{begin:'"',end:'"',contains:[{begin:'""',
+relevance:0}]}]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{
+className:"comment",begin:/;/,end:"$",relevance:0},{className:"built_in",
+begin:/(?:\$\$?|\.\.)\^?[a-zA-Z]+/},{className:"built_in",
+begin:/\$\$\$[a-zA-Z]+/},{className:"built_in",begin:/%[a-z]+(?:\.[a-z]+)*/},{
+className:"symbol",begin:/\^%?[a-zA-Z][\w]*/},{className:"keyword",
+begin:/##class|##super|#define|#dim/},{begin:/&sql\(/,end:/\)/,excludeBegin:!0,
+excludeEnd:!0,subLanguage:"sql"},{begin:/&(js|jscript|javascript)</,end:/>/,
+excludeBegin:!0,excludeEnd:!0,subLanguage:"javascript"},{begin:/&html<\s*</,
+end:/>\s*>/,subLanguage:"xml"}]})})());
+hljs.registerLanguage("cpp",(()=>{"use strict";function e(e){
+return((...e)=>e.map((e=>(e=>e?"string"==typeof e?e:e.source:null)(e))).join(""))("(",e,")?")
+}return t=>{const n=t.COMMENT("//","$",{contains:[{begin:/\\\n/}]
+}),r="[a-zA-Z_]\\w*::",a="(decltype\\(auto\\)|"+e(r)+"[a-zA-Z_]\\w*"+e("<[^<>]+>")+")",i={
+className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},s={className:"string",
+variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",
+contains:[t.BACKSLASH_ESCAPE]},{
+begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
+end:"'",illegal:"."},t.END_SAME_AS_BEGIN({
+begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={
+className:"number",variants:[{begin:"\\b(0b[01']+)"},{
+begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"
+},{
+begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
+}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
+"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
+},contains:[{begin:/\\\n/,relevance:0},t.inherit(s,{className:"meta-string"}),{
+className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"
+},n,t.C_BLOCK_COMMENT_MODE]},l={className:"title",begin:e(r)+t.IDENT_RE,
+relevance:0},d=e(r)+t.IDENT_RE+"\\s*\\(",u={
+keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",
+built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary",
+literal:"true false nullptr NULL"},m=[c,i,n,t.C_BLOCK_COMMENT_MODE,o,s],p={
+variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{
+beginKeywords:"new throw return else",end:/;/}],keywords:u,contains:m.concat([{
+begin:/\(/,end:/\)/,keywords:u,contains:m.concat(["self"]),relevance:0}]),
+relevance:0},_={className:"function",begin:"("+a+"[\\*&\\s]+)+"+d,
+returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:u,illegal:/[^\w\s\*&:<>.]/,
+contains:[{begin:"decltype\\(auto\\)",keywords:u,relevance:0},{begin:d,
+returnBegin:!0,contains:[l],relevance:0},{begin:/::/,relevance:0},{begin:/:/,
+endsWithParent:!0,contains:[s,o]},{className:"params",begin:/\(/,end:/\)/,
+keywords:u,relevance:0,contains:[n,t.C_BLOCK_COMMENT_MODE,s,o,i,{begin:/\(/,
+end:/\)/,keywords:u,relevance:0,contains:["self",n,t.C_BLOCK_COMMENT_MODE,s,o,i]
+}]},i,n,t.C_BLOCK_COMMENT_MODE,c]};return{name:"C++",
+aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"</",
+contains:[].concat(p,_,m,[c,{
+begin:"\\b(deque|list|queue|priority_queue|pair|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",
+end:">",keywords:u,contains:["self",i]},{begin:t.IDENT_RE+"::",keywords:u},{
+className:"class",beginKeywords:"enum class struct union",end:/[{;:<>=]/,
+contains:[{beginKeywords:"final class struct"},t.TITLE_MODE]}]),exports:{
+preprocessor:c,strings:s,keywords:u}}}})());
+hljs.registerLanguage("crmsh",(()=>{"use strict";return e=>{
+const t="group clone ms master location colocation order fencing_topology rsc_ticket acl_target acl_group user role tag xml"
+;return{name:"crmsh",aliases:["crm","pcmk"],case_insensitive:!0,keywords:{
+keyword:"params meta operations op rule attributes utilization read write deny defined not_defined in_range date spec in ref reference attribute type xpath version and or lt gt tag lte gte eq ne \\ number string",
+literal:"Master Started Slave Stopped start promote demote stop monitor true false"
+},contains:[e.HASH_COMMENT_MODE,{beginKeywords:"node",starts:{
+end:"\\s*([\\w_-]+:)?",starts:{className:"title",end:"\\s*[\\$\\w_][\\w_-]*"}}
+},{beginKeywords:"primitive rsc_template",starts:{className:"title",
+end:"\\s*[\\$\\w_][\\w_-]*",starts:{end:"\\s*@?[\\w_][\\w_\\.:-]*"}}},{
+begin:"\\b("+t.split(" ").join("|")+")\\s+",keywords:t,starts:{
+className:"title",end:"[\\$\\w_][\\w_-]*"}},{
+beginKeywords:"property rsc_defaults op_defaults",starts:{className:"title",
+end:"\\s*([\\w_-]+:)?"}},e.QUOTE_STRING_MODE,{className:"meta",
+begin:"(ocf|systemd|service|lsb):[\\w_:-]+",relevance:0},{className:"number",
+begin:"\\b\\d+(\\.\\d+)?(ms|s|h|m)?",relevance:0},{className:"literal",
+begin:"[-]?(infinity|inf)",relevance:0},{className:"attr",
+begin:/([A-Za-z$_#][\w_-]+)=/,relevance:0},{className:"tag",begin:"</?",
+end:"/?>",relevance:0}]}}})());
+hljs.registerLanguage("crystal",(()=>{"use strict";return e=>{
+const n="(_?[ui](8|16|32|64|128))?",i="[a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|[=!]~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~|]|//|//=|&[-+*]=?|&\\*\\*|\\[\\][=?]?",s="[A-Za-z_]\\w*(::\\w+)*(\\?|!)?",a={
+$pattern:"[a-zA-Z_]\\w*[!?=]?",
+keyword:"abstract alias annotation as as? asm begin break case class def do else elsif end ensure enum extend for fun if include instance_sizeof is_a? lib macro module next nil? of out pointerof private protected rescue responds_to? return require select self sizeof struct super then type typeof union uninitialized unless until verbatim when while with yield __DIR__ __END_LINE__ __FILE__ __LINE__",
+literal:"false nil true"},t={className:"subst",begin:/#\{/,end:/\}/,keywords:a
+},c={className:"template-variable",variants:[{begin:"\\{\\{",end:"\\}\\}"},{
+begin:"\\{%",end:"%\\}"}],keywords:a};function r(e,n){const i=[{begin:e,end:n}]
+;return i[0].contains=i,i}const l={className:"string",
+contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/
+},{begin:/`/,end:/`/},{begin:"%[Qwi]?\\(",end:"\\)",contains:r("\\(","\\)")},{
+begin:"%[Qwi]?\\[",end:"\\]",contains:r("\\[","\\]")},{begin:"%[Qwi]?\\{",
+end:/\}/,contains:r(/\{/,/\}/)},{begin:"%[Qwi]?<",end:">",contains:r("<",">")},{
+begin:"%[Qwi]?\\|",end:"\\|"},{begin:/<<-\w+$/,end:/^\s*\w+$/}],relevance:0},b={
+className:"string",variants:[{begin:"%q\\(",end:"\\)",contains:r("\\(","\\)")},{
+begin:"%q\\[",end:"\\]",contains:r("\\[","\\]")},{begin:"%q\\{",end:/\}/,
+contains:r(/\{/,/\}/)},{begin:"%q<",end:">",contains:r("<",">")},{begin:"%q\\|",
+end:"\\|"},{begin:/<<-'\w+'$/,end:/^\s*\w+$/}],relevance:0},o={
+begin:"(?!%\\})("+e.RE_STARTERS_RE+"|\\n|\\b(case|if|select|unless|until|when|while)\\b)\\s*",
+keywords:"case if select unless until when while",contains:[{className:"regexp",
+contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:"//[a-z]*",relevance:0},{
+begin:"/(?!\\/)",end:"/[a-z]*"}]}],relevance:0},g=[c,l,b,{className:"regexp",
+contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:"%r\\(",end:"\\)",
+contains:r("\\(","\\)")},{begin:"%r\\[",end:"\\]",contains:r("\\[","\\]")},{
+begin:"%r\\{",end:/\}/,contains:r(/\{/,/\}/)},{begin:"%r<",end:">",
+contains:r("<",">")},{begin:"%r\\|",end:"\\|"}],relevance:0},o,{
+className:"meta",begin:"@\\[",end:"\\]",
+contains:[e.inherit(e.QUOTE_STRING_MODE,{className:"meta-string"})]
+},e.HASH_COMMENT_MODE,{className:"class",beginKeywords:"class module struct",
+end:"$|;",illegal:/=/,contains:[e.HASH_COMMENT_MODE,e.inherit(e.TITLE_MODE,{
+begin:s}),{begin:"<"}]},{className:"class",beginKeywords:"lib enum union",
+end:"$|;",illegal:/=/,contains:[e.HASH_COMMENT_MODE,e.inherit(e.TITLE_MODE,{
+begin:s})]},{beginKeywords:"annotation",end:"$|;",illegal:/=/,
+contains:[e.HASH_COMMENT_MODE,e.inherit(e.TITLE_MODE,{begin:s})],relevance:2},{
+className:"function",beginKeywords:"def",end:/\B\b/,
+contains:[e.inherit(e.TITLE_MODE,{begin:i,endsParent:!0})]},{
+className:"function",beginKeywords:"fun macro",end:/\B\b/,
+contains:[e.inherit(e.TITLE_MODE,{begin:i,endsParent:!0})],relevance:2},{
+className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{
+className:"symbol",begin:":",contains:[l,{begin:i}],relevance:0},{
+className:"number",variants:[{begin:"\\b0b([01_]+)"+n},{begin:"\\b0o([0-7_]+)"+n
+},{begin:"\\b0x([A-Fa-f0-9_]+)"+n},{
+begin:"\\b([1-9][0-9_]*[0-9]|[0-9])(\\.[0-9][0-9_]*)?([eE]_?[-+]?[0-9_]*)?(_?f(32|64))?(?!_)"
+},{begin:"\\b([1-9][0-9_]*|0)"+n}],relevance:0}]
+;return t.contains=g,c.contains=g.slice(1),{name:"Crystal",aliases:["cr"],
+keywords:a,contains:g}}})());
+hljs.registerLanguage("csharp",(()=>{"use strict";return e=>{var n={
+keyword:["abstract","as","base","break","case","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]),
+built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","unit","ushort"],
+literal:["default","false","null","true"]},a=e.inherit(e.TITLE_MODE,{
+begin:"[a-zA-Z](\\.?\\w)*"}),i={className:"number",variants:[{
+begin:"\\b(0b[01']+)"},{
+begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{
+begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
+}],relevance:0},s={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]
+},t=e.inherit(s,{illegal:/\n/}),r={className:"subst",begin:/\{/,end:/\}/,
+keywords:n},l=e.inherit(r,{illegal:/\n/}),c={className:"string",begin:/\$"/,
+end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/
+},e.BACKSLASH_ESCAPE,l]},o={className:"string",begin:/\$@"/,end:'"',contains:[{
+begin:/\{\{/},{begin:/\}\}/},{begin:'""'},r]},d=e.inherit(o,{illegal:/\n/,
+contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},l]})
+;r.contains=[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,i,e.C_BLOCK_COMMENT_MODE],
+l.contains=[d,c,t,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,i,e.inherit(e.C_BLOCK_COMMENT_MODE,{
+illegal:/\n/})];var g={variants:[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
+},E={begin:"<",end:">",contains:[{beginKeywords:"in out"},a]
+},_=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",b={
+begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],
+keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,
+contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{
+begin:"\x3c!--|--\x3e"},{begin:"</?",end:">"}]}]
+}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",
+end:"$",keywords:{
+"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"
+}},g,i,{beginKeywords:"class interface",relevance:0,end:/[{;=]/,
+illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"
+},a,E,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",
+relevance:0,end:/[{;=]/,illegal:/[^\s:]/,
+contains:[a,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{
+beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/,
+contains:[a,E,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",
+begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{
+className:"meta-string",begin:/"/,end:/"/}]},{
+beginKeywords:"new return throw await else",relevance:0},{className:"function",
+begin:"("+_+"\\s+)+"+e.IDENT_RE+"\\s*(<.+>\\s*)?\\(",returnBegin:!0,
+end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{
+beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial",
+relevance:0},{begin:e.IDENT_RE+"\\s*(<.+>\\s*)?\\(",returnBegin:!0,
+contains:[e.TITLE_MODE,E],relevance:0},{className:"params",begin:/\(/,end:/\)/,
+excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,
+contains:[g,i,e.C_BLOCK_COMMENT_MODE]
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},b]}}})());
+hljs.registerLanguage("csp",(()=>{"use strict";return e=>({name:"CSP",
+case_insensitive:!1,keywords:{$pattern:"[a-zA-Z][a-zA-Z0-9_-]*",
+keyword:"base-uri child-src connect-src default-src font-src form-action frame-ancestors frame-src img-src media-src object-src plugin-types report-uri sandbox script-src style-src"
+},contains:[{className:"string",begin:"'",end:"'"},{className:"attribute",
+begin:"^Content",end:":",excludeEnd:!0}]})})());
+hljs.registerLanguage("css",(()=>{"use strict"
+;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],t=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],i=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],o=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],r=["align-content","align-items","align-self","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","auto","backface-visibility","background","background-attachment","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","clear","clip","clip-path","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","content","counter-increment","counter-reset","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-variant-ligatures","font-variation-settings","font-weight","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inherit","initial","justify-content","left","letter-spacing","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marks","mask","max-height","max-width","min-height","min-width","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","perspective","perspective-origin","pointer-events","position","quotes","resize","right","src","tab-size","table-layout","text-align","text-align-last","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-indent","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","white-space","widows","width","word-break","word-spacing","word-wrap","z-index"].reverse()
+;return n=>{const a=(e=>({IMPORTANT:{className:"meta",begin:"!important"},
+HEXCOLOR:{className:"number",begin:"#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})"},
+ATTRIBUTE_SELECTOR_MODE:{className:"selector-attr",begin:/\[/,end:/\]/,
+illegal:"$",contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]}
+}))(n),l=[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE];return{name:"CSS",
+case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"},
+classNameAliases:{keyframePosition:"selector-tag"},
+contains:[n.C_BLOCK_COMMENT_MODE,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/
+},n.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0
+},{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0
+},a.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{
+begin:":("+i.join("|")+")"},{begin:"::("+o.join("|")+")"}]},{
+className:"attribute",begin:"\\b("+r.join("|")+")\\b"},{begin:":",end:"[;}]",
+contains:[a.HEXCOLOR,a.IMPORTANT,n.CSS_NUMBER_MODE,...l,{
+begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri"
+},contains:[{className:"string",begin:/[^)]/,endsWithParent:!0,excludeEnd:!0}]
+},{className:"built_in",begin:/[\w-]+(?=\()/}]},{
+begin:(s=/@/,((...e)=>e.map((e=>(e=>e?"string"==typeof e?e:e.source:null)(e))).join(""))("(?=",s,")")),
+end:"[{;]",relevance:0,illegal:/:/,contains:[{className:"keyword",
+begin:/@-?\w[\w]*(-\w+)*/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,
+relevance:0,keywords:{$pattern:/[a-z-]+/,keyword:"and or not only",
+attribute:t.join(" ")},contains:[{begin:/[a-z-]+(?=:)/,className:"attribute"
+},...l,n.CSS_NUMBER_MODE]}]},{className:"selector-tag",
+begin:"\\b("+e.join("|")+")\\b"}]};var s}})());
+hljs.registerLanguage("d",(()=>{"use strict";return e=>{const a={
+$pattern:e.UNDERSCORE_IDENT_RE,
+keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",
+built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",
+literal:"false null true"
+},d="((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))",n="\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",t={
+className:"number",begin:"\\b"+d+"(L|u|U|Lu|LU|uL|UL)?",relevance:0},_={
+className:"number",
+begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|"+d+"(i|[fF]i|Li))",
+relevance:0},r={className:"string",begin:"'("+n+"|.)",end:"'",illegal:"."},i={
+className:"string",begin:'"',contains:[{begin:n,relevance:0}],end:'"[cwd]?'
+},s=e.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{
+name:"D",keywords:a,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,{
+className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},i,{
+className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",
+begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},_,t,r,{
+className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",
+begin:"#(line)",end:"$",relevance:5},{className:"keyword",
+begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}}})());
+hljs.registerLanguage("markdown",(()=>{"use strict";function n(...n){
+return n.map((n=>{return(e=n)?"string"==typeof e?e:e.source:null;var e
+})).join("")}return e=>{const a={begin:/<\/?[A-Za-z_]/,end:">",
+subLanguage:"xml",relevance:0},i={variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0
+},{begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/,
+relevance:2},{begin:n(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/),
+relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{
+begin:/\[.+?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{
+className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0,
+returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)",
+excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[",
+end:"\\]",excludeBegin:!0,excludeEnd:!0}]},s={className:"strong",contains:[],
+variants:[{begin:/_{2}/,end:/_{2}/},{begin:/\*{2}/,end:/\*{2}/}]},c={
+className:"emphasis",contains:[],variants:[{begin:/\*(?!\*)/,end:/\*/},{
+begin:/_(?!_)/,end:/_/,relevance:0}]};s.contains.push(c),c.contains.push(s)
+;let t=[a,i]
+;return s.contains=s.contains.concat(t),c.contains=c.contains.concat(t),
+t=t.concat(s,c),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{
+className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:t},{
+begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",
+contains:t}]}]},a,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",
+end:"\\s+",excludeEnd:!0},s,c,{className:"quote",begin:"^>\\s+",contains:t,
+end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{
+begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{
+begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",
+contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{
+begin:"^[-\\*]{3,}",end:"$"},i,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{
+className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{
+className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}})());
+hljs.registerLanguage("dart",(()=>{"use strict";return e=>{const n={
+className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"}]},a={className:"subst",
+variants:[{begin:/\$\{/,end:/\}/}],keywords:"true false null this is new super"
+},t={className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',
+end:'"""'},{begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"
+},{begin:"'''",end:"'''",contains:[e.BACKSLASH_ESCAPE,n,a]},{begin:'"""',
+end:'"""',contains:[e.BACKSLASH_ESCAPE,n,a]},{begin:"'",end:"'",illegal:"\\n",
+contains:[e.BACKSLASH_ESCAPE,n,a]},{begin:'"',end:'"',illegal:"\\n",
+contains:[e.BACKSLASH_ESCAPE,n,a]}]};a.contains=[e.C_NUMBER_MODE,t]
+;const i=["Comparable","DateTime","Duration","Function","Iterable","Iterator","List","Map","Match","Object","Pattern","RegExp","Set","Stopwatch","String","StringBuffer","StringSink","Symbol","Type","Uri","bool","double","int","num","Element","ElementList"],r=i.map((e=>e+"?"))
+;return{name:"Dart",keywords:{
+keyword:"abstract as assert async await break case catch class const continue covariant default deferred do dynamic else enum export extends extension external factory false final finally for Function get hide if implements import in inferface is late library mixin new null on operator part required rethrow return set show static super switch sync this throw true try typedef var void while with yield",
+built_in:i.concat(r).concat(["Never","Null","dynamic","print","document","querySelector","querySelectorAll","window"]),
+$pattern:/[A-Za-z][A-Za-z0-9_]*\??/},
+contains:[t,e.COMMENT(/\/\*\*(?!\/)/,/\*\//,{subLanguage:"markdown",relevance:0
+}),e.COMMENT(/\/{3,} ?/,/$/,{contains:[{subLanguage:"markdown",begin:".",
+end:"$",relevance:0}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{
+className:"class",beginKeywords:"class interface",end:/\{/,excludeEnd:!0,
+contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]
+},e.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}}})());
+hljs.registerLanguage("delphi",(()=>{"use strict";return e=>{
+const r="exports register file shl array record property for mod while set ally label uses raise not stored class safecall var interface or private static exit index inherited to else stdcall override shr asm far resourcestring finalization packed virtual out and protected library do xorwrite goto near function end div overload object unit begin string on inline repeat until destructor write message program with read initialization except default nil if case cdecl in downto threadvar of try pascal const external constructor type public then implementation finally published procedure absolute reintroduce operator as is abstract alias assembler bitpacked break continue cppdecl cvar enumerator experimental platform deprecated unimplemented dynamic export far16 forward generic helper implements interrupt iochecks local name nodefault noreturn nostackframe oldfpccall otherwise saveregisters softfloat specialize strict unaligned varargs ",a=[e.C_LINE_COMMENT_MODE,e.COMMENT(/\{/,/\}/,{
+relevance:0}),e.COMMENT(/\(\*/,/\*\)/,{relevance:10})],t={className:"meta",
+variants:[{begin:/\{\$/,end:/\}/},{begin:/\(\*\$/,end:/\*\)/}]},n={
+className:"string",begin:/'/,end:/'/,contains:[{begin:/''/}]},s={
+className:"string",begin:/(#\d+)+/},i={begin:e.IDENT_RE+"\\s*=\\s*class\\s*\\(",
+returnBegin:!0,contains:[e.TITLE_MODE]},c={className:"function",
+beginKeywords:"function constructor destructor procedure",end:/[:;]/,
+keywords:"function constructor|10 destructor|10 procedure|10",
+contains:[e.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:r,
+contains:[n,s,t].concat(a)},t].concat(a)};return{name:"Delphi",
+aliases:["dpr","dfm","pas","pascal","freepascal","lazarus","lpr","lfm"],
+case_insensitive:!0,keywords:r,illegal:/"|\$[G-Zg-z]|\/\*|<\/|\|/,
+contains:[n,s,e.NUMBER_MODE,{className:"number",relevance:0,variants:[{
+begin:"\\$[0-9A-Fa-f]+"},{begin:"&[0-7]+"},{begin:"%[01]+"}]},i,c,t].concat(a)}}
+})());
+hljs.registerLanguage("diff",(()=>{"use strict";return e=>({name:"Diff",
+aliases:["patch"],contains:[{className:"meta",relevance:10,variants:[{
+begin:/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/},{begin:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{
+begin:/^--- +\d+,\d+ +----$/}]},{className:"comment",variants:[{begin:/Index: /,
+end:/$/},{begin:/^index/,end:/$/},{begin:/={3,}/,end:/$/},{begin:/^-{3}/,end:/$/
+},{begin:/^\*{3} /,end:/$/},{begin:/^\+{3}/,end:/$/},{begin:/^\*{15}$/},{
+begin:/^diff --git/,end:/$/}]},{className:"addition",begin:/^\+/,end:/$/},{
+className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/,
+end:/$/}]})})());
+hljs.registerLanguage("django",(()=>{"use strict";return e=>{const t={
+begin:/\|[A-Za-z]+:?/,keywords:{
+name:"truncatewords removetags linebreaksbr yesno get_digit timesince random striptags filesizeformat escape linebreaks length_is ljust rjust cut urlize fix_ampersands title floatformat capfirst pprint divisibleby add make_list unordered_list urlencode timeuntil urlizetrunc wordcount stringformat linenumbers slice date dictsort dictsortreversed default_if_none pluralize lower join center default truncatewords_html upper length phone2numeric wordwrap time addslashes slugify first escapejs force_escape iriencode last safe safeseq truncatechars localize unlocalize localtime utc timezone"
+},contains:[e.QUOTE_STRING_MODE,e.APOS_STRING_MODE]};return{name:"Django",
+aliases:["jinja"],case_insensitive:!0,subLanguage:"xml",
+contains:[e.COMMENT(/\{%\s*comment\s*%\}/,/\{%\s*endcomment\s*%\}/),e.COMMENT(/\{#/,/#\}/),{
+className:"template-tag",begin:/\{%/,end:/%\}/,contains:[{className:"name",
+begin:/\w+/,keywords:{
+name:"comment endcomment load templatetag ifchanged endifchanged if endif firstof for endfor ifnotequal endifnotequal widthratio extends include spaceless endspaceless regroup ifequal endifequal ssi now with cycle url filter endfilter debug block endblock else autoescape endautoescape csrf_token empty elif endwith static trans blocktrans endblocktrans get_static_prefix get_media_prefix plural get_current_language language get_available_languages get_current_language_bidi get_language_info get_language_info_list localize endlocalize localtime endlocaltime timezone endtimezone get_current_timezone verbatim"
+},starts:{endsWithParent:!0,keywords:"in by as",contains:[t],relevance:0}}]},{
+className:"template-variable",begin:/\{\{/,end:/\}\}/,contains:[t]}]}}})());
+hljs.registerLanguage("dns",(()=>{"use strict";return d=>({name:"DNS Zone",
+aliases:["bind","zone"],keywords:{
+keyword:"IN A AAAA AFSDB APL CAA CDNSKEY CDS CERT CNAME DHCID DLV DNAME DNSKEY DS HIP IPSECKEY KEY KX LOC MX NAPTR NS NSEC NSEC3 NSEC3PARAM PTR RRSIG RP SIG SOA SRV SSHFP TA TKEY TLSA TSIG TXT"
+},contains:[d.COMMENT(";","$",{relevance:0}),{className:"meta",
+begin:/^\$(TTL|GENERATE|INCLUDE|ORIGIN)\b/},{className:"number",
+begin:"((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))\\b"
+},{className:"number",
+begin:"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\b"
+},d.inherit(d.NUMBER_MODE,{begin:/\b\d+[dhwm]?/})]})})());
+hljs.registerLanguage("dockerfile",(()=>{"use strict";return e=>({
+name:"Dockerfile",aliases:["docker"],case_insensitive:!0,
+keywords:"from maintainer expose env arg user onbuild stopsignal",
+contains:[e.HASH_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{
+beginKeywords:"run cmd entrypoint volume add copy workdir label healthcheck shell",
+starts:{end:/[^\\]$/,subLanguage:"bash"}}],illegal:"</"})})());
+hljs.registerLanguage("dos",(()=>{"use strict";return e=>{
+const t=e.COMMENT(/^\s*@?rem\b/,/$/,{relevance:10});return{
+name:"Batch file (DOS)",aliases:["bat","cmd"],case_insensitive:!0,
+illegal:/\/\*/,keywords:{
+keyword:"if else goto for in do call exit not exist errorlevel defined equ neq lss leq gtr geq",
+built_in:"prn nul lpt3 lpt2 lpt1 con com4 com3 com2 com1 aux shift cd dir echo setlocal endlocal set pause copy append assoc at attrib break cacls cd chcp chdir chkdsk chkntfs cls cmd color comp compact convert date dir diskcomp diskcopy doskey erase fs find findstr format ftype graftabl help keyb label md mkdir mode more move path pause print popd pushd promt rd recover rem rename replace restore rmdir shift sort start subst time title tree type ver verify vol ping net ipconfig taskkill xcopy ren del"
+},contains:[{className:"variable",begin:/%%[^ ]|%[^ ]+?%|![^ ]+?!/},{
+className:"function",begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)",
+end:"goto:eof",contains:[e.inherit(e.TITLE_MODE,{
+begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),t]},{
+className:"number",begin:"\\b\\d+",relevance:0},t]}}})());
+hljs.registerLanguage("dsconfig",(()=>{"use strict";return e=>({
+keywords:"dsconfig",contains:[{className:"keyword",begin:"^dsconfig",end:/\s/,
+excludeEnd:!0,relevance:10},{className:"built_in",
+begin:/(list|create|get|set|delete)-(\w+)/,end:/\s/,excludeEnd:!0,
+illegal:"!@#$%^&*()",relevance:10},{className:"built_in",begin:/--(\w+)/,
+end:/\s/,excludeEnd:!0},{className:"string",begin:/"/,end:/"/},{
+className:"string",begin:/'/,end:/'/},{className:"string",begin:/[\w\-?]+:\w+/,
+end:/\W/,relevance:0},{className:"string",begin:/\w+(\-\w+)*/,end:/(?=\W)/,
+relevance:0},e.HASH_COMMENT_MODE]})})());
+hljs.registerLanguage("dts",(()=>{"use strict";return e=>{const n={
+className:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{
+begin:'((u8?|U)|L)?"'}),{begin:'(u8?|U)?R"',end:'"',
+contains:[e.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},a={
+className:"number",variants:[{
+begin:"\\b(\\d+(\\.\\d*)?|\\.\\d+)(u|U|l|L|ul|UL|f|F)"},{begin:e.C_NUMBER_RE}],
+relevance:0},s={className:"meta",begin:"#",end:"$",keywords:{
+"meta-keyword":"if else elif endif define undef ifdef ifndef"},contains:[{
+begin:/\\\n/,relevance:0},{beginKeywords:"include",end:"$",keywords:{
+"meta-keyword":"include"},contains:[e.inherit(n,{className:"meta-string"}),{
+className:"meta-string",begin:"<",end:">",illegal:"\\n"}]
+},n,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},i={className:"variable",
+begin:/&[a-z\d_]*\b/},d={className:"meta-keyword",begin:"/[a-z][a-z\\d-]*/"},l={
+className:"symbol",begin:"^\\s*[a-zA-Z_][a-zA-Z\\d_]*:"},r={className:"params",
+begin:"<",end:">",contains:[a,i]},_={className:"class",
+begin:/[a-zA-Z_][a-zA-Z\d_@]*\s\{/,end:/[{;=]/,returnBegin:!0,excludeEnd:!0}
+;return{name:"Device Tree",keywords:"",contains:[{className:"class",
+begin:"/\\s*\\{",end:/\};/,relevance:10,
+contains:[i,d,l,_,r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,n]
+},i,d,l,_,r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,n,s,{
+begin:e.IDENT_RE+"::",keywords:""}]}}})());
+hljs.registerLanguage("dust",(()=>{"use strict";return e=>({name:"Dust",
+aliases:["dst"],case_insensitive:!0,subLanguage:"xml",contains:[{
+className:"template-tag",begin:/\{[#\/]/,end:/\}/,illegal:/;/,contains:[{
+className:"name",begin:/[a-zA-Z\.-]+/,starts:{endsWithParent:!0,relevance:0,
+contains:[e.QUOTE_STRING_MODE]}}]},{className:"template-variable",begin:/\{/,
+end:/\}/,illegal:/;/,keywords:"if eq ne lt lte gt gte select default math sep"}]
+})})());
+hljs.registerLanguage("ebnf",(()=>{"use strict";return e=>{
+const a=e.COMMENT(/\(\*/,/\*\)/);return{name:"Extended Backus-Naur Form",
+illegal:/\S/,contains:[a,{className:"attribute",
+begin:/^[ ]*[a-zA-Z]+([\s_-]+[a-zA-Z]+)*/},{begin:/=/,end:/[.;]/,contains:[a,{
+className:"meta",begin:/\?.*\?/},{className:"string",
+variants:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{begin:"`",end:"`"}]}]}]}}
+})());
+hljs.registerLanguage("elixir",(()=>{"use strict";return e=>{
+const n="[a-zA-Z_][a-zA-Z0-9_.]*(!|\\?)?",i={$pattern:n,
+keyword:"and false then defined module in return redo retry end for true self when next until do begin unless nil break not case cond alias while ensure or include use alias fn quote require import with|0"
+},a={className:"subst",begin:/#\{/,end:/\}/,keywords:i},s={className:"number",
+begin:"(\\b0o[0-7_]+)|(\\b0b[01_]+)|(\\b0x[0-9a-fA-F_]+)|(-?\\b[1-9][0-9_]*(\\.[0-9_]+([eE][-+]?[0-9]+)?)?)",
+relevance:0},b={className:"string",begin:"~[a-z](?=[/|([{<\"'])",contains:[{
+endsParent:!0,contains:[{contains:[e.BACKSLASH_ESCAPE,a],variants:[{begin:/"/,
+end:/"/},{begin:/'/,end:/'/},{begin:/\//,end:/\//},{begin:/\|/,end:/\|/},{
+begin:/\(/,end:/\)/},{begin:/\[/,end:/\]/},{begin:/\{/,end:/\}/},{begin:/</,
+end:/>/}]}]}]},d={className:"string",contains:[e.BACKSLASH_ESCAPE,a],variants:[{
+begin:/"""/,end:/"""/},{begin:/'''/,end:/'''/},{begin:/~S"""/,end:/"""/,
+contains:[]},{begin:/~S"/,end:/"/,contains:[]},{begin:/~S'''/,end:/'''/,
+contains:[]},{begin:/~S'/,end:/'/,contains:[]},{begin:/'/,end:/'/},{begin:/"/,
+end:/"/}]},r={className:"function",beginKeywords:"def defp defmacro",end:/\B\b/,
+contains:[e.inherit(e.TITLE_MODE,{begin:n,endsParent:!0})]},g=e.inherit(r,{
+className:"class",beginKeywords:"defimpl defmodule defprotocol defrecord",
+end:/\bdo\b|$|;/}),t=[d,{className:"string",begin:"~[A-Z](?=[/|([{<\"'])",
+contains:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/\//,end:/\//},{
+begin:/\|/,end:/\|/},{begin:/\(/,end:/\)/},{begin:/\[/,end:/\]/},{begin:/\{/,
+end:/\}/},{begin:/</,end:/>/}]},b,e.HASH_COMMENT_MODE,g,r,{begin:"::"},{
+className:"symbol",begin:":(?![\\s:])",contains:[d,{
+begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"
+}],relevance:0},{className:"symbol",begin:n+":(?!:)",relevance:0},s,{
+className:"variable",begin:"(\\$\\W)|((\\$|@@?)(\\w+))"},{begin:"->"},{
+begin:"("+e.RE_STARTERS_RE+")\\s*",contains:[e.HASH_COMMENT_MODE,{
+begin:/\/: (?=\d+\s*[,\]])/,relevance:0,contains:[s]},{className:"regexp",
+illegal:"\\n",contains:[e.BACKSLASH_ESCAPE,a],variants:[{begin:"/",end:"/[a-z]*"
+},{begin:"%r\\[",end:"\\][a-z]*"}]}],relevance:0}];return a.contains=t,{
+name:"Elixir",keywords:i,contains:t}}})());
+hljs.registerLanguage("elm",(()=>{"use strict";return e=>{const n={
+variants:[e.COMMENT("--","$"),e.COMMENT(/\{-/,/-\}/,{contains:["self"]})]},i={
+className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},s={begin:"\\(",end:"\\)",
+illegal:'"',contains:[{className:"type",
+begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},n]};return{name:"Elm",
+keywords:"let in if then else case of where module import exposing type alias as infix infixl infixr port effect command subscription",
+contains:[{beginKeywords:"port effect module",end:"exposing",
+keywords:"port effect module where command subscription exposing",
+contains:[s,n],illegal:"\\W\\.|;"},{begin:"import",end:"$",
+keywords:"import as exposing",contains:[s,n],illegal:"\\W\\.|;"},{begin:"type",
+end:"$",keywords:"type alias",contains:[i,s,{begin:/\{/,end:/\}/,
+contains:s.contains},n]},{beginKeywords:"infix infixl infixr",end:"$",
+contains:[e.C_NUMBER_MODE,n]},{begin:"port",end:"$",keywords:"port",contains:[n]
+},{className:"string",begin:"'\\\\?.",end:"'",illegal:"."
+},e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,i,e.inherit(e.TITLE_MODE,{
+begin:"^[_a-z][\\w']*"}),n,{begin:"->|<-"}],illegal:/;/}}})());
+hljs.registerLanguage("ruby",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>{
+const a="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",i={
+keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor __FILE__",
+built_in:"proc lambda",literal:"true false nil"},s={className:"doctag",
+begin:"@[A-Za-z]+"},r={begin:"#<",end:">"},b=[n.COMMENT("#","$",{contains:[s]
+}),n.COMMENT("^=begin","^=end",{contains:[s],relevance:10
+}),n.COMMENT("^__END__","\\n$")],c={className:"subst",begin:/#\{/,end:/\}/,
+keywords:i},t={className:"string",contains:[n.BACKSLASH_ESCAPE,c],variants:[{
+begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:/%[qQwWx]?\(/,
+end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{begin:/%[qQwWx]?\{/,end:/\}/},{
+begin:/%[qQwWx]?</,end:/>/},{begin:/%[qQwWx]?\//,end:/\//},{begin:/%[qQwWx]?%/,
+end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{begin:/%[qQwWx]?\|/,end:/\|/},{
+begin:/\B\?(\\\d{1,3})/},{begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{
+begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{
+begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{
+begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{
+begin:/<<[-~]?'?(\w+)\n(?:[^\n]*\n)*?\s*\1\b/,returnBegin:!0,contains:[{
+begin:/<<[-~]?'?/},n.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,
+contains:[n.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",d={className:"number",
+relevance:0,variants:[{
+begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{
+begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b"
+},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{
+begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{
+begin:"\\b0(_?[0-7])+r?i?\\b"}]},l={className:"params",begin:"\\(",end:"\\)",
+endsParent:!0,keywords:i},o=[t,{className:"class",beginKeywords:"class module",
+end:"$|;",illegal:/=/,contains:[n.inherit(n.TITLE_MODE,{
+begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|!)?"}),{begin:"<\\s*",contains:[{
+begin:"("+n.IDENT_RE+"::)?"+n.IDENT_RE,relevance:0}]}].concat(b)},{
+className:"function",begin:e(/def\s*/,(_=a+"\\s*(\\(|;|$)",e("(?=",_,")"))),
+relevance:0,keywords:"def",end:"$|;",contains:[n.inherit(n.TITLE_MODE,{begin:a
+}),l].concat(b)},{begin:n.IDENT_RE+"::"},{className:"symbol",
+begin:n.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol",
+begin:":(?!\\s)",contains:[t,{begin:a}],relevance:0},d,{className:"variable",
+begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{
+className:"params",begin:/\|/,end:/\|/,relevance:0,keywords:i},{
+begin:"("+n.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[{
+className:"regexp",contains:[n.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{
+begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{begin:"%r\\(",
+end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]
+}].concat(r,b),relevance:0}].concat(r,b);var _;c.contains=o,l.contains=o
+;const E=[{begin:/^\s*=>/,starts:{end:"$",contains:o}},{className:"meta",
+begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])",
+starts:{end:"$",contains:o}}];return b.unshift(r),{name:"Ruby",
+aliases:["rb","gemspec","podspec","thor","irb"],keywords:i,illegal:/\/\*/,
+contains:[n.SHEBANG({binary:"ruby"})].concat(E).concat(b).concat(o)}}})());
+hljs.registerLanguage("erb",(()=>{"use strict";return e=>({name:"ERB",
+subLanguage:"xml",contains:[e.COMMENT("<%#","%>"),{begin:"<%[%=-]?",
+end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0}]})})());
+hljs.registerLanguage("erlang-repl",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>({name:"Erlang REPL",keywords:{
+built_in:"spawn spawn_link self",
+keyword:"after and andalso|10 band begin bnot bor bsl bsr bxor case catch cond div end fun if let not of or orelse|10 query receive rem try when xor"
+},contains:[{className:"meta",begin:"^[0-9]+> ",relevance:10
+},n.COMMENT("%","$"),{className:"number",
+begin:"\\b(\\d+(_\\d+)*#[a-fA-F0-9]+(_[a-fA-F0-9]+)*|\\d+(_\\d+)*(\\.\\d+(_\\d+)*)?([eE][-+]?\\d+)?)",
+relevance:0},n.APOS_STRING_MODE,n.QUOTE_STRING_MODE,{
+begin:e(/\?(::)?/,/([A-Z]\w*)/,/((::)[A-Z]\w*)*/)},{begin:"->"},{begin:"ok"},{
+begin:"!"},{
+begin:"(\\b[a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*)|(\\b[a-z'][a-zA-Z0-9_']*)",
+relevance:0},{begin:"[A-Z][a-zA-Z0-9_']*",relevance:0}]})})());
+hljs.registerLanguage("erlang",(()=>{"use strict";return e=>{
+const n="[a-z'][a-zA-Z0-9_']*",r="("+n+":"+n+"|"+n+")",a={
+keyword:"after and andalso|10 band begin bnot bor bsl bzr bxor case catch cond div end fun if let not of orelse|10 query receive rem try when xor",
+literal:"false true"},i=e.COMMENT("%","$"),s={className:"number",
+begin:"\\b(\\d+(_\\d+)*#[a-fA-F0-9]+(_[a-fA-F0-9]+)*|\\d+(_\\d+)*(\\.\\d+(_\\d+)*)?([eE][-+]?\\d+)?)",
+relevance:0},c={begin:"fun\\s+"+n+"/\\d+"},t={begin:r+"\\(",end:"\\)",
+returnBegin:!0,relevance:0,contains:[{begin:r,relevance:0},{begin:"\\(",
+end:"\\)",endsWithParent:!0,returnEnd:!0,relevance:0}]},d={begin:/\{/,end:/\}/,
+relevance:0},o={begin:"\\b_([A-Z][A-Za-z0-9_]*)?",relevance:0},l={
+begin:"[A-Z][a-zA-Z0-9_]*",relevance:0},b={begin:"#"+e.UNDERSCORE_IDENT_RE,
+relevance:0,returnBegin:!0,contains:[{begin:"#"+e.UNDERSCORE_IDENT_RE,
+relevance:0},{begin:/\{/,end:/\}/,relevance:0}]},g={
+beginKeywords:"fun receive if try case",end:"end",keywords:a}
+;g.contains=[i,c,e.inherit(e.APOS_STRING_MODE,{className:""
+}),g,t,e.QUOTE_STRING_MODE,s,d,o,l,b]
+;const E=[i,c,g,t,e.QUOTE_STRING_MODE,s,d,o,l,b]
+;t.contains[1].contains=E,d.contains=E,b.contains[1].contains=E;const u={
+className:"params",begin:"\\(",end:"\\)",contains:E};return{name:"Erlang",
+aliases:["erl"],keywords:a,illegal:"(</|\\*=|\\+=|-=|/\\*|\\*/|\\(\\*|\\*\\))",
+contains:[{className:"function",begin:"^"+n+"\\s*\\(",end:"->",returnBegin:!0,
+illegal:"\\(|#|//|/\\*|\\\\|:|;",contains:[u,e.inherit(e.TITLE_MODE,{begin:n})],
+starts:{end:";|\\.",keywords:a,contains:E}},i,{begin:"^-",end:"\\.",relevance:0,
+excludeEnd:!0,returnBegin:!0,keywords:{$pattern:"-"+e.IDENT_RE,
+keyword:["-module","-record","-undef","-export","-ifdef","-ifndef","-author","-copyright","-doc","-vsn","-import","-include","-include_lib","-compile","-define","-else","-endif","-file","-behaviour","-behavior","-spec"].map((e=>e+"|1.5")).join(" ")
+},contains:[u]},s,e.QUOTE_STRING_MODE,b,o,l,d,{begin:/\.$/}]}}})());
+hljs.registerLanguage("excel",(()=>{"use strict";return E=>({
+name:"Excel formulae",aliases:["xlsx","xls"],case_insensitive:!0,keywords:{
+$pattern:/[a-zA-Z][\w\.]*/,
+built_in:"ABS ACCRINT ACCRINTM ACOS ACOSH ACOT ACOTH AGGREGATE ADDRESS AMORDEGRC AMORLINC AND ARABIC AREAS ASC ASIN ASINH ATAN ATAN2 ATANH AVEDEV AVERAGE AVERAGEA AVERAGEIF AVERAGEIFS BAHTTEXT BASE BESSELI BESSELJ BESSELK BESSELY BETADIST BETA.DIST BETAINV BETA.INV BIN2DEC BIN2HEX BIN2OCT BINOMDIST BINOM.DIST BINOM.DIST.RANGE BINOM.INV BITAND BITLSHIFT BITOR BITRSHIFT BITXOR CALL CEILING CEILING.MATH CEILING.PRECISE CELL CHAR CHIDIST CHIINV CHITEST CHISQ.DIST CHISQ.DIST.RT CHISQ.INV CHISQ.INV.RT CHISQ.TEST CHOOSE CLEAN CODE COLUMN COLUMNS COMBIN COMBINA COMPLEX CONCAT CONCATENATE CONFIDENCE CONFIDENCE.NORM CONFIDENCE.T CONVERT CORREL COS COSH COT COTH COUNT COUNTA COUNTBLANK COUNTIF COUNTIFS COUPDAYBS COUPDAYS COUPDAYSNC COUPNCD COUPNUM COUPPCD COVAR COVARIANCE.P COVARIANCE.S CRITBINOM CSC CSCH CUBEKPIMEMBER CUBEMEMBER CUBEMEMBERPROPERTY CUBERANKEDMEMBER CUBESET CUBESETCOUNT CUBEVALUE CUMIPMT CUMPRINC DATE DATEDIF DATEVALUE DAVERAGE DAY DAYS DAYS360 DB DBCS DCOUNT DCOUNTA DDB DEC2BIN DEC2HEX DEC2OCT DECIMAL DEGREES DELTA DEVSQ DGET DISC DMAX DMIN DOLLAR DOLLARDE DOLLARFR DPRODUCT DSTDEV DSTDEVP DSUM DURATION DVAR DVARP EDATE EFFECT ENCODEURL EOMONTH ERF ERF.PRECISE ERFC ERFC.PRECISE ERROR.TYPE EUROCONVERT EVEN EXACT EXP EXPON.DIST EXPONDIST FACT FACTDOUBLE FALSE|0 F.DIST FDIST F.DIST.RT FILTERXML FIND FINDB F.INV F.INV.RT FINV FISHER FISHERINV FIXED FLOOR FLOOR.MATH FLOOR.PRECISE FORECAST FORECAST.ETS FORECAST.ETS.CONFINT FORECAST.ETS.SEASONALITY FORECAST.ETS.STAT FORECAST.LINEAR FORMULATEXT FREQUENCY F.TEST FTEST FV FVSCHEDULE GAMMA GAMMA.DIST GAMMADIST GAMMA.INV GAMMAINV GAMMALN GAMMALN.PRECISE GAUSS GCD GEOMEAN GESTEP GETPIVOTDATA GROWTH HARMEAN HEX2BIN HEX2DEC HEX2OCT HLOOKUP HOUR HYPERLINK HYPGEOM.DIST HYPGEOMDIST IF IFERROR IFNA IFS IMABS IMAGINARY IMARGUMENT IMCONJUGATE IMCOS IMCOSH IMCOT IMCSC IMCSCH IMDIV IMEXP IMLN IMLOG10 IMLOG2 IMPOWER IMPRODUCT IMREAL IMSEC IMSECH IMSIN IMSINH IMSQRT IMSUB IMSUM IMTAN INDEX INDIRECT INFO INT INTERCEPT INTRATE IPMT IRR ISBLANK ISERR ISERROR ISEVEN ISFORMULA ISLOGICAL ISNA ISNONTEXT ISNUMBER ISODD ISREF ISTEXT ISO.CEILING ISOWEEKNUM ISPMT JIS KURT LARGE LCM LEFT LEFTB LEN LENB LINEST LN LOG LOG10 LOGEST LOGINV LOGNORM.DIST LOGNORMDIST LOGNORM.INV LOOKUP LOWER MATCH MAX MAXA MAXIFS MDETERM MDURATION MEDIAN MID MIDBs MIN MINIFS MINA MINUTE MINVERSE MIRR MMULT MOD MODE MODE.MULT MODE.SNGL MONTH MROUND MULTINOMIAL MUNIT N NA NEGBINOM.DIST NEGBINOMDIST NETWORKDAYS NETWORKDAYS.INTL NOMINAL NORM.DIST NORMDIST NORMINV NORM.INV NORM.S.DIST NORMSDIST NORM.S.INV NORMSINV NOT NOW NPER NPV NUMBERVALUE OCT2BIN OCT2DEC OCT2HEX ODD ODDFPRICE ODDFYIELD ODDLPRICE ODDLYIELD OFFSET OR PDURATION PEARSON PERCENTILE.EXC PERCENTILE.INC PERCENTILE PERCENTRANK.EXC PERCENTRANK.INC PERCENTRANK PERMUT PERMUTATIONA PHI PHONETIC PI PMT POISSON.DIST POISSON POWER PPMT PRICE PRICEDISC PRICEMAT PROB PRODUCT PROPER PV QUARTILE QUARTILE.EXC QUARTILE.INC QUOTIENT RADIANS RAND RANDBETWEEN RANK.AVG RANK.EQ RANK RATE RECEIVED REGISTER.ID REPLACE REPLACEB REPT RIGHT RIGHTB ROMAN ROUND ROUNDDOWN ROUNDUP ROW ROWS RRI RSQ RTD SEARCH SEARCHB SEC SECH SECOND SERIESSUM SHEET SHEETS SIGN SIN SINH SKEW SKEW.P SLN SLOPE SMALL SQL.REQUEST SQRT SQRTPI STANDARDIZE STDEV STDEV.P STDEV.S STDEVA STDEVP STDEVPA STEYX SUBSTITUTE SUBTOTAL SUM SUMIF SUMIFS SUMPRODUCT SUMSQ SUMX2MY2 SUMX2PY2 SUMXMY2 SWITCH SYD T TAN TANH TBILLEQ TBILLPRICE TBILLYIELD T.DIST T.DIST.2T T.DIST.RT TDIST TEXT TEXTJOIN TIME TIMEVALUE T.INV T.INV.2T TINV TODAY TRANSPOSE TREND TRIM TRIMMEAN TRUE|0 TRUNC T.TEST TTEST TYPE UNICHAR UNICODE UPPER VALUE VAR VAR.P VAR.S VARA VARP VARPA VDB VLOOKUP WEBSERVICE WEEKDAY WEEKNUM WEIBULL WEIBULL.DIST WORKDAY WORKDAY.INTL XIRR XNPV XOR YEAR YEARFRAC YIELD YIELDDISC YIELDMAT Z.TEST ZTEST"
+},contains:[{begin:/^=/,end:/[^=]/,returnEnd:!0,illegal:/=/,relevance:10},{
+className:"symbol",begin:/\b[A-Z]{1,2}\d+\b/,end:/[^\d]/,excludeEnd:!0,
+relevance:0},{className:"symbol",begin:/[A-Z]{0,2}\d*:[A-Z]{0,2}\d*/,relevance:0
+},E.BACKSLASH_ESCAPE,E.QUOTE_STRING_MODE,{className:"number",
+begin:E.NUMBER_RE+"(%)?",relevance:0},E.COMMENT(/\bN\(/,/\)/,{excludeBegin:!0,
+excludeEnd:!0,illegal:/\n/})]})})());
+hljs.registerLanguage("fix",(()=>{"use strict";return e=>({name:"FIX",
+contains:[{begin:/[^\u2401\u0001]+/,end:/[\u2401\u0001]/,excludeEnd:!0,
+returnBegin:!0,returnEnd:!1,contains:[{begin:/([^\u2401\u0001=]+)/,
+end:/=([^\u2401\u0001=]+)/,returnEnd:!0,returnBegin:!1,className:"attr"},{
+begin:/=/,end:/([\u2401\u0001])/,excludeEnd:!0,excludeBegin:!0,
+className:"string"}]}],case_insensitive:!0})})());
+hljs.registerLanguage("flix",(()=>{"use strict";return e=>({name:"Flix",
+keywords:{literal:"true false",
+keyword:"case class def else enum if impl import in lat rel index let match namespace switch type yield with"
+},contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"string",
+begin:/'(.|\\[xXuU][a-zA-Z0-9]+)'/},{className:"string",variants:[{begin:'"',
+end:'"'}]},{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,
+excludeEnd:!0,contains:[{className:"title",relevance:0,
+begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/
+}]},e.C_NUMBER_MODE]})})());
+hljs.registerLanguage("fortran",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>{const a={variants:[n.COMMENT("!","$",{relevance:0
+}),n.COMMENT("^C[ ]","$",{relevance:0}),n.COMMENT("^C$","$",{relevance:0})]
+},t=/(_[a-z_\d]+)?/,i=/([de][+-]?\d+)?/,c={className:"number",variants:[{
+begin:e(/\b\d+/,/\.(\d*)/,i,t)},{begin:e(/\b\d+/,i,t)},{begin:e(/\.\d+/,i,t)}],
+relevance:0},o={className:"function",
+beginKeywords:"subroutine function program",illegal:"[${=\\n]",
+contains:[n.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)"}]}
+;return{name:"Fortran",case_insensitive:!0,aliases:["f90","f95"],keywords:{
+literal:".False. .True.",
+keyword:"kind do concurrent local shared while private call intrinsic where elsewhere type endtype endmodule endselect endinterface end enddo endif if forall endforall only contains default return stop then block endblock endassociate public subroutine|10 function program .and. .or. .not. .le. .eq. .ge. .gt. .lt. goto save else use module select case access blank direct exist file fmt form formatted iostat name named nextrec number opened rec recl sequential status unformatted unit continue format pause cycle exit c_null_char c_alert c_backspace c_form_feed flush wait decimal round iomsg synchronous nopass non_overridable pass protected volatile abstract extends import non_intrinsic value deferred generic final enumerator class associate bind enum c_int c_short c_long c_long_long c_signed_char c_size_t c_int8_t c_int16_t c_int32_t c_int64_t c_int_least8_t c_int_least16_t c_int_least32_t c_int_least64_t c_int_fast8_t c_int_fast16_t c_int_fast32_t c_int_fast64_t c_intmax_t C_intptr_t c_float c_double c_long_double c_float_complex c_double_complex c_long_double_complex c_bool c_char c_null_ptr c_null_funptr c_new_line c_carriage_return c_horizontal_tab c_vertical_tab iso_c_binding c_loc c_funloc c_associated  c_f_pointer c_ptr c_funptr iso_fortran_env character_storage_size error_unit file_storage_size input_unit iostat_end iostat_eor numeric_storage_size output_unit c_f_procpointer ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode newunit contiguous recursive pad position action delim readwrite eor advance nml interface procedure namelist include sequence elemental pure impure integer real character complex logical codimension dimension allocatable|10 parameter external implicit|10 none double precision assign intent optional pointer target in out common equivalence data",
+built_in:"alog alog10 amax0 amax1 amin0 amin1 amod cabs ccos cexp clog csin csqrt dabs dacos dasin datan datan2 dcos dcosh ddim dexp dint dlog dlog10 dmax1 dmin1 dmod dnint dsign dsin dsinh dsqrt dtan dtanh float iabs idim idint idnint ifix isign max0 max1 min0 min1 sngl algama cdabs cdcos cdexp cdlog cdsin cdsqrt cqabs cqcos cqexp cqlog cqsin cqsqrt dcmplx dconjg derf derfc dfloat dgamma dimag dlgama iqint qabs qacos qasin qatan qatan2 qcmplx qconjg qcos qcosh qdim qerf qerfc qexp qgamma qimag qlgama qlog qlog10 qmax1 qmin1 qmod qnint qsign qsin qsinh qsqrt qtan qtanh abs acos aimag aint anint asin atan atan2 char cmplx conjg cos cosh exp ichar index int log log10 max min nint sign sin sinh sqrt tan tanh print write dim lge lgt lle llt mod nullify allocate deallocate adjustl adjustr all allocated any associated bit_size btest ceiling count cshift date_and_time digits dot_product eoshift epsilon exponent floor fraction huge iand ibclr ibits ibset ieor ior ishft ishftc lbound len_trim matmul maxexponent maxloc maxval merge minexponent minloc minval modulo mvbits nearest pack present product radix random_number random_seed range repeat reshape rrspacing scale scan selected_int_kind selected_real_kind set_exponent shape size spacing spread sum system_clock tiny transpose trim ubound unpack verify achar iachar transfer dble entry dprod cpu_time command_argument_count get_command get_command_argument get_environment_variable is_iostat_end ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode is_iostat_eor move_alloc new_line selected_char_kind same_type_as extends_type_of acosh asinh atanh bessel_j0 bessel_j1 bessel_jn bessel_y0 bessel_y1 bessel_yn erf erfc erfc_scaled gamma log_gamma hypot norm2 atomic_define atomic_ref execute_command_line leadz trailz storage_size merge_bits bge bgt ble blt dshiftl dshiftr findloc iall iany iparity image_index lcobound ucobound maskl maskr num_images parity popcnt poppar shifta shiftl shiftr this_image sync change team co_broadcast co_max co_min co_sum co_reduce"
+},illegal:/\/\*/,contains:[{className:"string",relevance:0,
+variants:[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE]},o,{begin:/^C\s*=(?!=)/,
+relevance:0},a,c]}}})());
+hljs.registerLanguage("fsharp",(()=>{"use strict";return e=>{const n={begin:"<",
+end:">",contains:[e.inherit(e.TITLE_MODE,{begin:/'[a-zA-Z0-9_]+/})]};return{
+name:"F#",aliases:["fs"],
+keywords:"abstract and as assert base begin class default delegate do done downcast downto elif else end exception extern false finally for fun function global if in inherit inline interface internal lazy let match member module mutable namespace new null of open or override private public rec return sig static struct then to true try type upcast use val void when while with yield",
+illegal:/\/\*/,contains:[{className:"keyword",begin:/\b(yield|return|let|do)!/
+},{className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},{
+className:"string",begin:'"""',end:'"""'},e.COMMENT("\\(\\*(\\s)","\\*\\)",{
+contains:["self"]}),{className:"class",beginKeywords:"type",end:"\\(|=|$",
+excludeEnd:!0,contains:[e.UNDERSCORE_TITLE_MODE,n]},{className:"meta",
+begin:"\\[<",end:">\\]",relevance:10},{className:"symbol",
+begin:"\\B('[A-Za-z])\\b",contains:[e.BACKSLASH_ESCAPE]
+},e.C_LINE_COMMENT_MODE,e.inherit(e.QUOTE_STRING_MODE,{illegal:null
+}),e.C_NUMBER_MODE]}}})());
+hljs.registerLanguage("gams",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>{const a={
+keyword:"abort acronym acronyms alias all and assign binary card diag display else eq file files for free ge gt if integer le loop lt maximizing minimizing model models ne negative no not option options or ord positive prod put putpage puttl repeat sameas semicont semiint smax smin solve sos1 sos2 sum system table then until using while xor yes",
+literal:"eps inf na",
+built_in:"abs arccos arcsin arctan arctan2 Beta betaReg binomial ceil centropy cos cosh cvPower div div0 eDist entropy errorf execSeed exp fact floor frac gamma gammaReg log logBeta logGamma log10 log2 mapVal max min mod ncpCM ncpF ncpVUpow ncpVUsin normal pi poly power randBinomial randLinear randTriangle round rPower sigmoid sign signPower sin sinh slexp sllog10 slrec sqexp sqlog10 sqr sqrec sqrt tan tanh trunc uniform uniformInt vcPower bool_and bool_eqv bool_imp bool_not bool_or bool_xor ifThen rel_eq rel_ge rel_gt rel_le rel_lt rel_ne gday gdow ghour gleap gmillisec gminute gmonth gsecond gyear jdate jnow jstart jtime errorLevel execError gamsRelease gamsVersion handleCollect handleDelete handleStatus handleSubmit heapFree heapLimit heapSize jobHandle jobKill jobStatus jobTerminate licenseLevel licenseStatus maxExecError sleep timeClose timeComp timeElapsed timeExec timeStart"
+},i={className:"symbol",variants:[{begin:/=[lgenxc]=/},{begin:/\$/}]},s={
+className:"comment",variants:[{begin:"'",end:"'"},{begin:'"',end:'"'}],
+illegal:"\\n",contains:[n.BACKSLASH_ESCAPE]},o={begin:"/",end:"/",keywords:a,
+contains:[s,n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,n.C_NUMBER_MODE]
+},t=/[a-z0-9&#*=?@\\><:,()$[\]_.{}!+%^-]+/,r={
+begin:/[a-z][a-z0-9_]*(\([a-z0-9_, ]*\))?[ \t]+/,excludeBegin:!0,end:"$",
+endsWithParent:!0,contains:[s,o,{className:"comment",
+begin:e(t,(l=e(/[ ]+/,t),e("(",l,")*"))),relevance:0}]};var l;return{
+name:"GAMS",aliases:["gms"],case_insensitive:!0,keywords:a,
+contains:[n.COMMENT(/^\$ontext/,/^\$offtext/),{className:"meta",
+begin:"^\\$[a-z0-9]+",end:"$",returnBegin:!0,contains:[{
+className:"meta-keyword",begin:"^\\$[a-z0-9]+"}]
+},n.COMMENT("^\\*","$"),n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,{
+beginKeywords:"set sets parameter parameters variable variables scalar scalars equation equations",
+end:";",
+contains:[n.COMMENT("^\\*","$"),n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,o,r]
+},{beginKeywords:"table",end:";",returnBegin:!0,contains:[{
+beginKeywords:"table",end:"$",contains:[r]
+},n.COMMENT("^\\*","$"),n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,n.C_NUMBER_MODE]
+},{className:"function",begin:/^[a-z][a-z0-9_,\-+' ()$]+\.{2}/,returnBegin:!0,
+contains:[{className:"title",begin:/^[a-z0-9_]+/},{className:"params",
+begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0},i]},n.C_NUMBER_MODE,i]}}
+})());
+hljs.registerLanguage("gauss",(()=>{"use strict";return e=>{const t={
+keyword:"bool break call callexe checkinterrupt clear clearg closeall cls comlog compile continue create debug declare delete disable dlibrary dllcall do dos ed edit else elseif enable end endfor endif endp endo errorlog errorlogat expr external fn for format goto gosub graph if keyword let lib library line load loadarray loadexe loadf loadk loadm loadp loads loadx local locate loopnextindex lprint lpwidth lshow matrix msym ndpclex new open output outwidth plot plotsym pop prcsn print printdos proc push retp return rndcon rndmod rndmult rndseed run save saveall screen scroll setarray show sparse stop string struct system trace trap threadfor threadendfor threadbegin threadjoin threadstat threadend until use while winprint ne ge le gt lt and xor or not eq eqv",
+built_in:"abs acf aconcat aeye amax amean AmericanBinomCall AmericanBinomCall_Greeks AmericanBinomCall_ImpVol AmericanBinomPut AmericanBinomPut_Greeks AmericanBinomPut_ImpVol AmericanBSCall AmericanBSCall_Greeks AmericanBSCall_ImpVol AmericanBSPut AmericanBSPut_Greeks AmericanBSPut_ImpVol amin amult annotationGetDefaults annotationSetBkd annotationSetFont annotationSetLineColor annotationSetLineStyle annotationSetLineThickness annualTradingDays arccos arcsin areshape arrayalloc arrayindex arrayinit arraytomat asciiload asclabel astd astds asum atan atan2 atranspose axmargin balance band bandchol bandcholsol bandltsol bandrv bandsolpd bar base10 begwind besselj bessely beta box boxcox cdfBeta cdfBetaInv cdfBinomial cdfBinomialInv cdfBvn cdfBvn2 cdfBvn2e cdfCauchy cdfCauchyInv cdfChic cdfChii cdfChinc cdfChincInv cdfExp cdfExpInv cdfFc cdfFnc cdfFncInv cdfGam cdfGenPareto cdfHyperGeo cdfLaplace cdfLaplaceInv cdfLogistic cdfLogisticInv cdfmControlCreate cdfMvn cdfMvn2e cdfMvnce cdfMvne cdfMvt2e cdfMvtce cdfMvte cdfN cdfN2 cdfNc cdfNegBinomial cdfNegBinomialInv cdfNi cdfPoisson cdfPoissonInv cdfRayleigh cdfRayleighInv cdfTc cdfTci cdfTnc cdfTvn cdfWeibull cdfWeibullInv cdir ceil ChangeDir chdir chiBarSquare chol choldn cholsol cholup chrs close code cols colsf combinate combinated complex con cond conj cons ConScore contour conv convertsatostr convertstrtosa corrm corrms corrvc corrx corrxs cos cosh counts countwts crossprd crout croutp csrcol csrlin csvReadM csvReadSA cumprodc cumsumc curve cvtos datacreate datacreatecomplex datalist dataload dataloop dataopen datasave date datestr datestring datestrymd dayinyr dayofweek dbAddDatabase dbClose dbCommit dbCreateQuery dbExecQuery dbGetConnectOptions dbGetDatabaseName dbGetDriverName dbGetDrivers dbGetHostName dbGetLastErrorNum dbGetLastErrorText dbGetNumericalPrecPolicy dbGetPassword dbGetPort dbGetTableHeaders dbGetTables dbGetUserName dbHasFeature dbIsDriverAvailable dbIsOpen dbIsOpenError dbOpen dbQueryBindValue dbQueryClear dbQueryCols dbQueryExecPrepared dbQueryFetchAllM dbQueryFetchAllSA dbQueryFetchOneM dbQueryFetchOneSA dbQueryFinish dbQueryGetBoundValue dbQueryGetBoundValues dbQueryGetField dbQueryGetLastErrorNum dbQueryGetLastErrorText dbQueryGetLastInsertID dbQueryGetLastQuery dbQueryGetPosition dbQueryIsActive dbQueryIsForwardOnly dbQueryIsNull dbQueryIsSelect dbQueryIsValid dbQueryPrepare dbQueryRows dbQuerySeek dbQuerySeekFirst dbQuerySeekLast dbQuerySeekNext dbQuerySeekPrevious dbQuerySetForwardOnly dbRemoveDatabase dbRollback dbSetConnectOptions dbSetDatabaseName dbSetHostName dbSetNumericalPrecPolicy dbSetPort dbSetUserName dbTransaction DeleteFile delif delrows denseToSp denseToSpRE denToZero design det detl dfft dffti diag diagrv digamma doswin DOSWinCloseall DOSWinOpen dotfeq dotfeqmt dotfge dotfgemt dotfgt dotfgtmt dotfle dotflemt dotflt dotfltmt dotfne dotfnemt draw drop dsCreate dstat dstatmt dstatmtControlCreate dtdate dtday dttime dttodtv dttostr dttoutc dtvnormal dtvtodt dtvtoutc dummy dummybr dummydn eig eigh eighv eigv elapsedTradingDays endwind envget eof eqSolve eqSolvemt eqSolvemtControlCreate eqSolvemtOutCreate eqSolveset erf erfc erfccplx erfcplx error etdays ethsec etstr EuropeanBinomCall EuropeanBinomCall_Greeks EuropeanBinomCall_ImpVol EuropeanBinomPut EuropeanBinomPut_Greeks EuropeanBinomPut_ImpVol EuropeanBSCall EuropeanBSCall_Greeks EuropeanBSCall_ImpVol EuropeanBSPut EuropeanBSPut_Greeks EuropeanBSPut_ImpVol exctsmpl exec execbg exp extern eye fcheckerr fclearerr feq feqmt fflush fft ffti fftm fftmi fftn fge fgemt fgets fgetsa fgetsat fgetst fgt fgtmt fileinfo filesa fle flemt floor flt fltmt fmod fne fnemt fonts fopen formatcv formatnv fputs fputst fseek fstrerror ftell ftocv ftos ftostrC gamma gammacplx gammaii gausset gdaAppend gdaCreate gdaDStat gdaDStatMat gdaGetIndex gdaGetName gdaGetNames gdaGetOrders gdaGetType gdaGetTypes gdaGetVarInfo gdaIsCplx gdaLoad gdaPack gdaRead gdaReadByIndex gdaReadSome gdaReadSparse gdaReadStruct gdaReportVarInfo gdaSave gdaUpdate gdaUpdateAndPack gdaVars gdaWrite gdaWrite32 gdaWriteSome getarray getdims getf getGAUSShome getmatrix getmatrix4D getname getnamef getNextTradingDay getNextWeekDay getnr getorders getpath getPreviousTradingDay getPreviousWeekDay getRow getscalar3D getscalar4D getTrRow getwind glm gradcplx gradMT gradMTm gradMTT gradMTTm gradp graphprt graphset hasimag header headermt hess hessMT hessMTg hessMTgw hessMTm hessMTmw hessMTT hessMTTg hessMTTgw hessMTTm hessMTw hessp hist histf histp hsec imag indcv indexcat indices indices2 indicesf indicesfn indnv indsav integrate1d integrateControlCreate intgrat2 intgrat3 inthp1 inthp2 inthp3 inthp4 inthpControlCreate intquad1 intquad2 intquad3 intrleav intrleavsa intrsect intsimp inv invpd invswp iscplx iscplxf isden isinfnanmiss ismiss key keyav keyw lag lag1 lagn lapEighb lapEighi lapEighvb lapEighvi lapgEig lapgEigh lapgEighv lapgEigv lapgSchur lapgSvdcst lapgSvds lapgSvdst lapSvdcusv lapSvds lapSvdusv ldlp ldlsol linSolve listwise ln lncdfbvn lncdfbvn2 lncdfmvn lncdfn lncdfn2 lncdfnc lnfact lngammacplx lnpdfmvn lnpdfmvt lnpdfn lnpdft loadd loadstruct loadwind loess loessmt loessmtControlCreate log loglog logx logy lower lowmat lowmat1 ltrisol lu lusol machEpsilon make makevars makewind margin matalloc matinit mattoarray maxbytes maxc maxindc maxv maxvec mbesselei mbesselei0 mbesselei1 mbesseli mbesseli0 mbesseli1 meanc median mergeby mergevar minc minindc minv miss missex missrv moment momentd movingave movingaveExpwgt movingaveWgt nextindex nextn nextnevn nextwind ntos null null1 numCombinations ols olsmt olsmtControlCreate olsqr olsqr2 olsqrmt ones optn optnevn orth outtyp pacf packedToSp packr parse pause pdfCauchy pdfChi pdfExp pdfGenPareto pdfHyperGeo pdfLaplace pdfLogistic pdfn pdfPoisson pdfRayleigh pdfWeibull pi pinv pinvmt plotAddArrow plotAddBar plotAddBox plotAddHist plotAddHistF plotAddHistP plotAddPolar plotAddScatter plotAddShape plotAddTextbox plotAddTS plotAddXY plotArea plotBar plotBox plotClearLayout plotContour plotCustomLayout plotGetDefaults plotHist plotHistF plotHistP plotLayout plotLogLog plotLogX plotLogY plotOpenWindow plotPolar plotSave plotScatter plotSetAxesPen plotSetBar plotSetBarFill plotSetBarStacked plotSetBkdColor plotSetFill plotSetGrid plotSetLegend plotSetLineColor plotSetLineStyle plotSetLineSymbol plotSetLineThickness plotSetNewWindow plotSetTitle plotSetWhichYAxis plotSetXAxisShow plotSetXLabel plotSetXRange plotSetXTicInterval plotSetXTicLabel plotSetYAxisShow plotSetYLabel plotSetYRange plotSetZAxisShow plotSetZLabel plotSurface plotTS plotXY polar polychar polyeval polygamma polyint polymake polymat polymroot polymult polyroot pqgwin previousindex princomp printfm printfmt prodc psi putarray putf putvals pvCreate pvGetIndex pvGetParNames pvGetParVector pvLength pvList pvPack pvPacki pvPackm pvPackmi pvPacks pvPacksi pvPacksm pvPacksmi pvPutParVector pvTest pvUnpack QNewton QNewtonmt QNewtonmtControlCreate QNewtonmtOutCreate QNewtonSet QProg QProgmt QProgmtInCreate qqr qqre qqrep qr qre qrep qrsol qrtsol qtyr qtyre qtyrep quantile quantiled qyr qyre qyrep qz rank rankindx readr real reclassify reclassifyCuts recode recserar recsercp recserrc rerun rescale reshape rets rev rfft rffti rfftip rfftn rfftnp rfftp rndBernoulli rndBeta rndBinomial rndCauchy rndChiSquare rndCon rndCreateState rndExp rndGamma rndGeo rndGumbel rndHyperGeo rndi rndKMbeta rndKMgam rndKMi rndKMn rndKMnb rndKMp rndKMu rndKMvm rndLaplace rndLCbeta rndLCgam rndLCi rndLCn rndLCnb rndLCp rndLCu rndLCvm rndLogNorm rndMTu rndMVn rndMVt rndn rndnb rndNegBinomial rndp rndPoisson rndRayleigh rndStateSkip rndu rndvm rndWeibull rndWishart rotater round rows rowsf rref sampleData satostrC saved saveStruct savewind scale scale3d scalerr scalinfnanmiss scalmiss schtoc schur searchsourcepath seekr select selif seqa seqm setdif setdifsa setvars setvwrmode setwind shell shiftr sin singleindex sinh sleep solpd sortc sortcc sortd sorthc sorthcc sortind sortindc sortmc sortr sortrc spBiconjGradSol spChol spConjGradSol spCreate spDenseSubmat spDiagRvMat spEigv spEye spLDL spline spLU spNumNZE spOnes spreadSheetReadM spreadSheetReadSA spreadSheetWrite spScale spSubmat spToDense spTrTDense spTScalar spZeros sqpSolve sqpSolveMT sqpSolveMTControlCreate sqpSolveMTlagrangeCreate sqpSolveMToutCreate sqpSolveSet sqrt statements stdc stdsc stocv stof strcombine strindx strlen strput strrindx strsect strsplit strsplitPad strtodt strtof strtofcplx strtriml strtrimr strtrunc strtruncl strtruncpad strtruncr submat subscat substute subvec sumc sumr surface svd svd1 svd2 svdcusv svds svdusv sysstate tab tan tanh tempname time timedt timestr timeutc title tkf2eps tkf2ps tocart todaydt toeplitz token topolar trapchk trigamma trimr trunc type typecv typef union unionsa uniqindx uniqindxsa unique uniquesa upmat upmat1 upper utctodt utctodtv utrisol vals varCovMS varCovXS varget vargetl varmall varmares varput varputl vartypef vcm vcms vcx vcxs vec vech vecr vector vget view viewxyz vlist vnamecv volume vput vread vtypecv wait waitc walkindex where window writer xlabel xlsGetSheetCount xlsGetSheetSize xlsGetSheetTypes xlsMakeRange xlsReadM xlsReadSA xlsWrite xlsWriteM xlsWriteSA xpnd xtics xy xyz ylabel ytics zeros zeta zlabel ztics cdfEmpirical dot h5create h5open h5read h5readAttribute h5write h5writeAttribute ldl plotAddErrorBar plotAddSurface plotCDFEmpirical plotSetColormap plotSetContourLabels plotSetLegendFont plotSetTextInterpreter plotSetXTicCount plotSetYTicCount plotSetZLevels powerm strjoin sylvester strtrim",
+literal:"DB_AFTER_LAST_ROW DB_ALL_TABLES DB_BATCH_OPERATIONS DB_BEFORE_FIRST_ROW DB_BLOB DB_EVENT_NOTIFICATIONS DB_FINISH_QUERY DB_HIGH_PRECISION DB_LAST_INSERT_ID DB_LOW_PRECISION_DOUBLE DB_LOW_PRECISION_INT32 DB_LOW_PRECISION_INT64 DB_LOW_PRECISION_NUMBERS DB_MULTIPLE_RESULT_SETS DB_NAMED_PLACEHOLDERS DB_POSITIONAL_PLACEHOLDERS DB_PREPARED_QUERIES DB_QUERY_SIZE DB_SIMPLE_LOCKING DB_SYSTEM_TABLES DB_TABLES DB_TRANSACTIONS DB_UNICODE DB_VIEWS __STDIN __STDOUT __STDERR __FILE_DIR"
+},a=e.COMMENT("@","@"),r={className:"meta",begin:"#",end:"$",keywords:{
+"meta-keyword":"define definecs|10 undef ifdef ifndef iflight ifdllcall ifmac ifos2win ifunix else endif lineson linesoff srcfile srcline"
+},contains:[{begin:/\\\n/,relevance:0},{beginKeywords:"include",end:"$",
+keywords:{"meta-keyword":"include"},contains:[{className:"meta-string",
+begin:'"',end:'"',illegal:"\\n"}]
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a]},n={begin:/\bstruct\s+/,
+end:/\s/,keywords:"struct",contains:[{className:"type",
+begin:e.UNDERSCORE_IDENT_RE,relevance:0}]},s=[{className:"params",begin:/\(/,
+end:/\)/,excludeBegin:!0,excludeEnd:!0,endsWithParent:!0,relevance:0,contains:[{
+className:"literal",begin:/\.\.\./},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,a,n]
+}],o={className:"title",begin:e.UNDERSCORE_IDENT_RE,relevance:0},d=(t,r,n)=>{
+const d=e.inherit({className:"function",beginKeywords:t,end:r,excludeEnd:!0,
+contains:[].concat(s)},n||{})
+;return d.contains.push(o),d.contains.push(e.C_NUMBER_MODE),
+d.contains.push(e.C_BLOCK_COMMENT_MODE),d.contains.push(a),d},l={
+className:"built_in",begin:"\\b("+t.built_in.split(" ").join("|")+")\\b"},i={
+className:"string",begin:'"',end:'"',contains:[e.BACKSLASH_ESCAPE],relevance:0
+},c={begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,keywords:t,
+relevance:0,contains:[{beginKeywords:t.keyword},l,{className:"built_in",
+begin:e.UNDERSCORE_IDENT_RE,relevance:0}]},p={begin:/\(/,end:/\)/,relevance:0,
+keywords:{built_in:t.built_in,literal:t.literal},
+contains:[e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,a,l,c,i,"self"]}
+;return c.contains.push(p),{name:"GAUSS",aliases:["gss"],case_insensitive:!0,
+keywords:t,illegal:/(\{[%#]|[%#]\}| <- )/,
+contains:[e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r,{
+className:"keyword",
+begin:/\bexternal (matrix|string|array|sparse matrix|struct|proc|keyword|fn)/
+},d("proc keyword",";"),d("fn","="),{beginKeywords:"for threadfor",end:/;/,
+relevance:0,contains:[e.C_BLOCK_COMMENT_MODE,a,p]},{variants:[{
+begin:e.UNDERSCORE_IDENT_RE+"\\."+e.UNDERSCORE_IDENT_RE},{
+begin:e.UNDERSCORE_IDENT_RE+"\\s*="}],relevance:0},c,n]}}})());
+hljs.registerLanguage("gcode",(()=>{"use strict";return e=>{
+const a=e.inherit(e.C_NUMBER_MODE,{
+begin:"([-+]?((\\.\\d+)|(\\d+)(\\.\\d*)?))|"+e.C_NUMBER_RE
+}),n=[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.COMMENT(/\(/,/\)/),a,e.inherit(e.APOS_STRING_MODE,{
+illegal:null}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null}),{className:"name",
+begin:"([G])([0-9]+\\.?[0-9]?)"},{className:"name",
+begin:"([M])([0-9]+\\.?[0-9]?)"},{className:"attr",begin:"(VC|VS|#)",
+end:"(\\d+)"},{className:"attr",begin:"(VZOFX|VZOFY|VZOFZ)"},{
+className:"built_in",
+begin:"(ATAN|ABS|ACOS|ASIN|SIN|COS|EXP|FIX|FUP|ROUND|LN|TAN)(\\[)",contains:[a],
+end:"\\]"},{className:"symbol",variants:[{begin:"N",end:"\\d+",illegal:"\\W"}]}]
+;return{name:"G-code (ISO 6983)",aliases:["nc"],case_insensitive:!0,keywords:{
+$pattern:"[A-Z_][A-Z0-9_.]*",
+keyword:"IF DO WHILE ENDWHILE CALL ENDIF SUB ENDSUB GOTO REPEAT ENDREPEAT EQ LT GT NE GE LE OR XOR"
+},contains:[{className:"meta",begin:"%"},{className:"meta",begin:"([O])([0-9]+)"
+}].concat(n)}}})());
+hljs.registerLanguage("gherkin",(()=>{"use strict";return e=>({name:"Gherkin",
+aliases:["feature"],
+keywords:"Feature Background Ability Business Need Scenario Scenarios Scenario Outline Scenario Template Examples Given And Then But When",
+contains:[{className:"symbol",begin:"\\*",relevance:0},{className:"meta",
+begin:"@[^@\\s]+"},{begin:"\\|",end:"\\|\\w*$",contains:[{className:"string",
+begin:"[^|]+"}]},{className:"variable",begin:"<",end:">"},e.HASH_COMMENT_MODE,{
+className:"string",begin:'"""',end:'"""'},e.QUOTE_STRING_MODE]})})());
+hljs.registerLanguage("glsl",(()=>{"use strict";return e=>({name:"GLSL",
+keywords:{
+keyword:"break continue discard do else for if return while switch case default attribute binding buffer ccw centroid centroid varying coherent column_major const cw depth_any depth_greater depth_less depth_unchanged early_fragment_tests equal_spacing flat fractional_even_spacing fractional_odd_spacing highp in index inout invariant invocations isolines layout line_strip lines lines_adjacency local_size_x local_size_y local_size_z location lowp max_vertices mediump noperspective offset origin_upper_left out packed patch pixel_center_integer point_mode points precise precision quads r11f_g11f_b10f r16 r16_snorm r16f r16i r16ui r32f r32i r32ui r8 r8_snorm r8i r8ui readonly restrict rg16 rg16_snorm rg16f rg16i rg16ui rg32f rg32i rg32ui rg8 rg8_snorm rg8i rg8ui rgb10_a2 rgb10_a2ui rgba16 rgba16_snorm rgba16f rgba16i rgba16ui rgba32f rgba32i rgba32ui rgba8 rgba8_snorm rgba8i rgba8ui row_major sample shared smooth std140 std430 stream triangle_strip triangles triangles_adjacency uniform varying vertices volatile writeonly",
+type:"atomic_uint bool bvec2 bvec3 bvec4 dmat2 dmat2x2 dmat2x3 dmat2x4 dmat3 dmat3x2 dmat3x3 dmat3x4 dmat4 dmat4x2 dmat4x3 dmat4x4 double dvec2 dvec3 dvec4 float iimage1D iimage1DArray iimage2D iimage2DArray iimage2DMS iimage2DMSArray iimage2DRect iimage3D iimageBuffer iimageCube iimageCubeArray image1D image1DArray image2D image2DArray image2DMS image2DMSArray image2DRect image3D imageBuffer imageCube imageCubeArray int isampler1D isampler1DArray isampler2D isampler2DArray isampler2DMS isampler2DMSArray isampler2DRect isampler3D isamplerBuffer isamplerCube isamplerCubeArray ivec2 ivec3 ivec4 mat2 mat2x2 mat2x3 mat2x4 mat3 mat3x2 mat3x3 mat3x4 mat4 mat4x2 mat4x3 mat4x4 sampler1D sampler1DArray sampler1DArrayShadow sampler1DShadow sampler2D sampler2DArray sampler2DArrayShadow sampler2DMS sampler2DMSArray sampler2DRect sampler2DRectShadow sampler2DShadow sampler3D samplerBuffer samplerCube samplerCubeArray samplerCubeArrayShadow samplerCubeShadow image1D uimage1DArray uimage2D uimage2DArray uimage2DMS uimage2DMSArray uimage2DRect uimage3D uimageBuffer uimageCube uimageCubeArray uint usampler1D usampler1DArray usampler2D usampler2DArray usampler2DMS usampler2DMSArray usampler2DRect usampler3D samplerBuffer usamplerCube usamplerCubeArray uvec2 uvec3 uvec4 vec2 vec3 vec4 void",
 built_in:"gl_MaxAtomicCounterBindings gl_MaxAtomicCounterBufferSize gl_MaxClipDistances gl_MaxClipPlanes gl_MaxCombinedAtomicCounterBuffers gl_MaxCombinedAtomicCounters gl_MaxCombinedImageUniforms gl_MaxCombinedImageUnitsAndFragmentOutputs gl_MaxCombinedTextureImageUnits gl_MaxComputeAtomicCounterBuffers gl_MaxComputeAtomicCounters gl_MaxComputeImageUniforms gl_MaxComputeTextureImageUnits gl_MaxComputeUniformComponents gl_MaxComputeWorkGroupCount gl_MaxComputeWorkGroupSize gl_MaxDrawBuffers gl_MaxFragmentAtomicCounterBuffers gl_MaxFragmentAtomicCounters gl_MaxFragmentImageUniforms gl_MaxFragmentInputComponents gl_MaxFragmentInputVectors gl_MaxFragmentUniformComponents gl_MaxFragmentUniformVectors gl_MaxGeometryAtomicCounterBuffers gl_MaxGeometryAtomicCounters gl_MaxGeometryImageUniforms gl_MaxGeometryInputComponents gl_MaxGeometryOutputComponents gl_MaxGeometryOutputVertices gl_MaxGeometryTextureImageUnits gl_MaxGeometryTotalOutputComponents gl_MaxGeometryUniformComponents gl_MaxGeometryVaryingComponents gl_MaxImageSamples gl_MaxImageUnits gl_MaxLights gl_MaxPatchVertices gl_MaxProgramTexelOffset gl_MaxTessControlAtomicCounterBuffers gl_MaxTessControlAtomicCounters gl_MaxTessControlImageUniforms gl_MaxTessControlInputComponents gl_MaxTessControlOutputComponents gl_MaxTessControlTextureImageUnits gl_MaxTessControlTotalOutputComponents gl_MaxTessControlUniformComponents gl_MaxTessEvaluationAtomicCounterBuffers gl_MaxTessEvaluationAtomicCounters gl_MaxTessEvaluationImageUniforms gl_MaxTessEvaluationInputComponents gl_MaxTessEvaluationOutputComponents gl_MaxTessEvaluationTextureImageUnits gl_MaxTessEvaluationUniformComponents gl_MaxTessGenLevel gl_MaxTessPatchComponents gl_MaxTextureCoords gl_MaxTextureImageUnits gl_MaxTextureUnits gl_MaxVaryingComponents gl_MaxVaryingFloats gl_MaxVaryingVectors gl_MaxVertexAtomicCounterBuffers gl_MaxVertexAtomicCounters gl_MaxVertexAttribs gl_MaxVertexImageUniforms gl_MaxVertexOutputComponents gl_MaxVertexOutputVectors gl_MaxVertexTextureImageUnits gl_MaxVertexUniformComponents gl_MaxVertexUniformVectors gl_MaxViewports gl_MinProgramTexelOffset gl_BackColor gl_BackLightModelProduct gl_BackLightProduct gl_BackMaterial gl_BackSecondaryColor gl_ClipDistance gl_ClipPlane gl_ClipVertex gl_Color gl_DepthRange gl_EyePlaneQ gl_EyePlaneR gl_EyePlaneS gl_EyePlaneT gl_Fog gl_FogCoord gl_FogFragCoord gl_FragColor gl_FragCoord gl_FragData gl_FragDepth gl_FrontColor gl_FrontFacing gl_FrontLightModelProduct gl_FrontLightProduct gl_FrontMaterial gl_FrontSecondaryColor gl_GlobalInvocationID gl_InstanceID gl_InvocationID gl_Layer gl_LightModel gl_LightSource gl_LocalInvocationID gl_LocalInvocationIndex gl_ModelViewMatrix gl_ModelViewMatrixInverse gl_ModelViewMatrixInverseTranspose gl_ModelViewMatrixTranspose gl_ModelViewProjectionMatrix gl_ModelViewProjectionMatrixInverse gl_ModelViewProjectionMatrixInverseTranspose gl_ModelViewProjectionMatrixTranspose gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 gl_Normal gl_NormalMatrix gl_NormalScale gl_NumSamples gl_NumWorkGroups gl_ObjectPlaneQ gl_ObjectPlaneR gl_ObjectPlaneS gl_ObjectPlaneT gl_PatchVerticesIn gl_Point gl_PointCoord gl_PointSize gl_Position gl_PrimitiveID gl_PrimitiveIDIn gl_ProjectionMatrix gl_ProjectionMatrixInverse gl_ProjectionMatrixInverseTranspose gl_ProjectionMatrixTranspose gl_SampleID gl_SampleMask gl_SampleMaskIn gl_SamplePosition gl_SecondaryColor gl_TessCoord gl_TessLevelInner gl_TessLevelOuter gl_TexCoord gl_TextureEnvColor gl_TextureMatrix gl_TextureMatrixInverse gl_TextureMatrixInverseTranspose gl_TextureMatrixTranspose gl_Vertex gl_VertexID gl_ViewportIndex gl_WorkGroupID gl_WorkGroupSize gl_in gl_out EmitStreamVertex EmitVertex EndPrimitive EndStreamPrimitive abs acos acosh all any asin asinh atan atanh atomicAdd atomicAnd atomicCompSwap atomicCounter atomicCounterDecrement atomicCounterIncrement atomicExchange atomicMax atomicMin atomicOr atomicXor barrier bitCount bitfieldExtract bitfieldInsert bitfieldReverse ceil clamp cos cosh cross dFdx dFdy degrees determinant distance dot equal exp exp2 faceforward findLSB findMSB floatBitsToInt floatBitsToUint floor fma fract frexp ftransform fwidth greaterThan greaterThanEqual groupMemoryBarrier imageAtomicAdd imageAtomicAnd imageAtomicCompSwap imageAtomicExchange imageAtomicMax imageAtomicMin imageAtomicOr imageAtomicXor imageLoad imageSize imageStore imulExtended intBitsToFloat interpolateAtCentroid interpolateAtOffset interpolateAtSample inverse inversesqrt isinf isnan ldexp length lessThan lessThanEqual log log2 matrixCompMult max memoryBarrier memoryBarrierAtomicCounter memoryBarrierBuffer memoryBarrierImage memoryBarrierShared min mix mod modf noise1 noise2 noise3 noise4 normalize not notEqual outerProduct packDouble2x32 packHalf2x16 packSnorm2x16 packSnorm4x8 packUnorm2x16 packUnorm4x8 pow radians reflect refract round roundEven shadow1D shadow1DLod shadow1DProj shadow1DProjLod shadow2D shadow2DLod shadow2DProj shadow2DProjLod sign sin sinh smoothstep sqrt step tan tanh texelFetch texelFetchOffset texture texture1D texture1DLod texture1DProj texture1DProjLod texture2D texture2DLod texture2DProj texture2DProjLod texture3D texture3DLod texture3DProj texture3DProjLod textureCube textureCubeLod textureGather textureGatherOffset textureGatherOffsets textureGrad textureGradOffset textureLod textureLodOffset textureOffset textureProj textureProjGrad textureProjGradOffset textureProjLod textureProjLodOffset textureProjOffset textureQueryLevels textureQueryLod textureSize transpose trunc uaddCarry uintBitsToFloat umulExtended unpackDouble2x32 unpackHalf2x16 unpackSnorm2x16 unpackSnorm4x8 unpackUnorm2x16 unpackUnorm4x8 usubBorrow",
-literal:"true false"},illegal:'"',contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.C_NUMBER_MODE,{className:"meta",begin:"#",end:"$"}]}}}());
-hljs.registerLanguage("gml",function(){return function(a){return{name:"GML",aliases:["gml","GML"],case_insensitive:!1,keywords:{keyword:"begin end if then else while do for break continue with until repeat exit and or xor not return mod div switch case default var globalvar enum #macro #region #endregion",built_in:"is_real is_string is_array is_undefined is_int32 is_int64 is_ptr is_vec3 is_vec4 is_matrix is_bool typeof variable_global_exists variable_global_get variable_global_set variable_instance_exists variable_instance_get variable_instance_set variable_instance_get_names array_length_1d array_length_2d array_height_2d array_equals array_create array_copy random random_range irandom irandom_range random_set_seed random_get_seed randomize randomise choose abs round floor ceil sign frac sqrt sqr exp ln log2 log10 sin cos tan arcsin arccos arctan arctan2 dsin dcos dtan darcsin darccos darctan darctan2 degtorad radtodeg power logn min max mean median clamp lerp dot_product dot_product_3d dot_product_normalised dot_product_3d_normalised dot_product_normalized dot_product_3d_normalized math_set_epsilon math_get_epsilon angle_difference point_distance_3d point_distance point_direction lengthdir_x lengthdir_y real string int64 ptr string_format chr ansi_char ord string_length string_byte_length string_pos string_copy string_char_at string_ord_at string_byte_at string_set_byte_at string_delete string_insert string_lower string_upper string_repeat string_letters string_digits string_lettersdigits string_replace string_replace_all string_count string_hash_to_newline clipboard_has_text clipboard_set_text clipboard_get_text date_current_datetime date_create_datetime date_valid_datetime date_inc_year date_inc_month date_inc_week date_inc_day date_inc_hour date_inc_minute date_inc_second date_get_year date_get_month date_get_week date_get_day date_get_hour date_get_minute date_get_second date_get_weekday date_get_day_of_year date_get_hour_of_year date_get_minute_of_year date_get_second_of_year date_year_span date_month_span date_week_span date_day_span date_hour_span date_minute_span date_second_span date_compare_datetime date_compare_date date_compare_time date_date_of date_time_of date_datetime_string date_date_string date_time_string date_days_in_month date_days_in_year date_leap_year date_is_today date_set_timezone date_get_timezone game_set_speed game_get_speed motion_set motion_add place_free place_empty place_meeting place_snapped move_random move_snap move_towards_point move_contact_solid move_contact_all move_outside_solid move_outside_all move_bounce_solid move_bounce_all move_wrap distance_to_point distance_to_object position_empty position_meeting path_start path_end mp_linear_step mp_potential_step mp_linear_step_object mp_potential_step_object mp_potential_settings mp_linear_path mp_potential_path mp_linear_path_object mp_potential_path_object mp_grid_create mp_grid_destroy mp_grid_clear_all mp_grid_clear_cell mp_grid_clear_rectangle mp_grid_add_cell mp_grid_get_cell mp_grid_add_rectangle mp_grid_add_instances mp_grid_path mp_grid_draw mp_grid_to_ds_grid collision_point collision_rectangle collision_circle collision_ellipse collision_line collision_point_list collision_rectangle_list collision_circle_list collision_ellipse_list collision_line_list instance_position_list instance_place_list point_in_rectangle point_in_triangle point_in_circle rectangle_in_rectangle rectangle_in_triangle rectangle_in_circle instance_find instance_exists instance_number instance_position instance_nearest instance_furthest instance_place instance_create_depth instance_create_layer instance_copy instance_change instance_destroy position_destroy position_change instance_id_get instance_deactivate_all instance_deactivate_object instance_deactivate_region instance_activate_all instance_activate_object instance_activate_region room_goto room_goto_previous room_goto_next room_previous room_next room_restart game_end game_restart game_load game_save game_save_buffer game_load_buffer event_perform event_user event_perform_object event_inherited show_debug_message show_debug_overlay debug_event debug_get_callstack alarm_get alarm_set font_texture_page_size keyboard_set_map keyboard_get_map keyboard_unset_map keyboard_check keyboard_check_pressed keyboard_check_released keyboard_check_direct keyboard_get_numlock keyboard_set_numlock keyboard_key_press keyboard_key_release keyboard_clear io_clear mouse_check_button mouse_check_button_pressed mouse_check_button_released mouse_wheel_up mouse_wheel_down mouse_clear draw_self draw_sprite draw_sprite_pos draw_sprite_ext draw_sprite_stretched draw_sprite_stretched_ext draw_sprite_tiled draw_sprite_tiled_ext draw_sprite_part draw_sprite_part_ext draw_sprite_general draw_clear draw_clear_alpha draw_point draw_line draw_line_width draw_rectangle draw_roundrect draw_roundrect_ext draw_triangle draw_circle draw_ellipse draw_set_circle_precision draw_arrow draw_button draw_path draw_healthbar draw_getpixel draw_getpixel_ext draw_set_colour draw_set_color draw_set_alpha draw_get_colour draw_get_color draw_get_alpha merge_colour make_colour_rgb make_colour_hsv colour_get_red colour_get_green colour_get_blue colour_get_hue colour_get_saturation colour_get_value merge_color make_color_rgb make_color_hsv color_get_red color_get_green color_get_blue color_get_hue color_get_saturation color_get_value merge_color screen_save screen_save_part draw_set_font draw_set_halign draw_set_valign draw_text draw_text_ext string_width string_height string_width_ext string_height_ext draw_text_transformed draw_text_ext_transformed draw_text_colour draw_text_ext_colour draw_text_transformed_colour draw_text_ext_transformed_colour draw_text_color draw_text_ext_color draw_text_transformed_color draw_text_ext_transformed_color draw_point_colour draw_line_colour draw_line_width_colour draw_rectangle_colour draw_roundrect_colour draw_roundrect_colour_ext draw_triangle_colour draw_circle_colour draw_ellipse_colour draw_point_color draw_line_color draw_line_width_color draw_rectangle_color draw_roundrect_color draw_roundrect_color_ext draw_triangle_color draw_circle_color draw_ellipse_color draw_primitive_begin draw_vertex draw_vertex_colour draw_vertex_color draw_primitive_end sprite_get_uvs font_get_uvs sprite_get_texture font_get_texture texture_get_width texture_get_height texture_get_uvs draw_primitive_begin_texture draw_vertex_texture draw_vertex_texture_colour draw_vertex_texture_color texture_global_scale surface_create surface_create_ext surface_resize surface_free surface_exists surface_get_width surface_get_height surface_get_texture surface_set_target surface_set_target_ext surface_reset_target surface_depth_disable surface_get_depth_disable draw_surface draw_surface_stretched draw_surface_tiled draw_surface_part draw_surface_ext draw_surface_stretched_ext draw_surface_tiled_ext draw_surface_part_ext draw_surface_general surface_getpixel surface_getpixel_ext surface_save surface_save_part surface_copy surface_copy_part application_surface_draw_enable application_get_position application_surface_enable application_surface_is_enabled display_get_width display_get_height display_get_orientation display_get_gui_width display_get_gui_height display_reset display_mouse_get_x display_mouse_get_y display_mouse_set display_set_ui_visibility window_set_fullscreen window_get_fullscreen window_set_caption window_set_min_width window_set_max_width window_set_min_height window_set_max_height window_get_visible_rects window_get_caption window_set_cursor window_get_cursor window_set_colour window_get_colour window_set_color window_get_color window_set_position window_set_size window_set_rectangle window_center window_get_x window_get_y window_get_width window_get_height window_mouse_get_x window_mouse_get_y window_mouse_set window_view_mouse_get_x window_view_mouse_get_y window_views_mouse_get_x window_views_mouse_get_y audio_listener_position audio_listener_velocity audio_listener_orientation audio_emitter_position audio_emitter_create audio_emitter_free audio_emitter_exists audio_emitter_pitch audio_emitter_velocity audio_emitter_falloff audio_emitter_gain audio_play_sound audio_play_sound_on audio_play_sound_at audio_stop_sound audio_resume_music audio_music_is_playing audio_resume_sound audio_pause_sound audio_pause_music audio_channel_num audio_sound_length audio_get_type audio_falloff_set_model audio_play_music audio_stop_music audio_master_gain audio_music_gain audio_sound_gain audio_sound_pitch audio_stop_all audio_resume_all audio_pause_all audio_is_playing audio_is_paused audio_exists audio_sound_set_track_position audio_sound_get_track_position audio_emitter_get_gain audio_emitter_get_pitch audio_emitter_get_x audio_emitter_get_y audio_emitter_get_z audio_emitter_get_vx audio_emitter_get_vy audio_emitter_get_vz audio_listener_set_position audio_listener_set_velocity audio_listener_set_orientation audio_listener_get_data audio_set_master_gain audio_get_master_gain audio_sound_get_gain audio_sound_get_pitch audio_get_name audio_sound_set_track_position audio_sound_get_track_position audio_create_stream audio_destroy_stream audio_create_sync_group audio_destroy_sync_group audio_play_in_sync_group audio_start_sync_group audio_stop_sync_group audio_pause_sync_group audio_resume_sync_group audio_sync_group_get_track_pos audio_sync_group_debug audio_sync_group_is_playing audio_debug audio_group_load audio_group_unload audio_group_is_loaded audio_group_load_progress audio_group_name audio_group_stop_all audio_group_set_gain audio_create_buffer_sound audio_free_buffer_sound audio_create_play_queue audio_free_play_queue audio_queue_sound audio_get_recorder_count audio_get_recorder_info audio_start_recording audio_stop_recording audio_sound_get_listener_mask audio_emitter_get_listener_mask audio_get_listener_mask audio_sound_set_listener_mask audio_emitter_set_listener_mask audio_set_listener_mask audio_get_listener_count audio_get_listener_info audio_system show_message show_message_async clickable_add clickable_add_ext clickable_change clickable_change_ext clickable_delete clickable_exists clickable_set_style show_question show_question_async get_integer get_string get_integer_async get_string_async get_login_async get_open_filename get_save_filename get_open_filename_ext get_save_filename_ext show_error highscore_clear highscore_add highscore_value highscore_name draw_highscore sprite_exists sprite_get_name sprite_get_number sprite_get_width sprite_get_height sprite_get_xoffset sprite_get_yoffset sprite_get_bbox_left sprite_get_bbox_right sprite_get_bbox_top sprite_get_bbox_bottom sprite_save sprite_save_strip sprite_set_cache_size sprite_set_cache_size_ext sprite_get_tpe sprite_prefetch sprite_prefetch_multi sprite_flush sprite_flush_multi sprite_set_speed sprite_get_speed_type sprite_get_speed font_exists font_get_name font_get_fontname font_get_bold font_get_italic font_get_first font_get_last font_get_size font_set_cache_size path_exists path_get_name path_get_length path_get_time path_get_kind path_get_closed path_get_precision path_get_number path_get_point_x path_get_point_y path_get_point_speed path_get_x path_get_y path_get_speed script_exists script_get_name timeline_add timeline_delete timeline_clear timeline_exists timeline_get_name timeline_moment_clear timeline_moment_add_script timeline_size timeline_max_moment object_exists object_get_name object_get_sprite object_get_solid object_get_visible object_get_persistent object_get_mask object_get_parent object_get_physics object_is_ancestor room_exists room_get_name sprite_set_offset sprite_duplicate sprite_assign sprite_merge sprite_add sprite_replace sprite_create_from_surface sprite_add_from_surface sprite_delete sprite_set_alpha_from_sprite sprite_collision_mask font_add_enable_aa font_add_get_enable_aa font_add font_add_sprite font_add_sprite_ext font_replace font_replace_sprite font_replace_sprite_ext font_delete path_set_kind path_set_closed path_set_precision path_add path_assign path_duplicate path_append path_delete path_add_point path_insert_point path_change_point path_delete_point path_clear_points path_reverse path_mirror path_flip path_rotate path_rescale path_shift script_execute object_set_sprite object_set_solid object_set_visible object_set_persistent object_set_mask room_set_width room_set_height room_set_persistent room_set_background_colour room_set_background_color room_set_view room_set_viewport room_get_viewport room_set_view_enabled room_add room_duplicate room_assign room_instance_add room_instance_clear room_get_camera room_set_camera asset_get_index asset_get_type file_text_open_from_string file_text_open_read file_text_open_write file_text_open_append file_text_close file_text_write_string file_text_write_real file_text_writeln file_text_read_string file_text_read_real file_text_readln file_text_eof file_text_eoln file_exists file_delete file_rename file_copy directory_exists directory_create directory_destroy file_find_first file_find_next file_find_close file_attributes filename_name filename_path filename_dir filename_drive filename_ext filename_change_ext file_bin_open file_bin_rewrite file_bin_close file_bin_position file_bin_size file_bin_seek file_bin_write_byte file_bin_read_byte parameter_count parameter_string environment_get_variable ini_open_from_string ini_open ini_close ini_read_string ini_read_real ini_write_string ini_write_real ini_key_exists ini_section_exists ini_key_delete ini_section_delete ds_set_precision ds_exists ds_stack_create ds_stack_destroy ds_stack_clear ds_stack_copy ds_stack_size ds_stack_empty ds_stack_push ds_stack_pop ds_stack_top ds_stack_write ds_stack_read ds_queue_create ds_queue_destroy ds_queue_clear ds_queue_copy ds_queue_size ds_queue_empty ds_queue_enqueue ds_queue_dequeue ds_queue_head ds_queue_tail ds_queue_write ds_queue_read ds_list_create ds_list_destroy ds_list_clear ds_list_copy ds_list_size ds_list_empty ds_list_add ds_list_insert ds_list_replace ds_list_delete ds_list_find_index ds_list_find_value ds_list_mark_as_list ds_list_mark_as_map ds_list_sort ds_list_shuffle ds_list_write ds_list_read ds_list_set ds_map_create ds_map_destroy ds_map_clear ds_map_copy ds_map_size ds_map_empty ds_map_add ds_map_add_list ds_map_add_map ds_map_replace ds_map_replace_map ds_map_replace_list ds_map_delete ds_map_exists ds_map_find_value ds_map_find_previous ds_map_find_next ds_map_find_first ds_map_find_last ds_map_write ds_map_read ds_map_secure_save ds_map_secure_load ds_map_secure_load_buffer ds_map_secure_save_buffer ds_map_set ds_priority_create ds_priority_destroy ds_priority_clear ds_priority_copy ds_priority_size ds_priority_empty ds_priority_add ds_priority_change_priority ds_priority_find_priority ds_priority_delete_value ds_priority_delete_min ds_priority_find_min ds_priority_delete_max ds_priority_find_max ds_priority_write ds_priority_read ds_grid_create ds_grid_destroy ds_grid_copy ds_grid_resize ds_grid_width ds_grid_height ds_grid_clear ds_grid_set ds_grid_add ds_grid_multiply ds_grid_set_region ds_grid_add_region ds_grid_multiply_region ds_grid_set_disk ds_grid_add_disk ds_grid_multiply_disk ds_grid_set_grid_region ds_grid_add_grid_region ds_grid_multiply_grid_region ds_grid_get ds_grid_get_sum ds_grid_get_max ds_grid_get_min ds_grid_get_mean ds_grid_get_disk_sum ds_grid_get_disk_min ds_grid_get_disk_max ds_grid_get_disk_mean ds_grid_value_exists ds_grid_value_x ds_grid_value_y ds_grid_value_disk_exists ds_grid_value_disk_x ds_grid_value_disk_y ds_grid_shuffle ds_grid_write ds_grid_read ds_grid_sort ds_grid_set ds_grid_get effect_create_below effect_create_above effect_clear part_type_create part_type_destroy part_type_exists part_type_clear part_type_shape part_type_sprite part_type_size part_type_scale part_type_orientation part_type_life part_type_step part_type_death part_type_speed part_type_direction part_type_gravity part_type_colour1 part_type_colour2 part_type_colour3 part_type_colour_mix part_type_colour_rgb part_type_colour_hsv part_type_color1 part_type_color2 part_type_color3 part_type_color_mix part_type_color_rgb part_type_color_hsv part_type_alpha1 part_type_alpha2 part_type_alpha3 part_type_blend part_system_create part_system_create_layer part_system_destroy part_system_exists part_system_clear part_system_draw_order part_system_depth part_system_position part_system_automatic_update part_system_automatic_draw part_system_update part_system_drawit part_system_get_layer part_system_layer part_particles_create part_particles_create_colour part_particles_create_color part_particles_clear part_particles_count part_emitter_create part_emitter_destroy part_emitter_destroy_all part_emitter_exists part_emitter_clear part_emitter_region part_emitter_burst part_emitter_stream external_call external_define external_free window_handle window_device matrix_get matrix_set matrix_build_identity matrix_build matrix_build_lookat matrix_build_projection_ortho matrix_build_projection_perspective matrix_build_projection_perspective_fov matrix_multiply matrix_transform_vertex matrix_stack_push matrix_stack_pop matrix_stack_multiply matrix_stack_set matrix_stack_clear matrix_stack_top matrix_stack_is_empty browser_input_capture os_get_config os_get_info os_get_language os_get_region os_lock_orientation display_get_dpi_x display_get_dpi_y display_set_gui_size display_set_gui_maximise display_set_gui_maximize device_mouse_dbclick_enable display_set_timing_method display_get_timing_method display_set_sleep_margin display_get_sleep_margin virtual_key_add virtual_key_hide virtual_key_delete virtual_key_show draw_enable_drawevent draw_enable_swf_aa draw_set_swf_aa_level draw_get_swf_aa_level draw_texture_flush draw_flush gpu_set_blendenable gpu_set_ztestenable gpu_set_zfunc gpu_set_zwriteenable gpu_set_lightingenable gpu_set_fog gpu_set_cullmode gpu_set_blendmode gpu_set_blendmode_ext gpu_set_blendmode_ext_sepalpha gpu_set_colorwriteenable gpu_set_colourwriteenable gpu_set_alphatestenable gpu_set_alphatestref gpu_set_alphatestfunc gpu_set_texfilter gpu_set_texfilter_ext gpu_set_texrepeat gpu_set_texrepeat_ext gpu_set_tex_filter gpu_set_tex_filter_ext gpu_set_tex_repeat gpu_set_tex_repeat_ext gpu_set_tex_mip_filter gpu_set_tex_mip_filter_ext gpu_set_tex_mip_bias gpu_set_tex_mip_bias_ext gpu_set_tex_min_mip gpu_set_tex_min_mip_ext gpu_set_tex_max_mip gpu_set_tex_max_mip_ext gpu_set_tex_max_aniso gpu_set_tex_max_aniso_ext gpu_set_tex_mip_enable gpu_set_tex_mip_enable_ext gpu_get_blendenable gpu_get_ztestenable gpu_get_zfunc gpu_get_zwriteenable gpu_get_lightingenable gpu_get_fog gpu_get_cullmode gpu_get_blendmode gpu_get_blendmode_ext gpu_get_blendmode_ext_sepalpha gpu_get_blendmode_src gpu_get_blendmode_dest gpu_get_blendmode_srcalpha gpu_get_blendmode_destalpha gpu_get_colorwriteenable gpu_get_colourwriteenable gpu_get_alphatestenable gpu_get_alphatestref gpu_get_alphatestfunc gpu_get_texfilter gpu_get_texfilter_ext gpu_get_texrepeat gpu_get_texrepeat_ext gpu_get_tex_filter gpu_get_tex_filter_ext gpu_get_tex_repeat gpu_get_tex_repeat_ext gpu_get_tex_mip_filter gpu_get_tex_mip_filter_ext gpu_get_tex_mip_bias gpu_get_tex_mip_bias_ext gpu_get_tex_min_mip gpu_get_tex_min_mip_ext gpu_get_tex_max_mip gpu_get_tex_max_mip_ext gpu_get_tex_max_aniso gpu_get_tex_max_aniso_ext gpu_get_tex_mip_enable gpu_get_tex_mip_enable_ext gpu_push_state gpu_pop_state gpu_get_state gpu_set_state draw_light_define_ambient draw_light_define_direction draw_light_define_point draw_light_enable draw_set_lighting draw_light_get_ambient draw_light_get draw_get_lighting shop_leave_rating url_get_domain url_open url_open_ext url_open_full get_timer achievement_login achievement_logout achievement_post achievement_increment achievement_post_score achievement_available achievement_show_achievements achievement_show_leaderboards achievement_load_friends achievement_load_leaderboard achievement_send_challenge achievement_load_progress achievement_reset achievement_login_status achievement_get_pic achievement_show_challenge_notifications achievement_get_challenges achievement_event achievement_show achievement_get_info cloud_file_save cloud_string_save cloud_synchronise ads_enable ads_disable ads_setup ads_engagement_launch ads_engagement_available ads_engagement_active ads_event ads_event_preload ads_set_reward_callback ads_get_display_height ads_get_display_width ads_move ads_interstitial_available ads_interstitial_display device_get_tilt_x device_get_tilt_y device_get_tilt_z device_is_keypad_open device_mouse_check_button device_mouse_check_button_pressed device_mouse_check_button_released device_mouse_x device_mouse_y device_mouse_raw_x device_mouse_raw_y device_mouse_x_to_gui device_mouse_y_to_gui iap_activate iap_status iap_enumerate_products iap_restore_all iap_acquire iap_consume iap_product_details iap_purchase_details facebook_init facebook_login facebook_status facebook_graph_request facebook_dialog facebook_logout facebook_launch_offerwall facebook_post_message facebook_send_invite facebook_user_id facebook_accesstoken facebook_check_permission facebook_request_read_permissions facebook_request_publish_permissions gamepad_is_supported gamepad_get_device_count gamepad_is_connected gamepad_get_description gamepad_get_button_threshold gamepad_set_button_threshold gamepad_get_axis_deadzone gamepad_set_axis_deadzone gamepad_button_count gamepad_button_check gamepad_button_check_pressed gamepad_button_check_released gamepad_button_value gamepad_axis_count gamepad_axis_value gamepad_set_vibration gamepad_set_colour gamepad_set_color os_is_paused window_has_focus code_is_compiled http_get http_get_file http_post_string http_request json_encode json_decode zip_unzip load_csv base64_encode base64_decode md5_string_unicode md5_string_utf8 md5_file os_is_network_connected sha1_string_unicode sha1_string_utf8 sha1_file os_powersave_enable analytics_event analytics_event_ext win8_livetile_tile_notification win8_livetile_tile_clear win8_livetile_badge_notification win8_livetile_badge_clear win8_livetile_queue_enable win8_secondarytile_pin win8_secondarytile_badge_notification win8_secondarytile_delete win8_livetile_notification_begin win8_livetile_notification_secondary_begin win8_livetile_notification_expiry win8_livetile_notification_tag win8_livetile_notification_text_add win8_livetile_notification_image_add win8_livetile_notification_end win8_appbar_enable win8_appbar_add_element win8_appbar_remove_element win8_settingscharm_add_entry win8_settingscharm_add_html_entry win8_settingscharm_add_xaml_entry win8_settingscharm_set_xaml_property win8_settingscharm_get_xaml_property win8_settingscharm_remove_entry win8_share_image win8_share_screenshot win8_share_file win8_share_url win8_share_text win8_search_enable win8_search_disable win8_search_add_suggestions win8_device_touchscreen_available win8_license_initialize_sandbox win8_license_trial_version winphone_license_trial_version winphone_tile_title winphone_tile_count winphone_tile_back_title winphone_tile_back_content winphone_tile_back_content_wide winphone_tile_front_image winphone_tile_front_image_small winphone_tile_front_image_wide winphone_tile_back_image winphone_tile_back_image_wide winphone_tile_background_colour winphone_tile_background_color winphone_tile_icon_image winphone_tile_small_icon_image winphone_tile_wide_content winphone_tile_cycle_images winphone_tile_small_background_image physics_world_create physics_world_gravity physics_world_update_speed physics_world_update_iterations physics_world_draw_debug physics_pause_enable physics_fixture_create physics_fixture_set_kinematic physics_fixture_set_density physics_fixture_set_awake physics_fixture_set_restitution physics_fixture_set_friction physics_fixture_set_collision_group physics_fixture_set_sensor physics_fixture_set_linear_damping physics_fixture_set_angular_damping physics_fixture_set_circle_shape physics_fixture_set_box_shape physics_fixture_set_edge_shape physics_fixture_set_polygon_shape physics_fixture_set_chain_shape physics_fixture_add_point physics_fixture_bind physics_fixture_bind_ext physics_fixture_delete physics_apply_force physics_apply_impulse physics_apply_angular_impulse physics_apply_local_force physics_apply_local_impulse physics_apply_torque physics_mass_properties physics_draw_debug physics_test_overlap physics_remove_fixture physics_set_friction physics_set_density physics_set_restitution physics_get_friction physics_get_density physics_get_restitution physics_joint_distance_create physics_joint_rope_create physics_joint_revolute_create physics_joint_prismatic_create physics_joint_pulley_create physics_joint_wheel_create physics_joint_weld_create physics_joint_friction_create physics_joint_gear_create physics_joint_enable_motor physics_joint_get_value physics_joint_set_value physics_joint_delete physics_particle_create physics_particle_delete physics_particle_delete_region_circle physics_particle_delete_region_box physics_particle_delete_region_poly physics_particle_set_flags physics_particle_set_category_flags physics_particle_draw physics_particle_draw_ext physics_particle_count physics_particle_get_data physics_particle_get_data_particle physics_particle_group_begin physics_particle_group_circle physics_particle_group_box physics_particle_group_polygon physics_particle_group_add_point physics_particle_group_end physics_particle_group_join physics_particle_group_delete physics_particle_group_count physics_particle_group_get_data physics_particle_group_get_mass physics_particle_group_get_inertia physics_particle_group_get_centre_x physics_particle_group_get_centre_y physics_particle_group_get_vel_x physics_particle_group_get_vel_y physics_particle_group_get_ang_vel physics_particle_group_get_x physics_particle_group_get_y physics_particle_group_get_angle physics_particle_set_group_flags physics_particle_get_group_flags physics_particle_get_max_count physics_particle_get_radius physics_particle_get_density physics_particle_get_damping physics_particle_get_gravity_scale physics_particle_set_max_count physics_particle_set_radius physics_particle_set_density physics_particle_set_damping physics_particle_set_gravity_scale network_create_socket network_create_socket_ext network_create_server network_create_server_raw network_connect network_connect_raw network_send_packet network_send_raw network_send_broadcast network_send_udp network_send_udp_raw network_set_timeout network_set_config network_resolve network_destroy buffer_create buffer_write buffer_read buffer_seek buffer_get_surface buffer_set_surface buffer_delete buffer_exists buffer_get_type buffer_get_alignment buffer_poke buffer_peek buffer_save buffer_save_ext buffer_load buffer_load_ext buffer_load_partial buffer_copy buffer_fill buffer_get_size buffer_tell buffer_resize buffer_md5 buffer_sha1 buffer_base64_encode buffer_base64_decode buffer_base64_decode_ext buffer_sizeof buffer_get_address buffer_create_from_vertex_buffer buffer_create_from_vertex_buffer_ext buffer_copy_from_vertex_buffer buffer_async_group_begin buffer_async_group_option buffer_async_group_end buffer_load_async buffer_save_async gml_release_mode gml_pragma steam_activate_overlay steam_is_overlay_enabled steam_is_overlay_activated steam_get_persona_name steam_initialised steam_is_cloud_enabled_for_app steam_is_cloud_enabled_for_account steam_file_persisted steam_get_quota_total steam_get_quota_free steam_file_write steam_file_write_file steam_file_read steam_file_delete steam_file_exists steam_file_size steam_file_share steam_is_screenshot_requested steam_send_screenshot steam_is_user_logged_on steam_get_user_steam_id steam_user_owns_dlc steam_user_installed_dlc steam_set_achievement steam_get_achievement steam_clear_achievement steam_set_stat_int steam_set_stat_float steam_set_stat_avg_rate steam_get_stat_int steam_get_stat_float steam_get_stat_avg_rate steam_reset_all_stats steam_reset_all_stats_achievements steam_stats_ready steam_create_leaderboard steam_upload_score steam_upload_score_ext steam_download_scores_around_user steam_download_scores steam_download_friends_scores steam_upload_score_buffer steam_upload_score_buffer_ext steam_current_game_language steam_available_languages steam_activate_overlay_browser steam_activate_overlay_user steam_activate_overlay_store steam_get_user_persona_name steam_get_app_id steam_get_user_account_id steam_ugc_download steam_ugc_create_item steam_ugc_start_item_update steam_ugc_set_item_title steam_ugc_set_item_description steam_ugc_set_item_visibility steam_ugc_set_item_tags steam_ugc_set_item_content steam_ugc_set_item_preview steam_ugc_submit_item_update steam_ugc_get_item_update_progress steam_ugc_subscribe_item steam_ugc_unsubscribe_item steam_ugc_num_subscribed_items steam_ugc_get_subscribed_items steam_ugc_get_item_install_info steam_ugc_get_item_update_info steam_ugc_request_item_details steam_ugc_create_query_user steam_ugc_create_query_user_ex steam_ugc_create_query_all steam_ugc_create_query_all_ex steam_ugc_query_set_cloud_filename_filter steam_ugc_query_set_match_any_tag steam_ugc_query_set_search_text steam_ugc_query_set_ranked_by_trend_days steam_ugc_query_add_required_tag steam_ugc_query_add_excluded_tag steam_ugc_query_set_return_long_description steam_ugc_query_set_return_total_only steam_ugc_query_set_allow_cached_response steam_ugc_send_query shader_set shader_get_name shader_reset shader_current shader_is_compiled shader_get_sampler_index shader_get_uniform shader_set_uniform_i shader_set_uniform_i_array shader_set_uniform_f shader_set_uniform_f_array shader_set_uniform_matrix shader_set_uniform_matrix_array shader_enable_corner_id texture_set_stage texture_get_texel_width texture_get_texel_height shaders_are_supported vertex_format_begin vertex_format_end vertex_format_delete vertex_format_add_position vertex_format_add_position_3d vertex_format_add_colour vertex_format_add_color vertex_format_add_normal vertex_format_add_texcoord vertex_format_add_textcoord vertex_format_add_custom vertex_create_buffer vertex_create_buffer_ext vertex_delete_buffer vertex_begin vertex_end vertex_position vertex_position_3d vertex_colour vertex_color vertex_argb vertex_texcoord vertex_normal vertex_float1 vertex_float2 vertex_float3 vertex_float4 vertex_ubyte4 vertex_submit vertex_freeze vertex_get_number vertex_get_buffer_size vertex_create_buffer_from_buffer vertex_create_buffer_from_buffer_ext push_local_notification push_get_first_local_notification push_get_next_local_notification push_cancel_local_notification skeleton_animation_set skeleton_animation_get skeleton_animation_mix skeleton_animation_set_ext skeleton_animation_get_ext skeleton_animation_get_duration skeleton_animation_get_frames skeleton_animation_clear skeleton_skin_set skeleton_skin_get skeleton_attachment_set skeleton_attachment_get skeleton_attachment_create skeleton_collision_draw_set skeleton_bone_data_get skeleton_bone_data_set skeleton_bone_state_get skeleton_bone_state_set skeleton_get_minmax skeleton_get_num_bounds skeleton_get_bounds skeleton_animation_get_frame skeleton_animation_set_frame draw_skeleton draw_skeleton_time draw_skeleton_instance draw_skeleton_collision skeleton_animation_list skeleton_skin_list skeleton_slot_data layer_get_id layer_get_id_at_depth layer_get_depth layer_create layer_destroy layer_destroy_instances layer_add_instance layer_has_instance layer_set_visible layer_get_visible layer_exists layer_x layer_y layer_get_x layer_get_y layer_hspeed layer_vspeed layer_get_hspeed layer_get_vspeed layer_script_begin layer_script_end layer_shader layer_get_script_begin layer_get_script_end layer_get_shader layer_set_target_room layer_get_target_room layer_reset_target_room layer_get_all layer_get_all_elements layer_get_name layer_depth layer_get_element_layer layer_get_element_type layer_element_move layer_force_draw_depth layer_is_draw_depth_forced layer_get_forced_depth layer_background_get_id layer_background_exists layer_background_create layer_background_destroy layer_background_visible layer_background_change layer_background_sprite layer_background_htiled layer_background_vtiled layer_background_stretch layer_background_yscale layer_background_xscale layer_background_blend layer_background_alpha layer_background_index layer_background_speed layer_background_get_visible layer_background_get_sprite layer_background_get_htiled layer_background_get_vtiled layer_background_get_stretch layer_background_get_yscale layer_background_get_xscale layer_background_get_blend layer_background_get_alpha layer_background_get_index layer_background_get_speed layer_sprite_get_id layer_sprite_exists layer_sprite_create layer_sprite_destroy layer_sprite_change layer_sprite_index layer_sprite_speed layer_sprite_xscale layer_sprite_yscale layer_sprite_angle layer_sprite_blend layer_sprite_alpha layer_sprite_x layer_sprite_y layer_sprite_get_sprite layer_sprite_get_index layer_sprite_get_speed layer_sprite_get_xscale layer_sprite_get_yscale layer_sprite_get_angle layer_sprite_get_blend layer_sprite_get_alpha layer_sprite_get_x layer_sprite_get_y layer_tilemap_get_id layer_tilemap_exists layer_tilemap_create layer_tilemap_destroy tilemap_tileset tilemap_x tilemap_y tilemap_set tilemap_set_at_pixel tilemap_get_tileset tilemap_get_tile_width tilemap_get_tile_height tilemap_get_width tilemap_get_height tilemap_get_x tilemap_get_y tilemap_get tilemap_get_at_pixel tilemap_get_cell_x_at_pixel tilemap_get_cell_y_at_pixel tilemap_clear draw_tilemap draw_tile tilemap_set_global_mask tilemap_get_global_mask tilemap_set_mask tilemap_get_mask tilemap_get_frame tile_set_empty tile_set_index tile_set_flip tile_set_mirror tile_set_rotate tile_get_empty tile_get_index tile_get_flip tile_get_mirror tile_get_rotate layer_tile_exists layer_tile_create layer_tile_destroy layer_tile_change layer_tile_xscale layer_tile_yscale layer_tile_blend layer_tile_alpha layer_tile_x layer_tile_y layer_tile_region layer_tile_visible layer_tile_get_sprite layer_tile_get_xscale layer_tile_get_yscale layer_tile_get_blend layer_tile_get_alpha layer_tile_get_x layer_tile_get_y layer_tile_get_region layer_tile_get_visible layer_instance_get_instance instance_activate_layer instance_deactivate_layer camera_create camera_create_view camera_destroy camera_apply camera_get_active camera_get_default camera_set_default camera_set_view_mat camera_set_proj_mat camera_set_update_script camera_set_begin_script camera_set_end_script camera_set_view_pos camera_set_view_size camera_set_view_speed camera_set_view_border camera_set_view_angle camera_set_view_target camera_get_view_mat camera_get_proj_mat camera_get_update_script camera_get_begin_script camera_get_end_script camera_get_view_x camera_get_view_y camera_get_view_width camera_get_view_height camera_get_view_speed_x camera_get_view_speed_y camera_get_view_border_x camera_get_view_border_y camera_get_view_angle camera_get_view_target view_get_camera view_get_visible view_get_xport view_get_yport view_get_wport view_get_hport view_get_surface_id view_set_camera view_set_visible view_set_xport view_set_yport view_set_wport view_set_hport view_set_surface_id gesture_drag_time gesture_drag_distance gesture_flick_speed gesture_double_tap_time gesture_double_tap_distance gesture_pinch_distance gesture_pinch_angle_towards gesture_pinch_angle_away gesture_rotate_time gesture_rotate_angle gesture_tap_count gesture_get_drag_time gesture_get_drag_distance gesture_get_flick_speed gesture_get_double_tap_time gesture_get_double_tap_distance gesture_get_pinch_distance gesture_get_pinch_angle_towards gesture_get_pinch_angle_away gesture_get_rotate_time gesture_get_rotate_angle gesture_get_tap_count keyboard_virtual_show keyboard_virtual_hide keyboard_virtual_status keyboard_virtual_height",literal:"self other all noone global local undefined pointer_invalid pointer_null path_action_stop path_action_restart path_action_continue path_action_reverse true false pi GM_build_date GM_version GM_runtime_version  timezone_local timezone_utc gamespeed_fps gamespeed_microseconds  ev_create ev_destroy ev_step ev_alarm ev_keyboard ev_mouse ev_collision ev_other ev_draw ev_draw_begin ev_draw_end ev_draw_pre ev_draw_post ev_keypress ev_keyrelease ev_trigger ev_left_button ev_right_button ev_middle_button ev_no_button ev_left_press ev_right_press ev_middle_press ev_left_release ev_right_release ev_middle_release ev_mouse_enter ev_mouse_leave ev_mouse_wheel_up ev_mouse_wheel_down ev_global_left_button ev_global_right_button ev_global_middle_button ev_global_left_press ev_global_right_press ev_global_middle_press ev_global_left_release ev_global_right_release ev_global_middle_release ev_joystick1_left ev_joystick1_right ev_joystick1_up ev_joystick1_down ev_joystick1_button1 ev_joystick1_button2 ev_joystick1_button3 ev_joystick1_button4 ev_joystick1_button5 ev_joystick1_button6 ev_joystick1_button7 ev_joystick1_button8 ev_joystick2_left ev_joystick2_right ev_joystick2_up ev_joystick2_down ev_joystick2_button1 ev_joystick2_button2 ev_joystick2_button3 ev_joystick2_button4 ev_joystick2_button5 ev_joystick2_button6 ev_joystick2_button7 ev_joystick2_button8 ev_outside ev_boundary ev_game_start ev_game_end ev_room_start ev_room_end ev_no_more_lives ev_animation_end ev_end_of_path ev_no_more_health ev_close_button ev_user0 ev_user1 ev_user2 ev_user3 ev_user4 ev_user5 ev_user6 ev_user7 ev_user8 ev_user9 ev_user10 ev_user11 ev_user12 ev_user13 ev_user14 ev_user15 ev_step_normal ev_step_begin ev_step_end ev_gui ev_gui_begin ev_gui_end ev_cleanup ev_gesture ev_gesture_tap ev_gesture_double_tap ev_gesture_drag_start ev_gesture_dragging ev_gesture_drag_end ev_gesture_flick ev_gesture_pinch_start ev_gesture_pinch_in ev_gesture_pinch_out ev_gesture_pinch_end ev_gesture_rotate_start ev_gesture_rotating ev_gesture_rotate_end ev_global_gesture_tap ev_global_gesture_double_tap ev_global_gesture_drag_start ev_global_gesture_dragging ev_global_gesture_drag_end ev_global_gesture_flick ev_global_gesture_pinch_start ev_global_gesture_pinch_in ev_global_gesture_pinch_out ev_global_gesture_pinch_end ev_global_gesture_rotate_start ev_global_gesture_rotating ev_global_gesture_rotate_end vk_nokey vk_anykey vk_enter vk_return vk_shift vk_control vk_alt vk_escape vk_space vk_backspace vk_tab vk_pause vk_printscreen vk_left vk_right vk_up vk_down vk_home vk_end vk_delete vk_insert vk_pageup vk_pagedown vk_f1 vk_f2 vk_f3 vk_f4 vk_f5 vk_f6 vk_f7 vk_f8 vk_f9 vk_f10 vk_f11 vk_f12 vk_numpad0 vk_numpad1 vk_numpad2 vk_numpad3 vk_numpad4 vk_numpad5 vk_numpad6 vk_numpad7 vk_numpad8 vk_numpad9 vk_divide vk_multiply vk_subtract vk_add vk_decimal vk_lshift vk_lcontrol vk_lalt vk_rshift vk_rcontrol vk_ralt  mb_any mb_none mb_left mb_right mb_middle c_aqua c_black c_blue c_dkgray c_fuchsia c_gray c_green c_lime c_ltgray c_maroon c_navy c_olive c_purple c_red c_silver c_teal c_white c_yellow c_orange fa_left fa_center fa_right fa_top fa_middle fa_bottom pr_pointlist pr_linelist pr_linestrip pr_trianglelist pr_trianglestrip pr_trianglefan bm_complex bm_normal bm_add bm_max bm_subtract bm_zero bm_one bm_src_colour bm_inv_src_colour bm_src_color bm_inv_src_color bm_src_alpha bm_inv_src_alpha bm_dest_alpha bm_inv_dest_alpha bm_dest_colour bm_inv_dest_colour bm_dest_color bm_inv_dest_color bm_src_alpha_sat tf_point tf_linear tf_anisotropic mip_off mip_on mip_markedonly audio_falloff_none audio_falloff_inverse_distance audio_falloff_inverse_distance_clamped audio_falloff_linear_distance audio_falloff_linear_distance_clamped audio_falloff_exponent_distance audio_falloff_exponent_distance_clamped audio_old_system audio_new_system audio_mono audio_stereo audio_3d cr_default cr_none cr_arrow cr_cross cr_beam cr_size_nesw cr_size_ns cr_size_nwse cr_size_we cr_uparrow cr_hourglass cr_drag cr_appstart cr_handpoint cr_size_all spritespeed_framespersecond spritespeed_framespergameframe asset_object asset_unknown asset_sprite asset_sound asset_room asset_path asset_script asset_font asset_timeline asset_tiles asset_shader fa_readonly fa_hidden fa_sysfile fa_volumeid fa_directory fa_archive  ds_type_map ds_type_list ds_type_stack ds_type_queue ds_type_grid ds_type_priority ef_explosion ef_ring ef_ellipse ef_firework ef_smoke ef_smokeup ef_star ef_spark ef_flare ef_cloud ef_rain ef_snow pt_shape_pixel pt_shape_disk pt_shape_square pt_shape_line pt_shape_star pt_shape_circle pt_shape_ring pt_shape_sphere pt_shape_flare pt_shape_spark pt_shape_explosion pt_shape_cloud pt_shape_smoke pt_shape_snow ps_distr_linear ps_distr_gaussian ps_distr_invgaussian ps_shape_rectangle ps_shape_ellipse ps_shape_diamond ps_shape_line ty_real ty_string dll_cdecl dll_stdcall matrix_view matrix_projection matrix_world os_win32 os_windows os_macosx os_ios os_android os_symbian os_linux os_unknown os_winphone os_tizen os_win8native os_wiiu os_3ds  os_psvita os_bb10 os_ps4 os_xboxone os_ps3 os_xbox360 os_uwp os_tvos os_switch browser_not_a_browser browser_unknown browser_ie browser_firefox browser_chrome browser_safari browser_safari_mobile browser_opera browser_tizen browser_edge browser_windows_store browser_ie_mobile  device_ios_unknown device_ios_iphone device_ios_iphone_retina device_ios_ipad device_ios_ipad_retina device_ios_iphone5 device_ios_iphone6 device_ios_iphone6plus device_emulator device_tablet display_landscape display_landscape_flipped display_portrait display_portrait_flipped tm_sleep tm_countvsyncs of_challenge_win of_challen ge_lose of_challenge_tie leaderboard_type_number leaderboard_type_time_mins_secs cmpfunc_never cmpfunc_less cmpfunc_equal cmpfunc_lessequal cmpfunc_greater cmpfunc_notequal cmpfunc_greaterequal cmpfunc_always cull_noculling cull_clockwise cull_counterclockwise lighttype_dir lighttype_point iap_ev_storeload iap_ev_product iap_ev_purchase iap_ev_consume iap_ev_restore iap_storeload_ok iap_storeload_failed iap_status_uninitialised iap_status_unavailable iap_status_loading iap_status_available iap_status_processing iap_status_restoring iap_failed iap_unavailable iap_available iap_purchased iap_canceled iap_refunded fb_login_default fb_login_fallback_to_webview fb_login_no_fallback_to_webview fb_login_forcing_webview fb_login_use_system_account fb_login_forcing_safari  phy_joint_anchor_1_x phy_joint_anchor_1_y phy_joint_anchor_2_x phy_joint_anchor_2_y phy_joint_reaction_force_x phy_joint_reaction_force_y phy_joint_reaction_torque phy_joint_motor_speed phy_joint_angle phy_joint_motor_torque phy_joint_max_motor_torque phy_joint_translation phy_joint_speed phy_joint_motor_force phy_joint_max_motor_force phy_joint_length_1 phy_joint_length_2 phy_joint_damping_ratio phy_joint_frequency phy_joint_lower_angle_limit phy_joint_upper_angle_limit phy_joint_angle_limits phy_joint_max_length phy_joint_max_torque phy_joint_max_force phy_debug_render_aabb phy_debug_render_collision_pairs phy_debug_render_coms phy_debug_render_core_shapes phy_debug_render_joints phy_debug_render_obb phy_debug_render_shapes  phy_particle_flag_water phy_particle_flag_zombie phy_particle_flag_wall phy_particle_flag_spring phy_particle_flag_elastic phy_particle_flag_viscous phy_particle_flag_powder phy_particle_flag_tensile phy_particle_flag_colourmixing phy_particle_flag_colormixing phy_particle_group_flag_solid phy_particle_group_flag_rigid phy_particle_data_flag_typeflags phy_particle_data_flag_position phy_particle_data_flag_velocity phy_particle_data_flag_colour phy_particle_data_flag_color phy_particle_data_flag_category  achievement_our_info achievement_friends_info achievement_leaderboard_info achievement_achievement_info achievement_filter_all_players achievement_filter_friends_only achievement_filter_favorites_only achievement_type_achievement_challenge achievement_type_score_challenge achievement_pic_loaded  achievement_show_ui achievement_show_profile achievement_show_leaderboard achievement_show_achievement achievement_show_bank achievement_show_friend_picker achievement_show_purchase_prompt network_socket_tcp network_socket_udp network_socket_bluetooth network_type_connect network_type_disconnect network_type_data network_type_non_blocking_connect network_config_connect_timeout network_config_use_non_blocking_socket network_config_enable_reliable_udp network_config_disable_reliable_udp buffer_fixed buffer_grow buffer_wrap buffer_fast buffer_vbuffer buffer_network buffer_u8 buffer_s8 buffer_u16 buffer_s16 buffer_u32 buffer_s32 buffer_u64 buffer_f16 buffer_f32 buffer_f64 buffer_bool buffer_text buffer_string buffer_surface_copy buffer_seek_start buffer_seek_relative buffer_seek_end buffer_generalerror buffer_outofspace buffer_outofbounds buffer_invalidtype  text_type button_type input_type ANSI_CHARSET DEFAULT_CHARSET EASTEUROPE_CHARSET RUSSIAN_CHARSET SYMBOL_CHARSET SHIFTJIS_CHARSET HANGEUL_CHARSET GB2312_CHARSET CHINESEBIG5_CHARSET JOHAB_CHARSET HEBREW_CHARSET ARABIC_CHARSET GREEK_CHARSET TURKISH_CHARSET VIETNAMESE_CHARSET THAI_CHARSET MAC_CHARSET BALTIC_CHARSET OEM_CHARSET  gp_face1 gp_face2 gp_face3 gp_face4 gp_shoulderl gp_shoulderr gp_shoulderlb gp_shoulderrb gp_select gp_start gp_stickl gp_stickr gp_padu gp_padd gp_padl gp_padr gp_axislh gp_axislv gp_axisrh gp_axisrv ov_friends ov_community ov_players ov_settings ov_gamegroup ov_achievements lb_sort_none lb_sort_ascending lb_sort_descending lb_disp_none lb_disp_numeric lb_disp_time_sec lb_disp_time_ms ugc_result_success ugc_filetype_community ugc_filetype_microtrans ugc_visibility_public ugc_visibility_friends_only ugc_visibility_private ugc_query_RankedByVote ugc_query_RankedByPublicationDate ugc_query_AcceptedForGameRankedByAcceptanceDate ugc_query_RankedByTrend ugc_query_FavoritedByFriendsRankedByPublicationDate ugc_query_CreatedByFriendsRankedByPublicationDate ugc_query_RankedByNumTimesReported ugc_query_CreatedByFollowedUsersRankedByPublicationDate ugc_query_NotYetRated ugc_query_RankedByTotalVotesAsc ugc_query_RankedByVotesUp ugc_query_RankedByTextSearch ugc_sortorder_CreationOrderDesc ugc_sortorder_CreationOrderAsc ugc_sortorder_TitleAsc ugc_sortorder_LastUpdatedDesc ugc_sortorder_SubscriptionDateDesc ugc_sortorder_VoteScoreDesc ugc_sortorder_ForModeration ugc_list_Published ugc_list_VotedOn ugc_list_VotedUp ugc_list_VotedDown ugc_list_WillVoteLater ugc_list_Favorited ugc_list_Subscribed ugc_list_UsedOrPlayed ugc_list_Followed ugc_match_Items ugc_match_Items_Mtx ugc_match_Items_ReadyToUse ugc_match_Collections ugc_match_Artwork ugc_match_Videos ugc_match_Screenshots ugc_match_AllGuides ugc_match_WebGuides ugc_match_IntegratedGuides ugc_match_UsableInGame ugc_match_ControllerBindings  vertex_usage_position vertex_usage_colour vertex_usage_color vertex_usage_normal vertex_usage_texcoord vertex_usage_textcoord vertex_usage_blendweight vertex_usage_blendindices vertex_usage_psize vertex_usage_tangent vertex_usage_binormal vertex_usage_fog vertex_usage_depth vertex_usage_sample vertex_type_float1 vertex_type_float2 vertex_type_float3 vertex_type_float4 vertex_type_colour vertex_type_color vertex_type_ubyte4 layerelementtype_undefined layerelementtype_background layerelementtype_instance layerelementtype_oldtilemap layerelementtype_sprite layerelementtype_tilemap layerelementtype_particlesystem layerelementtype_tile tile_rotate tile_flip tile_mirror tile_index_mask kbv_type_default kbv_type_ascii kbv_type_url kbv_type_email kbv_type_numbers kbv_type_phone kbv_type_phone_name kbv_returnkey_default kbv_returnkey_go kbv_returnkey_google kbv_returnkey_join kbv_returnkey_next kbv_returnkey_route kbv_returnkey_search kbv_returnkey_send kbv_returnkey_yahoo kbv_returnkey_done kbv_returnkey_continue kbv_returnkey_emergency kbv_autocapitalize_none kbv_autocapitalize_words kbv_autocapitalize_sentences kbv_autocapitalize_characters",
-symbol:"argument_relative argument argument0 argument1 argument2 argument3 argument4 argument5 argument6 argument7 argument8 argument9 argument10 argument11 argument12 argument13 argument14 argument15 argument_count x y xprevious yprevious xstart ystart hspeed vspeed direction speed friction gravity gravity_direction path_index path_position path_positionprevious path_speed path_scale path_orientation path_endaction object_index id solid persistent mask_index instance_count instance_id room_speed fps fps_real current_time current_year current_month current_day current_weekday current_hour current_minute current_second alarm timeline_index timeline_position timeline_speed timeline_running timeline_loop room room_first room_last room_width room_height room_caption room_persistent score lives health show_score show_lives show_health caption_score caption_lives caption_health event_type event_number event_object event_action application_surface gamemaker_pro gamemaker_registered gamemaker_version error_occurred error_last debug_mode keyboard_key keyboard_lastkey keyboard_lastchar keyboard_string mouse_x mouse_y mouse_button mouse_lastbutton cursor_sprite visible sprite_index sprite_width sprite_height sprite_xoffset sprite_yoffset image_number image_index image_speed depth image_xscale image_yscale image_angle image_alpha image_blend bbox_left bbox_right bbox_top bbox_bottom layer background_colour  background_showcolour background_color background_showcolor view_enabled view_current view_visible view_xview view_yview view_wview view_hview view_xport view_yport view_wport view_hport view_angle view_hborder view_vborder view_hspeed view_vspeed view_object view_surface_id view_camera game_id game_display_name game_project_name game_save_id working_directory temp_directory program_directory browser_width browser_height os_type os_device os_browser os_version display_aa async_load delta_time webgl_enabled event_data iap_data phy_rotation phy_position_x phy_position_y phy_angular_velocity phy_linear_velocity_x phy_linear_velocity_y phy_speed_x phy_speed_y phy_speed phy_angular_damping phy_linear_damping phy_bullet phy_fixed_rotation phy_active phy_mass phy_inertia phy_com_x phy_com_y phy_dynamic phy_kinematic phy_sleeping phy_collision_points phy_collision_x phy_collision_y phy_col_normal_x phy_col_normal_y phy_position_xprevious phy_position_yprevious"},
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE]}}}());
-hljs.registerLanguage("go",function(){return function(a){var b={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{name:"Go",
-aliases:["golang"],keywords:b,illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{begin:"`",end:"`"}]},{className:"number",variants:[{begin:a.C_NUMBER_RE+"[i]",relevance:1},a.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",end:"\\s*(\\{|$)",excludeEnd:!0,contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:b,illegal:/["']/}]}]}}}());
-hljs.registerLanguage("golo",function(){return function(a){return{name:"Golo",keywords:{keyword:"println readln print import module function local return let var while for foreach times in case when match with break continue augment augmentation each find filter reduce if then else otherwise try catch finally raise throw orIfNull DynamicObject|10 DynamicVariable struct Observable map set vector list array",literal:"true false null"},contains:[a.HASH_COMMENT_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,
-{className:"meta",begin:"@[A-Za-z]+"}]}}}());
-hljs.registerLanguage("gradle",function(){return function(a){return{name:"Gradle",case_insensitive:!0,keywords:{keyword:"task project allprojects subprojects artifacts buildscript configurations dependencies repositories sourceSets description delete from into include exclude source classpath destinationDir includes options sourceCompatibility targetCompatibility group flatDir doLast doFirst flatten todir fromdir ant def abstract break case catch continue default do else extends final finally for if implements instanceof native new private protected public return static switch synchronized throw throws transient try volatile while strictfp package import false null super this true antlrtask checkstyle codenarc copy boolean byte char class double float int interface long short void compile runTime file fileTree abs any append asList asWritable call collect compareTo count div dump each eachByte eachFile eachLine every find findAll flatten getAt getErr getIn getOut getText grep immutable inject inspect intersect invokeMethods isCase join leftShift minus multiply newInputStream newOutputStream newPrintWriter newReader newWriter next plus pop power previous print println push putAt read readBytes readLines reverse reverseEach round size sort splitEachLine step subMap times toInteger toList tokenize upto waitForOrKill withPrintWriter withReader withStream withWriter withWriterAppend write writeLine"},contains:[a.C_LINE_COMMENT_MODE,
-a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.REGEXP_MODE]}}}());
-hljs.registerLanguage("groovy",function(){return function(a){return{name:"Groovy",keywords:{literal:"true false null",keyword:"byte short char int long boolean float double void def as in assert trait super this abstract static volatile transient public private protected synchronized final class interface enum if else for while switch case break default continue throw throws try catch finally implements extends new import package return instanceof"},contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,
-contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",begin:'"""',end:'"""'},{className:"string",begin:"'''",end:"'''"},{className:"string",begin:"\\$/",end:"/\\$",relevance:10},a.APOS_STRING_MODE,{className:"regexp",begin:/~?\/[^\/\n]+\//,contains:[a.BACKSLASH_ESCAPE]},a.QUOTE_STRING_MODE,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},a.BINARY_NUMBER_MODE,{className:"class",beginKeywords:"class interface trait enum",
-end:"{",illegal:":",contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{className:"string",begin:/[^\?]{0}[A-Za-z0-9_$]+ *:/},{begin:/\?/,end:/:/},{className:"symbol",begin:"^\\s*[A-Za-z0-9_$]+:",relevance:0}],illegal:/#|<\//}}}());
-hljs.registerLanguage("haml",function(){return function(a){return{name:"HAML",case_insensitive:!0,contains:[{className:"meta",begin:"^!!!( (5|1\\.1|Strict|Frameset|Basic|Mobile|RDFa|XML\\b.*))?$",relevance:10},a.COMMENT("^\\s*(!=#|=#|-#|/).*$",!1,{relevance:0}),{begin:"^\\s*(-|=|!=)(?!#)",starts:{end:"\\n",subLanguage:"ruby"}},{className:"tag",begin:"^\\s*%",contains:[{className:"selector-tag",begin:"\\w+"},{className:"selector-id",begin:"#[\\w-]+"},{className:"selector-class",begin:"\\.[\\w-]+"},
-{begin:"{\\s*",end:"\\s*}",contains:[{begin:":\\w+\\s*=>",end:",\\s+",returnBegin:!0,endsWithParent:!0,contains:[{className:"attr",begin:":\\w+"},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{begin:"\\w+",relevance:0}]}]},{begin:"\\(\\s*",end:"\\s*\\)",excludeEnd:!0,contains:[{begin:"\\w+\\s*=",end:"\\s+",returnBegin:!0,endsWithParent:!0,contains:[{className:"attr",begin:"\\w+",relevance:0},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{begin:"\\w+",relevance:0}]}]}]},{begin:"^\\s*[=~]\\s*"},{begin:"#{",starts:{end:"}",
-subLanguage:"ruby"}}]}}}());
-hljs.registerLanguage("handlebars",function(){return function(a){var b={"builtin-name":"each in with if else unless bindattr action collection debugger log outlet template unbound view yield lookup"},c={begin:/".*?"|'.*?'|\[.*?\]|\w+/},d=a.inherit(c,{keywords:b,starts:{endsWithParent:!0,relevance:0,contains:[a.inherit(c,{relevance:0})]}});c=a.inherit(d,{className:"name"});d=a.inherit(d,{relevance:0});return{name:"Handlebars",aliases:["hbs","html.hbs","html.handlebars"],case_insensitive:!0,subLanguage:"xml",
-contains:[{begin:/\\\{\{/,skip:!0},{begin:/\\\\(?=\{\{)/,skip:!0},a.COMMENT(/\{\{!--/,/--\}\}/),a.COMMENT(/\{\{!/,/\}\}/),{className:"template-tag",begin:/\{\{\{\{(?!\/)/,end:/\}\}\}\}/,contains:[c],starts:{end:/\{\{\{\{\//,returnEnd:!0,subLanguage:"xml"}},{className:"template-tag",begin:/\{\{\{\{\//,end:/\}\}\}\}/,contains:[c]},{className:"template-tag",begin:/\{\{[#\/]/,end:/\}\}/,contains:[c]},{className:"template-variable",begin:/\{\{\{/,end:/\}\}\}/,keywords:b,contains:[d]},{className:"template-variable",
-begin:/\{\{/,end:/\}\}/,keywords:b,contains:[d]}]}}}());
-hljs.registerLanguage("haskell",function(){return function(a){var b={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},c={className:"meta",begin:"{-#",end:"#-}"},d={className:"meta",begin:"^#",end:"$"},e={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},f={begin:"\\(",end:"\\)",illegal:'"',contains:[c,d,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},a.inherit(a.TITLE_MODE,{begin:"[_a-z][\\w']*"}),b]};return{name:"Haskell",aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",
-contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[f,b],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[f,b],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",end:"where",keywords:"class family instance where",contains:[e,f,b]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[c,e,f,{begin:"{",end:"}",contains:f.contains},b]},{beginKeywords:"default",
-end:"$",contains:[e,f,b]},{beginKeywords:"infix infixl infixr",end:"$",contains:[a.C_NUMBER_MODE,b]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[e,a.QUOTE_STRING_MODE,b]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},c,d,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,e,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),b,{begin:"->|<-"}]}}}());
-hljs.registerLanguage("haxe",function(){return function(a){return{name:"Haxe",aliases:["hx"],keywords:{keyword:"break case cast catch continue default do dynamic else enum extern for function here if import in inline never new override package private get set public return static super switch this throw trace try typedef untyped using var while Int Float String Bool Dynamic Void Array ",built_in:"trace this",literal:"true false null _"},contains:[{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE,
-{className:"subst",begin:"\\$\\{",end:"\\}"},{className:"subst",begin:"\\$",end:"\\W}"}]},a.QUOTE_STRING_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.C_NUMBER_MODE,{className:"meta",begin:"@:",end:"$"},{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elseif end error"}},{className:"type",begin:":[ \t]*",end:"[^A-Za-z0-9_ \t\\->]",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:":[ \t]*",end:"\\W",excludeBegin:!0,excludeEnd:!0},{className:"type",begin:"new *",
-end:"\\W",excludeBegin:!0,excludeEnd:!0},{className:"class",beginKeywords:"enum",end:"\\{",contains:[a.TITLE_MODE]},{className:"class",beginKeywords:"abstract",end:"[\\{$]",contains:[{className:"type",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"type",begin:"from +",end:"\\W",excludeBegin:!0,excludeEnd:!0},{className:"type",begin:"to +",end:"\\W",excludeBegin:!0,excludeEnd:!0},a.TITLE_MODE],keywords:{keyword:"abstract from to"}},{className:"class",begin:"\\b(class|interface) +",
-end:"[\\{$]",excludeEnd:!0,keywords:"class interface",contains:[{className:"keyword",begin:"\\b(extends|implements) +",keywords:"extends implements",contains:[{className:"type",begin:a.IDENT_RE,relevance:0}]},a.TITLE_MODE]},{className:"function",beginKeywords:"function",end:"\\(",excludeEnd:!0,illegal:"\\S",contains:[a.TITLE_MODE]}],illegal:/<\//}}}());
-hljs.registerLanguage("hsp",function(){return function(a){return{name:"HSP",case_insensitive:!0,keywords:{$pattern:/[\w._]+/,keyword:"goto gosub return break repeat loop continue wait await dim sdim foreach dimtype dup dupptr end stop newmod delmod mref run exgoto on mcall assert logmes newlab resume yield onexit onerror onkey onclick oncmd exist delete mkdir chdir dirlist bload bsave bcopy memfile if else poke wpoke lpoke getstr chdpm memexpand memcpy memset notesel noteadd notedel noteload notesave randomize noteunsel noteget split strrep setease button chgdisp exec dialog mmload mmplay mmstop mci pset pget syscolor mes print title pos circle cls font sysfont objsize picload color palcolor palette redraw width gsel gcopy gzoom gmode bmpsave hsvcolor getkey listbox chkbox combox input mesbox buffer screen bgscr mouse objsel groll line clrobj boxf objprm objmode stick grect grotate gsquare gradf objimage objskip objenable celload celdiv celput newcom querycom delcom cnvstow comres axobj winobj sendmsg comevent comevarg sarrayconv callfunc cnvwtos comevdisp libptr system hspstat hspver stat cnt err strsize looplev sublev iparam wparam lparam refstr refdval int rnd strlen length length2 length3 length4 vartype gettime peek wpeek lpeek varptr varuse noteinfo instr abs limit getease str strmid strf getpath strtrim sin cos tan atan sqrt double absf expf logf limitf powf geteasef mousex mousey mousew hwnd hinstance hdc ginfo objinfo dirinfo sysinfo thismod __hspver__ __hsp30__ __date__ __time__ __line__ __file__ _debug __hspdef__ and or xor not screen_normal screen_palette screen_hide screen_fixedsize screen_tool screen_frame gmode_gdi gmode_mem gmode_rgb0 gmode_alpha gmode_rgb0alpha gmode_add gmode_sub gmode_pixela ginfo_mx ginfo_my ginfo_act ginfo_sel ginfo_wx1 ginfo_wy1 ginfo_wx2 ginfo_wy2 ginfo_vx ginfo_vy ginfo_sizex ginfo_sizey ginfo_winx ginfo_winy ginfo_mesx ginfo_mesy ginfo_r ginfo_g ginfo_b ginfo_paluse ginfo_dispx ginfo_dispy ginfo_cx ginfo_cy ginfo_intid ginfo_newid ginfo_sx ginfo_sy objinfo_mode objinfo_bmscr objinfo_hwnd notemax notesize dir_cur dir_exe dir_win dir_sys dir_cmdline dir_desktop dir_mydoc dir_tv font_normal font_bold font_italic font_underline font_strikeout font_antialias objmode_normal objmode_guifont objmode_usefont gsquare_grad msgothic msmincho do until while wend for next _break _continue switch case default swbreak swend ddim ldim alloc m_pi rad2deg deg2rad ease_linear ease_quad_in ease_quad_out ease_quad_inout ease_cubic_in ease_cubic_out ease_cubic_inout ease_quartic_in ease_quartic_out ease_quartic_inout ease_bounce_in ease_bounce_out ease_bounce_inout ease_shake_in ease_shake_out ease_shake_inout ease_loop"},contains:[a.C_LINE_COMMENT_MODE,
-a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{className:"string",begin:'{"',end:'"}',contains:[a.BACKSLASH_ESCAPE]},a.COMMENT(";","$",{relevance:0}),{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"addion cfunc cmd cmpopt comfunc const defcfunc deffunc define else endif enum epack func global if ifdef ifndef include modcfunc modfunc modinit modterm module pack packopt regcmd runtime undef usecom uselib"},contains:[a.inherit(a.QUOTE_STRING_MODE,{className:"meta-string"}),
-a.NUMBER_MODE,a.C_NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"symbol",begin:"^\\*(\\w+|@)"},a.NUMBER_MODE,a.C_NUMBER_MODE]}}}());
-hljs.registerLanguage("htmlbars",function(){return function(a){a.requireLanguage("handlebars");var b={endsWithParent:!0,relevance:0,keywords:{keyword:"as",built_in:"action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view"},contains:[a.QUOTE_STRING_MODE,{illegal:/\}\}/,begin:/[a-zA-Z0-9_]+=/,returnBegin:!0,relevance:0,contains:[{className:"attr",begin:/[a-zA-Z0-9_]+/}]},a.NUMBER_MODE]};
-return{name:"HTMLBars",case_insensitive:!0,subLanguage:"xml",contains:[a.COMMENT("{{!(--)?","(--)?}}"),{className:"template-tag",begin:/\{\{[#\/]/,end:/\}\}/,contains:[{className:"name",begin:/[a-zA-Z\.\-]+/,keywords:{"builtin-name":"action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view"},starts:b}]},{className:"template-variable",begin:/\{\{[a-zA-Z][a-zA-Z\-]+/,end:/\}\}/,keywords:{keyword:"as",
-built_in:"action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view"},contains:[a.QUOTE_STRING_MODE]}]}}}());
-hljs.registerLanguage("http",function(){return function(a){return{name:"HTTP",aliases:["https"],illegal:"\\S",contains:[{begin:"^HTTP/[0-9\\.]+",end:"$",contains:[{className:"number",begin:"\\b\\d{3}\\b"}]},{begin:"^[A-Z]+ (.*?) HTTP/[0-9\\.]+$",returnBegin:!0,end:"$",contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{begin:"HTTP/[0-9\\.]+"},{className:"keyword",begin:"[A-Z]+"}]},{className:"attribute",begin:"^\\w",end:": ",excludeEnd:!0,illegal:"\\n|\\s|=",starts:{end:"$",
-relevance:0}},{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}]}}}());
-hljs.registerLanguage("hy",function(){return function(a){var b={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},c=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),d=a.COMMENT(";","$",{relevance:0}),e={className:"literal",begin:/\b([Tt]rue|[Ff]alse|nil|None)\b/},f={begin:"[\\[\\{]",end:"[\\]\\}]"},h={className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},g=a.COMMENT("\\^\\{","\\}"),k={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},
-l={begin:"\\(",end:"\\)"},m={endsWithParent:!0,relevance:0},p={keywords:{$pattern:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*","builtin-name":"!= % %= & &= * ** **= *= *map + += , --build-class-- --import-- -= . / // //= /= < << <<= <= = > >= >> >>= @ @= ^ ^= abs accumulate all and any ap-compose ap-dotimes ap-each ap-each-while ap-filter ap-first ap-if ap-last ap-map ap-map-when ap-pipe ap-reduce ap-reject apply as-> ascii assert assoc bin break butlast callable calling-module-name car case cdr chain chr coll? combinations compile compress cond cons cons? continue count curry cut cycle dec def default-method defclass defmacro defmacro-alias defmacro/g! defmain defmethod defmulti defn defn-alias defnc defnr defreader defseq del delattr delete-route dict-comp dir disassemble dispatch-reader-macro distinct divmod do doto drop drop-last drop-while empty? end-sequence eval eval-and-compile eval-when-compile even? every? except exec filter first flatten float? fn fnc fnr for for* format fraction genexpr gensym get getattr global globals group-by hasattr hash hex id identity if if* if-not if-python2 import in inc input instance? integer integer-char? integer? interleave interpose is is-coll is-cons is-empty is-even is-every is-float is-instance is-integer is-integer-char is-iterable is-iterator is-keyword is-neg is-none is-not is-numeric is-odd is-pos is-string is-symbol is-zero isinstance islice issubclass iter iterable? iterate iterator? keyword keyword? lambda last len let lif lif-not list* list-comp locals loop macro-error macroexpand macroexpand-1 macroexpand-all map max merge-with method-decorator min multi-decorator multicombinations name neg? next none? nonlocal not not-in not? nth numeric? oct odd? open or ord partition permutations pos? post-route postwalk pow prewalk print product profile/calls profile/cpu put-route quasiquote quote raise range read read-str recursive-replace reduce remove repeat repeatedly repr require rest round route route-with-methods rwm second seq set-comp setattr setv some sorted string string? sum switch symbol? take take-nth take-while tee try unless unquote unquote-splicing vars walk when while with with* with-decorator with-gensyms xi xor yield yield-from zero? zip zip-longest | |= ~"},
-className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:m},q=[l,c,h,g,d,k,f,b,e,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];l.contains=[a.COMMENT("comment",""),p,m];m.contains=q;f.contains=q;return{name:"Hy",aliases:["hylang"],illegal:/\S/,contains:[a.SHEBANG(),l,c,h,g,d,k,f,b,e]}}}());
-hljs.registerLanguage("inform7",function(){return function(a){return{name:"Inform 7",aliases:["i7"],case_insensitive:!0,keywords:{keyword:"thing room person man woman animal container supporter backdrop door scenery open closed locked inside gender is are say understand kind of rule"},contains:[{className:"string",begin:'"',end:'"',relevance:0,contains:[{className:"subst",begin:"\\[",end:"\\]"}]},{className:"section",begin:/^(Volume|Book|Part|Chapter|Section|Table)\b/,end:"$"},{begin:/^(Check|Carry out|Report|Instead of|To|Rule|When|Before|After)\b/,
-end:":",contains:[{begin:"\\(This",end:"\\)"}]},{className:"comment",begin:"\\[",end:"\\]",contains:["self"]}]}}}());
-hljs.registerLanguage("ini",function(){return function(a){var b={className:"number",relevance:0,variants:[{begin:/([\+\-]+)?[\d]+_[\d_]+/},{begin:a.NUMBER_RE}]},c=a.COMMENT();c.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];var d={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)}/}]},e={className:"literal",begin:/\bon|off|true|false|yes|no\b/};a={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},
-{begin:'"',end:'"'},{begin:"'",end:"'"}]};return{name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,contains:[c,{className:"section",begin:/\[+/,end:/\]+/},{begin:/^[a-z0-9\[\]_\.-]+(?=\s*=\s*)/,className:"attr",starts:{end:/$/,contains:[c,{begin:/\[/,end:/\]/,contains:[c,e,d,a,b,"self"],relevance:0},e,d,a,b]}}]}}}());
-hljs.registerLanguage("irpf90",function(){return function(a){return{name:"IRPF90",case_insensitive:!0,keywords:{literal:".False. .True.",keyword:"kind do while private call intrinsic where elsewhere type endtype endmodule endselect endinterface end enddo endif if forall endforall only contains default return stop then public subroutine|10 function program .and. .or. .not. .le. .eq. .ge. .gt. .lt. goto save else use module select case access blank direct exist file fmt form formatted iostat name named nextrec number opened rec recl sequential status unformatted unit continue format pause cycle exit c_null_char c_alert c_backspace c_form_feed flush wait decimal round iomsg synchronous nopass non_overridable pass protected volatile abstract extends import non_intrinsic value deferred generic final enumerator class associate bind enum c_int c_short c_long c_long_long c_signed_char c_size_t c_int8_t c_int16_t c_int32_t c_int64_t c_int_least8_t c_int_least16_t c_int_least32_t c_int_least64_t c_int_fast8_t c_int_fast16_t c_int_fast32_t c_int_fast64_t c_intmax_t C_intptr_t c_float c_double c_long_double c_float_complex c_double_complex c_long_double_complex c_bool c_char c_null_ptr c_null_funptr c_new_line c_carriage_return c_horizontal_tab c_vertical_tab iso_c_binding c_loc c_funloc c_associated  c_f_pointer c_ptr c_funptr iso_fortran_env character_storage_size error_unit file_storage_size input_unit iostat_end iostat_eor numeric_storage_size output_unit c_f_procpointer ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode newunit contiguous recursive pad position action delim readwrite eor advance nml interface procedure namelist include sequence elemental pure integer real character complex logical dimension allocatable|10 parameter external implicit|10 none double precision assign intent optional pointer target in out common equivalence data begin_provider &begin_provider end_provider begin_shell end_shell begin_template end_template subst assert touch soft_touch provide no_dep free irp_if irp_else irp_endif irp_write irp_read",built_in:"alog alog10 amax0 amax1 amin0 amin1 amod cabs ccos cexp clog csin csqrt dabs dacos dasin datan datan2 dcos dcosh ddim dexp dint dlog dlog10 dmax1 dmin1 dmod dnint dsign dsin dsinh dsqrt dtan dtanh float iabs idim idint idnint ifix isign max0 max1 min0 min1 sngl algama cdabs cdcos cdexp cdlog cdsin cdsqrt cqabs cqcos cqexp cqlog cqsin cqsqrt dcmplx dconjg derf derfc dfloat dgamma dimag dlgama iqint qabs qacos qasin qatan qatan2 qcmplx qconjg qcos qcosh qdim qerf qerfc qexp qgamma qimag qlgama qlog qlog10 qmax1 qmin1 qmod qnint qsign qsin qsinh qsqrt qtan qtanh abs acos aimag aint anint asin atan atan2 char cmplx conjg cos cosh exp ichar index int log log10 max min nint sign sin sinh sqrt tan tanh print write dim lge lgt lle llt mod nullify allocate deallocate adjustl adjustr all allocated any associated bit_size btest ceiling count cshift date_and_time digits dot_product eoshift epsilon exponent floor fraction huge iand ibclr ibits ibset ieor ior ishft ishftc lbound len_trim matmul maxexponent maxloc maxval merge minexponent minloc minval modulo mvbits nearest pack present product radix random_number random_seed range repeat reshape rrspacing scale scan selected_int_kind selected_real_kind set_exponent shape size spacing spread sum system_clock tiny transpose trim ubound unpack verify achar iachar transfer dble entry dprod cpu_time command_argument_count get_command get_command_argument get_environment_variable is_iostat_end ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode is_iostat_eor move_alloc new_line selected_char_kind same_type_as extends_type_of acosh asinh atanh bessel_j0 bessel_j1 bessel_jn bessel_y0 bessel_y1 bessel_yn erf erfc erfc_scaled gamma log_gamma hypot norm2 atomic_define atomic_ref execute_command_line leadz trailz storage_size merge_bits bge bgt ble blt dshiftl dshiftr findloc iall iany iparity image_index lcobound ucobound maskl maskr num_images parity popcnt poppar shifta shiftl shiftr this_image IRP_ALIGN irp_here"},
-illegal:/\/\*/,contains:[a.inherit(a.APOS_STRING_MODE,{className:"string",relevance:0}),a.inherit(a.QUOTE_STRING_MODE,{className:"string",relevance:0}),{className:"function",beginKeywords:"subroutine function program",illegal:"[${=\\n]",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)"}]},a.COMMENT("!","$",{relevance:0}),a.COMMENT("begin_doc","end_doc",{relevance:10}),{className:"number",begin:"(?=\\b|\\+|\\-|\\.)(?:\\.|\\d+\\.?)\\d*([de][+-]?\\d+)?(_[a-z_\\d]+)?",relevance:0}]}}}());
-hljs.registerLanguage("isbl",function(){return function(a){var b={className:"number",begin:a.NUMBER_RE,relevance:0},c={className:"string",variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]},d={className:"doctag",begin:"\\b(?:TODO|DONE|BEGIN|END|STUB|CHG|FIXME|NOTE|BUG|XXX)\\b",relevance:0};d={variants:[{className:"comment",begin:"//",end:"$",relevance:0,contains:[a.PHRASAL_WORDS_MODE,d]},{className:"comment",begin:"/\\*",end:"\\*/",relevance:0,contains:[a.PHRASAL_WORDS_MODE,d]}]};var e={$pattern:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_!][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*",
-keyword:"and \u0438 else \u0438\u043d\u0430\u0447\u0435 endexcept endfinally endforeach \u043a\u043e\u043d\u0435\u0446\u0432\u0441\u0435 endif \u043a\u043e\u043d\u0435\u0446\u0435\u0441\u043b\u0438 endwhile \u043a\u043e\u043d\u0435\u0446\u043f\u043e\u043a\u0430 except exitfor finally foreach \u0432\u0441\u0435 if \u0435\u0441\u043b\u0438 in \u0432 not \u043d\u0435 or \u0438\u043b\u0438 try while \u043f\u043e\u043a\u0430 ",built_in:"SYSRES_CONST_ACCES_RIGHT_TYPE_EDIT SYSRES_CONST_ACCES_RIGHT_TYPE_FULL SYSRES_CONST_ACCES_RIGHT_TYPE_VIEW SYSRES_CONST_ACCESS_MODE_REQUISITE_CODE SYSRES_CONST_ACCESS_NO_ACCESS_VIEW SYSRES_CONST_ACCESS_NO_ACCESS_VIEW_CODE SYSRES_CONST_ACCESS_RIGHTS_ADD_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_ADD_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_CHANGE_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_CHANGE_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_DELETE_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_DELETE_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_EXECUTE_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_EXECUTE_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_NO_ACCESS_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_NO_ACCESS_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_RATIFY_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_RATIFY_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_VIEW SYSRES_CONST_ACCESS_RIGHTS_VIEW_CODE SYSRES_CONST_ACCESS_RIGHTS_VIEW_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_VIEW_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_TYPE_CHANGE SYSRES_CONST_ACCESS_TYPE_CHANGE_CODE SYSRES_CONST_ACCESS_TYPE_EXISTS SYSRES_CONST_ACCESS_TYPE_EXISTS_CODE SYSRES_CONST_ACCESS_TYPE_FULL SYSRES_CONST_ACCESS_TYPE_FULL_CODE SYSRES_CONST_ACCESS_TYPE_VIEW SYSRES_CONST_ACCESS_TYPE_VIEW_CODE SYSRES_CONST_ACTION_TYPE_ABORT SYSRES_CONST_ACTION_TYPE_ACCEPT SYSRES_CONST_ACTION_TYPE_ACCESS_RIGHTS SYSRES_CONST_ACTION_TYPE_ADD_ATTACHMENT SYSRES_CONST_ACTION_TYPE_CHANGE_CARD SYSRES_CONST_ACTION_TYPE_CHANGE_KIND SYSRES_CONST_ACTION_TYPE_CHANGE_STORAGE SYSRES_CONST_ACTION_TYPE_CONTINUE SYSRES_CONST_ACTION_TYPE_COPY SYSRES_CONST_ACTION_TYPE_CREATE SYSRES_CONST_ACTION_TYPE_CREATE_VERSION SYSRES_CONST_ACTION_TYPE_DELETE SYSRES_CONST_ACTION_TYPE_DELETE_ATTACHMENT SYSRES_CONST_ACTION_TYPE_DELETE_VERSION SYSRES_CONST_ACTION_TYPE_DISABLE_DELEGATE_ACCESS_RIGHTS SYSRES_CONST_ACTION_TYPE_ENABLE_DELEGATE_ACCESS_RIGHTS SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_CERTIFICATE SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_CERTIFICATE_AND_PASSWORD SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_PASSWORD SYSRES_CONST_ACTION_TYPE_EXPORT_WITH_LOCK SYSRES_CONST_ACTION_TYPE_EXPORT_WITHOUT_LOCK SYSRES_CONST_ACTION_TYPE_IMPORT_WITH_UNLOCK SYSRES_CONST_ACTION_TYPE_IMPORT_WITHOUT_UNLOCK SYSRES_CONST_ACTION_TYPE_LIFE_CYCLE_STAGE SYSRES_CONST_ACTION_TYPE_LOCK SYSRES_CONST_ACTION_TYPE_LOCK_FOR_SERVER SYSRES_CONST_ACTION_TYPE_LOCK_MODIFY SYSRES_CONST_ACTION_TYPE_MARK_AS_READED SYSRES_CONST_ACTION_TYPE_MARK_AS_UNREADED SYSRES_CONST_ACTION_TYPE_MODIFY SYSRES_CONST_ACTION_TYPE_MODIFY_CARD SYSRES_CONST_ACTION_TYPE_MOVE_TO_ARCHIVE SYSRES_CONST_ACTION_TYPE_OFF_ENCRYPTION SYSRES_CONST_ACTION_TYPE_PASSWORD_CHANGE SYSRES_CONST_ACTION_TYPE_PERFORM SYSRES_CONST_ACTION_TYPE_RECOVER_FROM_LOCAL_COPY SYSRES_CONST_ACTION_TYPE_RESTART SYSRES_CONST_ACTION_TYPE_RESTORE_FROM_ARCHIVE SYSRES_CONST_ACTION_TYPE_REVISION SYSRES_CONST_ACTION_TYPE_SEND_BY_MAIL SYSRES_CONST_ACTION_TYPE_SIGN SYSRES_CONST_ACTION_TYPE_START SYSRES_CONST_ACTION_TYPE_UNLOCK SYSRES_CONST_ACTION_TYPE_UNLOCK_FROM_SERVER SYSRES_CONST_ACTION_TYPE_VERSION_STATE SYSRES_CONST_ACTION_TYPE_VERSION_VISIBILITY SYSRES_CONST_ACTION_TYPE_VIEW SYSRES_CONST_ACTION_TYPE_VIEW_SHADOW_COPY SYSRES_CONST_ACTION_TYPE_WORKFLOW_DESCRIPTION_MODIFY SYSRES_CONST_ACTION_TYPE_WRITE_HISTORY SYSRES_CONST_ACTIVE_VERSION_STATE_PICK_VALUE SYSRES_CONST_ADD_REFERENCE_MODE_NAME SYSRES_CONST_ADDITION_REQUISITE_CODE SYSRES_CONST_ADDITIONAL_PARAMS_REQUISITE_CODE SYSRES_CONST_ADITIONAL_JOB_END_DATE_REQUISITE_NAME SYSRES_CONST_ADITIONAL_JOB_READ_REQUISITE_NAME SYSRES_CONST_ADITIONAL_JOB_START_DATE_REQUISITE_NAME SYSRES_CONST_ADITIONAL_JOB_STATE_REQUISITE_NAME SYSRES_CONST_ADMINISTRATION_HISTORY_ADDING_USER_TO_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_ADDING_USER_TO_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_COMP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_COMP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_USER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_USER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_CREATION SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_CREATION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_DELETION SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_DELETION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_COMP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_COMP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_FROM_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_FROM_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_RESTRICTION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_RESTRICTION_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_PRIVILEGE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_PRIVILEGE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_RIGHTS_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_RIGHTS_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_IS_MAIN_SERVER_CHANGED_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_IS_MAIN_SERVER_CHANGED_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_IS_PUBLIC_CHANGED_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_IS_PUBLIC_CHANGED_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_RESTRICTION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_RESTRICTION_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_PRIVILEGE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_PRIVILEGE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_RIGHTS_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_RIGHTS_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_CREATION SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_CREATION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_DELETION SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_DELETION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_CATEGORY_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_CATEGORY_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_COMP_TITLE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_COMP_TITLE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_FULL_NAME_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_FULL_NAME_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_PARENT_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_PARENT_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_AUTH_TYPE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_AUTH_TYPE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_LOGIN_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_LOGIN_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_STATUS_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_STATUS_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_USER_PASSWORD_CHANGE SYSRES_CONST_ADMINISTRATION_HISTORY_USER_PASSWORD_CHANGE_ACTION SYSRES_CONST_ALL_ACCEPT_CONDITION_RUS SYSRES_CONST_ALL_USERS_GROUP SYSRES_CONST_ALL_USERS_GROUP_NAME SYSRES_CONST_ALL_USERS_SERVER_GROUP_NAME SYSRES_CONST_ALLOWED_ACCESS_TYPE_CODE SYSRES_CONST_ALLOWED_ACCESS_TYPE_NAME SYSRES_CONST_APP_VIEWER_TYPE_REQUISITE_CODE SYSRES_CONST_APPROVING_SIGNATURE_NAME SYSRES_CONST_APPROVING_SIGNATURE_REQUISITE_CODE SYSRES_CONST_ASSISTANT_SUBSTITUE_TYPE SYSRES_CONST_ASSISTANT_SUBSTITUE_TYPE_CODE SYSRES_CONST_ATTACH_TYPE_COMPONENT_TOKEN SYSRES_CONST_ATTACH_TYPE_DOC SYSRES_CONST_ATTACH_TYPE_EDOC SYSRES_CONST_ATTACH_TYPE_FOLDER SYSRES_CONST_ATTACH_TYPE_JOB SYSRES_CONST_ATTACH_TYPE_REFERENCE SYSRES_CONST_ATTACH_TYPE_TASK SYSRES_CONST_AUTH_ENCODED_PASSWORD SYSRES_CONST_AUTH_ENCODED_PASSWORD_CODE SYSRES_CONST_AUTH_NOVELL SYSRES_CONST_AUTH_PASSWORD SYSRES_CONST_AUTH_PASSWORD_CODE SYSRES_CONST_AUTH_WINDOWS SYSRES_CONST_AUTHENTICATING_SIGNATURE_NAME SYSRES_CONST_AUTHENTICATING_SIGNATURE_REQUISITE_CODE SYSRES_CONST_AUTO_ENUM_METHOD_FLAG SYSRES_CONST_AUTO_NUMERATION_CODE SYSRES_CONST_AUTO_STRONG_ENUM_METHOD_FLAG SYSRES_CONST_AUTOTEXT_NAME_REQUISITE_CODE SYSRES_CONST_AUTOTEXT_TEXT_REQUISITE_CODE SYSRES_CONST_AUTOTEXT_USAGE_ALL SYSRES_CONST_AUTOTEXT_USAGE_ALL_CODE SYSRES_CONST_AUTOTEXT_USAGE_SIGN SYSRES_CONST_AUTOTEXT_USAGE_SIGN_CODE SYSRES_CONST_AUTOTEXT_USAGE_WORK SYSRES_CONST_AUTOTEXT_USAGE_WORK_CODE SYSRES_CONST_AUTOTEXT_USE_ANYWHERE_CODE SYSRES_CONST_AUTOTEXT_USE_ON_SIGNING_CODE SYSRES_CONST_AUTOTEXT_USE_ON_WORK_CODE SYSRES_CONST_BEGIN_DATE_REQUISITE_CODE SYSRES_CONST_BLACK_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_BLUE_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_BTN_PART SYSRES_CONST_CALCULATED_ROLE_TYPE_CODE SYSRES_CONST_CALL_TYPE_VARIABLE_BUTTON_VALUE SYSRES_CONST_CALL_TYPE_VARIABLE_PROGRAM_VALUE SYSRES_CONST_CANCEL_MESSAGE_FUNCTION_RESULT SYSRES_CONST_CARD_PART SYSRES_CONST_CARD_REFERENCE_MODE_NAME SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_ENCRYPT_VALUE SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_SIGN_AND_ENCRYPT_VALUE SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_SIGN_VALUE SYSRES_CONST_CHECK_PARAM_VALUE_DATE_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_FLOAT_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_INTEGER_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_PICK_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_REEFRENCE_PARAM_TYPE SYSRES_CONST_CLOSED_RECORD_FLAG_VALUE_FEMININE SYSRES_CONST_CLOSED_RECORD_FLAG_VALUE_MASCULINE SYSRES_CONST_CODE_COMPONENT_TYPE_ADMIN SYSRES_CONST_CODE_COMPONENT_TYPE_DEVELOPER SYSRES_CONST_CODE_COMPONENT_TYPE_DOCS SYSRES_CONST_CODE_COMPONENT_TYPE_EDOC_CARDS SYSRES_CONST_CODE_COMPONENT_TYPE_EXTERNAL_EXECUTABLE SYSRES_CONST_CODE_COMPONENT_TYPE_OTHER SYSRES_CONST_CODE_COMPONENT_TYPE_REFERENCE SYSRES_CONST_CODE_COMPONENT_TYPE_REPORT SYSRES_CONST_CODE_COMPONENT_TYPE_SCRIPT SYSRES_CONST_CODE_COMPONENT_TYPE_URL SYSRES_CONST_CODE_REQUISITE_ACCESS SYSRES_CONST_CODE_REQUISITE_CODE SYSRES_CONST_CODE_REQUISITE_COMPONENT SYSRES_CONST_CODE_REQUISITE_DESCRIPTION SYSRES_CONST_CODE_REQUISITE_EXCLUDE_COMPONENT SYSRES_CONST_CODE_REQUISITE_RECORD SYSRES_CONST_COMMENT_REQ_CODE SYSRES_CONST_COMMON_SETTINGS_REQUISITE_CODE SYSRES_CONST_COMP_CODE_GRD SYSRES_CONST_COMPONENT_GROUP_TYPE_REQUISITE_CODE SYSRES_CONST_COMPONENT_TYPE_ADMIN_COMPONENTS SYSRES_CONST_COMPONENT_TYPE_DEVELOPER_COMPONENTS SYSRES_CONST_COMPONENT_TYPE_DOCS SYSRES_CONST_COMPONENT_TYPE_EDOC_CARDS SYSRES_CONST_COMPONENT_TYPE_EDOCS SYSRES_CONST_COMPONENT_TYPE_EXTERNAL_EXECUTABLE SYSRES_CONST_COMPONENT_TYPE_OTHER SYSRES_CONST_COMPONENT_TYPE_REFERENCE_TYPES SYSRES_CONST_COMPONENT_TYPE_REFERENCES SYSRES_CONST_COMPONENT_TYPE_REPORTS SYSRES_CONST_COMPONENT_TYPE_SCRIPTS SYSRES_CONST_COMPONENT_TYPE_URL SYSRES_CONST_COMPONENTS_REMOTE_SERVERS_VIEW_CODE SYSRES_CONST_CONDITION_BLOCK_DESCRIPTION SYSRES_CONST_CONST_FIRM_STATUS_COMMON SYSRES_CONST_CONST_FIRM_STATUS_INDIVIDUAL SYSRES_CONST_CONST_NEGATIVE_VALUE SYSRES_CONST_CONST_POSITIVE_VALUE SYSRES_CONST_CONST_SERVER_STATUS_DONT_REPLICATE SYSRES_CONST_CONST_SERVER_STATUS_REPLICATE SYSRES_CONST_CONTENTS_REQUISITE_CODE SYSRES_CONST_DATA_TYPE_BOOLEAN SYSRES_CONST_DATA_TYPE_DATE SYSRES_CONST_DATA_TYPE_FLOAT SYSRES_CONST_DATA_TYPE_INTEGER SYSRES_CONST_DATA_TYPE_PICK SYSRES_CONST_DATA_TYPE_REFERENCE SYSRES_CONST_DATA_TYPE_STRING SYSRES_CONST_DATA_TYPE_TEXT SYSRES_CONST_DATA_TYPE_VARIANT SYSRES_CONST_DATE_CLOSE_REQ_CODE SYSRES_CONST_DATE_FORMAT_DATE_ONLY_CHAR SYSRES_CONST_DATE_OPEN_REQ_CODE SYSRES_CONST_DATE_REQUISITE SYSRES_CONST_DATE_REQUISITE_CODE SYSRES_CONST_DATE_REQUISITE_NAME SYSRES_CONST_DATE_REQUISITE_TYPE SYSRES_CONST_DATE_TYPE_CHAR SYSRES_CONST_DATETIME_FORMAT_VALUE SYSRES_CONST_DEA_ACCESS_RIGHTS_ACTION_CODE SYSRES_CONST_DESCRIPTION_LOCALIZE_ID_REQUISITE_CODE SYSRES_CONST_DESCRIPTION_REQUISITE_CODE SYSRES_CONST_DET1_PART SYSRES_CONST_DET2_PART SYSRES_CONST_DET3_PART SYSRES_CONST_DET4_PART SYSRES_CONST_DET5_PART SYSRES_CONST_DET6_PART SYSRES_CONST_DETAIL_DATASET_KEY_REQUISITE_CODE SYSRES_CONST_DETAIL_PICK_REQUISITE_CODE SYSRES_CONST_DETAIL_REQ_CODE SYSRES_CONST_DO_NOT_USE_ACCESS_TYPE_CODE SYSRES_CONST_DO_NOT_USE_ACCESS_TYPE_NAME SYSRES_CONST_DO_NOT_USE_ON_VIEW_ACCESS_TYPE_CODE SYSRES_CONST_DO_NOT_USE_ON_VIEW_ACCESS_TYPE_NAME SYSRES_CONST_DOCUMENT_STORAGES_CODE SYSRES_CONST_DOCUMENT_TEMPLATES_TYPE_NAME SYSRES_CONST_DOUBLE_REQUISITE_CODE SYSRES_CONST_EDITOR_CLOSE_FILE_OBSERV_TYPE_CODE SYSRES_CONST_EDITOR_CLOSE_PROCESS_OBSERV_TYPE_CODE SYSRES_CONST_EDITOR_TYPE_REQUISITE_CODE SYSRES_CONST_EDITORS_APPLICATION_NAME_REQUISITE_CODE SYSRES_CONST_EDITORS_CREATE_SEVERAL_PROCESSES_REQUISITE_CODE SYSRES_CONST_EDITORS_EXTENSION_REQUISITE_CODE SYSRES_CONST_EDITORS_OBSERVER_BY_PROCESS_TYPE SYSRES_CONST_EDITORS_REFERENCE_CODE SYSRES_CONST_EDITORS_REPLACE_SPEC_CHARS_REQUISITE_CODE SYSRES_CONST_EDITORS_USE_PLUGINS_REQUISITE_CODE SYSRES_CONST_EDITORS_VIEW_DOCUMENT_OPENED_TO_EDIT_CODE SYSRES_CONST_EDOC_CARD_TYPE_REQUISITE_CODE SYSRES_CONST_EDOC_CARD_TYPES_LINK_REQUISITE_CODE SYSRES_CONST_EDOC_CERTIFICATE_AND_PASSWORD_ENCODE_CODE SYSRES_CONST_EDOC_CERTIFICATE_ENCODE_CODE SYSRES_CONST_EDOC_DATE_REQUISITE_CODE SYSRES_CONST_EDOC_KIND_REFERENCE_CODE SYSRES_CONST_EDOC_KINDS_BY_TEMPLATE_ACTION_CODE SYSRES_CONST_EDOC_MANAGE_ACCESS_CODE SYSRES_CONST_EDOC_NONE_ENCODE_CODE SYSRES_CONST_EDOC_NUMBER_REQUISITE_CODE SYSRES_CONST_EDOC_PASSWORD_ENCODE_CODE SYSRES_CONST_EDOC_READONLY_ACCESS_CODE SYSRES_CONST_EDOC_SHELL_LIFE_TYPE_VIEW_VALUE SYSRES_CONST_EDOC_SIZE_RESTRICTION_PRIORITY_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_CHECK_ACCESS_RIGHTS_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_COMPUTER_NAME_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_DATABASE_NAME_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_EDIT_IN_STORAGE_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_LOCAL_PATH_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_SHARED_SOURCE_NAME_REQUISITE_CODE SYSRES_CONST_EDOC_TEMPLATE_REQUISITE_CODE SYSRES_CONST_EDOC_TYPES_REFERENCE_CODE SYSRES_CONST_EDOC_VERSION_ACTIVE_STAGE_CODE SYSRES_CONST_EDOC_VERSION_DESIGN_STAGE_CODE SYSRES_CONST_EDOC_VERSION_OBSOLETE_STAGE_CODE SYSRES_CONST_EDOC_WRITE_ACCES_CODE SYSRES_CONST_EDOCUMENT_CARD_REQUISITES_REFERENCE_CODE_SELECTED_REQUISITE SYSRES_CONST_ENCODE_CERTIFICATE_TYPE_CODE SYSRES_CONST_END_DATE_REQUISITE_CODE SYSRES_CONST_ENUMERATION_TYPE_REQUISITE_CODE SYSRES_CONST_EXECUTE_ACCESS_RIGHTS_TYPE_CODE SYSRES_CONST_EXECUTIVE_FILE_STORAGE_TYPE SYSRES_CONST_EXIST_CONST SYSRES_CONST_EXIST_VALUE SYSRES_CONST_EXPORT_LOCK_TYPE_ASK SYSRES_CONST_EXPORT_LOCK_TYPE_WITH_LOCK SYSRES_CONST_EXPORT_LOCK_TYPE_WITHOUT_LOCK SYSRES_CONST_EXPORT_VERSION_TYPE_ASK SYSRES_CONST_EXPORT_VERSION_TYPE_LAST SYSRES_CONST_EXPORT_VERSION_TYPE_LAST_ACTIVE SYSRES_CONST_EXTENSION_REQUISITE_CODE SYSRES_CONST_FILTER_NAME_REQUISITE_CODE SYSRES_CONST_FILTER_REQUISITE_CODE SYSRES_CONST_FILTER_TYPE_COMMON_CODE SYSRES_CONST_FILTER_TYPE_COMMON_NAME SYSRES_CONST_FILTER_TYPE_USER_CODE SYSRES_CONST_FILTER_TYPE_USER_NAME SYSRES_CONST_FILTER_VALUE_REQUISITE_NAME SYSRES_CONST_FLOAT_NUMBER_FORMAT_CHAR SYSRES_CONST_FLOAT_REQUISITE_TYPE SYSRES_CONST_FOLDER_AUTHOR_VALUE SYSRES_CONST_FOLDER_KIND_ANY_OBJECTS SYSRES_CONST_FOLDER_KIND_COMPONENTS SYSRES_CONST_FOLDER_KIND_EDOCS SYSRES_CONST_FOLDER_KIND_JOBS SYSRES_CONST_FOLDER_KIND_TASKS SYSRES_CONST_FOLDER_TYPE_COMMON SYSRES_CONST_FOLDER_TYPE_COMPONENT SYSRES_CONST_FOLDER_TYPE_FAVORITES SYSRES_CONST_FOLDER_TYPE_INBOX SYSRES_CONST_FOLDER_TYPE_OUTBOX SYSRES_CONST_FOLDER_TYPE_QUICK_LAUNCH SYSRES_CONST_FOLDER_TYPE_SEARCH SYSRES_CONST_FOLDER_TYPE_SHORTCUTS SYSRES_CONST_FOLDER_TYPE_USER SYSRES_CONST_FROM_DICTIONARY_ENUM_METHOD_FLAG SYSRES_CONST_FULL_SUBSTITUTE_TYPE SYSRES_CONST_FULL_SUBSTITUTE_TYPE_CODE SYSRES_CONST_FUNCTION_CANCEL_RESULT SYSRES_CONST_FUNCTION_CATEGORY_SYSTEM SYSRES_CONST_FUNCTION_CATEGORY_USER SYSRES_CONST_FUNCTION_FAILURE_RESULT SYSRES_CONST_FUNCTION_SAVE_RESULT SYSRES_CONST_GENERATED_REQUISITE SYSRES_CONST_GREEN_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_GROUP_ACCOUNT_TYPE_VALUE_CODE SYSRES_CONST_GROUP_CATEGORY_NORMAL_CODE SYSRES_CONST_GROUP_CATEGORY_NORMAL_NAME SYSRES_CONST_GROUP_CATEGORY_SERVICE_CODE SYSRES_CONST_GROUP_CATEGORY_SERVICE_NAME SYSRES_CONST_GROUP_COMMON_CATEGORY_FIELD_VALUE SYSRES_CONST_GROUP_FULL_NAME_REQUISITE_CODE SYSRES_CONST_GROUP_NAME_REQUISITE_CODE SYSRES_CONST_GROUP_RIGHTS_T_REQUISITE_CODE SYSRES_CONST_GROUP_SERVER_CODES_REQUISITE_CODE SYSRES_CONST_GROUP_SERVER_NAME_REQUISITE_CODE SYSRES_CONST_GROUP_SERVICE_CATEGORY_FIELD_VALUE SYSRES_CONST_GROUP_USER_REQUISITE_CODE SYSRES_CONST_GROUPS_REFERENCE_CODE SYSRES_CONST_GROUPS_REQUISITE_CODE SYSRES_CONST_HIDDEN_MODE_NAME SYSRES_CONST_HIGH_LVL_REQUISITE_CODE SYSRES_CONST_HISTORY_ACTION_CREATE_CODE SYSRES_CONST_HISTORY_ACTION_DELETE_CODE SYSRES_CONST_HISTORY_ACTION_EDIT_CODE SYSRES_CONST_HOUR_CHAR SYSRES_CONST_ID_REQUISITE_CODE SYSRES_CONST_IDSPS_REQUISITE_CODE SYSRES_CONST_IMAGE_MODE_COLOR SYSRES_CONST_IMAGE_MODE_GREYSCALE SYSRES_CONST_IMAGE_MODE_MONOCHROME SYSRES_CONST_IMPORTANCE_HIGH SYSRES_CONST_IMPORTANCE_LOW SYSRES_CONST_IMPORTANCE_NORMAL SYSRES_CONST_IN_DESIGN_VERSION_STATE_PICK_VALUE SYSRES_CONST_INCOMING_WORK_RULE_TYPE_CODE SYSRES_CONST_INT_REQUISITE SYSRES_CONST_INT_REQUISITE_TYPE SYSRES_CONST_INTEGER_NUMBER_FORMAT_CHAR SYSRES_CONST_INTEGER_TYPE_CHAR SYSRES_CONST_IS_GENERATED_REQUISITE_NEGATIVE_VALUE SYSRES_CONST_IS_PUBLIC_ROLE_REQUISITE_CODE SYSRES_CONST_IS_REMOTE_USER_NEGATIVE_VALUE SYSRES_CONST_IS_REMOTE_USER_POSITIVE_VALUE SYSRES_CONST_IS_STORED_REQUISITE_NEGATIVE_VALUE SYSRES_CONST_IS_STORED_REQUISITE_STORED_VALUE SYSRES_CONST_ITALIC_LIFE_CYCLE_STAGE_DRAW_STYLE SYSRES_CONST_JOB_BLOCK_DESCRIPTION SYSRES_CONST_JOB_KIND_CONTROL_JOB SYSRES_CONST_JOB_KIND_JOB SYSRES_CONST_JOB_KIND_NOTICE SYSRES_CONST_JOB_STATE_ABORTED SYSRES_CONST_JOB_STATE_COMPLETE SYSRES_CONST_JOB_STATE_WORKING SYSRES_CONST_KIND_REQUISITE_CODE SYSRES_CONST_KIND_REQUISITE_NAME SYSRES_CONST_KINDS_CREATE_SHADOW_COPIES_REQUISITE_CODE SYSRES_CONST_KINDS_DEFAULT_EDOC_LIFE_STAGE_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_ALL_TEPLATES_ALLOWED_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_ALLOW_LIFE_CYCLE_STAGE_CHANGING_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_ALLOW_MULTIPLE_ACTIVE_VERSIONS_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_SHARE_ACCES_RIGHTS_BY_DEFAULT_CODE SYSRES_CONST_KINDS_EDOC_TEMPLATE_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_TYPE_REQUISITE_CODE SYSRES_CONST_KINDS_SIGNERS_REQUISITES_CODE SYSRES_CONST_KOD_INPUT_TYPE SYSRES_CONST_LAST_UPDATE_DATE_REQUISITE_CODE SYSRES_CONST_LIFE_CYCLE_START_STAGE_REQUISITE_CODE SYSRES_CONST_LILAC_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_LINK_OBJECT_KIND_COMPONENT SYSRES_CONST_LINK_OBJECT_KIND_DOCUMENT SYSRES_CONST_LINK_OBJECT_KIND_EDOC SYSRES_CONST_LINK_OBJECT_KIND_FOLDER SYSRES_CONST_LINK_OBJECT_KIND_JOB SYSRES_CONST_LINK_OBJECT_KIND_REFERENCE SYSRES_CONST_LINK_OBJECT_KIND_TASK SYSRES_CONST_LINK_REF_TYPE_REQUISITE_CODE SYSRES_CONST_LIST_REFERENCE_MODE_NAME SYSRES_CONST_LOCALIZATION_DICTIONARY_MAIN_VIEW_CODE SYSRES_CONST_MAIN_VIEW_CODE SYSRES_CONST_MANUAL_ENUM_METHOD_FLAG SYSRES_CONST_MASTER_COMP_TYPE_REQUISITE_CODE SYSRES_CONST_MASTER_TABLE_REC_ID_REQUISITE_CODE SYSRES_CONST_MAXIMIZED_MODE_NAME SYSRES_CONST_ME_VALUE SYSRES_CONST_MESSAGE_ATTENTION_CAPTION SYSRES_CONST_MESSAGE_CONFIRMATION_CAPTION SYSRES_CONST_MESSAGE_ERROR_CAPTION SYSRES_CONST_MESSAGE_INFORMATION_CAPTION SYSRES_CONST_MINIMIZED_MODE_NAME SYSRES_CONST_MINUTE_CHAR SYSRES_CONST_MODULE_REQUISITE_CODE SYSRES_CONST_MONITORING_BLOCK_DESCRIPTION SYSRES_CONST_MONTH_FORMAT_VALUE SYSRES_CONST_NAME_LOCALIZE_ID_REQUISITE_CODE SYSRES_CONST_NAME_REQUISITE_CODE SYSRES_CONST_NAME_SINGULAR_REQUISITE_CODE SYSRES_CONST_NAMEAN_INPUT_TYPE SYSRES_CONST_NEGATIVE_PICK_VALUE SYSRES_CONST_NEGATIVE_VALUE SYSRES_CONST_NO SYSRES_CONST_NO_PICK_VALUE SYSRES_CONST_NO_SIGNATURE_REQUISITE_CODE SYSRES_CONST_NO_VALUE SYSRES_CONST_NONE_ACCESS_RIGHTS_TYPE_CODE SYSRES_CONST_NONOPERATING_RECORD_FLAG_VALUE SYSRES_CONST_NONOPERATING_RECORD_FLAG_VALUE_MASCULINE SYSRES_CONST_NORMAL_ACCESS_RIGHTS_TYPE_CODE SYSRES_CONST_NORMAL_LIFE_CYCLE_STAGE_DRAW_STYLE SYSRES_CONST_NORMAL_MODE_NAME SYSRES_CONST_NOT_ALLOWED_ACCESS_TYPE_CODE SYSRES_CONST_NOT_ALLOWED_ACCESS_TYPE_NAME SYSRES_CONST_NOTE_REQUISITE_CODE SYSRES_CONST_NOTICE_BLOCK_DESCRIPTION SYSRES_CONST_NUM_REQUISITE SYSRES_CONST_NUM_STR_REQUISITE_CODE SYSRES_CONST_NUMERATION_AUTO_NOT_STRONG SYSRES_CONST_NUMERATION_AUTO_STRONG SYSRES_CONST_NUMERATION_FROM_DICTONARY SYSRES_CONST_NUMERATION_MANUAL SYSRES_CONST_NUMERIC_TYPE_CHAR SYSRES_CONST_NUMREQ_REQUISITE_CODE SYSRES_CONST_OBSOLETE_VERSION_STATE_PICK_VALUE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_CODE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_FEMININE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_MASCULINE SYSRES_CONST_OPTIONAL_FORM_COMP_REQCODE_PREFIX SYSRES_CONST_ORANGE_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_ORIGINALREF_REQUISITE_CODE SYSRES_CONST_OURFIRM_REF_CODE SYSRES_CONST_OURFIRM_REQUISITE_CODE SYSRES_CONST_OURFIRM_VAR SYSRES_CONST_OUTGOING_WORK_RULE_TYPE_CODE SYSRES_CONST_PICK_NEGATIVE_RESULT SYSRES_CONST_PICK_POSITIVE_RESULT SYSRES_CONST_PICK_REQUISITE SYSRES_CONST_PICK_REQUISITE_TYPE SYSRES_CONST_PICK_TYPE_CHAR SYSRES_CONST_PLAN_STATUS_REQUISITE_CODE SYSRES_CONST_PLATFORM_VERSION_COMMENT SYSRES_CONST_PLUGINS_SETTINGS_DESCRIPTION_REQUISITE_CODE SYSRES_CONST_POSITIVE_PICK_VALUE SYSRES_CONST_POWER_TO_CREATE_ACTION_CODE SYSRES_CONST_POWER_TO_SIGN_ACTION_CODE SYSRES_CONST_PRIORITY_REQUISITE_CODE SYSRES_CONST_QUALIFIED_TASK_TYPE SYSRES_CONST_QUALIFIED_TASK_TYPE_CODE SYSRES_CONST_RECSTAT_REQUISITE_CODE SYSRES_CONST_RED_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_REF_ID_T_REF_TYPE_REQUISITE_CODE SYSRES_CONST_REF_REQUISITE SYSRES_CONST_REF_REQUISITE_TYPE SYSRES_CONST_REF_REQUISITES_REFERENCE_CODE_SELECTED_REQUISITE SYSRES_CONST_REFERENCE_RECORD_HISTORY_CREATE_ACTION_CODE SYSRES_CONST_REFERENCE_RECORD_HISTORY_DELETE_ACTION_CODE SYSRES_CONST_REFERENCE_RECORD_HISTORY_MODIFY_ACTION_CODE SYSRES_CONST_REFERENCE_TYPE_CHAR SYSRES_CONST_REFERENCE_TYPE_REQUISITE_NAME SYSRES_CONST_REFERENCES_ADD_PARAMS_REQUISITE_CODE SYSRES_CONST_REFERENCES_DISPLAY_REQUISITE_REQUISITE_CODE SYSRES_CONST_REMOTE_SERVER_STATUS_WORKING SYSRES_CONST_REMOTE_SERVER_TYPE_MAIN SYSRES_CONST_REMOTE_SERVER_TYPE_SECONDARY SYSRES_CONST_REMOTE_USER_FLAG_VALUE_CODE SYSRES_CONST_REPORT_APP_EDITOR_INTERNAL SYSRES_CONST_REPORT_BASE_REPORT_ID_REQUISITE_CODE SYSRES_CONST_REPORT_BASE_REPORT_REQUISITE_CODE SYSRES_CONST_REPORT_SCRIPT_REQUISITE_CODE SYSRES_CONST_REPORT_TEMPLATE_REQUISITE_CODE SYSRES_CONST_REPORT_VIEWER_CODE_REQUISITE_CODE SYSRES_CONST_REQ_ALLOW_COMPONENT_DEFAULT_VALUE SYSRES_CONST_REQ_ALLOW_RECORD_DEFAULT_VALUE SYSRES_CONST_REQ_ALLOW_SERVER_COMPONENT_DEFAULT_VALUE SYSRES_CONST_REQ_MODE_AVAILABLE_CODE SYSRES_CONST_REQ_MODE_EDIT_CODE SYSRES_CONST_REQ_MODE_HIDDEN_CODE SYSRES_CONST_REQ_MODE_NOT_AVAILABLE_CODE SYSRES_CONST_REQ_MODE_VIEW_CODE SYSRES_CONST_REQ_NUMBER_REQUISITE_CODE SYSRES_CONST_REQ_SECTION_VALUE SYSRES_CONST_REQ_TYPE_VALUE SYSRES_CONST_REQUISITE_FORMAT_BY_UNIT SYSRES_CONST_REQUISITE_FORMAT_DATE_FULL SYSRES_CONST_REQUISITE_FORMAT_DATE_TIME SYSRES_CONST_REQUISITE_FORMAT_LEFT SYSRES_CONST_REQUISITE_FORMAT_RIGHT SYSRES_CONST_REQUISITE_FORMAT_WITHOUT_UNIT SYSRES_CONST_REQUISITE_NUMBER_REQUISITE_CODE SYSRES_CONST_REQUISITE_SECTION_ACTIONS SYSRES_CONST_REQUISITE_SECTION_BUTTON SYSRES_CONST_REQUISITE_SECTION_BUTTONS SYSRES_CONST_REQUISITE_SECTION_CARD SYSRES_CONST_REQUISITE_SECTION_TABLE SYSRES_CONST_REQUISITE_SECTION_TABLE10 SYSRES_CONST_REQUISITE_SECTION_TABLE11 SYSRES_CONST_REQUISITE_SECTION_TABLE12 SYSRES_CONST_REQUISITE_SECTION_TABLE13 SYSRES_CONST_REQUISITE_SECTION_TABLE14 SYSRES_CONST_REQUISITE_SECTION_TABLE15 SYSRES_CONST_REQUISITE_SECTION_TABLE16 SYSRES_CONST_REQUISITE_SECTION_TABLE17 SYSRES_CONST_REQUISITE_SECTION_TABLE18 SYSRES_CONST_REQUISITE_SECTION_TABLE19 SYSRES_CONST_REQUISITE_SECTION_TABLE2 SYSRES_CONST_REQUISITE_SECTION_TABLE20 SYSRES_CONST_REQUISITE_SECTION_TABLE21 SYSRES_CONST_REQUISITE_SECTION_TABLE22 SYSRES_CONST_REQUISITE_SECTION_TABLE23 SYSRES_CONST_REQUISITE_SECTION_TABLE24 SYSRES_CONST_REQUISITE_SECTION_TABLE3 SYSRES_CONST_REQUISITE_SECTION_TABLE4 SYSRES_CONST_REQUISITE_SECTION_TABLE5 SYSRES_CONST_REQUISITE_SECTION_TABLE6 SYSRES_CONST_REQUISITE_SECTION_TABLE7 SYSRES_CONST_REQUISITE_SECTION_TABLE8 SYSRES_CONST_REQUISITE_SECTION_TABLE9 SYSRES_CONST_REQUISITES_PSEUDOREFERENCE_REQUISITE_NUMBER_REQUISITE_CODE SYSRES_CONST_RIGHT_ALIGNMENT_CODE SYSRES_CONST_ROLES_REFERENCE_CODE SYSRES_CONST_ROUTE_STEP_AFTER_RUS SYSRES_CONST_ROUTE_STEP_AND_CONDITION_RUS SYSRES_CONST_ROUTE_STEP_OR_CONDITION_RUS SYSRES_CONST_ROUTE_TYPE_COMPLEX SYSRES_CONST_ROUTE_TYPE_PARALLEL SYSRES_CONST_ROUTE_TYPE_SERIAL SYSRES_CONST_SBDATASETDESC_NEGATIVE_VALUE SYSRES_CONST_SBDATASETDESC_POSITIVE_VALUE SYSRES_CONST_SBVIEWSDESC_POSITIVE_VALUE SYSRES_CONST_SCRIPT_BLOCK_DESCRIPTION SYSRES_CONST_SEARCH_BY_TEXT_REQUISITE_CODE SYSRES_CONST_SEARCHES_COMPONENT_CONTENT SYSRES_CONST_SEARCHES_CRITERIA_ACTION_NAME SYSRES_CONST_SEARCHES_EDOC_CONTENT SYSRES_CONST_SEARCHES_FOLDER_CONTENT SYSRES_CONST_SEARCHES_JOB_CONTENT SYSRES_CONST_SEARCHES_REFERENCE_CODE SYSRES_CONST_SEARCHES_TASK_CONTENT SYSRES_CONST_SECOND_CHAR SYSRES_CONST_SECTION_REQUISITE_ACTIONS_VALUE SYSRES_CONST_SECTION_REQUISITE_CARD_VALUE SYSRES_CONST_SECTION_REQUISITE_CODE SYSRES_CONST_SECTION_REQUISITE_DETAIL_1_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_2_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_3_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_4_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_5_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_6_VALUE SYSRES_CONST_SELECT_REFERENCE_MODE_NAME SYSRES_CONST_SELECT_TYPE_SELECTABLE SYSRES_CONST_SELECT_TYPE_SELECTABLE_ONLY_CHILD SYSRES_CONST_SELECT_TYPE_SELECTABLE_WITH_CHILD SYSRES_CONST_SELECT_TYPE_UNSLECTABLE SYSRES_CONST_SERVER_TYPE_MAIN SYSRES_CONST_SERVICE_USER_CATEGORY_FIELD_VALUE SYSRES_CONST_SETTINGS_USER_REQUISITE_CODE SYSRES_CONST_SIGNATURE_AND_ENCODE_CERTIFICATE_TYPE_CODE SYSRES_CONST_SIGNATURE_CERTIFICATE_TYPE_CODE SYSRES_CONST_SINGULAR_TITLE_REQUISITE_CODE SYSRES_CONST_SQL_SERVER_AUTHENTIFICATION_FLAG_VALUE_CODE SYSRES_CONST_SQL_SERVER_ENCODE_AUTHENTIFICATION_FLAG_VALUE_CODE SYSRES_CONST_STANDART_ROUTE_REFERENCE_CODE SYSRES_CONST_STANDART_ROUTE_REFERENCE_COMMENT_REQUISITE_CODE SYSRES_CONST_STANDART_ROUTES_GROUPS_REFERENCE_CODE SYSRES_CONST_STATE_REQ_NAME SYSRES_CONST_STATE_REQUISITE_ACTIVE_VALUE SYSRES_CONST_STATE_REQUISITE_CLOSED_VALUE SYSRES_CONST_STATE_REQUISITE_CODE SYSRES_CONST_STATIC_ROLE_TYPE_CODE SYSRES_CONST_STATUS_PLAN_DEFAULT_VALUE SYSRES_CONST_STATUS_VALUE_AUTOCLEANING SYSRES_CONST_STATUS_VALUE_BLUE_SQUARE SYSRES_CONST_STATUS_VALUE_COMPLETE SYSRES_CONST_STATUS_VALUE_GREEN_SQUARE SYSRES_CONST_STATUS_VALUE_ORANGE_SQUARE SYSRES_CONST_STATUS_VALUE_PURPLE_SQUARE SYSRES_CONST_STATUS_VALUE_RED_SQUARE SYSRES_CONST_STATUS_VALUE_SUSPEND SYSRES_CONST_STATUS_VALUE_YELLOW_SQUARE SYSRES_CONST_STDROUTE_SHOW_TO_USERS_REQUISITE_CODE SYSRES_CONST_STORAGE_TYPE_FILE SYSRES_CONST_STORAGE_TYPE_SQL_SERVER SYSRES_CONST_STR_REQUISITE SYSRES_CONST_STRIKEOUT_LIFE_CYCLE_STAGE_DRAW_STYLE SYSRES_CONST_STRING_FORMAT_LEFT_ALIGN_CHAR SYSRES_CONST_STRING_FORMAT_RIGHT_ALIGN_CHAR SYSRES_CONST_STRING_REQUISITE_CODE SYSRES_CONST_STRING_REQUISITE_TYPE SYSRES_CONST_STRING_TYPE_CHAR SYSRES_CONST_SUBSTITUTES_PSEUDOREFERENCE_CODE SYSRES_CONST_SUBTASK_BLOCK_DESCRIPTION SYSRES_CONST_SYSTEM_SETTING_CURRENT_USER_PARAM_VALUE SYSRES_CONST_SYSTEM_SETTING_EMPTY_VALUE_PARAM_VALUE SYSRES_CONST_SYSTEM_VERSION_COMMENT SYSRES_CONST_TASK_ACCESS_TYPE_ALL SYSRES_CONST_TASK_ACCESS_TYPE_ALL_MEMBERS SYSRES_CONST_TASK_ACCESS_TYPE_MANUAL SYSRES_CONST_TASK_ENCODE_TYPE_CERTIFICATION SYSRES_CONST_TASK_ENCODE_TYPE_CERTIFICATION_AND_PASSWORD SYSRES_CONST_TASK_ENCODE_TYPE_NONE SYSRES_CONST_TASK_ENCODE_TYPE_PASSWORD SYSRES_CONST_TASK_ROUTE_ALL_CONDITION SYSRES_CONST_TASK_ROUTE_AND_CONDITION SYSRES_CONST_TASK_ROUTE_OR_CONDITION SYSRES_CONST_TASK_STATE_ABORTED SYSRES_CONST_TASK_STATE_COMPLETE SYSRES_CONST_TASK_STATE_CONTINUED SYSRES_CONST_TASK_STATE_CONTROL SYSRES_CONST_TASK_STATE_INIT SYSRES_CONST_TASK_STATE_WORKING SYSRES_CONST_TASK_TITLE SYSRES_CONST_TASK_TYPES_GROUPS_REFERENCE_CODE SYSRES_CONST_TASK_TYPES_REFERENCE_CODE SYSRES_CONST_TEMPLATES_REFERENCE_CODE SYSRES_CONST_TEST_DATE_REQUISITE_NAME SYSRES_CONST_TEST_DEV_DATABASE_NAME SYSRES_CONST_TEST_DEV_SYSTEM_CODE SYSRES_CONST_TEST_EDMS_DATABASE_NAME SYSRES_CONST_TEST_EDMS_MAIN_CODE SYSRES_CONST_TEST_EDMS_MAIN_DB_NAME SYSRES_CONST_TEST_EDMS_SECOND_CODE SYSRES_CONST_TEST_EDMS_SECOND_DB_NAME SYSRES_CONST_TEST_EDMS_SYSTEM_CODE SYSRES_CONST_TEST_NUMERIC_REQUISITE_NAME SYSRES_CONST_TEXT_REQUISITE SYSRES_CONST_TEXT_REQUISITE_CODE SYSRES_CONST_TEXT_REQUISITE_TYPE SYSRES_CONST_TEXT_TYPE_CHAR SYSRES_CONST_TYPE_CODE_REQUISITE_CODE SYSRES_CONST_TYPE_REQUISITE_CODE SYSRES_CONST_UNDEFINED_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_UNITS_SECTION_ID_REQUISITE_CODE SYSRES_CONST_UNITS_SECTION_REQUISITE_CODE SYSRES_CONST_UNOPERATING_RECORD_FLAG_VALUE_CODE SYSRES_CONST_UNSTORED_DATA_REQUISITE_CODE SYSRES_CONST_UNSTORED_DATA_REQUISITE_NAME SYSRES_CONST_USE_ACCESS_TYPE_CODE SYSRES_CONST_USE_ACCESS_TYPE_NAME SYSRES_CONST_USER_ACCOUNT_TYPE_VALUE_CODE SYSRES_CONST_USER_ADDITIONAL_INFORMATION_REQUISITE_CODE SYSRES_CONST_USER_AND_GROUP_ID_FROM_PSEUDOREFERENCE_REQUISITE_CODE SYSRES_CONST_USER_CATEGORY_NORMAL SYSRES_CONST_USER_CERTIFICATE_REQUISITE_CODE SYSRES_CONST_USER_CERTIFICATE_STATE_REQUISITE_CODE SYSRES_CONST_USER_CERTIFICATE_SUBJECT_NAME_REQUISITE_CODE SYSRES_CONST_USER_CERTIFICATE_THUMBPRINT_REQUISITE_CODE SYSRES_CONST_USER_COMMON_CATEGORY SYSRES_CONST_USER_COMMON_CATEGORY_CODE SYSRES_CONST_USER_FULL_NAME_REQUISITE_CODE SYSRES_CONST_USER_GROUP_TYPE_REQUISITE_CODE SYSRES_CONST_USER_LOGIN_REQUISITE_CODE SYSRES_CONST_USER_REMOTE_CONTROLLER_REQUISITE_CODE SYSRES_CONST_USER_REMOTE_SYSTEM_REQUISITE_CODE SYSRES_CONST_USER_RIGHTS_T_REQUISITE_CODE SYSRES_CONST_USER_SERVER_NAME_REQUISITE_CODE SYSRES_CONST_USER_SERVICE_CATEGORY SYSRES_CONST_USER_SERVICE_CATEGORY_CODE SYSRES_CONST_USER_STATUS_ADMINISTRATOR_CODE SYSRES_CONST_USER_STATUS_ADMINISTRATOR_NAME SYSRES_CONST_USER_STATUS_DEVELOPER_CODE SYSRES_CONST_USER_STATUS_DEVELOPER_NAME SYSRES_CONST_USER_STATUS_DISABLED_CODE SYSRES_CONST_USER_STATUS_DISABLED_NAME SYSRES_CONST_USER_STATUS_SYSTEM_DEVELOPER_CODE SYSRES_CONST_USER_STATUS_USER_CODE SYSRES_CONST_USER_STATUS_USER_NAME SYSRES_CONST_USER_STATUS_USER_NAME_DEPRECATED SYSRES_CONST_USER_TYPE_FIELD_VALUE_USER SYSRES_CONST_USER_TYPE_REQUISITE_CODE SYSRES_CONST_USERS_CONTROLLER_REQUISITE_CODE SYSRES_CONST_USERS_IS_MAIN_SERVER_REQUISITE_CODE SYSRES_CONST_USERS_REFERENCE_CODE SYSRES_CONST_USERS_REGISTRATION_CERTIFICATES_ACTION_NAME SYSRES_CONST_USERS_REQUISITE_CODE SYSRES_CONST_USERS_SYSTEM_REQUISITE_CODE SYSRES_CONST_USERS_USER_ACCESS_RIGHTS_TYPR_REQUISITE_CODE SYSRES_CONST_USERS_USER_AUTHENTICATION_REQUISITE_CODE SYSRES_CONST_USERS_USER_COMPONENT_REQUISITE_CODE SYSRES_CONST_USERS_USER_GROUP_REQUISITE_CODE SYSRES_CONST_USERS_VIEW_CERTIFICATES_ACTION_NAME SYSRES_CONST_VIEW_DEFAULT_CODE SYSRES_CONST_VIEW_DEFAULT_NAME SYSRES_CONST_VIEWER_REQUISITE_CODE SYSRES_CONST_WAITING_BLOCK_DESCRIPTION SYSRES_CONST_WIZARD_FORM_LABEL_TEST_STRING  SYSRES_CONST_WIZARD_QUERY_PARAM_HEIGHT_ETALON_STRING SYSRES_CONST_WIZARD_REFERENCE_COMMENT_REQUISITE_CODE SYSRES_CONST_WORK_RULES_DESCRIPTION_REQUISITE_CODE SYSRES_CONST_WORK_TIME_CALENDAR_REFERENCE_CODE SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE_CODE SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE_CODE_RUS SYSRES_CONST_WORK_WORKFLOW_SOFT_ROUTE_TYPE_VALUE_CODE_RUS SYSRES_CONST_WORKFLOW_ROUTE_TYPR_HARD SYSRES_CONST_WORKFLOW_ROUTE_TYPR_SOFT SYSRES_CONST_XML_ENCODING SYSRES_CONST_XREC_STAT_REQUISITE_CODE SYSRES_CONST_XRECID_FIELD_NAME SYSRES_CONST_YES SYSRES_CONST_YES_NO_2_REQUISITE_CODE SYSRES_CONST_YES_NO_REQUISITE_CODE SYSRES_CONST_YES_NO_T_REF_TYPE_REQUISITE_CODE SYSRES_CONST_YES_PICK_VALUE SYSRES_CONST_YES_VALUE CR FALSE nil NO_VALUE NULL TAB TRUE YES_VALUE ADMINISTRATORS_GROUP_NAME CUSTOMIZERS_GROUP_NAME DEVELOPERS_GROUP_NAME SERVICE_USERS_GROUP_NAME DECISION_BLOCK_FIRST_OPERAND_PROPERTY DECISION_BLOCK_NAME_PROPERTY DECISION_BLOCK_OPERATION_PROPERTY DECISION_BLOCK_RESULT_TYPE_PROPERTY DECISION_BLOCK_SECOND_OPERAND_PROPERTY ANY_FILE_EXTENTION COMPRESSED_DOCUMENT_EXTENSION EXTENDED_DOCUMENT_EXTENSION SHORT_COMPRESSED_DOCUMENT_EXTENSION SHORT_EXTENDED_DOCUMENT_EXTENSION JOB_BLOCK_ABORT_DEADLINE_PROPERTY JOB_BLOCK_AFTER_FINISH_EVENT JOB_BLOCK_AFTER_QUERY_PARAMETERS_EVENT JOB_BLOCK_ATTACHMENT_PROPERTY JOB_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY JOB_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY JOB_BLOCK_BEFORE_QUERY_PARAMETERS_EVENT JOB_BLOCK_BEFORE_START_EVENT JOB_BLOCK_CREATED_JOBS_PROPERTY JOB_BLOCK_DEADLINE_PROPERTY JOB_BLOCK_EXECUTION_RESULTS_PROPERTY JOB_BLOCK_IS_PARALLEL_PROPERTY JOB_BLOCK_IS_RELATIVE_ABORT_DEADLINE_PROPERTY JOB_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY JOB_BLOCK_JOB_TEXT_PROPERTY JOB_BLOCK_NAME_PROPERTY JOB_BLOCK_NEED_SIGN_ON_PERFORM_PROPERTY JOB_BLOCK_PERFORMER_PROPERTY JOB_BLOCK_RELATIVE_ABORT_DEADLINE_TYPE_PROPERTY JOB_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY JOB_BLOCK_SUBJECT_PROPERTY ENGLISH_LANGUAGE_CODE RUSSIAN_LANGUAGE_CODE smHidden smMaximized smMinimized smNormal wmNo wmYes COMPONENT_TOKEN_LINK_KIND DOCUMENT_LINK_KIND EDOCUMENT_LINK_KIND FOLDER_LINK_KIND JOB_LINK_KIND REFERENCE_LINK_KIND TASK_LINK_KIND COMPONENT_TOKEN_LOCK_TYPE EDOCUMENT_VERSION_LOCK_TYPE MONITOR_BLOCK_AFTER_FINISH_EVENT MONITOR_BLOCK_BEFORE_START_EVENT MONITOR_BLOCK_DEADLINE_PROPERTY MONITOR_BLOCK_INTERVAL_PROPERTY MONITOR_BLOCK_INTERVAL_TYPE_PROPERTY MONITOR_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY MONITOR_BLOCK_NAME_PROPERTY MONITOR_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY MONITOR_BLOCK_SEARCH_SCRIPT_PROPERTY NOTICE_BLOCK_AFTER_FINISH_EVENT NOTICE_BLOCK_ATTACHMENT_PROPERTY NOTICE_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY NOTICE_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY NOTICE_BLOCK_BEFORE_START_EVENT NOTICE_BLOCK_CREATED_NOTICES_PROPERTY NOTICE_BLOCK_DEADLINE_PROPERTY NOTICE_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY NOTICE_BLOCK_NAME_PROPERTY NOTICE_BLOCK_NOTICE_TEXT_PROPERTY NOTICE_BLOCK_PERFORMER_PROPERTY NOTICE_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY NOTICE_BLOCK_SUBJECT_PROPERTY dseAfterCancel dseAfterClose dseAfterDelete dseAfterDeleteOutOfTransaction dseAfterInsert dseAfterOpen dseAfterScroll dseAfterUpdate dseAfterUpdateOutOfTransaction dseBeforeCancel dseBeforeClose dseBeforeDelete dseBeforeDetailUpdate dseBeforeInsert dseBeforeOpen dseBeforeUpdate dseOnAnyRequisiteChange dseOnCloseRecord dseOnDeleteError dseOnOpenRecord dseOnPrepareUpdate dseOnUpdateError dseOnUpdateRatifiedRecord dseOnValidDelete dseOnValidUpdate reOnChange reOnChangeValues SELECTION_BEGIN_ROUTE_EVENT SELECTION_END_ROUTE_EVENT CURRENT_PERIOD_IS_REQUIRED PREVIOUS_CARD_TYPE_NAME SHOW_RECORD_PROPERTIES_FORM ACCESS_RIGHTS_SETTING_DIALOG_CODE ADMINISTRATOR_USER_CODE ANALYTIC_REPORT_TYPE asrtHideLocal asrtHideRemote CALCULATED_ROLE_TYPE_CODE COMPONENTS_REFERENCE_DEVELOPER_VIEW_CODE DCTS_TEST_PROTOCOLS_FOLDER_PATH E_EDOC_VERSION_ALREADY_APPROVINGLY_SIGNED E_EDOC_VERSION_ALREADY_APPROVINGLY_SIGNED_BY_USER E_EDOC_VERSION_ALREDY_SIGNED E_EDOC_VERSION_ALREDY_SIGNED_BY_USER EDOC_TYPES_CODE_REQUISITE_FIELD_NAME EDOCUMENTS_ALIAS_NAME FILES_FOLDER_PATH FILTER_OPERANDS_DELIMITER FILTER_OPERATIONS_DELIMITER FORMCARD_NAME FORMLIST_NAME GET_EXTENDED_DOCUMENT_EXTENSION_CREATION_MODE GET_EXTENDED_DOCUMENT_EXTENSION_IMPORT_MODE INTEGRATED_REPORT_TYPE IS_BUILDER_APPLICATION_ROLE IS_BUILDER_APPLICATION_ROLE2 IS_BUILDER_USERS ISBSYSDEV LOG_FOLDER_PATH mbCancel mbNo mbNoToAll mbOK mbYes mbYesToAll MEMORY_DATASET_DESRIPTIONS_FILENAME mrNo mrNoToAll mrYes mrYesToAll MULTIPLE_SELECT_DIALOG_CODE NONOPERATING_RECORD_FLAG_FEMININE NONOPERATING_RECORD_FLAG_MASCULINE OPERATING_RECORD_FLAG_FEMININE OPERATING_RECORD_FLAG_MASCULINE PROFILING_SETTINGS_COMMON_SETTINGS_CODE_VALUE PROGRAM_INITIATED_LOOKUP_ACTION ratDelete ratEdit ratInsert REPORT_TYPE REQUIRED_PICK_VALUES_VARIABLE rmCard rmList SBRTE_PROGID_DEV SBRTE_PROGID_RELEASE STATIC_ROLE_TYPE_CODE SUPPRESS_EMPTY_TEMPLATE_CREATION SYSTEM_USER_CODE UPDATE_DIALOG_DATASET USED_IN_OBJECT_HINT_PARAM USER_INITIATED_LOOKUP_ACTION USER_NAME_FORMAT USER_SELECTION_RESTRICTIONS WORKFLOW_TEST_PROTOCOLS_FOLDER_PATH ELS_SUBTYPE_CONTROL_NAME ELS_FOLDER_KIND_CONTROL_NAME REPEAT_PROCESS_CURRENT_OBJECT_EXCEPTION_NAME PRIVILEGE_COMPONENT_FULL_ACCESS PRIVILEGE_DEVELOPMENT_EXPORT PRIVILEGE_DEVELOPMENT_IMPORT PRIVILEGE_DOCUMENT_DELETE PRIVILEGE_ESD PRIVILEGE_FOLDER_DELETE PRIVILEGE_MANAGE_ACCESS_RIGHTS PRIVILEGE_MANAGE_REPLICATION PRIVILEGE_MANAGE_SESSION_SERVER PRIVILEGE_OBJECT_FULL_ACCESS PRIVILEGE_OBJECT_VIEW PRIVILEGE_RESERVE_LICENSE PRIVILEGE_SYSTEM_CUSTOMIZE PRIVILEGE_SYSTEM_DEVELOP PRIVILEGE_SYSTEM_INSTALL PRIVILEGE_TASK_DELETE PRIVILEGE_USER_PLUGIN_SETTINGS_CUSTOMIZE PRIVILEGES_PSEUDOREFERENCE_CODE ACCESS_TYPES_PSEUDOREFERENCE_CODE ALL_AVAILABLE_COMPONENTS_PSEUDOREFERENCE_CODE ALL_AVAILABLE_PRIVILEGES_PSEUDOREFERENCE_CODE ALL_REPLICATE_COMPONENTS_PSEUDOREFERENCE_CODE AVAILABLE_DEVELOPERS_COMPONENTS_PSEUDOREFERENCE_CODE COMPONENTS_PSEUDOREFERENCE_CODE FILTRATER_SETTINGS_CONFLICTS_PSEUDOREFERENCE_CODE GROUPS_PSEUDOREFERENCE_CODE RECEIVE_PROTOCOL_PSEUDOREFERENCE_CODE REFERENCE_REQUISITE_PSEUDOREFERENCE_CODE REFERENCE_REQUISITES_PSEUDOREFERENCE_CODE REFTYPES_PSEUDOREFERENCE_CODE REPLICATION_SEANCES_DIARY_PSEUDOREFERENCE_CODE SEND_PROTOCOL_PSEUDOREFERENCE_CODE SUBSTITUTES_PSEUDOREFERENCE_CODE SYSTEM_SETTINGS_PSEUDOREFERENCE_CODE UNITS_PSEUDOREFERENCE_CODE USERS_PSEUDOREFERENCE_CODE VIEWERS_PSEUDOREFERENCE_CODE CERTIFICATE_TYPE_ENCRYPT CERTIFICATE_TYPE_SIGN CERTIFICATE_TYPE_SIGN_AND_ENCRYPT STORAGE_TYPE_FILE STORAGE_TYPE_NAS_CIFS STORAGE_TYPE_SAPERION STORAGE_TYPE_SQL_SERVER COMPTYPE2_REQUISITE_DOCUMENTS_VALUE COMPTYPE2_REQUISITE_TASKS_VALUE COMPTYPE2_REQUISITE_FOLDERS_VALUE COMPTYPE2_REQUISITE_REFERENCES_VALUE SYSREQ_CODE SYSREQ_COMPTYPE2 SYSREQ_CONST_AVAILABLE_FOR_WEB SYSREQ_CONST_COMMON_CODE SYSREQ_CONST_COMMON_VALUE SYSREQ_CONST_FIRM_CODE SYSREQ_CONST_FIRM_STATUS SYSREQ_CONST_FIRM_VALUE SYSREQ_CONST_SERVER_STATUS SYSREQ_CONTENTS SYSREQ_DATE_OPEN SYSREQ_DATE_CLOSE SYSREQ_DESCRIPTION SYSREQ_DESCRIPTION_LOCALIZE_ID SYSREQ_DOUBLE SYSREQ_EDOC_ACCESS_TYPE SYSREQ_EDOC_AUTHOR SYSREQ_EDOC_CREATED SYSREQ_EDOC_DELEGATE_RIGHTS_REQUISITE_CODE SYSREQ_EDOC_EDITOR SYSREQ_EDOC_ENCODE_TYPE SYSREQ_EDOC_ENCRYPTION_PLUGIN_NAME SYSREQ_EDOC_ENCRYPTION_PLUGIN_VERSION SYSREQ_EDOC_EXPORT_DATE SYSREQ_EDOC_EXPORTER SYSREQ_EDOC_KIND SYSREQ_EDOC_LIFE_STAGE_NAME SYSREQ_EDOC_LOCKED_FOR_SERVER_CODE SYSREQ_EDOC_MODIFIED SYSREQ_EDOC_NAME SYSREQ_EDOC_NOTE SYSREQ_EDOC_QUALIFIED_ID SYSREQ_EDOC_SESSION_KEY SYSREQ_EDOC_SESSION_KEY_ENCRYPTION_PLUGIN_NAME SYSREQ_EDOC_SESSION_KEY_ENCRYPTION_PLUGIN_VERSION SYSREQ_EDOC_SIGNATURE_TYPE SYSREQ_EDOC_SIGNED SYSREQ_EDOC_STORAGE SYSREQ_EDOC_STORAGES_ARCHIVE_STORAGE SYSREQ_EDOC_STORAGES_CHECK_RIGHTS SYSREQ_EDOC_STORAGES_COMPUTER_NAME SYSREQ_EDOC_STORAGES_EDIT_IN_STORAGE SYSREQ_EDOC_STORAGES_EXECUTIVE_STORAGE SYSREQ_EDOC_STORAGES_FUNCTION SYSREQ_EDOC_STORAGES_INITIALIZED SYSREQ_EDOC_STORAGES_LOCAL_PATH SYSREQ_EDOC_STORAGES_SAPERION_DATABASE_NAME SYSREQ_EDOC_STORAGES_SEARCH_BY_TEXT SYSREQ_EDOC_STORAGES_SERVER_NAME SYSREQ_EDOC_STORAGES_SHARED_SOURCE_NAME SYSREQ_EDOC_STORAGES_TYPE SYSREQ_EDOC_TEXT_MODIFIED SYSREQ_EDOC_TYPE_ACT_CODE SYSREQ_EDOC_TYPE_ACT_DESCRIPTION SYSREQ_EDOC_TYPE_ACT_DESCRIPTION_LOCALIZE_ID SYSREQ_EDOC_TYPE_ACT_ON_EXECUTE SYSREQ_EDOC_TYPE_ACT_ON_EXECUTE_EXISTS SYSREQ_EDOC_TYPE_ACT_SECTION SYSREQ_EDOC_TYPE_ADD_PARAMS SYSREQ_EDOC_TYPE_COMMENT SYSREQ_EDOC_TYPE_EVENT_TEXT SYSREQ_EDOC_TYPE_NAME_IN_SINGULAR SYSREQ_EDOC_TYPE_NAME_IN_SINGULAR_LOCALIZE_ID SYSREQ_EDOC_TYPE_NAME_LOCALIZE_ID SYSREQ_EDOC_TYPE_NUMERATION_METHOD SYSREQ_EDOC_TYPE_PSEUDO_REQUISITE_CODE SYSREQ_EDOC_TYPE_REQ_CODE SYSREQ_EDOC_TYPE_REQ_DESCRIPTION SYSREQ_EDOC_TYPE_REQ_DESCRIPTION_LOCALIZE_ID SYSREQ_EDOC_TYPE_REQ_IS_LEADING SYSREQ_EDOC_TYPE_REQ_IS_REQUIRED SYSREQ_EDOC_TYPE_REQ_NUMBER SYSREQ_EDOC_TYPE_REQ_ON_CHANGE SYSREQ_EDOC_TYPE_REQ_ON_CHANGE_EXISTS SYSREQ_EDOC_TYPE_REQ_ON_SELECT SYSREQ_EDOC_TYPE_REQ_ON_SELECT_KIND SYSREQ_EDOC_TYPE_REQ_SECTION SYSREQ_EDOC_TYPE_VIEW_CARD SYSREQ_EDOC_TYPE_VIEW_CODE SYSREQ_EDOC_TYPE_VIEW_COMMENT SYSREQ_EDOC_TYPE_VIEW_IS_MAIN SYSREQ_EDOC_TYPE_VIEW_NAME SYSREQ_EDOC_TYPE_VIEW_NAME_LOCALIZE_ID SYSREQ_EDOC_VERSION_AUTHOR SYSREQ_EDOC_VERSION_CRC SYSREQ_EDOC_VERSION_DATA SYSREQ_EDOC_VERSION_EDITOR SYSREQ_EDOC_VERSION_EXPORT_DATE SYSREQ_EDOC_VERSION_EXPORTER SYSREQ_EDOC_VERSION_HIDDEN SYSREQ_EDOC_VERSION_LIFE_STAGE SYSREQ_EDOC_VERSION_MODIFIED SYSREQ_EDOC_VERSION_NOTE SYSREQ_EDOC_VERSION_SIGNATURE_TYPE SYSREQ_EDOC_VERSION_SIGNED SYSREQ_EDOC_VERSION_SIZE SYSREQ_EDOC_VERSION_SOURCE SYSREQ_EDOC_VERSION_TEXT_MODIFIED SYSREQ_EDOCKIND_DEFAULT_VERSION_STATE_CODE SYSREQ_FOLDER_KIND SYSREQ_FUNC_CATEGORY SYSREQ_FUNC_COMMENT SYSREQ_FUNC_GROUP SYSREQ_FUNC_GROUP_COMMENT SYSREQ_FUNC_GROUP_NUMBER SYSREQ_FUNC_HELP SYSREQ_FUNC_PARAM_DEF_VALUE SYSREQ_FUNC_PARAM_IDENT SYSREQ_FUNC_PARAM_NUMBER SYSREQ_FUNC_PARAM_TYPE SYSREQ_FUNC_TEXT SYSREQ_GROUP_CATEGORY SYSREQ_ID SYSREQ_LAST_UPDATE SYSREQ_LEADER_REFERENCE SYSREQ_LINE_NUMBER SYSREQ_MAIN_RECORD_ID SYSREQ_NAME SYSREQ_NAME_LOCALIZE_ID SYSREQ_NOTE SYSREQ_ORIGINAL_RECORD SYSREQ_OUR_FIRM SYSREQ_PROFILING_SETTINGS_BATCH_LOGING SYSREQ_PROFILING_SETTINGS_BATCH_SIZE SYSREQ_PROFILING_SETTINGS_PROFILING_ENABLED SYSREQ_PROFILING_SETTINGS_SQL_PROFILING_ENABLED SYSREQ_PROFILING_SETTINGS_START_LOGGED SYSREQ_RECORD_STATUS SYSREQ_REF_REQ_FIELD_NAME SYSREQ_REF_REQ_FORMAT SYSREQ_REF_REQ_GENERATED SYSREQ_REF_REQ_LENGTH SYSREQ_REF_REQ_PRECISION SYSREQ_REF_REQ_REFERENCE SYSREQ_REF_REQ_SECTION SYSREQ_REF_REQ_STORED SYSREQ_REF_REQ_TOKENS SYSREQ_REF_REQ_TYPE SYSREQ_REF_REQ_VIEW SYSREQ_REF_TYPE_ACT_CODE SYSREQ_REF_TYPE_ACT_DESCRIPTION SYSREQ_REF_TYPE_ACT_DESCRIPTION_LOCALIZE_ID SYSREQ_REF_TYPE_ACT_ON_EXECUTE SYSREQ_REF_TYPE_ACT_ON_EXECUTE_EXISTS SYSREQ_REF_TYPE_ACT_SECTION SYSREQ_REF_TYPE_ADD_PARAMS SYSREQ_REF_TYPE_COMMENT SYSREQ_REF_TYPE_COMMON_SETTINGS SYSREQ_REF_TYPE_DISPLAY_REQUISITE_NAME SYSREQ_REF_TYPE_EVENT_TEXT SYSREQ_REF_TYPE_MAIN_LEADING_REF SYSREQ_REF_TYPE_NAME_IN_SINGULAR SYSREQ_REF_TYPE_NAME_IN_SINGULAR_LOCALIZE_ID SYSREQ_REF_TYPE_NAME_LOCALIZE_ID SYSREQ_REF_TYPE_NUMERATION_METHOD SYSREQ_REF_TYPE_REQ_CODE SYSREQ_REF_TYPE_REQ_DESCRIPTION SYSREQ_REF_TYPE_REQ_DESCRIPTION_LOCALIZE_ID SYSREQ_REF_TYPE_REQ_IS_CONTROL SYSREQ_REF_TYPE_REQ_IS_FILTER SYSREQ_REF_TYPE_REQ_IS_LEADING SYSREQ_REF_TYPE_REQ_IS_REQUIRED SYSREQ_REF_TYPE_REQ_NUMBER SYSREQ_REF_TYPE_REQ_ON_CHANGE SYSREQ_REF_TYPE_REQ_ON_CHANGE_EXISTS SYSREQ_REF_TYPE_REQ_ON_SELECT SYSREQ_REF_TYPE_REQ_ON_SELECT_KIND SYSREQ_REF_TYPE_REQ_SECTION SYSREQ_REF_TYPE_VIEW_CARD SYSREQ_REF_TYPE_VIEW_CODE SYSREQ_REF_TYPE_VIEW_COMMENT SYSREQ_REF_TYPE_VIEW_IS_MAIN SYSREQ_REF_TYPE_VIEW_NAME SYSREQ_REF_TYPE_VIEW_NAME_LOCALIZE_ID SYSREQ_REFERENCE_TYPE_ID SYSREQ_STATE SYSREQ_STAT\u0415 SYSREQ_SYSTEM_SETTINGS_VALUE SYSREQ_TYPE SYSREQ_UNIT SYSREQ_UNIT_ID SYSREQ_USER_GROUPS_GROUP_FULL_NAME SYSREQ_USER_GROUPS_GROUP_NAME SYSREQ_USER_GROUPS_GROUP_SERVER_NAME SYSREQ_USERS_ACCESS_RIGHTS SYSREQ_USERS_AUTHENTICATION SYSREQ_USERS_CATEGORY SYSREQ_USERS_COMPONENT SYSREQ_USERS_COMPONENT_USER_IS_PUBLIC SYSREQ_USERS_DOMAIN SYSREQ_USERS_FULL_USER_NAME SYSREQ_USERS_GROUP SYSREQ_USERS_IS_MAIN_SERVER SYSREQ_USERS_LOGIN SYSREQ_USERS_REFERENCE_USER_IS_PUBLIC SYSREQ_USERS_STATUS SYSREQ_USERS_USER_CERTIFICATE SYSREQ_USERS_USER_CERTIFICATE_INFO SYSREQ_USERS_USER_CERTIFICATE_PLUGIN_NAME SYSREQ_USERS_USER_CERTIFICATE_PLUGIN_VERSION SYSREQ_USERS_USER_CERTIFICATE_STATE SYSREQ_USERS_USER_CERTIFICATE_SUBJECT_NAME SYSREQ_USERS_USER_CERTIFICATE_THUMBPRINT SYSREQ_USERS_USER_DEFAULT_CERTIFICATE SYSREQ_USERS_USER_DESCRIPTION SYSREQ_USERS_USER_GLOBAL_NAME SYSREQ_USERS_USER_LOGIN SYSREQ_USERS_USER_MAIN_SERVER SYSREQ_USERS_USER_TYPE SYSREQ_WORK_RULES_FOLDER_ID RESULT_VAR_NAME RESULT_VAR_NAME_ENG AUTO_NUMERATION_RULE_ID CANT_CHANGE_ID_REQUISITE_RULE_ID CANT_CHANGE_OURFIRM_REQUISITE_RULE_ID CHECK_CHANGING_REFERENCE_RECORD_USE_RULE_ID CHECK_CODE_REQUISITE_RULE_ID CHECK_DELETING_REFERENCE_RECORD_USE_RULE_ID CHECK_FILTRATER_CHANGES_RULE_ID CHECK_RECORD_INTERVAL_RULE_ID CHECK_REFERENCE_INTERVAL_RULE_ID CHECK_REQUIRED_DATA_FULLNESS_RULE_ID CHECK_REQUIRED_REQUISITES_FULLNESS_RULE_ID MAKE_RECORD_UNRATIFIED_RULE_ID RESTORE_AUTO_NUMERATION_RULE_ID SET_FIRM_CONTEXT_FROM_RECORD_RULE_ID SET_FIRST_RECORD_IN_LIST_FORM_RULE_ID SET_IDSPS_VALUE_RULE_ID SET_NEXT_CODE_VALUE_RULE_ID SET_OURFIRM_BOUNDS_RULE_ID SET_OURFIRM_REQUISITE_RULE_ID SCRIPT_BLOCK_AFTER_FINISH_EVENT SCRIPT_BLOCK_BEFORE_START_EVENT SCRIPT_BLOCK_EXECUTION_RESULTS_PROPERTY SCRIPT_BLOCK_NAME_PROPERTY SCRIPT_BLOCK_SCRIPT_PROPERTY SUBTASK_BLOCK_ABORT_DEADLINE_PROPERTY SUBTASK_BLOCK_AFTER_FINISH_EVENT SUBTASK_BLOCK_ASSIGN_PARAMS_EVENT SUBTASK_BLOCK_ATTACHMENTS_PROPERTY SUBTASK_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY SUBTASK_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY SUBTASK_BLOCK_BEFORE_START_EVENT SUBTASK_BLOCK_CREATED_TASK_PROPERTY SUBTASK_BLOCK_CREATION_EVENT SUBTASK_BLOCK_DEADLINE_PROPERTY SUBTASK_BLOCK_IMPORTANCE_PROPERTY SUBTASK_BLOCK_INITIATOR_PROPERTY SUBTASK_BLOCK_IS_RELATIVE_ABORT_DEADLINE_PROPERTY SUBTASK_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY SUBTASK_BLOCK_JOBS_TYPE_PROPERTY SUBTASK_BLOCK_NAME_PROPERTY SUBTASK_BLOCK_PARALLEL_ROUTE_PROPERTY SUBTASK_BLOCK_PERFORMERS_PROPERTY SUBTASK_BLOCK_RELATIVE_ABORT_DEADLINE_TYPE_PROPERTY SUBTASK_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY SUBTASK_BLOCK_REQUIRE_SIGN_PROPERTY SUBTASK_BLOCK_STANDARD_ROUTE_PROPERTY SUBTASK_BLOCK_START_EVENT SUBTASK_BLOCK_STEP_CONTROL_PROPERTY SUBTASK_BLOCK_SUBJECT_PROPERTY SUBTASK_BLOCK_TASK_CONTROL_PROPERTY SUBTASK_BLOCK_TEXT_PROPERTY SUBTASK_BLOCK_UNLOCK_ATTACHMENTS_ON_STOP_PROPERTY SUBTASK_BLOCK_USE_STANDARD_ROUTE_PROPERTY SUBTASK_BLOCK_WAIT_FOR_TASK_COMPLETE_PROPERTY SYSCOMP_CONTROL_JOBS SYSCOMP_FOLDERS SYSCOMP_JOBS SYSCOMP_NOTICES SYSCOMP_TASKS SYSDLG_CREATE_EDOCUMENT SYSDLG_CREATE_EDOCUMENT_VERSION SYSDLG_CURRENT_PERIOD SYSDLG_EDIT_FUNCTION_HELP SYSDLG_EDOCUMENT_KINDS_FOR_TEMPLATE SYSDLG_EXPORT_MULTIPLE_EDOCUMENTS SYSDLG_EXPORT_SINGLE_EDOCUMENT SYSDLG_IMPORT_EDOCUMENT SYSDLG_MULTIPLE_SELECT SYSDLG_SETUP_ACCESS_RIGHTS SYSDLG_SETUP_DEFAULT_RIGHTS SYSDLG_SETUP_FILTER_CONDITION SYSDLG_SETUP_SIGN_RIGHTS SYSDLG_SETUP_TASK_OBSERVERS SYSDLG_SETUP_TASK_ROUTE SYSDLG_SETUP_USERS_LIST SYSDLG_SIGN_EDOCUMENT SYSDLG_SIGN_MULTIPLE_EDOCUMENTS SYSREF_ACCESS_RIGHTS_TYPES SYSREF_ADMINISTRATION_HISTORY SYSREF_ALL_AVAILABLE_COMPONENTS SYSREF_ALL_AVAILABLE_PRIVILEGES SYSREF_ALL_REPLICATING_COMPONENTS SYSREF_AVAILABLE_DEVELOPERS_COMPONENTS SYSREF_CALENDAR_EVENTS SYSREF_COMPONENT_TOKEN_HISTORY SYSREF_COMPONENT_TOKENS SYSREF_COMPONENTS SYSREF_CONSTANTS SYSREF_DATA_RECEIVE_PROTOCOL SYSREF_DATA_SEND_PROTOCOL SYSREF_DIALOGS SYSREF_DIALOGS_REQUISITES SYSREF_EDITORS SYSREF_EDOC_CARDS SYSREF_EDOC_TYPES SYSREF_EDOCUMENT_CARD_REQUISITES SYSREF_EDOCUMENT_CARD_TYPES SYSREF_EDOCUMENT_CARD_TYPES_REFERENCE SYSREF_EDOCUMENT_CARDS SYSREF_EDOCUMENT_HISTORY SYSREF_EDOCUMENT_KINDS SYSREF_EDOCUMENT_REQUISITES SYSREF_EDOCUMENT_SIGNATURES SYSREF_EDOCUMENT_TEMPLATES SYSREF_EDOCUMENT_TEXT_STORAGES SYSREF_EDOCUMENT_VIEWS SYSREF_FILTERER_SETUP_CONFLICTS SYSREF_FILTRATER_SETTING_CONFLICTS SYSREF_FOLDER_HISTORY SYSREF_FOLDERS SYSREF_FUNCTION_GROUPS SYSREF_FUNCTION_PARAMS SYSREF_FUNCTIONS SYSREF_JOB_HISTORY SYSREF_LINKS SYSREF_LOCALIZATION_DICTIONARY SYSREF_LOCALIZATION_LANGUAGES SYSREF_MODULES SYSREF_PRIVILEGES SYSREF_RECORD_HISTORY SYSREF_REFERENCE_REQUISITES SYSREF_REFERENCE_TYPE_VIEWS SYSREF_REFERENCE_TYPES SYSREF_REFERENCES SYSREF_REFERENCES_REQUISITES SYSREF_REMOTE_SERVERS SYSREF_REPLICATION_SESSIONS_LOG SYSREF_REPLICATION_SESSIONS_PROTOCOL SYSREF_REPORTS SYSREF_ROLES SYSREF_ROUTE_BLOCK_GROUPS SYSREF_ROUTE_BLOCKS SYSREF_SCRIPTS SYSREF_SEARCHES SYSREF_SERVER_EVENTS SYSREF_SERVER_EVENTS_HISTORY SYSREF_STANDARD_ROUTE_GROUPS SYSREF_STANDARD_ROUTES SYSREF_STATUSES SYSREF_SYSTEM_SETTINGS SYSREF_TASK_HISTORY SYSREF_TASK_KIND_GROUPS SYSREF_TASK_KINDS SYSREF_TASK_RIGHTS SYSREF_TASK_SIGNATURES SYSREF_TASKS SYSREF_UNITS SYSREF_USER_GROUPS SYSREF_USER_GROUPS_REFERENCE SYSREF_USER_SUBSTITUTION SYSREF_USERS SYSREF_USERS_REFERENCE SYSREF_VIEWERS SYSREF_WORKING_TIME_CALENDARS ACCESS_RIGHTS_TABLE_NAME EDMS_ACCESS_TABLE_NAME EDOC_TYPES_TABLE_NAME TEST_DEV_DB_NAME TEST_DEV_SYSTEM_CODE TEST_EDMS_DB_NAME TEST_EDMS_MAIN_CODE TEST_EDMS_MAIN_DB_NAME TEST_EDMS_SECOND_CODE TEST_EDMS_SECOND_DB_NAME TEST_EDMS_SYSTEM_CODE TEST_ISB5_MAIN_CODE TEST_ISB5_SECOND_CODE TEST_SQL_SERVER_2005_NAME TEST_SQL_SERVER_NAME ATTENTION_CAPTION cbsCommandLinks cbsDefault CONFIRMATION_CAPTION ERROR_CAPTION INFORMATION_CAPTION mrCancel mrOk EDOC_VERSION_ACTIVE_STAGE_CODE EDOC_VERSION_DESIGN_STAGE_CODE EDOC_VERSION_OBSOLETE_STAGE_CODE cpDataEnciphermentEnabled cpDigitalSignatureEnabled cpID cpIssuer cpPluginVersion cpSerial cpSubjectName cpSubjSimpleName cpValidFromDate cpValidToDate ISBL_SYNTAX NO_SYNTAX XML_SYNTAX WAIT_BLOCK_AFTER_FINISH_EVENT WAIT_BLOCK_BEFORE_START_EVENT WAIT_BLOCK_DEADLINE_PROPERTY WAIT_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY WAIT_BLOCK_NAME_PROPERTY WAIT_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY SYSRES_COMMON SYSRES_CONST SYSRES_MBFUNC SYSRES_SBDATA SYSRES_SBGUI SYSRES_SBINTF SYSRES_SBREFDSC SYSRES_SQLERRORS SYSRES_SYSCOMP atUser atGroup atRole aemEnabledAlways aemDisabledAlways aemEnabledOnBrowse aemEnabledOnEdit aemDisabledOnBrowseEmpty apBegin apEnd alLeft alRight asmNever asmNoButCustomize asmAsLastTime asmYesButCustomize asmAlways cirCommon cirRevoked ctSignature ctEncode ctSignatureEncode clbUnchecked clbChecked clbGrayed ceISB ceAlways ceNever ctDocument ctReference ctScript ctUnknown ctReport ctDialog ctFunction ctFolder ctEDocument ctTask ctJob ctNotice ctControlJob cfInternal cfDisplay ciUnspecified ciWrite ciRead ckFolder ckEDocument ckTask ckJob ckComponentToken ckAny ckReference ckScript ckReport ckDialog ctISBLEditor ctBevel ctButton ctCheckListBox ctComboBox ctComboEdit ctGrid ctDBCheckBox ctDBComboBox ctDBEdit ctDBEllipsis ctDBMemo ctDBNavigator ctDBRadioGroup ctDBStatusLabel ctEdit ctGroupBox ctInplaceHint ctMemo ctPanel ctListBox ctRadioButton ctRichEdit ctTabSheet ctWebBrowser ctImage ctHyperLink ctLabel ctDBMultiEllipsis ctRibbon ctRichView ctInnerPanel ctPanelGroup ctBitButton cctDate cctInteger cctNumeric cctPick cctReference cctString cctText cltInternal cltPrimary cltGUI dseBeforeOpen dseAfterOpen dseBeforeClose dseAfterClose dseOnValidDelete dseBeforeDelete dseAfterDelete dseAfterDeleteOutOfTransaction dseOnDeleteError dseBeforeInsert dseAfterInsert dseOnValidUpdate dseBeforeUpdate dseOnUpdateRatifiedRecord dseAfterUpdate dseAfterUpdateOutOfTransaction dseOnUpdateError dseAfterScroll dseOnOpenRecord dseOnCloseRecord dseBeforeCancel dseAfterCancel dseOnUpdateDeadlockError dseBeforeDetailUpdate dseOnPrepareUpdate dseOnAnyRequisiteChange dssEdit dssInsert dssBrowse dssInActive dftDate dftShortDate dftDateTime dftTimeStamp dotDays dotHours dotMinutes dotSeconds dtkndLocal dtkndUTC arNone arView arEdit arFull ddaView ddaEdit emLock emEdit emSign emExportWithLock emImportWithUnlock emChangeVersionNote emOpenForModify emChangeLifeStage emDelete emCreateVersion emImport emUnlockExportedWithLock emStart emAbort emReInit emMarkAsReaded emMarkAsUnreaded emPerform emAccept emResume emChangeRights emEditRoute emEditObserver emRecoveryFromLocalCopy emChangeWorkAccessType emChangeEncodeTypeToCertificate emChangeEncodeTypeToPassword emChangeEncodeTypeToNone emChangeEncodeTypeToCertificatePassword emChangeStandardRoute emGetText emOpenForView emMoveToStorage emCreateObject emChangeVersionHidden emDeleteVersion emChangeLifeCycleStage emApprovingSign emExport emContinue emLockFromEdit emUnLockForEdit emLockForServer emUnlockFromServer emDelegateAccessRights emReEncode ecotFile ecotProcess eaGet eaCopy eaCreate eaCreateStandardRoute edltAll edltNothing edltQuery essmText essmCard esvtLast esvtLastActive esvtSpecified edsfExecutive edsfArchive edstSQLServer edstFile edvstNone edvstEDocumentVersionCopy edvstFile edvstTemplate edvstScannedFile vsDefault vsDesign vsActive vsObsolete etNone etCertificate etPassword etCertificatePassword ecException ecWarning ecInformation estAll estApprovingOnly evtLast evtLastActive evtQuery fdtString fdtNumeric fdtInteger fdtDate fdtText fdtUnknown fdtWideString fdtLargeInteger ftInbox ftOutbox ftFavorites ftCommonFolder ftUserFolder ftComponents ftQuickLaunch ftShortcuts ftSearch grhAuto grhX1 grhX2 grhX3 hltText hltRTF hltHTML iffBMP iffJPEG iffMultiPageTIFF iffSinglePageTIFF iffTIFF iffPNG im8bGrayscale im24bRGB im1bMonochrome itBMP itJPEG itWMF itPNG ikhInformation ikhWarning ikhError ikhNoIcon icUnknown icScript icFunction icIntegratedReport icAnalyticReport icDataSetEventHandler icActionHandler icFormEventHandler icLookUpEventHandler icRequisiteChangeEventHandler icBeforeSearchEventHandler icRoleCalculation icSelectRouteEventHandler icBlockPropertyCalculation icBlockQueryParamsEventHandler icChangeSearchResultEventHandler icBlockEventHandler icSubTaskInitEventHandler icEDocDataSetEventHandler icEDocLookUpEventHandler icEDocActionHandler icEDocFormEventHandler icEDocRequisiteChangeEventHandler icStructuredConversionRule icStructuredConversionEventBefore icStructuredConversionEventAfter icWizardEventHandler icWizardFinishEventHandler icWizardStepEventHandler icWizardStepFinishEventHandler icWizardActionEnableEventHandler icWizardActionExecuteEventHandler icCreateJobsHandler icCreateNoticesHandler icBeforeLookUpEventHandler icAfterLookUpEventHandler icTaskAbortEventHandler icWorkflowBlockActionHandler icDialogDataSetEventHandler icDialogActionHandler icDialogLookUpEventHandler icDialogRequisiteChangeEventHandler icDialogFormEventHandler icDialogValidCloseEventHandler icBlockFormEventHandler icTaskFormEventHandler icReferenceMethod icEDocMethod icDialogMethod icProcessMessageHandler isShow isHide isByUserSettings jkJob jkNotice jkControlJob jtInner jtLeft jtRight jtFull jtCross lbpAbove lbpBelow lbpLeft lbpRight eltPerConnection eltPerUser sfcUndefined sfcBlack sfcGreen sfcRed sfcBlue sfcOrange sfcLilac sfsItalic sfsStrikeout sfsNormal ldctStandardRoute ldctWizard ldctScript ldctFunction ldctRouteBlock ldctIntegratedReport ldctAnalyticReport ldctReferenceType ldctEDocumentType ldctDialog ldctServerEvents mrcrtNone mrcrtUser mrcrtMaximal mrcrtCustom vtEqual vtGreaterOrEqual vtLessOrEqual vtRange rdYesterday rdToday rdTomorrow rdThisWeek rdThisMonth rdThisYear rdNextMonth rdNextWeek rdLastWeek rdLastMonth rdWindow rdFile rdPrinter rdtString rdtNumeric rdtInteger rdtDate rdtReference rdtAccount rdtText rdtPick rdtUnknown rdtLargeInteger rdtDocument reOnChange reOnChangeValues ttGlobal ttLocal ttUser ttSystem ssmBrowse ssmSelect ssmMultiSelect ssmBrowseModal smSelect smLike smCard stNone stAuthenticating stApproving sctString sctStream sstAnsiSort sstNaturalSort svtEqual svtContain soatString soatNumeric soatInteger soatDatetime soatReferenceRecord soatText soatPick soatBoolean soatEDocument soatAccount soatIntegerCollection soatNumericCollection soatStringCollection soatPickCollection soatDatetimeCollection soatBooleanCollection soatReferenceRecordCollection soatEDocumentCollection soatAccountCollection soatContents soatUnknown tarAbortByUser tarAbortByWorkflowException tvtAllWords tvtExactPhrase tvtAnyWord usNone usCompleted usRedSquare usBlueSquare usYellowSquare usGreenSquare usOrangeSquare usPurpleSquare usFollowUp utUnknown utUser utDeveloper utAdministrator utSystemDeveloper utDisconnected btAnd btDetailAnd btOr btNotOr btOnly vmView vmSelect vmNavigation vsmSingle vsmMultiple vsmMultipleCheck vsmNoSelection wfatPrevious wfatNext wfatCancel wfatFinish wfepUndefined wfepText3 wfepText6 wfepText9 wfepSpinEdit wfepDropDown wfepRadioGroup wfepFlag wfepText12 wfepText15 wfepText18 wfepText21 wfepText24 wfepText27 wfepText30 wfepRadioGroupColumn1 wfepRadioGroupColumn2 wfepRadioGroupColumn3 wfetQueryParameter wfetText wfetDelimiter wfetLabel wptString wptInteger wptNumeric wptBoolean wptDateTime wptPick wptText wptUser wptUserList wptEDocumentInfo wptEDocumentInfoList wptReferenceRecordInfo wptReferenceRecordInfoList wptFolderInfo wptTaskInfo wptContents wptFileName wptDate wsrComplete wsrGoNext wsrGoPrevious wsrCustom wsrCancel wsrGoFinal wstForm wstEDocument wstTaskCard wstReferenceRecordCard wstFinal waAll waPerformers waManual wsbStart wsbFinish wsbNotice wsbStep wsbDecision wsbWait wsbMonitor wsbScript wsbConnector wsbSubTask wsbLifeCycleStage wsbPause wdtInteger wdtFloat wdtString wdtPick wdtDateTime wdtBoolean wdtTask wdtJob wdtFolder wdtEDocument wdtReferenceRecord wdtUser wdtGroup wdtRole wdtIntegerCollection wdtFloatCollection wdtStringCollection wdtPickCollection wdtDateTimeCollection wdtBooleanCollection wdtTaskCollection wdtJobCollection wdtFolderCollection wdtEDocumentCollection wdtReferenceRecordCollection wdtUserCollection wdtGroupCollection wdtRoleCollection wdtContents wdtUserList wdtSearchDescription wdtDeadLine wdtPickSet wdtAccountCollection wiLow wiNormal wiHigh wrtSoft wrtHard wsInit wsRunning wsDone wsControlled wsAborted wsContinued wtmFull wtmFromCurrent wtmOnlyCurrent ",
+literal:"true false"},illegal:'"',
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.C_NUMBER_MODE,{
+className:"meta",begin:"#",end:"$"}]})})());
+hljs.registerLanguage("gml",(()=>{"use strict";return e=>({name:"GML",
+aliases:["GML"],case_insensitive:!1,keywords:{
+keyword:"begin end if then else while do for break continue with until repeat exit and or xor not return mod div switch case default var globalvar enum function constructor delete #macro #region #endregion",
+built_in:"is_real is_string is_array is_undefined is_int32 is_int64 is_ptr is_vec3 is_vec4 is_matrix is_bool is_method is_struct is_infinity is_nan is_numeric typeof variable_global_exists variable_global_get variable_global_set variable_instance_exists variable_instance_get variable_instance_set variable_instance_get_names variable_struct_exists variable_struct_get variable_struct_get_names variable_struct_names_count variable_struct_remove variable_struct_set array_delete array_insert array_length array_length_1d array_length_2d array_height_2d array_equals array_create array_copy array_pop array_push array_resize array_sort random random_range irandom irandom_range random_set_seed random_get_seed randomize randomise choose abs round floor ceil sign frac sqrt sqr exp ln log2 log10 sin cos tan arcsin arccos arctan arctan2 dsin dcos dtan darcsin darccos darctan darctan2 degtorad radtodeg power logn min max mean median clamp lerp dot_product dot_product_3d dot_product_normalised dot_product_3d_normalised dot_product_normalized dot_product_3d_normalized math_set_epsilon math_get_epsilon angle_difference point_distance_3d point_distance point_direction lengthdir_x lengthdir_y real string int64 ptr string_format chr ansi_char ord string_length string_byte_length string_pos string_copy string_char_at string_ord_at string_byte_at string_set_byte_at string_delete string_insert string_lower string_upper string_repeat string_letters string_digits string_lettersdigits string_replace string_replace_all string_count string_hash_to_newline clipboard_has_text clipboard_set_text clipboard_get_text date_current_datetime date_create_datetime date_valid_datetime date_inc_year date_inc_month date_inc_week date_inc_day date_inc_hour date_inc_minute date_inc_second date_get_year date_get_month date_get_week date_get_day date_get_hour date_get_minute date_get_second date_get_weekday date_get_day_of_year date_get_hour_of_year date_get_minute_of_year date_get_second_of_year date_year_span date_month_span date_week_span date_day_span date_hour_span date_minute_span date_second_span date_compare_datetime date_compare_date date_compare_time date_date_of date_time_of date_datetime_string date_date_string date_time_string date_days_in_month date_days_in_year date_leap_year date_is_today date_set_timezone date_get_timezone game_set_speed game_get_speed motion_set motion_add place_free place_empty place_meeting place_snapped move_random move_snap move_towards_point move_contact_solid move_contact_all move_outside_solid move_outside_all move_bounce_solid move_bounce_all move_wrap distance_to_point distance_to_object position_empty position_meeting path_start path_end mp_linear_step mp_potential_step mp_linear_step_object mp_potential_step_object mp_potential_settings mp_linear_path mp_potential_path mp_linear_path_object mp_potential_path_object mp_grid_create mp_grid_destroy mp_grid_clear_all mp_grid_clear_cell mp_grid_clear_rectangle mp_grid_add_cell mp_grid_get_cell mp_grid_add_rectangle mp_grid_add_instances mp_grid_path mp_grid_draw mp_grid_to_ds_grid collision_point collision_rectangle collision_circle collision_ellipse collision_line collision_point_list collision_rectangle_list collision_circle_list collision_ellipse_list collision_line_list instance_position_list instance_place_list point_in_rectangle point_in_triangle point_in_circle rectangle_in_rectangle rectangle_in_triangle rectangle_in_circle instance_find instance_exists instance_number instance_position instance_nearest instance_furthest instance_place instance_create_depth instance_create_layer instance_copy instance_change instance_destroy position_destroy position_change instance_id_get instance_deactivate_all instance_deactivate_object instance_deactivate_region instance_activate_all instance_activate_object instance_activate_region room_goto room_goto_previous room_goto_next room_previous room_next room_restart game_end game_restart game_load game_save game_save_buffer game_load_buffer event_perform event_user event_perform_object event_inherited show_debug_message show_debug_overlay debug_event debug_get_callstack alarm_get alarm_set font_texture_page_size keyboard_set_map keyboard_get_map keyboard_unset_map keyboard_check keyboard_check_pressed keyboard_check_released keyboard_check_direct keyboard_get_numlock keyboard_set_numlock keyboard_key_press keyboard_key_release keyboard_clear io_clear mouse_check_button mouse_check_button_pressed mouse_check_button_released mouse_wheel_up mouse_wheel_down mouse_clear draw_self draw_sprite draw_sprite_pos draw_sprite_ext draw_sprite_stretched draw_sprite_stretched_ext draw_sprite_tiled draw_sprite_tiled_ext draw_sprite_part draw_sprite_part_ext draw_sprite_general draw_clear draw_clear_alpha draw_point draw_line draw_line_width draw_rectangle draw_roundrect draw_roundrect_ext draw_triangle draw_circle draw_ellipse draw_set_circle_precision draw_arrow draw_button draw_path draw_healthbar draw_getpixel draw_getpixel_ext draw_set_colour draw_set_color draw_set_alpha draw_get_colour draw_get_color draw_get_alpha merge_colour make_colour_rgb make_colour_hsv colour_get_red colour_get_green colour_get_blue colour_get_hue colour_get_saturation colour_get_value merge_color make_color_rgb make_color_hsv color_get_red color_get_green color_get_blue color_get_hue color_get_saturation color_get_value merge_color screen_save screen_save_part draw_set_font draw_set_halign draw_set_valign draw_text draw_text_ext string_width string_height string_width_ext string_height_ext draw_text_transformed draw_text_ext_transformed draw_text_colour draw_text_ext_colour draw_text_transformed_colour draw_text_ext_transformed_colour draw_text_color draw_text_ext_color draw_text_transformed_color draw_text_ext_transformed_color draw_point_colour draw_line_colour draw_line_width_colour draw_rectangle_colour draw_roundrect_colour draw_roundrect_colour_ext draw_triangle_colour draw_circle_colour draw_ellipse_colour draw_point_color draw_line_color draw_line_width_color draw_rectangle_color draw_roundrect_color draw_roundrect_color_ext draw_triangle_color draw_circle_color draw_ellipse_color draw_primitive_begin draw_vertex draw_vertex_colour draw_vertex_color draw_primitive_end sprite_get_uvs font_get_uvs sprite_get_texture font_get_texture texture_get_width texture_get_height texture_get_uvs draw_primitive_begin_texture draw_vertex_texture draw_vertex_texture_colour draw_vertex_texture_color texture_global_scale surface_create surface_create_ext surface_resize surface_free surface_exists surface_get_width surface_get_height surface_get_texture surface_set_target surface_set_target_ext surface_reset_target surface_depth_disable surface_get_depth_disable draw_surface draw_surface_stretched draw_surface_tiled draw_surface_part draw_surface_ext draw_surface_stretched_ext draw_surface_tiled_ext draw_surface_part_ext draw_surface_general surface_getpixel surface_getpixel_ext surface_save surface_save_part surface_copy surface_copy_part application_surface_draw_enable application_get_position application_surface_enable application_surface_is_enabled display_get_width display_get_height display_get_orientation display_get_gui_width display_get_gui_height display_reset display_mouse_get_x display_mouse_get_y display_mouse_set display_set_ui_visibility window_set_fullscreen window_get_fullscreen window_set_caption window_set_min_width window_set_max_width window_set_min_height window_set_max_height window_get_visible_rects window_get_caption window_set_cursor window_get_cursor window_set_colour window_get_colour window_set_color window_get_color window_set_position window_set_size window_set_rectangle window_center window_get_x window_get_y window_get_width window_get_height window_mouse_get_x window_mouse_get_y window_mouse_set window_view_mouse_get_x window_view_mouse_get_y window_views_mouse_get_x window_views_mouse_get_y audio_listener_position audio_listener_velocity audio_listener_orientation audio_emitter_position audio_emitter_create audio_emitter_free audio_emitter_exists audio_emitter_pitch audio_emitter_velocity audio_emitter_falloff audio_emitter_gain audio_play_sound audio_play_sound_on audio_play_sound_at audio_stop_sound audio_resume_music audio_music_is_playing audio_resume_sound audio_pause_sound audio_pause_music audio_channel_num audio_sound_length audio_get_type audio_falloff_set_model audio_play_music audio_stop_music audio_master_gain audio_music_gain audio_sound_gain audio_sound_pitch audio_stop_all audio_resume_all audio_pause_all audio_is_playing audio_is_paused audio_exists audio_sound_set_track_position audio_sound_get_track_position audio_emitter_get_gain audio_emitter_get_pitch audio_emitter_get_x audio_emitter_get_y audio_emitter_get_z audio_emitter_get_vx audio_emitter_get_vy audio_emitter_get_vz audio_listener_set_position audio_listener_set_velocity audio_listener_set_orientation audio_listener_get_data audio_set_master_gain audio_get_master_gain audio_sound_get_gain audio_sound_get_pitch audio_get_name audio_sound_set_track_position audio_sound_get_track_position audio_create_stream audio_destroy_stream audio_create_sync_group audio_destroy_sync_group audio_play_in_sync_group audio_start_sync_group audio_stop_sync_group audio_pause_sync_group audio_resume_sync_group audio_sync_group_get_track_pos audio_sync_group_debug audio_sync_group_is_playing audio_debug audio_group_load audio_group_unload audio_group_is_loaded audio_group_load_progress audio_group_name audio_group_stop_all audio_group_set_gain audio_create_buffer_sound audio_free_buffer_sound audio_create_play_queue audio_free_play_queue audio_queue_sound audio_get_recorder_count audio_get_recorder_info audio_start_recording audio_stop_recording audio_sound_get_listener_mask audio_emitter_get_listener_mask audio_get_listener_mask audio_sound_set_listener_mask audio_emitter_set_listener_mask audio_set_listener_mask audio_get_listener_count audio_get_listener_info audio_system show_message show_message_async clickable_add clickable_add_ext clickable_change clickable_change_ext clickable_delete clickable_exists clickable_set_style show_question show_question_async get_integer get_string get_integer_async get_string_async get_login_async get_open_filename get_save_filename get_open_filename_ext get_save_filename_ext show_error highscore_clear highscore_add highscore_value highscore_name draw_highscore sprite_exists sprite_get_name sprite_get_number sprite_get_width sprite_get_height sprite_get_xoffset sprite_get_yoffset sprite_get_bbox_left sprite_get_bbox_right sprite_get_bbox_top sprite_get_bbox_bottom sprite_save sprite_save_strip sprite_set_cache_size sprite_set_cache_size_ext sprite_get_tpe sprite_prefetch sprite_prefetch_multi sprite_flush sprite_flush_multi sprite_set_speed sprite_get_speed_type sprite_get_speed font_exists font_get_name font_get_fontname font_get_bold font_get_italic font_get_first font_get_last font_get_size font_set_cache_size path_exists path_get_name path_get_length path_get_time path_get_kind path_get_closed path_get_precision path_get_number path_get_point_x path_get_point_y path_get_point_speed path_get_x path_get_y path_get_speed script_exists script_get_name timeline_add timeline_delete timeline_clear timeline_exists timeline_get_name timeline_moment_clear timeline_moment_add_script timeline_size timeline_max_moment object_exists object_get_name object_get_sprite object_get_solid object_get_visible object_get_persistent object_get_mask object_get_parent object_get_physics object_is_ancestor room_exists room_get_name sprite_set_offset sprite_duplicate sprite_assign sprite_merge sprite_add sprite_replace sprite_create_from_surface sprite_add_from_surface sprite_delete sprite_set_alpha_from_sprite sprite_collision_mask font_add_enable_aa font_add_get_enable_aa font_add font_add_sprite font_add_sprite_ext font_replace font_replace_sprite font_replace_sprite_ext font_delete path_set_kind path_set_closed path_set_precision path_add path_assign path_duplicate path_append path_delete path_add_point path_insert_point path_change_point path_delete_point path_clear_points path_reverse path_mirror path_flip path_rotate path_rescale path_shift script_execute object_set_sprite object_set_solid object_set_visible object_set_persistent object_set_mask room_set_width room_set_height room_set_persistent room_set_background_colour room_set_background_color room_set_view room_set_viewport room_get_viewport room_set_view_enabled room_add room_duplicate room_assign room_instance_add room_instance_clear room_get_camera room_set_camera asset_get_index asset_get_type file_text_open_from_string file_text_open_read file_text_open_write file_text_open_append file_text_close file_text_write_string file_text_write_real file_text_writeln file_text_read_string file_text_read_real file_text_readln file_text_eof file_text_eoln file_exists file_delete file_rename file_copy directory_exists directory_create directory_destroy file_find_first file_find_next file_find_close file_attributes filename_name filename_path filename_dir filename_drive filename_ext filename_change_ext file_bin_open file_bin_rewrite file_bin_close file_bin_position file_bin_size file_bin_seek file_bin_write_byte file_bin_read_byte parameter_count parameter_string environment_get_variable ini_open_from_string ini_open ini_close ini_read_string ini_read_real ini_write_string ini_write_real ini_key_exists ini_section_exists ini_key_delete ini_section_delete ds_set_precision ds_exists ds_stack_create ds_stack_destroy ds_stack_clear ds_stack_copy ds_stack_size ds_stack_empty ds_stack_push ds_stack_pop ds_stack_top ds_stack_write ds_stack_read ds_queue_create ds_queue_destroy ds_queue_clear ds_queue_copy ds_queue_size ds_queue_empty ds_queue_enqueue ds_queue_dequeue ds_queue_head ds_queue_tail ds_queue_write ds_queue_read ds_list_create ds_list_destroy ds_list_clear ds_list_copy ds_list_size ds_list_empty ds_list_add ds_list_insert ds_list_replace ds_list_delete ds_list_find_index ds_list_find_value ds_list_mark_as_list ds_list_mark_as_map ds_list_sort ds_list_shuffle ds_list_write ds_list_read ds_list_set ds_map_create ds_map_destroy ds_map_clear ds_map_copy ds_map_size ds_map_empty ds_map_add ds_map_add_list ds_map_add_map ds_map_replace ds_map_replace_map ds_map_replace_list ds_map_delete ds_map_exists ds_map_find_value ds_map_find_previous ds_map_find_next ds_map_find_first ds_map_find_last ds_map_write ds_map_read ds_map_secure_save ds_map_secure_load ds_map_secure_load_buffer ds_map_secure_save_buffer ds_map_set ds_priority_create ds_priority_destroy ds_priority_clear ds_priority_copy ds_priority_size ds_priority_empty ds_priority_add ds_priority_change_priority ds_priority_find_priority ds_priority_delete_value ds_priority_delete_min ds_priority_find_min ds_priority_delete_max ds_priority_find_max ds_priority_write ds_priority_read ds_grid_create ds_grid_destroy ds_grid_copy ds_grid_resize ds_grid_width ds_grid_height ds_grid_clear ds_grid_set ds_grid_add ds_grid_multiply ds_grid_set_region ds_grid_add_region ds_grid_multiply_region ds_grid_set_disk ds_grid_add_disk ds_grid_multiply_disk ds_grid_set_grid_region ds_grid_add_grid_region ds_grid_multiply_grid_region ds_grid_get ds_grid_get_sum ds_grid_get_max ds_grid_get_min ds_grid_get_mean ds_grid_get_disk_sum ds_grid_get_disk_min ds_grid_get_disk_max ds_grid_get_disk_mean ds_grid_value_exists ds_grid_value_x ds_grid_value_y ds_grid_value_disk_exists ds_grid_value_disk_x ds_grid_value_disk_y ds_grid_shuffle ds_grid_write ds_grid_read ds_grid_sort ds_grid_set ds_grid_get effect_create_below effect_create_above effect_clear part_type_create part_type_destroy part_type_exists part_type_clear part_type_shape part_type_sprite part_type_size part_type_scale part_type_orientation part_type_life part_type_step part_type_death part_type_speed part_type_direction part_type_gravity part_type_colour1 part_type_colour2 part_type_colour3 part_type_colour_mix part_type_colour_rgb part_type_colour_hsv part_type_color1 part_type_color2 part_type_color3 part_type_color_mix part_type_color_rgb part_type_color_hsv part_type_alpha1 part_type_alpha2 part_type_alpha3 part_type_blend part_system_create part_system_create_layer part_system_destroy part_system_exists part_system_clear part_system_draw_order part_system_depth part_system_position part_system_automatic_update part_system_automatic_draw part_system_update part_system_drawit part_system_get_layer part_system_layer part_particles_create part_particles_create_colour part_particles_create_color part_particles_clear part_particles_count part_emitter_create part_emitter_destroy part_emitter_destroy_all part_emitter_exists part_emitter_clear part_emitter_region part_emitter_burst part_emitter_stream external_call external_define external_free window_handle window_device matrix_get matrix_set matrix_build_identity matrix_build matrix_build_lookat matrix_build_projection_ortho matrix_build_projection_perspective matrix_build_projection_perspective_fov matrix_multiply matrix_transform_vertex matrix_stack_push matrix_stack_pop matrix_stack_multiply matrix_stack_set matrix_stack_clear matrix_stack_top matrix_stack_is_empty browser_input_capture os_get_config os_get_info os_get_language os_get_region os_lock_orientation display_get_dpi_x display_get_dpi_y display_set_gui_size display_set_gui_maximise display_set_gui_maximize device_mouse_dbclick_enable display_set_timing_method display_get_timing_method display_set_sleep_margin display_get_sleep_margin virtual_key_add virtual_key_hide virtual_key_delete virtual_key_show draw_enable_drawevent draw_enable_swf_aa draw_set_swf_aa_level draw_get_swf_aa_level draw_texture_flush draw_flush gpu_set_blendenable gpu_set_ztestenable gpu_set_zfunc gpu_set_zwriteenable gpu_set_lightingenable gpu_set_fog gpu_set_cullmode gpu_set_blendmode gpu_set_blendmode_ext gpu_set_blendmode_ext_sepalpha gpu_set_colorwriteenable gpu_set_colourwriteenable gpu_set_alphatestenable gpu_set_alphatestref gpu_set_alphatestfunc gpu_set_texfilter gpu_set_texfilter_ext gpu_set_texrepeat gpu_set_texrepeat_ext gpu_set_tex_filter gpu_set_tex_filter_ext gpu_set_tex_repeat gpu_set_tex_repeat_ext gpu_set_tex_mip_filter gpu_set_tex_mip_filter_ext gpu_set_tex_mip_bias gpu_set_tex_mip_bias_ext gpu_set_tex_min_mip gpu_set_tex_min_mip_ext gpu_set_tex_max_mip gpu_set_tex_max_mip_ext gpu_set_tex_max_aniso gpu_set_tex_max_aniso_ext gpu_set_tex_mip_enable gpu_set_tex_mip_enable_ext gpu_get_blendenable gpu_get_ztestenable gpu_get_zfunc gpu_get_zwriteenable gpu_get_lightingenable gpu_get_fog gpu_get_cullmode gpu_get_blendmode gpu_get_blendmode_ext gpu_get_blendmode_ext_sepalpha gpu_get_blendmode_src gpu_get_blendmode_dest gpu_get_blendmode_srcalpha gpu_get_blendmode_destalpha gpu_get_colorwriteenable gpu_get_colourwriteenable gpu_get_alphatestenable gpu_get_alphatestref gpu_get_alphatestfunc gpu_get_texfilter gpu_get_texfilter_ext gpu_get_texrepeat gpu_get_texrepeat_ext gpu_get_tex_filter gpu_get_tex_filter_ext gpu_get_tex_repeat gpu_get_tex_repeat_ext gpu_get_tex_mip_filter gpu_get_tex_mip_filter_ext gpu_get_tex_mip_bias gpu_get_tex_mip_bias_ext gpu_get_tex_min_mip gpu_get_tex_min_mip_ext gpu_get_tex_max_mip gpu_get_tex_max_mip_ext gpu_get_tex_max_aniso gpu_get_tex_max_aniso_ext gpu_get_tex_mip_enable gpu_get_tex_mip_enable_ext gpu_push_state gpu_pop_state gpu_get_state gpu_set_state draw_light_define_ambient draw_light_define_direction draw_light_define_point draw_light_enable draw_set_lighting draw_light_get_ambient draw_light_get draw_get_lighting shop_leave_rating url_get_domain url_open url_open_ext url_open_full get_timer achievement_login achievement_logout achievement_post achievement_increment achievement_post_score achievement_available achievement_show_achievements achievement_show_leaderboards achievement_load_friends achievement_load_leaderboard achievement_send_challenge achievement_load_progress achievement_reset achievement_login_status achievement_get_pic achievement_show_challenge_notifications achievement_get_challenges achievement_event achievement_show achievement_get_info cloud_file_save cloud_string_save cloud_synchronise ads_enable ads_disable ads_setup ads_engagement_launch ads_engagement_available ads_engagement_active ads_event ads_event_preload ads_set_reward_callback ads_get_display_height ads_get_display_width ads_move ads_interstitial_available ads_interstitial_display device_get_tilt_x device_get_tilt_y device_get_tilt_z device_is_keypad_open device_mouse_check_button device_mouse_check_button_pressed device_mouse_check_button_released device_mouse_x device_mouse_y device_mouse_raw_x device_mouse_raw_y device_mouse_x_to_gui device_mouse_y_to_gui iap_activate iap_status iap_enumerate_products iap_restore_all iap_acquire iap_consume iap_product_details iap_purchase_details facebook_init facebook_login facebook_status facebook_graph_request facebook_dialog facebook_logout facebook_launch_offerwall facebook_post_message facebook_send_invite facebook_user_id facebook_accesstoken facebook_check_permission facebook_request_read_permissions facebook_request_publish_permissions gamepad_is_supported gamepad_get_device_count gamepad_is_connected gamepad_get_description gamepad_get_button_threshold gamepad_set_button_threshold gamepad_get_axis_deadzone gamepad_set_axis_deadzone gamepad_button_count gamepad_button_check gamepad_button_check_pressed gamepad_button_check_released gamepad_button_value gamepad_axis_count gamepad_axis_value gamepad_set_vibration gamepad_set_colour gamepad_set_color os_is_paused window_has_focus code_is_compiled http_get http_get_file http_post_string http_request json_encode json_decode zip_unzip load_csv base64_encode base64_decode md5_string_unicode md5_string_utf8 md5_file os_is_network_connected sha1_string_unicode sha1_string_utf8 sha1_file os_powersave_enable analytics_event analytics_event_ext win8_livetile_tile_notification win8_livetile_tile_clear win8_livetile_badge_notification win8_livetile_badge_clear win8_livetile_queue_enable win8_secondarytile_pin win8_secondarytile_badge_notification win8_secondarytile_delete win8_livetile_notification_begin win8_livetile_notification_secondary_begin win8_livetile_notification_expiry win8_livetile_notification_tag win8_livetile_notification_text_add win8_livetile_notification_image_add win8_livetile_notification_end win8_appbar_enable win8_appbar_add_element win8_appbar_remove_element win8_settingscharm_add_entry win8_settingscharm_add_html_entry win8_settingscharm_add_xaml_entry win8_settingscharm_set_xaml_property win8_settingscharm_get_xaml_property win8_settingscharm_remove_entry win8_share_image win8_share_screenshot win8_share_file win8_share_url win8_share_text win8_search_enable win8_search_disable win8_search_add_suggestions win8_device_touchscreen_available win8_license_initialize_sandbox win8_license_trial_version winphone_license_trial_version winphone_tile_title winphone_tile_count winphone_tile_back_title winphone_tile_back_content winphone_tile_back_content_wide winphone_tile_front_image winphone_tile_front_image_small winphone_tile_front_image_wide winphone_tile_back_image winphone_tile_back_image_wide winphone_tile_background_colour winphone_tile_background_color winphone_tile_icon_image winphone_tile_small_icon_image winphone_tile_wide_content winphone_tile_cycle_images winphone_tile_small_background_image physics_world_create physics_world_gravity physics_world_update_speed physics_world_update_iterations physics_world_draw_debug physics_pause_enable physics_fixture_create physics_fixture_set_kinematic physics_fixture_set_density physics_fixture_set_awake physics_fixture_set_restitution physics_fixture_set_friction physics_fixture_set_collision_group physics_fixture_set_sensor physics_fixture_set_linear_damping physics_fixture_set_angular_damping physics_fixture_set_circle_shape physics_fixture_set_box_shape physics_fixture_set_edge_shape physics_fixture_set_polygon_shape physics_fixture_set_chain_shape physics_fixture_add_point physics_fixture_bind physics_fixture_bind_ext physics_fixture_delete physics_apply_force physics_apply_impulse physics_apply_angular_impulse physics_apply_local_force physics_apply_local_impulse physics_apply_torque physics_mass_properties physics_draw_debug physics_test_overlap physics_remove_fixture physics_set_friction physics_set_density physics_set_restitution physics_get_friction physics_get_density physics_get_restitution physics_joint_distance_create physics_joint_rope_create physics_joint_revolute_create physics_joint_prismatic_create physics_joint_pulley_create physics_joint_wheel_create physics_joint_weld_create physics_joint_friction_create physics_joint_gear_create physics_joint_enable_motor physics_joint_get_value physics_joint_set_value physics_joint_delete physics_particle_create physics_particle_delete physics_particle_delete_region_circle physics_particle_delete_region_box physics_particle_delete_region_poly physics_particle_set_flags physics_particle_set_category_flags physics_particle_draw physics_particle_draw_ext physics_particle_count physics_particle_get_data physics_particle_get_data_particle physics_particle_group_begin physics_particle_group_circle physics_particle_group_box physics_particle_group_polygon physics_particle_group_add_point physics_particle_group_end physics_particle_group_join physics_particle_group_delete physics_particle_group_count physics_particle_group_get_data physics_particle_group_get_mass physics_particle_group_get_inertia physics_particle_group_get_centre_x physics_particle_group_get_centre_y physics_particle_group_get_vel_x physics_particle_group_get_vel_y physics_particle_group_get_ang_vel physics_particle_group_get_x physics_particle_group_get_y physics_particle_group_get_angle physics_particle_set_group_flags physics_particle_get_group_flags physics_particle_get_max_count physics_particle_get_radius physics_particle_get_density physics_particle_get_damping physics_particle_get_gravity_scale physics_particle_set_max_count physics_particle_set_radius physics_particle_set_density physics_particle_set_damping physics_particle_set_gravity_scale network_create_socket network_create_socket_ext network_create_server network_create_server_raw network_connect network_connect_raw network_send_packet network_send_raw network_send_broadcast network_send_udp network_send_udp_raw network_set_timeout network_set_config network_resolve network_destroy buffer_create buffer_write buffer_read buffer_seek buffer_get_surface buffer_set_surface buffer_delete buffer_exists buffer_get_type buffer_get_alignment buffer_poke buffer_peek buffer_save buffer_save_ext buffer_load buffer_load_ext buffer_load_partial buffer_copy buffer_fill buffer_get_size buffer_tell buffer_resize buffer_md5 buffer_sha1 buffer_base64_encode buffer_base64_decode buffer_base64_decode_ext buffer_sizeof buffer_get_address buffer_create_from_vertex_buffer buffer_create_from_vertex_buffer_ext buffer_copy_from_vertex_buffer buffer_async_group_begin buffer_async_group_option buffer_async_group_end buffer_load_async buffer_save_async gml_release_mode gml_pragma steam_activate_overlay steam_is_overlay_enabled steam_is_overlay_activated steam_get_persona_name steam_initialised steam_is_cloud_enabled_for_app steam_is_cloud_enabled_for_account steam_file_persisted steam_get_quota_total steam_get_quota_free steam_file_write steam_file_write_file steam_file_read steam_file_delete steam_file_exists steam_file_size steam_file_share steam_is_screenshot_requested steam_send_screenshot steam_is_user_logged_on steam_get_user_steam_id steam_user_owns_dlc steam_user_installed_dlc steam_set_achievement steam_get_achievement steam_clear_achievement steam_set_stat_int steam_set_stat_float steam_set_stat_avg_rate steam_get_stat_int steam_get_stat_float steam_get_stat_avg_rate steam_reset_all_stats steam_reset_all_stats_achievements steam_stats_ready steam_create_leaderboard steam_upload_score steam_upload_score_ext steam_download_scores_around_user steam_download_scores steam_download_friends_scores steam_upload_score_buffer steam_upload_score_buffer_ext steam_current_game_language steam_available_languages steam_activate_overlay_browser steam_activate_overlay_user steam_activate_overlay_store steam_get_user_persona_name steam_get_app_id steam_get_user_account_id steam_ugc_download steam_ugc_create_item steam_ugc_start_item_update steam_ugc_set_item_title steam_ugc_set_item_description steam_ugc_set_item_visibility steam_ugc_set_item_tags steam_ugc_set_item_content steam_ugc_set_item_preview steam_ugc_submit_item_update steam_ugc_get_item_update_progress steam_ugc_subscribe_item steam_ugc_unsubscribe_item steam_ugc_num_subscribed_items steam_ugc_get_subscribed_items steam_ugc_get_item_install_info steam_ugc_get_item_update_info steam_ugc_request_item_details steam_ugc_create_query_user steam_ugc_create_query_user_ex steam_ugc_create_query_all steam_ugc_create_query_all_ex steam_ugc_query_set_cloud_filename_filter steam_ugc_query_set_match_any_tag steam_ugc_query_set_search_text steam_ugc_query_set_ranked_by_trend_days steam_ugc_query_add_required_tag steam_ugc_query_add_excluded_tag steam_ugc_query_set_return_long_description steam_ugc_query_set_return_total_only steam_ugc_query_set_allow_cached_response steam_ugc_send_query shader_set shader_get_name shader_reset shader_current shader_is_compiled shader_get_sampler_index shader_get_uniform shader_set_uniform_i shader_set_uniform_i_array shader_set_uniform_f shader_set_uniform_f_array shader_set_uniform_matrix shader_set_uniform_matrix_array shader_enable_corner_id texture_set_stage texture_get_texel_width texture_get_texel_height shaders_are_supported vertex_format_begin vertex_format_end vertex_format_delete vertex_format_add_position vertex_format_add_position_3d vertex_format_add_colour vertex_format_add_color vertex_format_add_normal vertex_format_add_texcoord vertex_format_add_textcoord vertex_format_add_custom vertex_create_buffer vertex_create_buffer_ext vertex_delete_buffer vertex_begin vertex_end vertex_position vertex_position_3d vertex_colour vertex_color vertex_argb vertex_texcoord vertex_normal vertex_float1 vertex_float2 vertex_float3 vertex_float4 vertex_ubyte4 vertex_submit vertex_freeze vertex_get_number vertex_get_buffer_size vertex_create_buffer_from_buffer vertex_create_buffer_from_buffer_ext push_local_notification push_get_first_local_notification push_get_next_local_notification push_cancel_local_notification skeleton_animation_set skeleton_animation_get skeleton_animation_mix skeleton_animation_set_ext skeleton_animation_get_ext skeleton_animation_get_duration skeleton_animation_get_frames skeleton_animation_clear skeleton_skin_set skeleton_skin_get skeleton_attachment_set skeleton_attachment_get skeleton_attachment_create skeleton_collision_draw_set skeleton_bone_data_get skeleton_bone_data_set skeleton_bone_state_get skeleton_bone_state_set skeleton_get_minmax skeleton_get_num_bounds skeleton_get_bounds skeleton_animation_get_frame skeleton_animation_set_frame draw_skeleton draw_skeleton_time draw_skeleton_instance draw_skeleton_collision skeleton_animation_list skeleton_skin_list skeleton_slot_data layer_get_id layer_get_id_at_depth layer_get_depth layer_create layer_destroy layer_destroy_instances layer_add_instance layer_has_instance layer_set_visible layer_get_visible layer_exists layer_x layer_y layer_get_x layer_get_y layer_hspeed layer_vspeed layer_get_hspeed layer_get_vspeed layer_script_begin layer_script_end layer_shader layer_get_script_begin layer_get_script_end layer_get_shader layer_set_target_room layer_get_target_room layer_reset_target_room layer_get_all layer_get_all_elements layer_get_name layer_depth layer_get_element_layer layer_get_element_type layer_element_move layer_force_draw_depth layer_is_draw_depth_forced layer_get_forced_depth layer_background_get_id layer_background_exists layer_background_create layer_background_destroy layer_background_visible layer_background_change layer_background_sprite layer_background_htiled layer_background_vtiled layer_background_stretch layer_background_yscale layer_background_xscale layer_background_blend layer_background_alpha layer_background_index layer_background_speed layer_background_get_visible layer_background_get_sprite layer_background_get_htiled layer_background_get_vtiled layer_background_get_stretch layer_background_get_yscale layer_background_get_xscale layer_background_get_blend layer_background_get_alpha layer_background_get_index layer_background_get_speed layer_sprite_get_id layer_sprite_exists layer_sprite_create layer_sprite_destroy layer_sprite_change layer_sprite_index layer_sprite_speed layer_sprite_xscale layer_sprite_yscale layer_sprite_angle layer_sprite_blend layer_sprite_alpha layer_sprite_x layer_sprite_y layer_sprite_get_sprite layer_sprite_get_index layer_sprite_get_speed layer_sprite_get_xscale layer_sprite_get_yscale layer_sprite_get_angle layer_sprite_get_blend layer_sprite_get_alpha layer_sprite_get_x layer_sprite_get_y layer_tilemap_get_id layer_tilemap_exists layer_tilemap_create layer_tilemap_destroy tilemap_tileset tilemap_x tilemap_y tilemap_set tilemap_set_at_pixel tilemap_get_tileset tilemap_get_tile_width tilemap_get_tile_height tilemap_get_width tilemap_get_height tilemap_get_x tilemap_get_y tilemap_get tilemap_get_at_pixel tilemap_get_cell_x_at_pixel tilemap_get_cell_y_at_pixel tilemap_clear draw_tilemap draw_tile tilemap_set_global_mask tilemap_get_global_mask tilemap_set_mask tilemap_get_mask tilemap_get_frame tile_set_empty tile_set_index tile_set_flip tile_set_mirror tile_set_rotate tile_get_empty tile_get_index tile_get_flip tile_get_mirror tile_get_rotate layer_tile_exists layer_tile_create layer_tile_destroy layer_tile_change layer_tile_xscale layer_tile_yscale layer_tile_blend layer_tile_alpha layer_tile_x layer_tile_y layer_tile_region layer_tile_visible layer_tile_get_sprite layer_tile_get_xscale layer_tile_get_yscale layer_tile_get_blend layer_tile_get_alpha layer_tile_get_x layer_tile_get_y layer_tile_get_region layer_tile_get_visible layer_instance_get_instance instance_activate_layer instance_deactivate_layer camera_create camera_create_view camera_destroy camera_apply camera_get_active camera_get_default camera_set_default camera_set_view_mat camera_set_proj_mat camera_set_update_script camera_set_begin_script camera_set_end_script camera_set_view_pos camera_set_view_size camera_set_view_speed camera_set_view_border camera_set_view_angle camera_set_view_target camera_get_view_mat camera_get_proj_mat camera_get_update_script camera_get_begin_script camera_get_end_script camera_get_view_x camera_get_view_y camera_get_view_width camera_get_view_height camera_get_view_speed_x camera_get_view_speed_y camera_get_view_border_x camera_get_view_border_y camera_get_view_angle camera_get_view_target view_get_camera view_get_visible view_get_xport view_get_yport view_get_wport view_get_hport view_get_surface_id view_set_camera view_set_visible view_set_xport view_set_yport view_set_wport view_set_hport view_set_surface_id gesture_drag_time gesture_drag_distance gesture_flick_speed gesture_double_tap_time gesture_double_tap_distance gesture_pinch_distance gesture_pinch_angle_towards gesture_pinch_angle_away gesture_rotate_time gesture_rotate_angle gesture_tap_count gesture_get_drag_time gesture_get_drag_distance gesture_get_flick_speed gesture_get_double_tap_time gesture_get_double_tap_distance gesture_get_pinch_distance gesture_get_pinch_angle_towards gesture_get_pinch_angle_away gesture_get_rotate_time gesture_get_rotate_angle gesture_get_tap_count keyboard_virtual_show keyboard_virtual_hide keyboard_virtual_status keyboard_virtual_height",
+literal:"self other all noone global local undefined pointer_invalid pointer_null path_action_stop path_action_restart path_action_continue path_action_reverse true false pi GM_build_date GM_version GM_runtime_version  timezone_local timezone_utc gamespeed_fps gamespeed_microseconds  ev_create ev_destroy ev_step ev_alarm ev_keyboard ev_mouse ev_collision ev_other ev_draw ev_draw_begin ev_draw_end ev_draw_pre ev_draw_post ev_keypress ev_keyrelease ev_trigger ev_left_button ev_right_button ev_middle_button ev_no_button ev_left_press ev_right_press ev_middle_press ev_left_release ev_right_release ev_middle_release ev_mouse_enter ev_mouse_leave ev_mouse_wheel_up ev_mouse_wheel_down ev_global_left_button ev_global_right_button ev_global_middle_button ev_global_left_press ev_global_right_press ev_global_middle_press ev_global_left_release ev_global_right_release ev_global_middle_release ev_joystick1_left ev_joystick1_right ev_joystick1_up ev_joystick1_down ev_joystick1_button1 ev_joystick1_button2 ev_joystick1_button3 ev_joystick1_button4 ev_joystick1_button5 ev_joystick1_button6 ev_joystick1_button7 ev_joystick1_button8 ev_joystick2_left ev_joystick2_right ev_joystick2_up ev_joystick2_down ev_joystick2_button1 ev_joystick2_button2 ev_joystick2_button3 ev_joystick2_button4 ev_joystick2_button5 ev_joystick2_button6 ev_joystick2_button7 ev_joystick2_button8 ev_outside ev_boundary ev_game_start ev_game_end ev_room_start ev_room_end ev_no_more_lives ev_animation_end ev_end_of_path ev_no_more_health ev_close_button ev_user0 ev_user1 ev_user2 ev_user3 ev_user4 ev_user5 ev_user6 ev_user7 ev_user8 ev_user9 ev_user10 ev_user11 ev_user12 ev_user13 ev_user14 ev_user15 ev_step_normal ev_step_begin ev_step_end ev_gui ev_gui_begin ev_gui_end ev_cleanup ev_gesture ev_gesture_tap ev_gesture_double_tap ev_gesture_drag_start ev_gesture_dragging ev_gesture_drag_end ev_gesture_flick ev_gesture_pinch_start ev_gesture_pinch_in ev_gesture_pinch_out ev_gesture_pinch_end ev_gesture_rotate_start ev_gesture_rotating ev_gesture_rotate_end ev_global_gesture_tap ev_global_gesture_double_tap ev_global_gesture_drag_start ev_global_gesture_dragging ev_global_gesture_drag_end ev_global_gesture_flick ev_global_gesture_pinch_start ev_global_gesture_pinch_in ev_global_gesture_pinch_out ev_global_gesture_pinch_end ev_global_gesture_rotate_start ev_global_gesture_rotating ev_global_gesture_rotate_end vk_nokey vk_anykey vk_enter vk_return vk_shift vk_control vk_alt vk_escape vk_space vk_backspace vk_tab vk_pause vk_printscreen vk_left vk_right vk_up vk_down vk_home vk_end vk_delete vk_insert vk_pageup vk_pagedown vk_f1 vk_f2 vk_f3 vk_f4 vk_f5 vk_f6 vk_f7 vk_f8 vk_f9 vk_f10 vk_f11 vk_f12 vk_numpad0 vk_numpad1 vk_numpad2 vk_numpad3 vk_numpad4 vk_numpad5 vk_numpad6 vk_numpad7 vk_numpad8 vk_numpad9 vk_divide vk_multiply vk_subtract vk_add vk_decimal vk_lshift vk_lcontrol vk_lalt vk_rshift vk_rcontrol vk_ralt  mb_any mb_none mb_left mb_right mb_middle c_aqua c_black c_blue c_dkgray c_fuchsia c_gray c_green c_lime c_ltgray c_maroon c_navy c_olive c_purple c_red c_silver c_teal c_white c_yellow c_orange fa_left fa_center fa_right fa_top fa_middle fa_bottom pr_pointlist pr_linelist pr_linestrip pr_trianglelist pr_trianglestrip pr_trianglefan bm_complex bm_normal bm_add bm_max bm_subtract bm_zero bm_one bm_src_colour bm_inv_src_colour bm_src_color bm_inv_src_color bm_src_alpha bm_inv_src_alpha bm_dest_alpha bm_inv_dest_alpha bm_dest_colour bm_inv_dest_colour bm_dest_color bm_inv_dest_color bm_src_alpha_sat tf_point tf_linear tf_anisotropic mip_off mip_on mip_markedonly audio_falloff_none audio_falloff_inverse_distance audio_falloff_inverse_distance_clamped audio_falloff_linear_distance audio_falloff_linear_distance_clamped audio_falloff_exponent_distance audio_falloff_exponent_distance_clamped audio_old_system audio_new_system audio_mono audio_stereo audio_3d cr_default cr_none cr_arrow cr_cross cr_beam cr_size_nesw cr_size_ns cr_size_nwse cr_size_we cr_uparrow cr_hourglass cr_drag cr_appstart cr_handpoint cr_size_all spritespeed_framespersecond spritespeed_framespergameframe asset_object asset_unknown asset_sprite asset_sound asset_room asset_path asset_script asset_font asset_timeline asset_tiles asset_shader fa_readonly fa_hidden fa_sysfile fa_volumeid fa_directory fa_archive  ds_type_map ds_type_list ds_type_stack ds_type_queue ds_type_grid ds_type_priority ef_explosion ef_ring ef_ellipse ef_firework ef_smoke ef_smokeup ef_star ef_spark ef_flare ef_cloud ef_rain ef_snow pt_shape_pixel pt_shape_disk pt_shape_square pt_shape_line pt_shape_star pt_shape_circle pt_shape_ring pt_shape_sphere pt_shape_flare pt_shape_spark pt_shape_explosion pt_shape_cloud pt_shape_smoke pt_shape_snow ps_distr_linear ps_distr_gaussian ps_distr_invgaussian ps_shape_rectangle ps_shape_ellipse ps_shape_diamond ps_shape_line ty_real ty_string dll_cdecl dll_stdcall matrix_view matrix_projection matrix_world os_win32 os_windows os_macosx os_ios os_android os_symbian os_linux os_unknown os_winphone os_tizen os_win8native os_wiiu os_3ds  os_psvita os_bb10 os_ps4 os_xboxone os_ps3 os_xbox360 os_uwp os_tvos os_switch browser_not_a_browser browser_unknown browser_ie browser_firefox browser_chrome browser_safari browser_safari_mobile browser_opera browser_tizen browser_edge browser_windows_store browser_ie_mobile  device_ios_unknown device_ios_iphone device_ios_iphone_retina device_ios_ipad device_ios_ipad_retina device_ios_iphone5 device_ios_iphone6 device_ios_iphone6plus device_emulator device_tablet display_landscape display_landscape_flipped display_portrait display_portrait_flipped tm_sleep tm_countvsyncs of_challenge_win of_challen ge_lose of_challenge_tie leaderboard_type_number leaderboard_type_time_mins_secs cmpfunc_never cmpfunc_less cmpfunc_equal cmpfunc_lessequal cmpfunc_greater cmpfunc_notequal cmpfunc_greaterequal cmpfunc_always cull_noculling cull_clockwise cull_counterclockwise lighttype_dir lighttype_point iap_ev_storeload iap_ev_product iap_ev_purchase iap_ev_consume iap_ev_restore iap_storeload_ok iap_storeload_failed iap_status_uninitialised iap_status_unavailable iap_status_loading iap_status_available iap_status_processing iap_status_restoring iap_failed iap_unavailable iap_available iap_purchased iap_canceled iap_refunded fb_login_default fb_login_fallback_to_webview fb_login_no_fallback_to_webview fb_login_forcing_webview fb_login_use_system_account fb_login_forcing_safari  phy_joint_anchor_1_x phy_joint_anchor_1_y phy_joint_anchor_2_x phy_joint_anchor_2_y phy_joint_reaction_force_x phy_joint_reaction_force_y phy_joint_reaction_torque phy_joint_motor_speed phy_joint_angle phy_joint_motor_torque phy_joint_max_motor_torque phy_joint_translation phy_joint_speed phy_joint_motor_force phy_joint_max_motor_force phy_joint_length_1 phy_joint_length_2 phy_joint_damping_ratio phy_joint_frequency phy_joint_lower_angle_limit phy_joint_upper_angle_limit phy_joint_angle_limits phy_joint_max_length phy_joint_max_torque phy_joint_max_force phy_debug_render_aabb phy_debug_render_collision_pairs phy_debug_render_coms phy_debug_render_core_shapes phy_debug_render_joints phy_debug_render_obb phy_debug_render_shapes  phy_particle_flag_water phy_particle_flag_zombie phy_particle_flag_wall phy_particle_flag_spring phy_particle_flag_elastic phy_particle_flag_viscous phy_particle_flag_powder phy_particle_flag_tensile phy_particle_flag_colourmixing phy_particle_flag_colormixing phy_particle_group_flag_solid phy_particle_group_flag_rigid phy_particle_data_flag_typeflags phy_particle_data_flag_position phy_particle_data_flag_velocity phy_particle_data_flag_colour phy_particle_data_flag_color phy_particle_data_flag_category  achievement_our_info achievement_friends_info achievement_leaderboard_info achievement_achievement_info achievement_filter_all_players achievement_filter_friends_only achievement_filter_favorites_only achievement_type_achievement_challenge achievement_type_score_challenge achievement_pic_loaded  achievement_show_ui achievement_show_profile achievement_show_leaderboard achievement_show_achievement achievement_show_bank achievement_show_friend_picker achievement_show_purchase_prompt network_socket_tcp network_socket_udp network_socket_bluetooth network_type_connect network_type_disconnect network_type_data network_type_non_blocking_connect network_config_connect_timeout network_config_use_non_blocking_socket network_config_enable_reliable_udp network_config_disable_reliable_udp buffer_fixed buffer_grow buffer_wrap buffer_fast buffer_vbuffer buffer_network buffer_u8 buffer_s8 buffer_u16 buffer_s16 buffer_u32 buffer_s32 buffer_u64 buffer_f16 buffer_f32 buffer_f64 buffer_bool buffer_text buffer_string buffer_surface_copy buffer_seek_start buffer_seek_relative buffer_seek_end buffer_generalerror buffer_outofspace buffer_outofbounds buffer_invalidtype  text_type button_type input_type ANSI_CHARSET DEFAULT_CHARSET EASTEUROPE_CHARSET RUSSIAN_CHARSET SYMBOL_CHARSET SHIFTJIS_CHARSET HANGEUL_CHARSET GB2312_CHARSET CHINESEBIG5_CHARSET JOHAB_CHARSET HEBREW_CHARSET ARABIC_CHARSET GREEK_CHARSET TURKISH_CHARSET VIETNAMESE_CHARSET THAI_CHARSET MAC_CHARSET BALTIC_CHARSET OEM_CHARSET  gp_face1 gp_face2 gp_face3 gp_face4 gp_shoulderl gp_shoulderr gp_shoulderlb gp_shoulderrb gp_select gp_start gp_stickl gp_stickr gp_padu gp_padd gp_padl gp_padr gp_axislh gp_axislv gp_axisrh gp_axisrv ov_friends ov_community ov_players ov_settings ov_gamegroup ov_achievements lb_sort_none lb_sort_ascending lb_sort_descending lb_disp_none lb_disp_numeric lb_disp_time_sec lb_disp_time_ms ugc_result_success ugc_filetype_community ugc_filetype_microtrans ugc_visibility_public ugc_visibility_friends_only ugc_visibility_private ugc_query_RankedByVote ugc_query_RankedByPublicationDate ugc_query_AcceptedForGameRankedByAcceptanceDate ugc_query_RankedByTrend ugc_query_FavoritedByFriendsRankedByPublicationDate ugc_query_CreatedByFriendsRankedByPublicationDate ugc_query_RankedByNumTimesReported ugc_query_CreatedByFollowedUsersRankedByPublicationDate ugc_query_NotYetRated ugc_query_RankedByTotalVotesAsc ugc_query_RankedByVotesUp ugc_query_RankedByTextSearch ugc_sortorder_CreationOrderDesc ugc_sortorder_CreationOrderAsc ugc_sortorder_TitleAsc ugc_sortorder_LastUpdatedDesc ugc_sortorder_SubscriptionDateDesc ugc_sortorder_VoteScoreDesc ugc_sortorder_ForModeration ugc_list_Published ugc_list_VotedOn ugc_list_VotedUp ugc_list_VotedDown ugc_list_WillVoteLater ugc_list_Favorited ugc_list_Subscribed ugc_list_UsedOrPlayed ugc_list_Followed ugc_match_Items ugc_match_Items_Mtx ugc_match_Items_ReadyToUse ugc_match_Collections ugc_match_Artwork ugc_match_Videos ugc_match_Screenshots ugc_match_AllGuides ugc_match_WebGuides ugc_match_IntegratedGuides ugc_match_UsableInGame ugc_match_ControllerBindings  vertex_usage_position vertex_usage_colour vertex_usage_color vertex_usage_normal vertex_usage_texcoord vertex_usage_textcoord vertex_usage_blendweight vertex_usage_blendindices vertex_usage_psize vertex_usage_tangent vertex_usage_binormal vertex_usage_fog vertex_usage_depth vertex_usage_sample vertex_type_float1 vertex_type_float2 vertex_type_float3 vertex_type_float4 vertex_type_colour vertex_type_color vertex_type_ubyte4 layerelementtype_undefined layerelementtype_background layerelementtype_instance layerelementtype_oldtilemap layerelementtype_sprite layerelementtype_tilemap layerelementtype_particlesystem layerelementtype_tile tile_rotate tile_flip tile_mirror tile_index_mask kbv_type_default kbv_type_ascii kbv_type_url kbv_type_email kbv_type_numbers kbv_type_phone kbv_type_phone_name kbv_returnkey_default kbv_returnkey_go kbv_returnkey_google kbv_returnkey_join kbv_returnkey_next kbv_returnkey_route kbv_returnkey_search kbv_returnkey_send kbv_returnkey_yahoo kbv_returnkey_done kbv_returnkey_continue kbv_returnkey_emergency kbv_autocapitalize_none kbv_autocapitalize_words kbv_autocapitalize_sentences kbv_autocapitalize_characters",
+symbol:"argument_relative argument argument0 argument1 argument2 argument3 argument4 argument5 argument6 argument7 argument8 argument9 argument10 argument11 argument12 argument13 argument14 argument15 argument_count x|0 y|0 xprevious yprevious xstart ystart hspeed vspeed direction speed friction gravity gravity_direction path_index path_position path_positionprevious path_speed path_scale path_orientation path_endaction object_index id solid persistent mask_index instance_count instance_id room_speed fps fps_real current_time current_year current_month current_day current_weekday current_hour current_minute current_second alarm timeline_index timeline_position timeline_speed timeline_running timeline_loop room room_first room_last room_width room_height room_caption room_persistent score lives health show_score show_lives show_health caption_score caption_lives caption_health event_type event_number event_object event_action application_surface gamemaker_pro gamemaker_registered gamemaker_version error_occurred error_last debug_mode keyboard_key keyboard_lastkey keyboard_lastchar keyboard_string mouse_x mouse_y mouse_button mouse_lastbutton cursor_sprite visible sprite_index sprite_width sprite_height sprite_xoffset sprite_yoffset image_number image_index image_speed depth image_xscale image_yscale image_angle image_alpha image_blend bbox_left bbox_right bbox_top bbox_bottom layer background_colour  background_showcolour background_color background_showcolor view_enabled view_current view_visible view_xview view_yview view_wview view_hview view_xport view_yport view_wport view_hport view_angle view_hborder view_vborder view_hspeed view_vspeed view_object view_surface_id view_camera game_id game_display_name game_project_name game_save_id working_directory temp_directory program_directory browser_width browser_height os_type os_device os_browser os_version display_aa async_load delta_time webgl_enabled event_data iap_data phy_rotation phy_position_x phy_position_y phy_angular_velocity phy_linear_velocity_x phy_linear_velocity_y phy_speed_x phy_speed_y phy_speed phy_angular_damping phy_linear_damping phy_bullet phy_fixed_rotation phy_active phy_mass phy_inertia phy_com_x phy_com_y phy_dynamic phy_kinematic phy_sleeping phy_collision_points phy_collision_x phy_collision_y phy_col_normal_x phy_col_normal_y phy_position_xprevious phy_position_yprevious"
+},
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE]
+})})());
+hljs.registerLanguage("go",(()=>{"use strict";return e=>{const n={
+keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",
+literal:"true false iota nil",
+built_in:"append cap close complex copy imag len make new panic print println real recover delete"
+};return{name:"Go",aliases:["golang"],keywords:n,illegal:"</",
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"string",
+variants:[e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{begin:"`",end:"`"}]},{
+className:"number",variants:[{begin:e.C_NUMBER_RE+"[i]",relevance:1
+},e.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",
+end:"\\s*(\\{|$)",excludeEnd:!0,contains:[e.TITLE_MODE,{className:"params",
+begin:/\(/,end:/\)/,keywords:n,illegal:/["']/}]}]}}})());
+hljs.registerLanguage("golo",(()=>{"use strict";return e=>({name:"Golo",
+keywords:{
+keyword:"println readln print import module function local return let var while for foreach times in case when match with break continue augment augmentation each find filter reduce if then else otherwise try catch finally raise throw orIfNull DynamicObject|10 DynamicVariable struct Observable map set vector list array",
+literal:"true false null"},
+contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,{
+className:"meta",begin:"@[A-Za-z]+"}]})})());
+hljs.registerLanguage("gradle",(()=>{"use strict";return e=>({name:"Gradle",
+case_insensitive:!0,keywords:{
+keyword:"task project allprojects subprojects artifacts buildscript configurations dependencies repositories sourceSets description delete from into include exclude source classpath destinationDir includes options sourceCompatibility targetCompatibility group flatDir doLast doFirst flatten todir fromdir ant def abstract break case catch continue default do else extends final finally for if implements instanceof native new private protected public return static switch synchronized throw throws transient try volatile while strictfp package import false null super this true antlrtask checkstyle codenarc copy boolean byte char class double float int interface long short void compile runTime file fileTree abs any append asList asWritable call collect compareTo count div dump each eachByte eachFile eachLine every find findAll flatten getAt getErr getIn getOut getText grep immutable inject inspect intersect invokeMethods isCase join leftShift minus multiply newInputStream newOutputStream newPrintWriter newReader newWriter next plus pop power previous print println push putAt read readBytes readLines reverse reverseEach round size sort splitEachLine step subMap times toInteger toList tokenize upto waitForOrKill withPrintWriter withReader withStream withWriter withWriterAppend write writeLine"
+},
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,e.REGEXP_MODE]
+})})());
+hljs.registerLanguage("groovy",(()=>{"use strict";function e(e,n={}){
+return n.variants=e,n}return n=>{
+const a="[A-Za-z0-9_$]+",t=e([n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,n.COMMENT("/\\*\\*","\\*/",{
+relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",
+begin:"@[A-Za-z]+"}]})]),s={className:"regexp",begin:/~?\/[^\/\n]+\//,
+contains:[n.BACKSLASH_ESCAPE]
+},i=e([n.BINARY_NUMBER_MODE,n.C_NUMBER_MODE]),r=e([{begin:/"""/,end:/"""/},{
+begin:/'''/,end:/'''/},{begin:"\\$/",end:"/\\$",relevance:10
+},n.APOS_STRING_MODE,n.QUOTE_STRING_MODE],{className:"string"});return{
+name:"Groovy",keywords:{built_in:"this super",literal:"true false null",
+keyword:"byte short char int long boolean float double void def as in assert trait abstract static volatile transient public private protected synchronized final class interface enum if else for while switch case break default continue throw throws try catch finally implements extends new import package return instanceof"
+},contains:[n.SHEBANG({binary:"groovy",relevance:10}),t,r,s,i,{
+className:"class",beginKeywords:"class interface trait enum",end:/\{/,
+illegal:":",contains:[{beginKeywords:"extends implements"
+},n.UNDERSCORE_TITLE_MODE]},{className:"meta",begin:"@[A-Za-z]+",relevance:0},{
+className:"attr",begin:a+"[ \t]*:",relevance:0},{begin:/\?/,end:/:/,relevance:0,
+contains:[t,r,s,i,"self"]},{className:"symbol",
+begin:"^[ \t]*"+(l=a+":",((...e)=>e.map((e=>(e=>e?"string"==typeof e?e:e.source:null)(e))).join(""))("(?=",l,")")),
+excludeBegin:!0,end:a+":",relevance:0}],illegal:/#|<\//};var l}})());
+hljs.registerLanguage("haml",(()=>{"use strict";return e=>({name:"HAML",
+case_insensitive:!0,contains:[{className:"meta",
+begin:"^!!!( (5|1\\.1|Strict|Frameset|Basic|Mobile|RDFa|XML\\b.*))?$",
+relevance:10},e.COMMENT("^\\s*(!=#|=#|-#|/).*$",!1,{relevance:0}),{
+begin:"^\\s*(-|=|!=)(?!#)",starts:{end:"\\n",subLanguage:"ruby"}},{
+className:"tag",begin:"^\\s*%",contains:[{className:"selector-tag",begin:"\\w+"
+},{className:"selector-id",begin:"#[\\w-]+"},{className:"selector-class",
+begin:"\\.[\\w-]+"},{begin:/\{\s*/,end:/\s*\}/,contains:[{begin:":\\w+\\s*=>",
+end:",\\s+",returnBegin:!0,endsWithParent:!0,contains:[{className:"attr",
+begin:":\\w+"},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{begin:"\\w+",relevance:0
+}]}]},{begin:"\\(\\s*",end:"\\s*\\)",excludeEnd:!0,contains:[{begin:"\\w+\\s*=",
+end:"\\s+",returnBegin:!0,endsWithParent:!0,contains:[{className:"attr",
+begin:"\\w+",relevance:0},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{begin:"\\w+",
+relevance:0}]}]}]},{begin:"^\\s*[=~]\\s*"},{begin:/#\{/,starts:{end:/\}/,
+subLanguage:"ruby"}}]})})());
+hljs.registerLanguage("handlebars",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function n(...n){
+return n.map((n=>e(n))).join("")}return a=>{const t={
+"builtin-name":["action","bindattr","collection","component","concat","debugger","each","each-in","get","hash","if","in","input","link-to","loc","log","lookup","mut","outlet","partial","query-params","render","template","textarea","unbound","unless","view","with","yield"]
+},s=/\[\]|\[[^\]]+\]/,i=/[^\s!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]+/,r=((...n)=>"("+n.map((n=>e(n))).join("|")+")")(/""|"[^"]+"/,/''|'[^']+'/,s,i),l=n(n("(",/\.|\.\/|\//,")?"),r,(h=n(/(\.|\/)/,r),
+n("(",h,")*"))),c=n("(",s,"|",i,")(?==)"),o={begin:l,lexemes:/[\w.\/]+/
+},m=a.inherit(o,{keywords:{literal:["true","false","undefined","null"]}}),d={
+begin:/\(/,end:/\)/},g={className:"attr",begin:c,relevance:0,starts:{begin:/=/,
+end:/=/,starts:{
+contains:[a.NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,m,d]}}},b={
+contains:[a.NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{begin:/as\s+\|/,
+keywords:{keyword:"as"},end:/\|/,contains:[{begin:/\w+/}]},g,m,d],returnEnd:!0
+},u=a.inherit(o,{className:"name",keywords:t,starts:a.inherit(b,{end:/\)/})})
+;var h;d.contains=[u];const N=a.inherit(o,{keywords:t,className:"name",
+starts:a.inherit(b,{end:/\}\}/})}),p=a.inherit(o,{keywords:t,className:"name"
+}),E=a.inherit(o,{className:"name",keywords:t,starts:a.inherit(b,{end:/\}\}/})})
+;return{name:"Handlebars",
+aliases:["hbs","html.hbs","html.handlebars","htmlbars"],case_insensitive:!0,
+subLanguage:"xml",contains:[{begin:/\\\{\{/,skip:!0},{begin:/\\\\(?=\{\{)/,
+skip:!0},a.COMMENT(/\{\{!--/,/--\}\}/),a.COMMENT(/\{\{!/,/\}\}/),{
+className:"template-tag",begin:/\{\{\{\{(?!\/)/,end:/\}\}\}\}/,contains:[N],
+starts:{end:/\{\{\{\{\//,returnEnd:!0,subLanguage:"xml"}},{
+className:"template-tag",begin:/\{\{\{\{\//,end:/\}\}\}\}/,contains:[p]},{
+className:"template-tag",begin:/\{\{#/,end:/\}\}/,contains:[N]},{
+className:"template-tag",begin:/\{\{(?=else\}\})/,end:/\}\}/,keywords:"else"},{
+className:"template-tag",begin:/\{\{(?=else if)/,end:/\}\}/,keywords:"else if"
+},{className:"template-tag",begin:/\{\{\//,end:/\}\}/,contains:[p]},{
+className:"template-variable",begin:/\{\{\{/,end:/\}\}\}/,contains:[E]},{
+className:"template-variable",begin:/\{\{/,end:/\}\}/,contains:[E]}]}}})());
+hljs.registerLanguage("haskell",(()=>{"use strict";return e=>{const n={
+variants:[e.COMMENT("--","$"),e.COMMENT(/\{-/,/-\}/,{contains:["self"]})]},i={
+className:"meta",begin:/\{-#/,end:/#-\}/},a={className:"meta",begin:"^#",end:"$"
+},s={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},l={begin:"\\(",
+end:"\\)",illegal:'"',contains:[i,a,{className:"type",
+begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},e.inherit(e.TITLE_MODE,{
+begin:"[_a-z][\\w']*"}),n]};return{name:"Haskell",aliases:["hs"],
+keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",
+contains:[{beginKeywords:"module",end:"where",keywords:"module where",
+contains:[l,n],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",
+keywords:"import qualified as hiding",contains:[l,n],illegal:"\\W\\.|;"},{
+className:"class",begin:"^(\\s*)?(class|instance)\\b",end:"where",
+keywords:"class family instance where",contains:[s,l,n]},{className:"class",
+begin:"\\b(data|(new)?type)\\b",end:"$",
+keywords:"data family type newtype deriving",contains:[i,s,l,{begin:/\{/,
+end:/\}/,contains:l.contains},n]},{beginKeywords:"default",end:"$",
+contains:[s,l,n]},{beginKeywords:"infix infixl infixr",end:"$",
+contains:[e.C_NUMBER_MODE,n]},{begin:"\\bforeign\\b",end:"$",
+keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",
+contains:[s,e.QUOTE_STRING_MODE,n]},{className:"meta",
+begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"
+},i,a,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,s,e.inherit(e.TITLE_MODE,{
+begin:"^[_a-z][\\w']*"}),n,{begin:"->|<-"}]}}})());
+hljs.registerLanguage("haxe",(()=>{"use strict";return e=>({name:"Haxe",
+aliases:["hx"],keywords:{
+keyword:"break case cast catch continue default do dynamic else enum extern for function here if import in inline never new override package private get set public return static super switch this throw trace try typedef untyped using var while Int Float String Bool Dynamic Void Array ",
+built_in:"trace this",literal:"true false null _"},contains:[{
+className:"string",begin:"'",end:"'",contains:[e.BACKSLASH_ESCAPE,{
+className:"subst",begin:"\\$\\{",end:"\\}"},{className:"subst",begin:"\\$",
+end:/\W\}/}]
+},e.QUOTE_STRING_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.C_NUMBER_MODE,{
+className:"meta",begin:"@:",end:"$"},{className:"meta",begin:"#",end:"$",
+keywords:{"meta-keyword":"if else elseif end error"}},{className:"type",
+begin:":[ \t]*",end:"[^A-Za-z0-9_ \t\\->]",excludeBegin:!0,excludeEnd:!0,
+relevance:0},{className:"type",begin:":[ \t]*",end:"\\W",excludeBegin:!0,
+excludeEnd:!0},{className:"type",begin:"new *",end:"\\W",excludeBegin:!0,
+excludeEnd:!0},{className:"class",beginKeywords:"enum",end:"\\{",
+contains:[e.TITLE_MODE]},{className:"class",beginKeywords:"abstract",
+end:"[\\{$]",contains:[{className:"type",begin:"\\(",end:"\\)",excludeBegin:!0,
+excludeEnd:!0},{className:"type",begin:"from +",end:"\\W",excludeBegin:!0,
+excludeEnd:!0},{className:"type",begin:"to +",end:"\\W",excludeBegin:!0,
+excludeEnd:!0},e.TITLE_MODE],keywords:{keyword:"abstract from to"}},{
+className:"class",begin:"\\b(class|interface) +",end:"[\\{$]",excludeEnd:!0,
+keywords:"class interface",contains:[{className:"keyword",
+begin:"\\b(extends|implements) +",keywords:"extends implements",contains:[{
+className:"type",begin:e.IDENT_RE,relevance:0}]},e.TITLE_MODE]},{
+className:"function",beginKeywords:"function",end:"\\(",excludeEnd:!0,
+illegal:"\\S",contains:[e.TITLE_MODE]}],illegal:/<\//})})());
+hljs.registerLanguage("hsp",(()=>{"use strict";return e=>({name:"HSP",
+case_insensitive:!0,keywords:{$pattern:/[\w._]+/,
+keyword:"goto gosub return break repeat loop continue wait await dim sdim foreach dimtype dup dupptr end stop newmod delmod mref run exgoto on mcall assert logmes newlab resume yield onexit onerror onkey onclick oncmd exist delete mkdir chdir dirlist bload bsave bcopy memfile if else poke wpoke lpoke getstr chdpm memexpand memcpy memset notesel noteadd notedel noteload notesave randomize noteunsel noteget split strrep setease button chgdisp exec dialog mmload mmplay mmstop mci pset pget syscolor mes print title pos circle cls font sysfont objsize picload color palcolor palette redraw width gsel gcopy gzoom gmode bmpsave hsvcolor getkey listbox chkbox combox input mesbox buffer screen bgscr mouse objsel groll line clrobj boxf objprm objmode stick grect grotate gsquare gradf objimage objskip objenable celload celdiv celput newcom querycom delcom cnvstow comres axobj winobj sendmsg comevent comevarg sarrayconv callfunc cnvwtos comevdisp libptr system hspstat hspver stat cnt err strsize looplev sublev iparam wparam lparam refstr refdval int rnd strlen length length2 length3 length4 vartype gettime peek wpeek lpeek varptr varuse noteinfo instr abs limit getease str strmid strf getpath strtrim sin cos tan atan sqrt double absf expf logf limitf powf geteasef mousex mousey mousew hwnd hinstance hdc ginfo objinfo dirinfo sysinfo thismod __hspver__ __hsp30__ __date__ __time__ __line__ __file__ _debug __hspdef__ and or xor not screen_normal screen_palette screen_hide screen_fixedsize screen_tool screen_frame gmode_gdi gmode_mem gmode_rgb0 gmode_alpha gmode_rgb0alpha gmode_add gmode_sub gmode_pixela ginfo_mx ginfo_my ginfo_act ginfo_sel ginfo_wx1 ginfo_wy1 ginfo_wx2 ginfo_wy2 ginfo_vx ginfo_vy ginfo_sizex ginfo_sizey ginfo_winx ginfo_winy ginfo_mesx ginfo_mesy ginfo_r ginfo_g ginfo_b ginfo_paluse ginfo_dispx ginfo_dispy ginfo_cx ginfo_cy ginfo_intid ginfo_newid ginfo_sx ginfo_sy objinfo_mode objinfo_bmscr objinfo_hwnd notemax notesize dir_cur dir_exe dir_win dir_sys dir_cmdline dir_desktop dir_mydoc dir_tv font_normal font_bold font_italic font_underline font_strikeout font_antialias objmode_normal objmode_guifont objmode_usefont gsquare_grad msgothic msmincho do until while wend for next _break _continue switch case default swbreak swend ddim ldim alloc m_pi rad2deg deg2rad ease_linear ease_quad_in ease_quad_out ease_quad_inout ease_cubic_in ease_cubic_out ease_cubic_inout ease_quartic_in ease_quartic_out ease_quartic_inout ease_bounce_in ease_bounce_out ease_bounce_inout ease_shake_in ease_shake_out ease_shake_inout ease_loop"
+},
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{
+className:"string",begin:/\{"/,end:/"\}/,contains:[e.BACKSLASH_ESCAPE]
+},e.COMMENT(";","$",{relevance:0}),{className:"meta",begin:"#",end:"$",
+keywords:{
+"meta-keyword":"addion cfunc cmd cmpopt comfunc const defcfunc deffunc define else endif enum epack func global if ifdef ifndef include modcfunc modfunc modinit modterm module pack packopt regcmd runtime undef usecom uselib"
+},contains:[e.inherit(e.QUOTE_STRING_MODE,{className:"meta-string"
+}),e.NUMBER_MODE,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]
+},{className:"symbol",begin:"^\\*(\\w+|@)"},e.NUMBER_MODE,e.C_NUMBER_MODE]})
+})());
+hljs.registerLanguage("htmlbars",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function n(...n){
+return n.map((n=>e(n))).join("")}return a=>{const t=function(a){const t={
+"builtin-name":["action","bindattr","collection","component","concat","debugger","each","each-in","get","hash","if","in","input","link-to","loc","log","lookup","mut","outlet","partial","query-params","render","template","textarea","unbound","unless","view","with","yield"]
+},s=/\[\]|\[[^\]]+\]/,i=/[^\s!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]+/,r=((...n)=>"("+n.map((n=>e(n))).join("|")+")")(/""|"[^"]+"/,/''|'[^']+'/,s,i),l=n(n("(",/\.|\.\/|\//,")?"),r,(c=n(/(\.|\/)/,r),
+n("(",c,")*")));var c;const o=n("(",s,"|",i,")(?==)"),m={begin:l,
+lexemes:/[\w.\/]+/},d=a.inherit(m,{keywords:{
+literal:["true","false","undefined","null"]}}),g={begin:/\(/,end:/\)/},b={
+className:"attr",begin:o,relevance:0,starts:{begin:/=/,end:/=/,starts:{
+contains:[a.NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,d,g]}}},u={
+contains:[a.NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{begin:/as\s+\|/,
+keywords:{keyword:"as"},end:/\|/,contains:[{begin:/\w+/}]},b,d,g],returnEnd:!0
+},h=a.inherit(m,{className:"name",keywords:t,starts:a.inherit(u,{end:/\)/})})
+;g.contains=[h];const N=a.inherit(m,{keywords:t,className:"name",
+starts:a.inherit(u,{end:/\}\}/})}),p=a.inherit(m,{keywords:t,className:"name"
+}),E=a.inherit(m,{className:"name",keywords:t,starts:a.inherit(u,{end:/\}\}/})})
+;return{name:"Handlebars",
+aliases:["hbs","html.hbs","html.handlebars","htmlbars"],case_insensitive:!0,
+subLanguage:"xml",contains:[{begin:/\\\{\{/,skip:!0},{begin:/\\\\(?=\{\{)/,
+skip:!0},a.COMMENT(/\{\{!--/,/--\}\}/),a.COMMENT(/\{\{!/,/\}\}/),{
+className:"template-tag",begin:/\{\{\{\{(?!\/)/,end:/\}\}\}\}/,contains:[N],
+starts:{end:/\{\{\{\{\//,returnEnd:!0,subLanguage:"xml"}},{
+className:"template-tag",begin:/\{\{\{\{\//,end:/\}\}\}\}/,contains:[p]},{
+className:"template-tag",begin:/\{\{#/,end:/\}\}/,contains:[N]},{
+className:"template-tag",begin:/\{\{(?=else\}\})/,end:/\}\}/,keywords:"else"},{
+className:"template-tag",begin:/\{\{(?=else if)/,end:/\}\}/,keywords:"else if"
+},{className:"template-tag",begin:/\{\{\//,end:/\}\}/,contains:[p]},{
+className:"template-variable",begin:/\{\{\{/,end:/\}\}\}/,contains:[E]},{
+className:"template-variable",begin:/\{\{/,end:/\}\}/,contains:[E]}]}}(a)
+;return t.name="HTMLbars",a.getLanguage("handlebars")&&(t.disableAutodetect=!0),
+t}})());
+hljs.registerLanguage("http",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>{const a="HTTP/(2|1\\.[01])",s=[{className:"attribute",
+begin:e("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{contains:[{
+className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}
+},{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{
+name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+a+" \\d{3})",
+end:/$/,contains:[{className:"meta",begin:a},{className:"number",
+begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:s}},{
+begin:"(?=^[A-Z]+ (.*?) "+a+"$)",end:/$/,contains:[{className:"string",
+begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:a},{
+className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:s}
+}]}}})());
+hljs.registerLanguage("hy",(()=>{"use strict";return e=>{
+var a="a-zA-Z_\\-!.?+*=<>&#'",t="["+a+"]["+a+"0-9/;:]*",i={$pattern:t,
+"builtin-name":"!= % %= & &= * ** **= *= *map + += , --build-class-- --import-- -= . / // //= /= < << <<= <= = > >= >> >>= @ @= ^ ^= abs accumulate all and any ap-compose ap-dotimes ap-each ap-each-while ap-filter ap-first ap-if ap-last ap-map ap-map-when ap-pipe ap-reduce ap-reject apply as-> ascii assert assoc bin break butlast callable calling-module-name car case cdr chain chr coll? combinations compile compress cond cons cons? continue count curry cut cycle dec def default-method defclass defmacro defmacro-alias defmacro/g! defmain defmethod defmulti defn defn-alias defnc defnr defreader defseq del delattr delete-route dict-comp dir disassemble dispatch-reader-macro distinct divmod do doto drop drop-last drop-while empty? end-sequence eval eval-and-compile eval-when-compile even? every? except exec filter first flatten float? fn fnc fnr for for* format fraction genexpr gensym get getattr global globals group-by hasattr hash hex id identity if if* if-not if-python2 import in inc input instance? integer integer-char? integer? interleave interpose is is-coll is-cons is-empty is-even is-every is-float is-instance is-integer is-integer-char is-iterable is-iterator is-keyword is-neg is-none is-not is-numeric is-odd is-pos is-string is-symbol is-zero isinstance islice issubclass iter iterable? iterate iterator? keyword keyword? lambda last len let lif lif-not list* list-comp locals loop macro-error macroexpand macroexpand-1 macroexpand-all map max merge-with method-decorator min multi-decorator multicombinations name neg? next none? nonlocal not not-in not? nth numeric? oct odd? open or ord partition permutations pos? post-route postwalk pow prewalk print product profile/calls profile/cpu put-route quasiquote quote raise range read read-str recursive-replace reduce remove repeat repeatedly repr require rest round route route-with-methods rwm second seq set-comp setattr setv some sorted string string? sum switch symbol? take take-nth take-while tee try unless unquote unquote-splicing vars walk when while with with* with-decorator with-gensyms xi xor yield yield-from zero? zip zip-longest | |= ~"
+},r={begin:t,relevance:0},n={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",
+relevance:0},s=e.inherit(e.QUOTE_STRING_MODE,{illegal:null
+}),o=e.COMMENT(";","$",{relevance:0}),l={className:"literal",
+begin:/\b([Tt]rue|[Ff]alse|nil|None)\b/},c={begin:"[\\[\\{]",end:"[\\]\\}]"},d={
+className:"comment",begin:"\\^"+t},m=e.COMMENT("\\^\\{","\\}"),p={
+className:"symbol",begin:"[:]{1,2}"+t},u={begin:"\\(",end:"\\)"},f={
+endsWithParent:!0,relevance:0},g={className:"name",relevance:0,keywords:i,
+begin:t,starts:f},h=[u,s,d,m,o,p,c,n,l,r]
+;return u.contains=[e.COMMENT("comment",""),g,f],f.contains=h,c.contains=h,{
+name:"Hy",aliases:["hylang"],illegal:/\S/,
+contains:[e.SHEBANG(),u,s,d,m,o,p,c,n,l]}}})());
+hljs.registerLanguage("inform7",(()=>{"use strict";return e=>({name:"Inform 7",
+aliases:["i7"],case_insensitive:!0,keywords:{
+keyword:"thing room person man woman animal container supporter backdrop door scenery open closed locked inside gender is are say understand kind of rule"
+},contains:[{className:"string",begin:'"',end:'"',relevance:0,contains:[{
+className:"subst",begin:"\\[",end:"\\]"}]},{className:"section",
+begin:/^(Volume|Book|Part|Chapter|Section|Table)\b/,end:"$"},{
+begin:/^(Check|Carry out|Report|Instead of|To|Rule|When|Before|After)\b/,
+end:":",contains:[{begin:"\\(This",end:"\\)"}]},{className:"comment",
+begin:"\\[",end:"\\]",contains:["self"]}]})})());
+hljs.registerLanguage("ini",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function n(...n){
+return n.map((n=>e(n))).join("")}return s=>{const a={className:"number",
+relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{begin:s.NUMBER_RE}]
+},i=s.COMMENT();i.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];const t={
+className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)\}/
+}]},r={className:"literal",begin:/\bon|off|true|false|yes|no\b/},l={
+className:"string",contains:[s.BACKSLASH_ESCAPE],variants:[{begin:"'''",
+end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'
+},{begin:"'",end:"'"}]},c={begin:/\[/,end:/\]/,contains:[i,r,t,l,a,"self"],
+relevance:0
+},g="("+[/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/].map((n=>e(n))).join("|")+")"
+;return{name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,
+contains:[i,{className:"section",begin:/\[+/,end:/\]+/},{
+begin:n(g,"(\\s*\\.\\s*",g,")*",n("(?=",/\s*=\s*[^#\s]/,")")),className:"attr",
+starts:{end:/$/,contains:[i,c,r,t,l,a]}}]}}})());
+hljs.registerLanguage("irpf90",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>{const t=/(_[a-z_\d]+)?/,i=/([de][+-]?\d+)?/,a={
+className:"number",variants:[{begin:e(/\b\d+/,/\.(\d*)/,i,t)},{
+begin:e(/\b\d+/,i,t)},{begin:e(/\.\d+/,i,t)}],relevance:0};return{name:"IRPF90",
+case_insensitive:!0,keywords:{literal:".False. .True.",
+keyword:"kind do while private call intrinsic where elsewhere type endtype endmodule endselect endinterface end enddo endif if forall endforall only contains default return stop then public subroutine|10 function program .and. .or. .not. .le. .eq. .ge. .gt. .lt. goto save else use module select case access blank direct exist file fmt form formatted iostat name named nextrec number opened rec recl sequential status unformatted unit continue format pause cycle exit c_null_char c_alert c_backspace c_form_feed flush wait decimal round iomsg synchronous nopass non_overridable pass protected volatile abstract extends import non_intrinsic value deferred generic final enumerator class associate bind enum c_int c_short c_long c_long_long c_signed_char c_size_t c_int8_t c_int16_t c_int32_t c_int64_t c_int_least8_t c_int_least16_t c_int_least32_t c_int_least64_t c_int_fast8_t c_int_fast16_t c_int_fast32_t c_int_fast64_t c_intmax_t C_intptr_t c_float c_double c_long_double c_float_complex c_double_complex c_long_double_complex c_bool c_char c_null_ptr c_null_funptr c_new_line c_carriage_return c_horizontal_tab c_vertical_tab iso_c_binding c_loc c_funloc c_associated  c_f_pointer c_ptr c_funptr iso_fortran_env character_storage_size error_unit file_storage_size input_unit iostat_end iostat_eor numeric_storage_size output_unit c_f_procpointer ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode newunit contiguous recursive pad position action delim readwrite eor advance nml interface procedure namelist include sequence elemental pure integer real character complex logical dimension allocatable|10 parameter external implicit|10 none double precision assign intent optional pointer target in out common equivalence data begin_provider &begin_provider end_provider begin_shell end_shell begin_template end_template subst assert touch soft_touch provide no_dep free irp_if irp_else irp_endif irp_write irp_read",
+built_in:"alog alog10 amax0 amax1 amin0 amin1 amod cabs ccos cexp clog csin csqrt dabs dacos dasin datan datan2 dcos dcosh ddim dexp dint dlog dlog10 dmax1 dmin1 dmod dnint dsign dsin dsinh dsqrt dtan dtanh float iabs idim idint idnint ifix isign max0 max1 min0 min1 sngl algama cdabs cdcos cdexp cdlog cdsin cdsqrt cqabs cqcos cqexp cqlog cqsin cqsqrt dcmplx dconjg derf derfc dfloat dgamma dimag dlgama iqint qabs qacos qasin qatan qatan2 qcmplx qconjg qcos qcosh qdim qerf qerfc qexp qgamma qimag qlgama qlog qlog10 qmax1 qmin1 qmod qnint qsign qsin qsinh qsqrt qtan qtanh abs acos aimag aint anint asin atan atan2 char cmplx conjg cos cosh exp ichar index int log log10 max min nint sign sin sinh sqrt tan tanh print write dim lge lgt lle llt mod nullify allocate deallocate adjustl adjustr all allocated any associated bit_size btest ceiling count cshift date_and_time digits dot_product eoshift epsilon exponent floor fraction huge iand ibclr ibits ibset ieor ior ishft ishftc lbound len_trim matmul maxexponent maxloc maxval merge minexponent minloc minval modulo mvbits nearest pack present product radix random_number random_seed range repeat reshape rrspacing scale scan selected_int_kind selected_real_kind set_exponent shape size spacing spread sum system_clock tiny transpose trim ubound unpack verify achar iachar transfer dble entry dprod cpu_time command_argument_count get_command get_command_argument get_environment_variable is_iostat_end ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode is_iostat_eor move_alloc new_line selected_char_kind same_type_as extends_type_of acosh asinh atanh bessel_j0 bessel_j1 bessel_jn bessel_y0 bessel_y1 bessel_yn erf erfc erfc_scaled gamma log_gamma hypot norm2 atomic_define atomic_ref execute_command_line leadz trailz storage_size merge_bits bge bgt ble blt dshiftl dshiftr findloc iall iany iparity image_index lcobound ucobound maskl maskr num_images parity popcnt poppar shifta shiftl shiftr this_image IRP_ALIGN irp_here"
+},illegal:/\/\*/,contains:[n.inherit(n.APOS_STRING_MODE,{className:"string",
+relevance:0}),n.inherit(n.QUOTE_STRING_MODE,{className:"string",relevance:0}),{
+className:"function",beginKeywords:"subroutine function program",
+illegal:"[${=\\n]",contains:[n.UNDERSCORE_TITLE_MODE,{className:"params",
+begin:"\\(",end:"\\)"}]},n.COMMENT("!","$",{relevance:0
+}),n.COMMENT("begin_doc","end_doc",{relevance:10}),a]}}})());
+hljs.registerLanguage("isbl",(()=>{"use strict";return S=>{
+const E="[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_!][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*",_={
+className:"number",begin:S.NUMBER_RE,relevance:0},T={className:"string",
+variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]},R={className:"doctag",
+begin:"\\b(?:TODO|DONE|BEGIN|END|STUB|CHG|FIXME|NOTE|BUG|XXX)\\b",relevance:0
+},O={variants:[{className:"comment",begin:"//",end:"$",relevance:0,
+contains:[S.PHRASAL_WORDS_MODE,R]},{className:"comment",begin:"/\\*",end:"\\*/",
+relevance:0,contains:[S.PHRASAL_WORDS_MODE,R]}]},C={$pattern:E,
+keyword:"and \u0438 else \u0438\u043d\u0430\u0447\u0435 endexcept endfinally endforeach \u043a\u043e\u043d\u0435\u0446\u0432\u0441\u0435 endif \u043a\u043e\u043d\u0435\u0446\u0435\u0441\u043b\u0438 endwhile \u043a\u043e\u043d\u0435\u0446\u043f\u043e\u043a\u0430 except exitfor finally foreach \u0432\u0441\u0435 if \u0435\u0441\u043b\u0438 in \u0432 not \u043d\u0435 or \u0438\u043b\u0438 try while \u043f\u043e\u043a\u0430 ",
+built_in:"SYSRES_CONST_ACCES_RIGHT_TYPE_EDIT SYSRES_CONST_ACCES_RIGHT_TYPE_FULL SYSRES_CONST_ACCES_RIGHT_TYPE_VIEW SYSRES_CONST_ACCESS_MODE_REQUISITE_CODE SYSRES_CONST_ACCESS_NO_ACCESS_VIEW SYSRES_CONST_ACCESS_NO_ACCESS_VIEW_CODE SYSRES_CONST_ACCESS_RIGHTS_ADD_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_ADD_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_CHANGE_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_CHANGE_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_DELETE_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_DELETE_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_EXECUTE_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_EXECUTE_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_NO_ACCESS_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_NO_ACCESS_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_RATIFY_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_RATIFY_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_VIEW SYSRES_CONST_ACCESS_RIGHTS_VIEW_CODE SYSRES_CONST_ACCESS_RIGHTS_VIEW_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_VIEW_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_TYPE_CHANGE SYSRES_CONST_ACCESS_TYPE_CHANGE_CODE SYSRES_CONST_ACCESS_TYPE_EXISTS SYSRES_CONST_ACCESS_TYPE_EXISTS_CODE SYSRES_CONST_ACCESS_TYPE_FULL SYSRES_CONST_ACCESS_TYPE_FULL_CODE SYSRES_CONST_ACCESS_TYPE_VIEW SYSRES_CONST_ACCESS_TYPE_VIEW_CODE SYSRES_CONST_ACTION_TYPE_ABORT SYSRES_CONST_ACTION_TYPE_ACCEPT SYSRES_CONST_ACTION_TYPE_ACCESS_RIGHTS SYSRES_CONST_ACTION_TYPE_ADD_ATTACHMENT SYSRES_CONST_ACTION_TYPE_CHANGE_CARD SYSRES_CONST_ACTION_TYPE_CHANGE_KIND SYSRES_CONST_ACTION_TYPE_CHANGE_STORAGE SYSRES_CONST_ACTION_TYPE_CONTINUE SYSRES_CONST_ACTION_TYPE_COPY SYSRES_CONST_ACTION_TYPE_CREATE SYSRES_CONST_ACTION_TYPE_CREATE_VERSION SYSRES_CONST_ACTION_TYPE_DELETE SYSRES_CONST_ACTION_TYPE_DELETE_ATTACHMENT SYSRES_CONST_ACTION_TYPE_DELETE_VERSION SYSRES_CONST_ACTION_TYPE_DISABLE_DELEGATE_ACCESS_RIGHTS SYSRES_CONST_ACTION_TYPE_ENABLE_DELEGATE_ACCESS_RIGHTS SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_CERTIFICATE SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_CERTIFICATE_AND_PASSWORD SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_PASSWORD SYSRES_CONST_ACTION_TYPE_EXPORT_WITH_LOCK SYSRES_CONST_ACTION_TYPE_EXPORT_WITHOUT_LOCK SYSRES_CONST_ACTION_TYPE_IMPORT_WITH_UNLOCK SYSRES_CONST_ACTION_TYPE_IMPORT_WITHOUT_UNLOCK SYSRES_CONST_ACTION_TYPE_LIFE_CYCLE_STAGE SYSRES_CONST_ACTION_TYPE_LOCK SYSRES_CONST_ACTION_TYPE_LOCK_FOR_SERVER SYSRES_CONST_ACTION_TYPE_LOCK_MODIFY SYSRES_CONST_ACTION_TYPE_MARK_AS_READED SYSRES_CONST_ACTION_TYPE_MARK_AS_UNREADED SYSRES_CONST_ACTION_TYPE_MODIFY SYSRES_CONST_ACTION_TYPE_MODIFY_CARD SYSRES_CONST_ACTION_TYPE_MOVE_TO_ARCHIVE SYSRES_CONST_ACTION_TYPE_OFF_ENCRYPTION SYSRES_CONST_ACTION_TYPE_PASSWORD_CHANGE SYSRES_CONST_ACTION_TYPE_PERFORM SYSRES_CONST_ACTION_TYPE_RECOVER_FROM_LOCAL_COPY SYSRES_CONST_ACTION_TYPE_RESTART SYSRES_CONST_ACTION_TYPE_RESTORE_FROM_ARCHIVE SYSRES_CONST_ACTION_TYPE_REVISION SYSRES_CONST_ACTION_TYPE_SEND_BY_MAIL SYSRES_CONST_ACTION_TYPE_SIGN SYSRES_CONST_ACTION_TYPE_START SYSRES_CONST_ACTION_TYPE_UNLOCK SYSRES_CONST_ACTION_TYPE_UNLOCK_FROM_SERVER SYSRES_CONST_ACTION_TYPE_VERSION_STATE SYSRES_CONST_ACTION_TYPE_VERSION_VISIBILITY SYSRES_CONST_ACTION_TYPE_VIEW SYSRES_CONST_ACTION_TYPE_VIEW_SHADOW_COPY SYSRES_CONST_ACTION_TYPE_WORKFLOW_DESCRIPTION_MODIFY SYSRES_CONST_ACTION_TYPE_WRITE_HISTORY SYSRES_CONST_ACTIVE_VERSION_STATE_PICK_VALUE SYSRES_CONST_ADD_REFERENCE_MODE_NAME SYSRES_CONST_ADDITION_REQUISITE_CODE SYSRES_CONST_ADDITIONAL_PARAMS_REQUISITE_CODE SYSRES_CONST_ADITIONAL_JOB_END_DATE_REQUISITE_NAME SYSRES_CONST_ADITIONAL_JOB_READ_REQUISITE_NAME SYSRES_CONST_ADITIONAL_JOB_START_DATE_REQUISITE_NAME SYSRES_CONST_ADITIONAL_JOB_STATE_REQUISITE_NAME SYSRES_CONST_ADMINISTRATION_HISTORY_ADDING_USER_TO_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_ADDING_USER_TO_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_COMP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_COMP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_USER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_USER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_CREATION SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_CREATION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_DELETION SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_DELETION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_COMP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_COMP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_FROM_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_FROM_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_RESTRICTION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_RESTRICTION_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_PRIVILEGE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_PRIVILEGE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_RIGHTS_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_RIGHTS_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_IS_MAIN_SERVER_CHANGED_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_IS_MAIN_SERVER_CHANGED_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_IS_PUBLIC_CHANGED_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_IS_PUBLIC_CHANGED_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_RESTRICTION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_RESTRICTION_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_PRIVILEGE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_PRIVILEGE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_RIGHTS_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_RIGHTS_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_CREATION SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_CREATION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_DELETION SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_DELETION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_CATEGORY_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_CATEGORY_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_COMP_TITLE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_COMP_TITLE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_FULL_NAME_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_FULL_NAME_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_PARENT_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_PARENT_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_AUTH_TYPE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_AUTH_TYPE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_LOGIN_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_LOGIN_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_STATUS_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_STATUS_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_USER_PASSWORD_CHANGE SYSRES_CONST_ADMINISTRATION_HISTORY_USER_PASSWORD_CHANGE_ACTION SYSRES_CONST_ALL_ACCEPT_CONDITION_RUS SYSRES_CONST_ALL_USERS_GROUP SYSRES_CONST_ALL_USERS_GROUP_NAME SYSRES_CONST_ALL_USERS_SERVER_GROUP_NAME SYSRES_CONST_ALLOWED_ACCESS_TYPE_CODE SYSRES_CONST_ALLOWED_ACCESS_TYPE_NAME SYSRES_CONST_APP_VIEWER_TYPE_REQUISITE_CODE SYSRES_CONST_APPROVING_SIGNATURE_NAME SYSRES_CONST_APPROVING_SIGNATURE_REQUISITE_CODE SYSRES_CONST_ASSISTANT_SUBSTITUE_TYPE SYSRES_CONST_ASSISTANT_SUBSTITUE_TYPE_CODE SYSRES_CONST_ATTACH_TYPE_COMPONENT_TOKEN SYSRES_CONST_ATTACH_TYPE_DOC SYSRES_CONST_ATTACH_TYPE_EDOC SYSRES_CONST_ATTACH_TYPE_FOLDER SYSRES_CONST_ATTACH_TYPE_JOB SYSRES_CONST_ATTACH_TYPE_REFERENCE SYSRES_CONST_ATTACH_TYPE_TASK SYSRES_CONST_AUTH_ENCODED_PASSWORD SYSRES_CONST_AUTH_ENCODED_PASSWORD_CODE SYSRES_CONST_AUTH_NOVELL SYSRES_CONST_AUTH_PASSWORD SYSRES_CONST_AUTH_PASSWORD_CODE SYSRES_CONST_AUTH_WINDOWS SYSRES_CONST_AUTHENTICATING_SIGNATURE_NAME SYSRES_CONST_AUTHENTICATING_SIGNATURE_REQUISITE_CODE SYSRES_CONST_AUTO_ENUM_METHOD_FLAG SYSRES_CONST_AUTO_NUMERATION_CODE SYSRES_CONST_AUTO_STRONG_ENUM_METHOD_FLAG SYSRES_CONST_AUTOTEXT_NAME_REQUISITE_CODE SYSRES_CONST_AUTOTEXT_TEXT_REQUISITE_CODE SYSRES_CONST_AUTOTEXT_USAGE_ALL SYSRES_CONST_AUTOTEXT_USAGE_ALL_CODE SYSRES_CONST_AUTOTEXT_USAGE_SIGN SYSRES_CONST_AUTOTEXT_USAGE_SIGN_CODE SYSRES_CONST_AUTOTEXT_USAGE_WORK SYSRES_CONST_AUTOTEXT_USAGE_WORK_CODE SYSRES_CONST_AUTOTEXT_USE_ANYWHERE_CODE SYSRES_CONST_AUTOTEXT_USE_ON_SIGNING_CODE SYSRES_CONST_AUTOTEXT_USE_ON_WORK_CODE SYSRES_CONST_BEGIN_DATE_REQUISITE_CODE SYSRES_CONST_BLACK_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_BLUE_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_BTN_PART SYSRES_CONST_CALCULATED_ROLE_TYPE_CODE SYSRES_CONST_CALL_TYPE_VARIABLE_BUTTON_VALUE SYSRES_CONST_CALL_TYPE_VARIABLE_PROGRAM_VALUE SYSRES_CONST_CANCEL_MESSAGE_FUNCTION_RESULT SYSRES_CONST_CARD_PART SYSRES_CONST_CARD_REFERENCE_MODE_NAME SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_ENCRYPT_VALUE SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_SIGN_AND_ENCRYPT_VALUE SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_SIGN_VALUE SYSRES_CONST_CHECK_PARAM_VALUE_DATE_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_FLOAT_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_INTEGER_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_PICK_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_REEFRENCE_PARAM_TYPE SYSRES_CONST_CLOSED_RECORD_FLAG_VALUE_FEMININE SYSRES_CONST_CLOSED_RECORD_FLAG_VALUE_MASCULINE SYSRES_CONST_CODE_COMPONENT_TYPE_ADMIN SYSRES_CONST_CODE_COMPONENT_TYPE_DEVELOPER SYSRES_CONST_CODE_COMPONENT_TYPE_DOCS SYSRES_CONST_CODE_COMPONENT_TYPE_EDOC_CARDS SYSRES_CONST_CODE_COMPONENT_TYPE_EXTERNAL_EXECUTABLE SYSRES_CONST_CODE_COMPONENT_TYPE_OTHER SYSRES_CONST_CODE_COMPONENT_TYPE_REFERENCE SYSRES_CONST_CODE_COMPONENT_TYPE_REPORT SYSRES_CONST_CODE_COMPONENT_TYPE_SCRIPT SYSRES_CONST_CODE_COMPONENT_TYPE_URL SYSRES_CONST_CODE_REQUISITE_ACCESS SYSRES_CONST_CODE_REQUISITE_CODE SYSRES_CONST_CODE_REQUISITE_COMPONENT SYSRES_CONST_CODE_REQUISITE_DESCRIPTION SYSRES_CONST_CODE_REQUISITE_EXCLUDE_COMPONENT SYSRES_CONST_CODE_REQUISITE_RECORD SYSRES_CONST_COMMENT_REQ_CODE SYSRES_CONST_COMMON_SETTINGS_REQUISITE_CODE SYSRES_CONST_COMP_CODE_GRD SYSRES_CONST_COMPONENT_GROUP_TYPE_REQUISITE_CODE SYSRES_CONST_COMPONENT_TYPE_ADMIN_COMPONENTS SYSRES_CONST_COMPONENT_TYPE_DEVELOPER_COMPONENTS SYSRES_CONST_COMPONENT_TYPE_DOCS SYSRES_CONST_COMPONENT_TYPE_EDOC_CARDS SYSRES_CONST_COMPONENT_TYPE_EDOCS SYSRES_CONST_COMPONENT_TYPE_EXTERNAL_EXECUTABLE SYSRES_CONST_COMPONENT_TYPE_OTHER SYSRES_CONST_COMPONENT_TYPE_REFERENCE_TYPES SYSRES_CONST_COMPONENT_TYPE_REFERENCES SYSRES_CONST_COMPONENT_TYPE_REPORTS SYSRES_CONST_COMPONENT_TYPE_SCRIPTS SYSRES_CONST_COMPONENT_TYPE_URL SYSRES_CONST_COMPONENTS_REMOTE_SERVERS_VIEW_CODE SYSRES_CONST_CONDITION_BLOCK_DESCRIPTION SYSRES_CONST_CONST_FIRM_STATUS_COMMON SYSRES_CONST_CONST_FIRM_STATUS_INDIVIDUAL SYSRES_CONST_CONST_NEGATIVE_VALUE SYSRES_CONST_CONST_POSITIVE_VALUE SYSRES_CONST_CONST_SERVER_STATUS_DONT_REPLICATE SYSRES_CONST_CONST_SERVER_STATUS_REPLICATE SYSRES_CONST_CONTENTS_REQUISITE_CODE SYSRES_CONST_DATA_TYPE_BOOLEAN SYSRES_CONST_DATA_TYPE_DATE SYSRES_CONST_DATA_TYPE_FLOAT SYSRES_CONST_DATA_TYPE_INTEGER SYSRES_CONST_DATA_TYPE_PICK SYSRES_CONST_DATA_TYPE_REFERENCE SYSRES_CONST_DATA_TYPE_STRING SYSRES_CONST_DATA_TYPE_TEXT SYSRES_CONST_DATA_TYPE_VARIANT SYSRES_CONST_DATE_CLOSE_REQ_CODE SYSRES_CONST_DATE_FORMAT_DATE_ONLY_CHAR SYSRES_CONST_DATE_OPEN_REQ_CODE SYSRES_CONST_DATE_REQUISITE SYSRES_CONST_DATE_REQUISITE_CODE SYSRES_CONST_DATE_REQUISITE_NAME SYSRES_CONST_DATE_REQUISITE_TYPE SYSRES_CONST_DATE_TYPE_CHAR SYSRES_CONST_DATETIME_FORMAT_VALUE SYSRES_CONST_DEA_ACCESS_RIGHTS_ACTION_CODE SYSRES_CONST_DESCRIPTION_LOCALIZE_ID_REQUISITE_CODE SYSRES_CONST_DESCRIPTION_REQUISITE_CODE SYSRES_CONST_DET1_PART SYSRES_CONST_DET2_PART SYSRES_CONST_DET3_PART SYSRES_CONST_DET4_PART SYSRES_CONST_DET5_PART SYSRES_CONST_DET6_PART SYSRES_CONST_DETAIL_DATASET_KEY_REQUISITE_CODE SYSRES_CONST_DETAIL_PICK_REQUISITE_CODE SYSRES_CONST_DETAIL_REQ_CODE SYSRES_CONST_DO_NOT_USE_ACCESS_TYPE_CODE SYSRES_CONST_DO_NOT_USE_ACCESS_TYPE_NAME SYSRES_CONST_DO_NOT_USE_ON_VIEW_ACCESS_TYPE_CODE SYSRES_CONST_DO_NOT_USE_ON_VIEW_ACCESS_TYPE_NAME SYSRES_CONST_DOCUMENT_STORAGES_CODE SYSRES_CONST_DOCUMENT_TEMPLATES_TYPE_NAME SYSRES_CONST_DOUBLE_REQUISITE_CODE SYSRES_CONST_EDITOR_CLOSE_FILE_OBSERV_TYPE_CODE SYSRES_CONST_EDITOR_CLOSE_PROCESS_OBSERV_TYPE_CODE SYSRES_CONST_EDITOR_TYPE_REQUISITE_CODE SYSRES_CONST_EDITORS_APPLICATION_NAME_REQUISITE_CODE SYSRES_CONST_EDITORS_CREATE_SEVERAL_PROCESSES_REQUISITE_CODE SYSRES_CONST_EDITORS_EXTENSION_REQUISITE_CODE SYSRES_CONST_EDITORS_OBSERVER_BY_PROCESS_TYPE SYSRES_CONST_EDITORS_REFERENCE_CODE SYSRES_CONST_EDITORS_REPLACE_SPEC_CHARS_REQUISITE_CODE SYSRES_CONST_EDITORS_USE_PLUGINS_REQUISITE_CODE SYSRES_CONST_EDITORS_VIEW_DOCUMENT_OPENED_TO_EDIT_CODE SYSRES_CONST_EDOC_CARD_TYPE_REQUISITE_CODE SYSRES_CONST_EDOC_CARD_TYPES_LINK_REQUISITE_CODE SYSRES_CONST_EDOC_CERTIFICATE_AND_PASSWORD_ENCODE_CODE SYSRES_CONST_EDOC_CERTIFICATE_ENCODE_CODE SYSRES_CONST_EDOC_DATE_REQUISITE_CODE SYSRES_CONST_EDOC_KIND_REFERENCE_CODE SYSRES_CONST_EDOC_KINDS_BY_TEMPLATE_ACTION_CODE SYSRES_CONST_EDOC_MANAGE_ACCESS_CODE SYSRES_CONST_EDOC_NONE_ENCODE_CODE SYSRES_CONST_EDOC_NUMBER_REQUISITE_CODE SYSRES_CONST_EDOC_PASSWORD_ENCODE_CODE SYSRES_CONST_EDOC_READONLY_ACCESS_CODE SYSRES_CONST_EDOC_SHELL_LIFE_TYPE_VIEW_VALUE SYSRES_CONST_EDOC_SIZE_RESTRICTION_PRIORITY_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_CHECK_ACCESS_RIGHTS_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_COMPUTER_NAME_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_DATABASE_NAME_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_EDIT_IN_STORAGE_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_LOCAL_PATH_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_SHARED_SOURCE_NAME_REQUISITE_CODE SYSRES_CONST_EDOC_TEMPLATE_REQUISITE_CODE SYSRES_CONST_EDOC_TYPES_REFERENCE_CODE SYSRES_CONST_EDOC_VERSION_ACTIVE_STAGE_CODE SYSRES_CONST_EDOC_VERSION_DESIGN_STAGE_CODE SYSRES_CONST_EDOC_VERSION_OBSOLETE_STAGE_CODE SYSRES_CONST_EDOC_WRITE_ACCES_CODE SYSRES_CONST_EDOCUMENT_CARD_REQUISITES_REFERENCE_CODE_SELECTED_REQUISITE SYSRES_CONST_ENCODE_CERTIFICATE_TYPE_CODE SYSRES_CONST_END_DATE_REQUISITE_CODE SYSRES_CONST_ENUMERATION_TYPE_REQUISITE_CODE SYSRES_CONST_EXECUTE_ACCESS_RIGHTS_TYPE_CODE SYSRES_CONST_EXECUTIVE_FILE_STORAGE_TYPE SYSRES_CONST_EXIST_CONST SYSRES_CONST_EXIST_VALUE SYSRES_CONST_EXPORT_LOCK_TYPE_ASK SYSRES_CONST_EXPORT_LOCK_TYPE_WITH_LOCK SYSRES_CONST_EXPORT_LOCK_TYPE_WITHOUT_LOCK SYSRES_CONST_EXPORT_VERSION_TYPE_ASK SYSRES_CONST_EXPORT_VERSION_TYPE_LAST SYSRES_CONST_EXPORT_VERSION_TYPE_LAST_ACTIVE SYSRES_CONST_EXTENSION_REQUISITE_CODE SYSRES_CONST_FILTER_NAME_REQUISITE_CODE SYSRES_CONST_FILTER_REQUISITE_CODE SYSRES_CONST_FILTER_TYPE_COMMON_CODE SYSRES_CONST_FILTER_TYPE_COMMON_NAME SYSRES_CONST_FILTER_TYPE_USER_CODE SYSRES_CONST_FILTER_TYPE_USER_NAME SYSRES_CONST_FILTER_VALUE_REQUISITE_NAME SYSRES_CONST_FLOAT_NUMBER_FORMAT_CHAR SYSRES_CONST_FLOAT_REQUISITE_TYPE SYSRES_CONST_FOLDER_AUTHOR_VALUE SYSRES_CONST_FOLDER_KIND_ANY_OBJECTS SYSRES_CONST_FOLDER_KIND_COMPONENTS SYSRES_CONST_FOLDER_KIND_EDOCS SYSRES_CONST_FOLDER_KIND_JOBS SYSRES_CONST_FOLDER_KIND_TASKS SYSRES_CONST_FOLDER_TYPE_COMMON SYSRES_CONST_FOLDER_TYPE_COMPONENT SYSRES_CONST_FOLDER_TYPE_FAVORITES SYSRES_CONST_FOLDER_TYPE_INBOX SYSRES_CONST_FOLDER_TYPE_OUTBOX SYSRES_CONST_FOLDER_TYPE_QUICK_LAUNCH SYSRES_CONST_FOLDER_TYPE_SEARCH SYSRES_CONST_FOLDER_TYPE_SHORTCUTS SYSRES_CONST_FOLDER_TYPE_USER SYSRES_CONST_FROM_DICTIONARY_ENUM_METHOD_FLAG SYSRES_CONST_FULL_SUBSTITUTE_TYPE SYSRES_CONST_FULL_SUBSTITUTE_TYPE_CODE SYSRES_CONST_FUNCTION_CANCEL_RESULT SYSRES_CONST_FUNCTION_CATEGORY_SYSTEM SYSRES_CONST_FUNCTION_CATEGORY_USER SYSRES_CONST_FUNCTION_FAILURE_RESULT SYSRES_CONST_FUNCTION_SAVE_RESULT SYSRES_CONST_GENERATED_REQUISITE SYSRES_CONST_GREEN_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_GROUP_ACCOUNT_TYPE_VALUE_CODE SYSRES_CONST_GROUP_CATEGORY_NORMAL_CODE SYSRES_CONST_GROUP_CATEGORY_NORMAL_NAME SYSRES_CONST_GROUP_CATEGORY_SERVICE_CODE SYSRES_CONST_GROUP_CATEGORY_SERVICE_NAME SYSRES_CONST_GROUP_COMMON_CATEGORY_FIELD_VALUE SYSRES_CONST_GROUP_FULL_NAME_REQUISITE_CODE SYSRES_CONST_GROUP_NAME_REQUISITE_CODE SYSRES_CONST_GROUP_RIGHTS_T_REQUISITE_CODE SYSRES_CONST_GROUP_SERVER_CODES_REQUISITE_CODE SYSRES_CONST_GROUP_SERVER_NAME_REQUISITE_CODE SYSRES_CONST_GROUP_SERVICE_CATEGORY_FIELD_VALUE SYSRES_CONST_GROUP_USER_REQUISITE_CODE SYSRES_CONST_GROUPS_REFERENCE_CODE SYSRES_CONST_GROUPS_REQUISITE_CODE SYSRES_CONST_HIDDEN_MODE_NAME SYSRES_CONST_HIGH_LVL_REQUISITE_CODE SYSRES_CONST_HISTORY_ACTION_CREATE_CODE SYSRES_CONST_HISTORY_ACTION_DELETE_CODE SYSRES_CONST_HISTORY_ACTION_EDIT_CODE SYSRES_CONST_HOUR_CHAR SYSRES_CONST_ID_REQUISITE_CODE SYSRES_CONST_IDSPS_REQUISITE_CODE SYSRES_CONST_IMAGE_MODE_COLOR SYSRES_CONST_IMAGE_MODE_GREYSCALE SYSRES_CONST_IMAGE_MODE_MONOCHROME SYSRES_CONST_IMPORTANCE_HIGH SYSRES_CONST_IMPORTANCE_LOW SYSRES_CONST_IMPORTANCE_NORMAL SYSRES_CONST_IN_DESIGN_VERSION_STATE_PICK_VALUE SYSRES_CONST_INCOMING_WORK_RULE_TYPE_CODE SYSRES_CONST_INT_REQUISITE SYSRES_CONST_INT_REQUISITE_TYPE SYSRES_CONST_INTEGER_NUMBER_FORMAT_CHAR SYSRES_CONST_INTEGER_TYPE_CHAR SYSRES_CONST_IS_GENERATED_REQUISITE_NEGATIVE_VALUE SYSRES_CONST_IS_PUBLIC_ROLE_REQUISITE_CODE SYSRES_CONST_IS_REMOTE_USER_NEGATIVE_VALUE SYSRES_CONST_IS_REMOTE_USER_POSITIVE_VALUE SYSRES_CONST_IS_STORED_REQUISITE_NEGATIVE_VALUE SYSRES_CONST_IS_STORED_REQUISITE_STORED_VALUE SYSRES_CONST_ITALIC_LIFE_CYCLE_STAGE_DRAW_STYLE SYSRES_CONST_JOB_BLOCK_DESCRIPTION SYSRES_CONST_JOB_KIND_CONTROL_JOB SYSRES_CONST_JOB_KIND_JOB SYSRES_CONST_JOB_KIND_NOTICE SYSRES_CONST_JOB_STATE_ABORTED SYSRES_CONST_JOB_STATE_COMPLETE SYSRES_CONST_JOB_STATE_WORKING SYSRES_CONST_KIND_REQUISITE_CODE SYSRES_CONST_KIND_REQUISITE_NAME SYSRES_CONST_KINDS_CREATE_SHADOW_COPIES_REQUISITE_CODE SYSRES_CONST_KINDS_DEFAULT_EDOC_LIFE_STAGE_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_ALL_TEPLATES_ALLOWED_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_ALLOW_LIFE_CYCLE_STAGE_CHANGING_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_ALLOW_MULTIPLE_ACTIVE_VERSIONS_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_SHARE_ACCES_RIGHTS_BY_DEFAULT_CODE SYSRES_CONST_KINDS_EDOC_TEMPLATE_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_TYPE_REQUISITE_CODE SYSRES_CONST_KINDS_SIGNERS_REQUISITES_CODE SYSRES_CONST_KOD_INPUT_TYPE SYSRES_CONST_LAST_UPDATE_DATE_REQUISITE_CODE SYSRES_CONST_LIFE_CYCLE_START_STAGE_REQUISITE_CODE SYSRES_CONST_LILAC_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_LINK_OBJECT_KIND_COMPONENT SYSRES_CONST_LINK_OBJECT_KIND_DOCUMENT SYSRES_CONST_LINK_OBJECT_KIND_EDOC SYSRES_CONST_LINK_OBJECT_KIND_FOLDER SYSRES_CONST_LINK_OBJECT_KIND_JOB SYSRES_CONST_LINK_OBJECT_KIND_REFERENCE SYSRES_CONST_LINK_OBJECT_KIND_TASK SYSRES_CONST_LINK_REF_TYPE_REQUISITE_CODE SYSRES_CONST_LIST_REFERENCE_MODE_NAME SYSRES_CONST_LOCALIZATION_DICTIONARY_MAIN_VIEW_CODE SYSRES_CONST_MAIN_VIEW_CODE SYSRES_CONST_MANUAL_ENUM_METHOD_FLAG SYSRES_CONST_MASTER_COMP_TYPE_REQUISITE_CODE SYSRES_CONST_MASTER_TABLE_REC_ID_REQUISITE_CODE SYSRES_CONST_MAXIMIZED_MODE_NAME SYSRES_CONST_ME_VALUE SYSRES_CONST_MESSAGE_ATTENTION_CAPTION SYSRES_CONST_MESSAGE_CONFIRMATION_CAPTION SYSRES_CONST_MESSAGE_ERROR_CAPTION SYSRES_CONST_MESSAGE_INFORMATION_CAPTION SYSRES_CONST_MINIMIZED_MODE_NAME SYSRES_CONST_MINUTE_CHAR SYSRES_CONST_MODULE_REQUISITE_CODE SYSRES_CONST_MONITORING_BLOCK_DESCRIPTION SYSRES_CONST_MONTH_FORMAT_VALUE SYSRES_CONST_NAME_LOCALIZE_ID_REQUISITE_CODE SYSRES_CONST_NAME_REQUISITE_CODE SYSRES_CONST_NAME_SINGULAR_REQUISITE_CODE SYSRES_CONST_NAMEAN_INPUT_TYPE SYSRES_CONST_NEGATIVE_PICK_VALUE SYSRES_CONST_NEGATIVE_VALUE SYSRES_CONST_NO SYSRES_CONST_NO_PICK_VALUE SYSRES_CONST_NO_SIGNATURE_REQUISITE_CODE SYSRES_CONST_NO_VALUE SYSRES_CONST_NONE_ACCESS_RIGHTS_TYPE_CODE SYSRES_CONST_NONOPERATING_RECORD_FLAG_VALUE SYSRES_CONST_NONOPERATING_RECORD_FLAG_VALUE_MASCULINE SYSRES_CONST_NORMAL_ACCESS_RIGHTS_TYPE_CODE SYSRES_CONST_NORMAL_LIFE_CYCLE_STAGE_DRAW_STYLE SYSRES_CONST_NORMAL_MODE_NAME SYSRES_CONST_NOT_ALLOWED_ACCESS_TYPE_CODE SYSRES_CONST_NOT_ALLOWED_ACCESS_TYPE_NAME SYSRES_CONST_NOTE_REQUISITE_CODE SYSRES_CONST_NOTICE_BLOCK_DESCRIPTION SYSRES_CONST_NUM_REQUISITE SYSRES_CONST_NUM_STR_REQUISITE_CODE SYSRES_CONST_NUMERATION_AUTO_NOT_STRONG SYSRES_CONST_NUMERATION_AUTO_STRONG SYSRES_CONST_NUMERATION_FROM_DICTONARY SYSRES_CONST_NUMERATION_MANUAL SYSRES_CONST_NUMERIC_TYPE_CHAR SYSRES_CONST_NUMREQ_REQUISITE_CODE SYSRES_CONST_OBSOLETE_VERSION_STATE_PICK_VALUE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_CODE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_FEMININE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_MASCULINE SYSRES_CONST_OPTIONAL_FORM_COMP_REQCODE_PREFIX SYSRES_CONST_ORANGE_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_ORIGINALREF_REQUISITE_CODE SYSRES_CONST_OURFIRM_REF_CODE SYSRES_CONST_OURFIRM_REQUISITE_CODE SYSRES_CONST_OURFIRM_VAR SYSRES_CONST_OUTGOING_WORK_RULE_TYPE_CODE SYSRES_CONST_PICK_NEGATIVE_RESULT SYSRES_CONST_PICK_POSITIVE_RESULT SYSRES_CONST_PICK_REQUISITE SYSRES_CONST_PICK_REQUISITE_TYPE SYSRES_CONST_PICK_TYPE_CHAR SYSRES_CONST_PLAN_STATUS_REQUISITE_CODE SYSRES_CONST_PLATFORM_VERSION_COMMENT SYSRES_CONST_PLUGINS_SETTINGS_DESCRIPTION_REQUISITE_CODE SYSRES_CONST_POSITIVE_PICK_VALUE SYSRES_CONST_POWER_TO_CREATE_ACTION_CODE SYSRES_CONST_POWER_TO_SIGN_ACTION_CODE SYSRES_CONST_PRIORITY_REQUISITE_CODE SYSRES_CONST_QUALIFIED_TASK_TYPE SYSRES_CONST_QUALIFIED_TASK_TYPE_CODE SYSRES_CONST_RECSTAT_REQUISITE_CODE SYSRES_CONST_RED_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_REF_ID_T_REF_TYPE_REQUISITE_CODE SYSRES_CONST_REF_REQUISITE SYSRES_CONST_REF_REQUISITE_TYPE SYSRES_CONST_REF_REQUISITES_REFERENCE_CODE_SELECTED_REQUISITE SYSRES_CONST_REFERENCE_RECORD_HISTORY_CREATE_ACTION_CODE SYSRES_CONST_REFERENCE_RECORD_HISTORY_DELETE_ACTION_CODE SYSRES_CONST_REFERENCE_RECORD_HISTORY_MODIFY_ACTION_CODE SYSRES_CONST_REFERENCE_TYPE_CHAR SYSRES_CONST_REFERENCE_TYPE_REQUISITE_NAME SYSRES_CONST_REFERENCES_ADD_PARAMS_REQUISITE_CODE SYSRES_CONST_REFERENCES_DISPLAY_REQUISITE_REQUISITE_CODE SYSRES_CONST_REMOTE_SERVER_STATUS_WORKING SYSRES_CONST_REMOTE_SERVER_TYPE_MAIN SYSRES_CONST_REMOTE_SERVER_TYPE_SECONDARY SYSRES_CONST_REMOTE_USER_FLAG_VALUE_CODE SYSRES_CONST_REPORT_APP_EDITOR_INTERNAL SYSRES_CONST_REPORT_BASE_REPORT_ID_REQUISITE_CODE SYSRES_CONST_REPORT_BASE_REPORT_REQUISITE_CODE SYSRES_CONST_REPORT_SCRIPT_REQUISITE_CODE SYSRES_CONST_REPORT_TEMPLATE_REQUISITE_CODE SYSRES_CONST_REPORT_VIEWER_CODE_REQUISITE_CODE SYSRES_CONST_REQ_ALLOW_COMPONENT_DEFAULT_VALUE SYSRES_CONST_REQ_ALLOW_RECORD_DEFAULT_VALUE SYSRES_CONST_REQ_ALLOW_SERVER_COMPONENT_DEFAULT_VALUE SYSRES_CONST_REQ_MODE_AVAILABLE_CODE SYSRES_CONST_REQ_MODE_EDIT_CODE SYSRES_CONST_REQ_MODE_HIDDEN_CODE SYSRES_CONST_REQ_MODE_NOT_AVAILABLE_CODE SYSRES_CONST_REQ_MODE_VIEW_CODE SYSRES_CONST_REQ_NUMBER_REQUISITE_CODE SYSRES_CONST_REQ_SECTION_VALUE SYSRES_CONST_REQ_TYPE_VALUE SYSRES_CONST_REQUISITE_FORMAT_BY_UNIT SYSRES_CONST_REQUISITE_FORMAT_DATE_FULL SYSRES_CONST_REQUISITE_FORMAT_DATE_TIME SYSRES_CONST_REQUISITE_FORMAT_LEFT SYSRES_CONST_REQUISITE_FORMAT_RIGHT SYSRES_CONST_REQUISITE_FORMAT_WITHOUT_UNIT SYSRES_CONST_REQUISITE_NUMBER_REQUISITE_CODE SYSRES_CONST_REQUISITE_SECTION_ACTIONS SYSRES_CONST_REQUISITE_SECTION_BUTTON SYSRES_CONST_REQUISITE_SECTION_BUTTONS SYSRES_CONST_REQUISITE_SECTION_CARD SYSRES_CONST_REQUISITE_SECTION_TABLE SYSRES_CONST_REQUISITE_SECTION_TABLE10 SYSRES_CONST_REQUISITE_SECTION_TABLE11 SYSRES_CONST_REQUISITE_SECTION_TABLE12 SYSRES_CONST_REQUISITE_SECTION_TABLE13 SYSRES_CONST_REQUISITE_SECTION_TABLE14 SYSRES_CONST_REQUISITE_SECTION_TABLE15 SYSRES_CONST_REQUISITE_SECTION_TABLE16 SYSRES_CONST_REQUISITE_SECTION_TABLE17 SYSRES_CONST_REQUISITE_SECTION_TABLE18 SYSRES_CONST_REQUISITE_SECTION_TABLE19 SYSRES_CONST_REQUISITE_SECTION_TABLE2 SYSRES_CONST_REQUISITE_SECTION_TABLE20 SYSRES_CONST_REQUISITE_SECTION_TABLE21 SYSRES_CONST_REQUISITE_SECTION_TABLE22 SYSRES_CONST_REQUISITE_SECTION_TABLE23 SYSRES_CONST_REQUISITE_SECTION_TABLE24 SYSRES_CONST_REQUISITE_SECTION_TABLE3 SYSRES_CONST_REQUISITE_SECTION_TABLE4 SYSRES_CONST_REQUISITE_SECTION_TABLE5 SYSRES_CONST_REQUISITE_SECTION_TABLE6 SYSRES_CONST_REQUISITE_SECTION_TABLE7 SYSRES_CONST_REQUISITE_SECTION_TABLE8 SYSRES_CONST_REQUISITE_SECTION_TABLE9 SYSRES_CONST_REQUISITES_PSEUDOREFERENCE_REQUISITE_NUMBER_REQUISITE_CODE SYSRES_CONST_RIGHT_ALIGNMENT_CODE SYSRES_CONST_ROLES_REFERENCE_CODE SYSRES_CONST_ROUTE_STEP_AFTER_RUS SYSRES_CONST_ROUTE_STEP_AND_CONDITION_RUS SYSRES_CONST_ROUTE_STEP_OR_CONDITION_RUS SYSRES_CONST_ROUTE_TYPE_COMPLEX SYSRES_CONST_ROUTE_TYPE_PARALLEL SYSRES_CONST_ROUTE_TYPE_SERIAL SYSRES_CONST_SBDATASETDESC_NEGATIVE_VALUE SYSRES_CONST_SBDATASETDESC_POSITIVE_VALUE SYSRES_CONST_SBVIEWSDESC_POSITIVE_VALUE SYSRES_CONST_SCRIPT_BLOCK_DESCRIPTION SYSRES_CONST_SEARCH_BY_TEXT_REQUISITE_CODE SYSRES_CONST_SEARCHES_COMPONENT_CONTENT SYSRES_CONST_SEARCHES_CRITERIA_ACTION_NAME SYSRES_CONST_SEARCHES_EDOC_CONTENT SYSRES_CONST_SEARCHES_FOLDER_CONTENT SYSRES_CONST_SEARCHES_JOB_CONTENT SYSRES_CONST_SEARCHES_REFERENCE_CODE SYSRES_CONST_SEARCHES_TASK_CONTENT SYSRES_CONST_SECOND_CHAR SYSRES_CONST_SECTION_REQUISITE_ACTIONS_VALUE SYSRES_CONST_SECTION_REQUISITE_CARD_VALUE SYSRES_CONST_SECTION_REQUISITE_CODE SYSRES_CONST_SECTION_REQUISITE_DETAIL_1_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_2_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_3_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_4_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_5_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_6_VALUE SYSRES_CONST_SELECT_REFERENCE_MODE_NAME SYSRES_CONST_SELECT_TYPE_SELECTABLE SYSRES_CONST_SELECT_TYPE_SELECTABLE_ONLY_CHILD SYSRES_CONST_SELECT_TYPE_SELECTABLE_WITH_CHILD SYSRES_CONST_SELECT_TYPE_UNSLECTABLE SYSRES_CONST_SERVER_TYPE_MAIN SYSRES_CONST_SERVICE_USER_CATEGORY_FIELD_VALUE SYSRES_CONST_SETTINGS_USER_REQUISITE_CODE SYSRES_CONST_SIGNATURE_AND_ENCODE_CERTIFICATE_TYPE_CODE SYSRES_CONST_SIGNATURE_CERTIFICATE_TYPE_CODE SYSRES_CONST_SINGULAR_TITLE_REQUISITE_CODE SYSRES_CONST_SQL_SERVER_AUTHENTIFICATION_FLAG_VALUE_CODE SYSRES_CONST_SQL_SERVER_ENCODE_AUTHENTIFICATION_FLAG_VALUE_CODE SYSRES_CONST_STANDART_ROUTE_REFERENCE_CODE SYSRES_CONST_STANDART_ROUTE_REFERENCE_COMMENT_REQUISITE_CODE SYSRES_CONST_STANDART_ROUTES_GROUPS_REFERENCE_CODE SYSRES_CONST_STATE_REQ_NAME SYSRES_CONST_STATE_REQUISITE_ACTIVE_VALUE SYSRES_CONST_STATE_REQUISITE_CLOSED_VALUE SYSRES_CONST_STATE_REQUISITE_CODE SYSRES_CONST_STATIC_ROLE_TYPE_CODE SYSRES_CONST_STATUS_PLAN_DEFAULT_VALUE SYSRES_CONST_STATUS_VALUE_AUTOCLEANING SYSRES_CONST_STATUS_VALUE_BLUE_SQUARE SYSRES_CONST_STATUS_VALUE_COMPLETE SYSRES_CONST_STATUS_VALUE_GREEN_SQUARE SYSRES_CONST_STATUS_VALUE_ORANGE_SQUARE SYSRES_CONST_STATUS_VALUE_PURPLE_SQUARE SYSRES_CONST_STATUS_VALUE_RED_SQUARE SYSRES_CONST_STATUS_VALUE_SUSPEND SYSRES_CONST_STATUS_VALUE_YELLOW_SQUARE SYSRES_CONST_STDROUTE_SHOW_TO_USERS_REQUISITE_CODE SYSRES_CONST_STORAGE_TYPE_FILE SYSRES_CONST_STORAGE_TYPE_SQL_SERVER SYSRES_CONST_STR_REQUISITE SYSRES_CONST_STRIKEOUT_LIFE_CYCLE_STAGE_DRAW_STYLE SYSRES_CONST_STRING_FORMAT_LEFT_ALIGN_CHAR SYSRES_CONST_STRING_FORMAT_RIGHT_ALIGN_CHAR SYSRES_CONST_STRING_REQUISITE_CODE SYSRES_CONST_STRING_REQUISITE_TYPE SYSRES_CONST_STRING_TYPE_CHAR SYSRES_CONST_SUBSTITUTES_PSEUDOREFERENCE_CODE SYSRES_CONST_SUBTASK_BLOCK_DESCRIPTION SYSRES_CONST_SYSTEM_SETTING_CURRENT_USER_PARAM_VALUE SYSRES_CONST_SYSTEM_SETTING_EMPTY_VALUE_PARAM_VALUE SYSRES_CONST_SYSTEM_VERSION_COMMENT SYSRES_CONST_TASK_ACCESS_TYPE_ALL SYSRES_CONST_TASK_ACCESS_TYPE_ALL_MEMBERS SYSRES_CONST_TASK_ACCESS_TYPE_MANUAL SYSRES_CONST_TASK_ENCODE_TYPE_CERTIFICATION SYSRES_CONST_TASK_ENCODE_TYPE_CERTIFICATION_AND_PASSWORD SYSRES_CONST_TASK_ENCODE_TYPE_NONE SYSRES_CONST_TASK_ENCODE_TYPE_PASSWORD SYSRES_CONST_TASK_ROUTE_ALL_CONDITION SYSRES_CONST_TASK_ROUTE_AND_CONDITION SYSRES_CONST_TASK_ROUTE_OR_CONDITION SYSRES_CONST_TASK_STATE_ABORTED SYSRES_CONST_TASK_STATE_COMPLETE SYSRES_CONST_TASK_STATE_CONTINUED SYSRES_CONST_TASK_STATE_CONTROL SYSRES_CONST_TASK_STATE_INIT SYSRES_CONST_TASK_STATE_WORKING SYSRES_CONST_TASK_TITLE SYSRES_CONST_TASK_TYPES_GROUPS_REFERENCE_CODE SYSRES_CONST_TASK_TYPES_REFERENCE_CODE SYSRES_CONST_TEMPLATES_REFERENCE_CODE SYSRES_CONST_TEST_DATE_REQUISITE_NAME SYSRES_CONST_TEST_DEV_DATABASE_NAME SYSRES_CONST_TEST_DEV_SYSTEM_CODE SYSRES_CONST_TEST_EDMS_DATABASE_NAME SYSRES_CONST_TEST_EDMS_MAIN_CODE SYSRES_CONST_TEST_EDMS_MAIN_DB_NAME SYSRES_CONST_TEST_EDMS_SECOND_CODE SYSRES_CONST_TEST_EDMS_SECOND_DB_NAME SYSRES_CONST_TEST_EDMS_SYSTEM_CODE SYSRES_CONST_TEST_NUMERIC_REQUISITE_NAME SYSRES_CONST_TEXT_REQUISITE SYSRES_CONST_TEXT_REQUISITE_CODE SYSRES_CONST_TEXT_REQUISITE_TYPE SYSRES_CONST_TEXT_TYPE_CHAR SYSRES_CONST_TYPE_CODE_REQUISITE_CODE SYSRES_CONST_TYPE_REQUISITE_CODE SYSRES_CONST_UNDEFINED_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_UNITS_SECTION_ID_REQUISITE_CODE SYSRES_CONST_UNITS_SECTION_REQUISITE_CODE SYSRES_CONST_UNOPERATING_RECORD_FLAG_VALUE_CODE SYSRES_CONST_UNSTORED_DATA_REQUISITE_CODE SYSRES_CONST_UNSTORED_DATA_REQUISITE_NAME SYSRES_CONST_USE_ACCESS_TYPE_CODE SYSRES_CONST_USE_ACCESS_TYPE_NAME SYSRES_CONST_USER_ACCOUNT_TYPE_VALUE_CODE SYSRES_CONST_USER_ADDITIONAL_INFORMATION_REQUISITE_CODE SYSRES_CONST_USER_AND_GROUP_ID_FROM_PSEUDOREFERENCE_REQUISITE_CODE SYSRES_CONST_USER_CATEGORY_NORMAL SYSRES_CONST_USER_CERTIFICATE_REQUISITE_CODE SYSRES_CONST_USER_CERTIFICATE_STATE_REQUISITE_CODE SYSRES_CONST_USER_CERTIFICATE_SUBJECT_NAME_REQUISITE_CODE SYSRES_CONST_USER_CERTIFICATE_THUMBPRINT_REQUISITE_CODE SYSRES_CONST_USER_COMMON_CATEGORY SYSRES_CONST_USER_COMMON_CATEGORY_CODE SYSRES_CONST_USER_FULL_NAME_REQUISITE_CODE SYSRES_CONST_USER_GROUP_TYPE_REQUISITE_CODE SYSRES_CONST_USER_LOGIN_REQUISITE_CODE SYSRES_CONST_USER_REMOTE_CONTROLLER_REQUISITE_CODE SYSRES_CONST_USER_REMOTE_SYSTEM_REQUISITE_CODE SYSRES_CONST_USER_RIGHTS_T_REQUISITE_CODE SYSRES_CONST_USER_SERVER_NAME_REQUISITE_CODE SYSRES_CONST_USER_SERVICE_CATEGORY SYSRES_CONST_USER_SERVICE_CATEGORY_CODE SYSRES_CONST_USER_STATUS_ADMINISTRATOR_CODE SYSRES_CONST_USER_STATUS_ADMINISTRATOR_NAME SYSRES_CONST_USER_STATUS_DEVELOPER_CODE SYSRES_CONST_USER_STATUS_DEVELOPER_NAME SYSRES_CONST_USER_STATUS_DISABLED_CODE SYSRES_CONST_USER_STATUS_DISABLED_NAME SYSRES_CONST_USER_STATUS_SYSTEM_DEVELOPER_CODE SYSRES_CONST_USER_STATUS_USER_CODE SYSRES_CONST_USER_STATUS_USER_NAME SYSRES_CONST_USER_STATUS_USER_NAME_DEPRECATED SYSRES_CONST_USER_TYPE_FIELD_VALUE_USER SYSRES_CONST_USER_TYPE_REQUISITE_CODE SYSRES_CONST_USERS_CONTROLLER_REQUISITE_CODE SYSRES_CONST_USERS_IS_MAIN_SERVER_REQUISITE_CODE SYSRES_CONST_USERS_REFERENCE_CODE SYSRES_CONST_USERS_REGISTRATION_CERTIFICATES_ACTION_NAME SYSRES_CONST_USERS_REQUISITE_CODE SYSRES_CONST_USERS_SYSTEM_REQUISITE_CODE SYSRES_CONST_USERS_USER_ACCESS_RIGHTS_TYPR_REQUISITE_CODE SYSRES_CONST_USERS_USER_AUTHENTICATION_REQUISITE_CODE SYSRES_CONST_USERS_USER_COMPONENT_REQUISITE_CODE SYSRES_CONST_USERS_USER_GROUP_REQUISITE_CODE SYSRES_CONST_USERS_VIEW_CERTIFICATES_ACTION_NAME SYSRES_CONST_VIEW_DEFAULT_CODE SYSRES_CONST_VIEW_DEFAULT_NAME SYSRES_CONST_VIEWER_REQUISITE_CODE SYSRES_CONST_WAITING_BLOCK_DESCRIPTION SYSRES_CONST_WIZARD_FORM_LABEL_TEST_STRING  SYSRES_CONST_WIZARD_QUERY_PARAM_HEIGHT_ETALON_STRING SYSRES_CONST_WIZARD_REFERENCE_COMMENT_REQUISITE_CODE SYSRES_CONST_WORK_RULES_DESCRIPTION_REQUISITE_CODE SYSRES_CONST_WORK_TIME_CALENDAR_REFERENCE_CODE SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE_CODE SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE_CODE_RUS SYSRES_CONST_WORK_WORKFLOW_SOFT_ROUTE_TYPE_VALUE_CODE_RUS SYSRES_CONST_WORKFLOW_ROUTE_TYPR_HARD SYSRES_CONST_WORKFLOW_ROUTE_TYPR_SOFT SYSRES_CONST_XML_ENCODING SYSRES_CONST_XREC_STAT_REQUISITE_CODE SYSRES_CONST_XRECID_FIELD_NAME SYSRES_CONST_YES SYSRES_CONST_YES_NO_2_REQUISITE_CODE SYSRES_CONST_YES_NO_REQUISITE_CODE SYSRES_CONST_YES_NO_T_REF_TYPE_REQUISITE_CODE SYSRES_CONST_YES_PICK_VALUE SYSRES_CONST_YES_VALUE CR FALSE nil NO_VALUE NULL TAB TRUE YES_VALUE ADMINISTRATORS_GROUP_NAME CUSTOMIZERS_GROUP_NAME DEVELOPERS_GROUP_NAME SERVICE_USERS_GROUP_NAME DECISION_BLOCK_FIRST_OPERAND_PROPERTY DECISION_BLOCK_NAME_PROPERTY DECISION_BLOCK_OPERATION_PROPERTY DECISION_BLOCK_RESULT_TYPE_PROPERTY DECISION_BLOCK_SECOND_OPERAND_PROPERTY ANY_FILE_EXTENTION COMPRESSED_DOCUMENT_EXTENSION EXTENDED_DOCUMENT_EXTENSION SHORT_COMPRESSED_DOCUMENT_EXTENSION SHORT_EXTENDED_DOCUMENT_EXTENSION JOB_BLOCK_ABORT_DEADLINE_PROPERTY JOB_BLOCK_AFTER_FINISH_EVENT JOB_BLOCK_AFTER_QUERY_PARAMETERS_EVENT JOB_BLOCK_ATTACHMENT_PROPERTY JOB_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY JOB_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY JOB_BLOCK_BEFORE_QUERY_PARAMETERS_EVENT JOB_BLOCK_BEFORE_START_EVENT JOB_BLOCK_CREATED_JOBS_PROPERTY JOB_BLOCK_DEADLINE_PROPERTY JOB_BLOCK_EXECUTION_RESULTS_PROPERTY JOB_BLOCK_IS_PARALLEL_PROPERTY JOB_BLOCK_IS_RELATIVE_ABORT_DEADLINE_PROPERTY JOB_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY JOB_BLOCK_JOB_TEXT_PROPERTY JOB_BLOCK_NAME_PROPERTY JOB_BLOCK_NEED_SIGN_ON_PERFORM_PROPERTY JOB_BLOCK_PERFORMER_PROPERTY JOB_BLOCK_RELATIVE_ABORT_DEADLINE_TYPE_PROPERTY JOB_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY JOB_BLOCK_SUBJECT_PROPERTY ENGLISH_LANGUAGE_CODE RUSSIAN_LANGUAGE_CODE smHidden smMaximized smMinimized smNormal wmNo wmYes COMPONENT_TOKEN_LINK_KIND DOCUMENT_LINK_KIND EDOCUMENT_LINK_KIND FOLDER_LINK_KIND JOB_LINK_KIND REFERENCE_LINK_KIND TASK_LINK_KIND COMPONENT_TOKEN_LOCK_TYPE EDOCUMENT_VERSION_LOCK_TYPE MONITOR_BLOCK_AFTER_FINISH_EVENT MONITOR_BLOCK_BEFORE_START_EVENT MONITOR_BLOCK_DEADLINE_PROPERTY MONITOR_BLOCK_INTERVAL_PROPERTY MONITOR_BLOCK_INTERVAL_TYPE_PROPERTY MONITOR_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY MONITOR_BLOCK_NAME_PROPERTY MONITOR_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY MONITOR_BLOCK_SEARCH_SCRIPT_PROPERTY NOTICE_BLOCK_AFTER_FINISH_EVENT NOTICE_BLOCK_ATTACHMENT_PROPERTY NOTICE_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY NOTICE_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY NOTICE_BLOCK_BEFORE_START_EVENT NOTICE_BLOCK_CREATED_NOTICES_PROPERTY NOTICE_BLOCK_DEADLINE_PROPERTY NOTICE_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY NOTICE_BLOCK_NAME_PROPERTY NOTICE_BLOCK_NOTICE_TEXT_PROPERTY NOTICE_BLOCK_PERFORMER_PROPERTY NOTICE_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY NOTICE_BLOCK_SUBJECT_PROPERTY dseAfterCancel dseAfterClose dseAfterDelete dseAfterDeleteOutOfTransaction dseAfterInsert dseAfterOpen dseAfterScroll dseAfterUpdate dseAfterUpdateOutOfTransaction dseBeforeCancel dseBeforeClose dseBeforeDelete dseBeforeDetailUpdate dseBeforeInsert dseBeforeOpen dseBeforeUpdate dseOnAnyRequisiteChange dseOnCloseRecord dseOnDeleteError dseOnOpenRecord dseOnPrepareUpdate dseOnUpdateError dseOnUpdateRatifiedRecord dseOnValidDelete dseOnValidUpdate reOnChange reOnChangeValues SELECTION_BEGIN_ROUTE_EVENT SELECTION_END_ROUTE_EVENT CURRENT_PERIOD_IS_REQUIRED PREVIOUS_CARD_TYPE_NAME SHOW_RECORD_PROPERTIES_FORM ACCESS_RIGHTS_SETTING_DIALOG_CODE ADMINISTRATOR_USER_CODE ANALYTIC_REPORT_TYPE asrtHideLocal asrtHideRemote CALCULATED_ROLE_TYPE_CODE COMPONENTS_REFERENCE_DEVELOPER_VIEW_CODE DCTS_TEST_PROTOCOLS_FOLDER_PATH E_EDOC_VERSION_ALREADY_APPROVINGLY_SIGNED E_EDOC_VERSION_ALREADY_APPROVINGLY_SIGNED_BY_USER E_EDOC_VERSION_ALREDY_SIGNED E_EDOC_VERSION_ALREDY_SIGNED_BY_USER EDOC_TYPES_CODE_REQUISITE_FIELD_NAME EDOCUMENTS_ALIAS_NAME FILES_FOLDER_PATH FILTER_OPERANDS_DELIMITER FILTER_OPERATIONS_DELIMITER FORMCARD_NAME FORMLIST_NAME GET_EXTENDED_DOCUMENT_EXTENSION_CREATION_MODE GET_EXTENDED_DOCUMENT_EXTENSION_IMPORT_MODE INTEGRATED_REPORT_TYPE IS_BUILDER_APPLICATION_ROLE IS_BUILDER_APPLICATION_ROLE2 IS_BUILDER_USERS ISBSYSDEV LOG_FOLDER_PATH mbCancel mbNo mbNoToAll mbOK mbYes mbYesToAll MEMORY_DATASET_DESRIPTIONS_FILENAME mrNo mrNoToAll mrYes mrYesToAll MULTIPLE_SELECT_DIALOG_CODE NONOPERATING_RECORD_FLAG_FEMININE NONOPERATING_RECORD_FLAG_MASCULINE OPERATING_RECORD_FLAG_FEMININE OPERATING_RECORD_FLAG_MASCULINE PROFILING_SETTINGS_COMMON_SETTINGS_CODE_VALUE PROGRAM_INITIATED_LOOKUP_ACTION ratDelete ratEdit ratInsert REPORT_TYPE REQUIRED_PICK_VALUES_VARIABLE rmCard rmList SBRTE_PROGID_DEV SBRTE_PROGID_RELEASE STATIC_ROLE_TYPE_CODE SUPPRESS_EMPTY_TEMPLATE_CREATION SYSTEM_USER_CODE UPDATE_DIALOG_DATASET USED_IN_OBJECT_HINT_PARAM USER_INITIATED_LOOKUP_ACTION USER_NAME_FORMAT USER_SELECTION_RESTRICTIONS WORKFLOW_TEST_PROTOCOLS_FOLDER_PATH ELS_SUBTYPE_CONTROL_NAME ELS_FOLDER_KIND_CONTROL_NAME REPEAT_PROCESS_CURRENT_OBJECT_EXCEPTION_NAME PRIVILEGE_COMPONENT_FULL_ACCESS PRIVILEGE_DEVELOPMENT_EXPORT PRIVILEGE_DEVELOPMENT_IMPORT PRIVILEGE_DOCUMENT_DELETE PRIVILEGE_ESD PRIVILEGE_FOLDER_DELETE PRIVILEGE_MANAGE_ACCESS_RIGHTS PRIVILEGE_MANAGE_REPLICATION PRIVILEGE_MANAGE_SESSION_SERVER PRIVILEGE_OBJECT_FULL_ACCESS PRIVILEGE_OBJECT_VIEW PRIVILEGE_RESERVE_LICENSE PRIVILEGE_SYSTEM_CUSTOMIZE PRIVILEGE_SYSTEM_DEVELOP PRIVILEGE_SYSTEM_INSTALL PRIVILEGE_TASK_DELETE PRIVILEGE_USER_PLUGIN_SETTINGS_CUSTOMIZE PRIVILEGES_PSEUDOREFERENCE_CODE ACCESS_TYPES_PSEUDOREFERENCE_CODE ALL_AVAILABLE_COMPONENTS_PSEUDOREFERENCE_CODE ALL_AVAILABLE_PRIVILEGES_PSEUDOREFERENCE_CODE ALL_REPLICATE_COMPONENTS_PSEUDOREFERENCE_CODE AVAILABLE_DEVELOPERS_COMPONENTS_PSEUDOREFERENCE_CODE COMPONENTS_PSEUDOREFERENCE_CODE FILTRATER_SETTINGS_CONFLICTS_PSEUDOREFERENCE_CODE GROUPS_PSEUDOREFERENCE_CODE RECEIVE_PROTOCOL_PSEUDOREFERENCE_CODE REFERENCE_REQUISITE_PSEUDOREFERENCE_CODE REFERENCE_REQUISITES_PSEUDOREFERENCE_CODE REFTYPES_PSEUDOREFERENCE_CODE REPLICATION_SEANCES_DIARY_PSEUDOREFERENCE_CODE SEND_PROTOCOL_PSEUDOREFERENCE_CODE SUBSTITUTES_PSEUDOREFERENCE_CODE SYSTEM_SETTINGS_PSEUDOREFERENCE_CODE UNITS_PSEUDOREFERENCE_CODE USERS_PSEUDOREFERENCE_CODE VIEWERS_PSEUDOREFERENCE_CODE CERTIFICATE_TYPE_ENCRYPT CERTIFICATE_TYPE_SIGN CERTIFICATE_TYPE_SIGN_AND_ENCRYPT STORAGE_TYPE_FILE STORAGE_TYPE_NAS_CIFS STORAGE_TYPE_SAPERION STORAGE_TYPE_SQL_SERVER COMPTYPE2_REQUISITE_DOCUMENTS_VALUE COMPTYPE2_REQUISITE_TASKS_VALUE COMPTYPE2_REQUISITE_FOLDERS_VALUE COMPTYPE2_REQUISITE_REFERENCES_VALUE SYSREQ_CODE SYSREQ_COMPTYPE2 SYSREQ_CONST_AVAILABLE_FOR_WEB SYSREQ_CONST_COMMON_CODE SYSREQ_CONST_COMMON_VALUE SYSREQ_CONST_FIRM_CODE SYSREQ_CONST_FIRM_STATUS SYSREQ_CONST_FIRM_VALUE SYSREQ_CONST_SERVER_STATUS SYSREQ_CONTENTS SYSREQ_DATE_OPEN SYSREQ_DATE_CLOSE SYSREQ_DESCRIPTION SYSREQ_DESCRIPTION_LOCALIZE_ID SYSREQ_DOUBLE SYSREQ_EDOC_ACCESS_TYPE SYSREQ_EDOC_AUTHOR SYSREQ_EDOC_CREATED SYSREQ_EDOC_DELEGATE_RIGHTS_REQUISITE_CODE SYSREQ_EDOC_EDITOR SYSREQ_EDOC_ENCODE_TYPE SYSREQ_EDOC_ENCRYPTION_PLUGIN_NAME SYSREQ_EDOC_ENCRYPTION_PLUGIN_VERSION SYSREQ_EDOC_EXPORT_DATE SYSREQ_EDOC_EXPORTER SYSREQ_EDOC_KIND SYSREQ_EDOC_LIFE_STAGE_NAME SYSREQ_EDOC_LOCKED_FOR_SERVER_CODE SYSREQ_EDOC_MODIFIED SYSREQ_EDOC_NAME SYSREQ_EDOC_NOTE SYSREQ_EDOC_QUALIFIED_ID SYSREQ_EDOC_SESSION_KEY SYSREQ_EDOC_SESSION_KEY_ENCRYPTION_PLUGIN_NAME SYSREQ_EDOC_SESSION_KEY_ENCRYPTION_PLUGIN_VERSION SYSREQ_EDOC_SIGNATURE_TYPE SYSREQ_EDOC_SIGNED SYSREQ_EDOC_STORAGE SYSREQ_EDOC_STORAGES_ARCHIVE_STORAGE SYSREQ_EDOC_STORAGES_CHECK_RIGHTS SYSREQ_EDOC_STORAGES_COMPUTER_NAME SYSREQ_EDOC_STORAGES_EDIT_IN_STORAGE SYSREQ_EDOC_STORAGES_EXECUTIVE_STORAGE SYSREQ_EDOC_STORAGES_FUNCTION SYSREQ_EDOC_STORAGES_INITIALIZED SYSREQ_EDOC_STORAGES_LOCAL_PATH SYSREQ_EDOC_STORAGES_SAPERION_DATABASE_NAME SYSREQ_EDOC_STORAGES_SEARCH_BY_TEXT SYSREQ_EDOC_STORAGES_SERVER_NAME SYSREQ_EDOC_STORAGES_SHARED_SOURCE_NAME SYSREQ_EDOC_STORAGES_TYPE SYSREQ_EDOC_TEXT_MODIFIED SYSREQ_EDOC_TYPE_ACT_CODE SYSREQ_EDOC_TYPE_ACT_DESCRIPTION SYSREQ_EDOC_TYPE_ACT_DESCRIPTION_LOCALIZE_ID SYSREQ_EDOC_TYPE_ACT_ON_EXECUTE SYSREQ_EDOC_TYPE_ACT_ON_EXECUTE_EXISTS SYSREQ_EDOC_TYPE_ACT_SECTION SYSREQ_EDOC_TYPE_ADD_PARAMS SYSREQ_EDOC_TYPE_COMMENT SYSREQ_EDOC_TYPE_EVENT_TEXT SYSREQ_EDOC_TYPE_NAME_IN_SINGULAR SYSREQ_EDOC_TYPE_NAME_IN_SINGULAR_LOCALIZE_ID SYSREQ_EDOC_TYPE_NAME_LOCALIZE_ID SYSREQ_EDOC_TYPE_NUMERATION_METHOD SYSREQ_EDOC_TYPE_PSEUDO_REQUISITE_CODE SYSREQ_EDOC_TYPE_REQ_CODE SYSREQ_EDOC_TYPE_REQ_DESCRIPTION SYSREQ_EDOC_TYPE_REQ_DESCRIPTION_LOCALIZE_ID SYSREQ_EDOC_TYPE_REQ_IS_LEADING SYSREQ_EDOC_TYPE_REQ_IS_REQUIRED SYSREQ_EDOC_TYPE_REQ_NUMBER SYSREQ_EDOC_TYPE_REQ_ON_CHANGE SYSREQ_EDOC_TYPE_REQ_ON_CHANGE_EXISTS SYSREQ_EDOC_TYPE_REQ_ON_SELECT SYSREQ_EDOC_TYPE_REQ_ON_SELECT_KIND SYSREQ_EDOC_TYPE_REQ_SECTION SYSREQ_EDOC_TYPE_VIEW_CARD SYSREQ_EDOC_TYPE_VIEW_CODE SYSREQ_EDOC_TYPE_VIEW_COMMENT SYSREQ_EDOC_TYPE_VIEW_IS_MAIN SYSREQ_EDOC_TYPE_VIEW_NAME SYSREQ_EDOC_TYPE_VIEW_NAME_LOCALIZE_ID SYSREQ_EDOC_VERSION_AUTHOR SYSREQ_EDOC_VERSION_CRC SYSREQ_EDOC_VERSION_DATA SYSREQ_EDOC_VERSION_EDITOR SYSREQ_EDOC_VERSION_EXPORT_DATE SYSREQ_EDOC_VERSION_EXPORTER SYSREQ_EDOC_VERSION_HIDDEN SYSREQ_EDOC_VERSION_LIFE_STAGE SYSREQ_EDOC_VERSION_MODIFIED SYSREQ_EDOC_VERSION_NOTE SYSREQ_EDOC_VERSION_SIGNATURE_TYPE SYSREQ_EDOC_VERSION_SIGNED SYSREQ_EDOC_VERSION_SIZE SYSREQ_EDOC_VERSION_SOURCE SYSREQ_EDOC_VERSION_TEXT_MODIFIED SYSREQ_EDOCKIND_DEFAULT_VERSION_STATE_CODE SYSREQ_FOLDER_KIND SYSREQ_FUNC_CATEGORY SYSREQ_FUNC_COMMENT SYSREQ_FUNC_GROUP SYSREQ_FUNC_GROUP_COMMENT SYSREQ_FUNC_GROUP_NUMBER SYSREQ_FUNC_HELP SYSREQ_FUNC_PARAM_DEF_VALUE SYSREQ_FUNC_PARAM_IDENT SYSREQ_FUNC_PARAM_NUMBER SYSREQ_FUNC_PARAM_TYPE SYSREQ_FUNC_TEXT SYSREQ_GROUP_CATEGORY SYSREQ_ID SYSREQ_LAST_UPDATE SYSREQ_LEADER_REFERENCE SYSREQ_LINE_NUMBER SYSREQ_MAIN_RECORD_ID SYSREQ_NAME SYSREQ_NAME_LOCALIZE_ID SYSREQ_NOTE SYSREQ_ORIGINAL_RECORD SYSREQ_OUR_FIRM SYSREQ_PROFILING_SETTINGS_BATCH_LOGING SYSREQ_PROFILING_SETTINGS_BATCH_SIZE SYSREQ_PROFILING_SETTINGS_PROFILING_ENABLED SYSREQ_PROFILING_SETTINGS_SQL_PROFILING_ENABLED SYSREQ_PROFILING_SETTINGS_START_LOGGED SYSREQ_RECORD_STATUS SYSREQ_REF_REQ_FIELD_NAME SYSREQ_REF_REQ_FORMAT SYSREQ_REF_REQ_GENERATED SYSREQ_REF_REQ_LENGTH SYSREQ_REF_REQ_PRECISION SYSREQ_REF_REQ_REFERENCE SYSREQ_REF_REQ_SECTION SYSREQ_REF_REQ_STORED SYSREQ_REF_REQ_TOKENS SYSREQ_REF_REQ_TYPE SYSREQ_REF_REQ_VIEW SYSREQ_REF_TYPE_ACT_CODE SYSREQ_REF_TYPE_ACT_DESCRIPTION SYSREQ_REF_TYPE_ACT_DESCRIPTION_LOCALIZE_ID SYSREQ_REF_TYPE_ACT_ON_EXECUTE SYSREQ_REF_TYPE_ACT_ON_EXECUTE_EXISTS SYSREQ_REF_TYPE_ACT_SECTION SYSREQ_REF_TYPE_ADD_PARAMS SYSREQ_REF_TYPE_COMMENT SYSREQ_REF_TYPE_COMMON_SETTINGS SYSREQ_REF_TYPE_DISPLAY_REQUISITE_NAME SYSREQ_REF_TYPE_EVENT_TEXT SYSREQ_REF_TYPE_MAIN_LEADING_REF SYSREQ_REF_TYPE_NAME_IN_SINGULAR SYSREQ_REF_TYPE_NAME_IN_SINGULAR_LOCALIZE_ID SYSREQ_REF_TYPE_NAME_LOCALIZE_ID SYSREQ_REF_TYPE_NUMERATION_METHOD SYSREQ_REF_TYPE_REQ_CODE SYSREQ_REF_TYPE_REQ_DESCRIPTION SYSREQ_REF_TYPE_REQ_DESCRIPTION_LOCALIZE_ID SYSREQ_REF_TYPE_REQ_IS_CONTROL SYSREQ_REF_TYPE_REQ_IS_FILTER SYSREQ_REF_TYPE_REQ_IS_LEADING SYSREQ_REF_TYPE_REQ_IS_REQUIRED SYSREQ_REF_TYPE_REQ_NUMBER SYSREQ_REF_TYPE_REQ_ON_CHANGE SYSREQ_REF_TYPE_REQ_ON_CHANGE_EXISTS SYSREQ_REF_TYPE_REQ_ON_SELECT SYSREQ_REF_TYPE_REQ_ON_SELECT_KIND SYSREQ_REF_TYPE_REQ_SECTION SYSREQ_REF_TYPE_VIEW_CARD SYSREQ_REF_TYPE_VIEW_CODE SYSREQ_REF_TYPE_VIEW_COMMENT SYSREQ_REF_TYPE_VIEW_IS_MAIN SYSREQ_REF_TYPE_VIEW_NAME SYSREQ_REF_TYPE_VIEW_NAME_LOCALIZE_ID SYSREQ_REFERENCE_TYPE_ID SYSREQ_STATE SYSREQ_STAT\u0415 SYSREQ_SYSTEM_SETTINGS_VALUE SYSREQ_TYPE SYSREQ_UNIT SYSREQ_UNIT_ID SYSREQ_USER_GROUPS_GROUP_FULL_NAME SYSREQ_USER_GROUPS_GROUP_NAME SYSREQ_USER_GROUPS_GROUP_SERVER_NAME SYSREQ_USERS_ACCESS_RIGHTS SYSREQ_USERS_AUTHENTICATION SYSREQ_USERS_CATEGORY SYSREQ_USERS_COMPONENT SYSREQ_USERS_COMPONENT_USER_IS_PUBLIC SYSREQ_USERS_DOMAIN SYSREQ_USERS_FULL_USER_NAME SYSREQ_USERS_GROUP SYSREQ_USERS_IS_MAIN_SERVER SYSREQ_USERS_LOGIN SYSREQ_USERS_REFERENCE_USER_IS_PUBLIC SYSREQ_USERS_STATUS SYSREQ_USERS_USER_CERTIFICATE SYSREQ_USERS_USER_CERTIFICATE_INFO SYSREQ_USERS_USER_CERTIFICATE_PLUGIN_NAME SYSREQ_USERS_USER_CERTIFICATE_PLUGIN_VERSION SYSREQ_USERS_USER_CERTIFICATE_STATE SYSREQ_USERS_USER_CERTIFICATE_SUBJECT_NAME SYSREQ_USERS_USER_CERTIFICATE_THUMBPRINT SYSREQ_USERS_USER_DEFAULT_CERTIFICATE SYSREQ_USERS_USER_DESCRIPTION SYSREQ_USERS_USER_GLOBAL_NAME SYSREQ_USERS_USER_LOGIN SYSREQ_USERS_USER_MAIN_SERVER SYSREQ_USERS_USER_TYPE SYSREQ_WORK_RULES_FOLDER_ID RESULT_VAR_NAME RESULT_VAR_NAME_ENG AUTO_NUMERATION_RULE_ID CANT_CHANGE_ID_REQUISITE_RULE_ID CANT_CHANGE_OURFIRM_REQUISITE_RULE_ID CHECK_CHANGING_REFERENCE_RECORD_USE_RULE_ID CHECK_CODE_REQUISITE_RULE_ID CHECK_DELETING_REFERENCE_RECORD_USE_RULE_ID CHECK_FILTRATER_CHANGES_RULE_ID CHECK_RECORD_INTERVAL_RULE_ID CHECK_REFERENCE_INTERVAL_RULE_ID CHECK_REQUIRED_DATA_FULLNESS_RULE_ID CHECK_REQUIRED_REQUISITES_FULLNESS_RULE_ID MAKE_RECORD_UNRATIFIED_RULE_ID RESTORE_AUTO_NUMERATION_RULE_ID SET_FIRM_CONTEXT_FROM_RECORD_RULE_ID SET_FIRST_RECORD_IN_LIST_FORM_RULE_ID SET_IDSPS_VALUE_RULE_ID SET_NEXT_CODE_VALUE_RULE_ID SET_OURFIRM_BOUNDS_RULE_ID SET_OURFIRM_REQUISITE_RULE_ID SCRIPT_BLOCK_AFTER_FINISH_EVENT SCRIPT_BLOCK_BEFORE_START_EVENT SCRIPT_BLOCK_EXECUTION_RESULTS_PROPERTY SCRIPT_BLOCK_NAME_PROPERTY SCRIPT_BLOCK_SCRIPT_PROPERTY SUBTASK_BLOCK_ABORT_DEADLINE_PROPERTY SUBTASK_BLOCK_AFTER_FINISH_EVENT SUBTASK_BLOCK_ASSIGN_PARAMS_EVENT SUBTASK_BLOCK_ATTACHMENTS_PROPERTY SUBTASK_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY SUBTASK_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY SUBTASK_BLOCK_BEFORE_START_EVENT SUBTASK_BLOCK_CREATED_TASK_PROPERTY SUBTASK_BLOCK_CREATION_EVENT SUBTASK_BLOCK_DEADLINE_PROPERTY SUBTASK_BLOCK_IMPORTANCE_PROPERTY SUBTASK_BLOCK_INITIATOR_PROPERTY SUBTASK_BLOCK_IS_RELATIVE_ABORT_DEADLINE_PROPERTY SUBTASK_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY SUBTASK_BLOCK_JOBS_TYPE_PROPERTY SUBTASK_BLOCK_NAME_PROPERTY SUBTASK_BLOCK_PARALLEL_ROUTE_PROPERTY SUBTASK_BLOCK_PERFORMERS_PROPERTY SUBTASK_BLOCK_RELATIVE_ABORT_DEADLINE_TYPE_PROPERTY SUBTASK_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY SUBTASK_BLOCK_REQUIRE_SIGN_PROPERTY SUBTASK_BLOCK_STANDARD_ROUTE_PROPERTY SUBTASK_BLOCK_START_EVENT SUBTASK_BLOCK_STEP_CONTROL_PROPERTY SUBTASK_BLOCK_SUBJECT_PROPERTY SUBTASK_BLOCK_TASK_CONTROL_PROPERTY SUBTASK_BLOCK_TEXT_PROPERTY SUBTASK_BLOCK_UNLOCK_ATTACHMENTS_ON_STOP_PROPERTY SUBTASK_BLOCK_USE_STANDARD_ROUTE_PROPERTY SUBTASK_BLOCK_WAIT_FOR_TASK_COMPLETE_PROPERTY SYSCOMP_CONTROL_JOBS SYSCOMP_FOLDERS SYSCOMP_JOBS SYSCOMP_NOTICES SYSCOMP_TASKS SYSDLG_CREATE_EDOCUMENT SYSDLG_CREATE_EDOCUMENT_VERSION SYSDLG_CURRENT_PERIOD SYSDLG_EDIT_FUNCTION_HELP SYSDLG_EDOCUMENT_KINDS_FOR_TEMPLATE SYSDLG_EXPORT_MULTIPLE_EDOCUMENTS SYSDLG_EXPORT_SINGLE_EDOCUMENT SYSDLG_IMPORT_EDOCUMENT SYSDLG_MULTIPLE_SELECT SYSDLG_SETUP_ACCESS_RIGHTS SYSDLG_SETUP_DEFAULT_RIGHTS SYSDLG_SETUP_FILTER_CONDITION SYSDLG_SETUP_SIGN_RIGHTS SYSDLG_SETUP_TASK_OBSERVERS SYSDLG_SETUP_TASK_ROUTE SYSDLG_SETUP_USERS_LIST SYSDLG_SIGN_EDOCUMENT SYSDLG_SIGN_MULTIPLE_EDOCUMENTS SYSREF_ACCESS_RIGHTS_TYPES SYSREF_ADMINISTRATION_HISTORY SYSREF_ALL_AVAILABLE_COMPONENTS SYSREF_ALL_AVAILABLE_PRIVILEGES SYSREF_ALL_REPLICATING_COMPONENTS SYSREF_AVAILABLE_DEVELOPERS_COMPONENTS SYSREF_CALENDAR_EVENTS SYSREF_COMPONENT_TOKEN_HISTORY SYSREF_COMPONENT_TOKENS SYSREF_COMPONENTS SYSREF_CONSTANTS SYSREF_DATA_RECEIVE_PROTOCOL SYSREF_DATA_SEND_PROTOCOL SYSREF_DIALOGS SYSREF_DIALOGS_REQUISITES SYSREF_EDITORS SYSREF_EDOC_CARDS SYSREF_EDOC_TYPES SYSREF_EDOCUMENT_CARD_REQUISITES SYSREF_EDOCUMENT_CARD_TYPES SYSREF_EDOCUMENT_CARD_TYPES_REFERENCE SYSREF_EDOCUMENT_CARDS SYSREF_EDOCUMENT_HISTORY SYSREF_EDOCUMENT_KINDS SYSREF_EDOCUMENT_REQUISITES SYSREF_EDOCUMENT_SIGNATURES SYSREF_EDOCUMENT_TEMPLATES SYSREF_EDOCUMENT_TEXT_STORAGES SYSREF_EDOCUMENT_VIEWS SYSREF_FILTERER_SETUP_CONFLICTS SYSREF_FILTRATER_SETTING_CONFLICTS SYSREF_FOLDER_HISTORY SYSREF_FOLDERS SYSREF_FUNCTION_GROUPS SYSREF_FUNCTION_PARAMS SYSREF_FUNCTIONS SYSREF_JOB_HISTORY SYSREF_LINKS SYSREF_LOCALIZATION_DICTIONARY SYSREF_LOCALIZATION_LANGUAGES SYSREF_MODULES SYSREF_PRIVILEGES SYSREF_RECORD_HISTORY SYSREF_REFERENCE_REQUISITES SYSREF_REFERENCE_TYPE_VIEWS SYSREF_REFERENCE_TYPES SYSREF_REFERENCES SYSREF_REFERENCES_REQUISITES SYSREF_REMOTE_SERVERS SYSREF_REPLICATION_SESSIONS_LOG SYSREF_REPLICATION_SESSIONS_PROTOCOL SYSREF_REPORTS SYSREF_ROLES SYSREF_ROUTE_BLOCK_GROUPS SYSREF_ROUTE_BLOCKS SYSREF_SCRIPTS SYSREF_SEARCHES SYSREF_SERVER_EVENTS SYSREF_SERVER_EVENTS_HISTORY SYSREF_STANDARD_ROUTE_GROUPS SYSREF_STANDARD_ROUTES SYSREF_STATUSES SYSREF_SYSTEM_SETTINGS SYSREF_TASK_HISTORY SYSREF_TASK_KIND_GROUPS SYSREF_TASK_KINDS SYSREF_TASK_RIGHTS SYSREF_TASK_SIGNATURES SYSREF_TASKS SYSREF_UNITS SYSREF_USER_GROUPS SYSREF_USER_GROUPS_REFERENCE SYSREF_USER_SUBSTITUTION SYSREF_USERS SYSREF_USERS_REFERENCE SYSREF_VIEWERS SYSREF_WORKING_TIME_CALENDARS ACCESS_RIGHTS_TABLE_NAME EDMS_ACCESS_TABLE_NAME EDOC_TYPES_TABLE_NAME TEST_DEV_DB_NAME TEST_DEV_SYSTEM_CODE TEST_EDMS_DB_NAME TEST_EDMS_MAIN_CODE TEST_EDMS_MAIN_DB_NAME TEST_EDMS_SECOND_CODE TEST_EDMS_SECOND_DB_NAME TEST_EDMS_SYSTEM_CODE TEST_ISB5_MAIN_CODE TEST_ISB5_SECOND_CODE TEST_SQL_SERVER_2005_NAME TEST_SQL_SERVER_NAME ATTENTION_CAPTION cbsCommandLinks cbsDefault CONFIRMATION_CAPTION ERROR_CAPTION INFORMATION_CAPTION mrCancel mrOk EDOC_VERSION_ACTIVE_STAGE_CODE EDOC_VERSION_DESIGN_STAGE_CODE EDOC_VERSION_OBSOLETE_STAGE_CODE cpDataEnciphermentEnabled cpDigitalSignatureEnabled cpID cpIssuer cpPluginVersion cpSerial cpSubjectName cpSubjSimpleName cpValidFromDate cpValidToDate ISBL_SYNTAX NO_SYNTAX XML_SYNTAX WAIT_BLOCK_AFTER_FINISH_EVENT WAIT_BLOCK_BEFORE_START_EVENT WAIT_BLOCK_DEADLINE_PROPERTY WAIT_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY WAIT_BLOCK_NAME_PROPERTY WAIT_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY SYSRES_COMMON SYSRES_CONST SYSRES_MBFUNC SYSRES_SBDATA SYSRES_SBGUI SYSRES_SBINTF SYSRES_SBREFDSC SYSRES_SQLERRORS SYSRES_SYSCOMP atUser atGroup atRole aemEnabledAlways aemDisabledAlways aemEnabledOnBrowse aemEnabledOnEdit aemDisabledOnBrowseEmpty apBegin apEnd alLeft alRight asmNever asmNoButCustomize asmAsLastTime asmYesButCustomize asmAlways cirCommon cirRevoked ctSignature ctEncode ctSignatureEncode clbUnchecked clbChecked clbGrayed ceISB ceAlways ceNever ctDocument ctReference ctScript ctUnknown ctReport ctDialog ctFunction ctFolder ctEDocument ctTask ctJob ctNotice ctControlJob cfInternal cfDisplay ciUnspecified ciWrite ciRead ckFolder ckEDocument ckTask ckJob ckComponentToken ckAny ckReference ckScript ckReport ckDialog ctISBLEditor ctBevel ctButton ctCheckListBox ctComboBox ctComboEdit ctGrid ctDBCheckBox ctDBComboBox ctDBEdit ctDBEllipsis ctDBMemo ctDBNavigator ctDBRadioGroup ctDBStatusLabel ctEdit ctGroupBox ctInplaceHint ctMemo ctPanel ctListBox ctRadioButton ctRichEdit ctTabSheet ctWebBrowser ctImage ctHyperLink ctLabel ctDBMultiEllipsis ctRibbon ctRichView ctInnerPanel ctPanelGroup ctBitButton cctDate cctInteger cctNumeric cctPick cctReference cctString cctText cltInternal cltPrimary cltGUI dseBeforeOpen dseAfterOpen dseBeforeClose dseAfterClose dseOnValidDelete dseBeforeDelete dseAfterDelete dseAfterDeleteOutOfTransaction dseOnDeleteError dseBeforeInsert dseAfterInsert dseOnValidUpdate dseBeforeUpdate dseOnUpdateRatifiedRecord dseAfterUpdate dseAfterUpdateOutOfTransaction dseOnUpdateError dseAfterScroll dseOnOpenRecord dseOnCloseRecord dseBeforeCancel dseAfterCancel dseOnUpdateDeadlockError dseBeforeDetailUpdate dseOnPrepareUpdate dseOnAnyRequisiteChange dssEdit dssInsert dssBrowse dssInActive dftDate dftShortDate dftDateTime dftTimeStamp dotDays dotHours dotMinutes dotSeconds dtkndLocal dtkndUTC arNone arView arEdit arFull ddaView ddaEdit emLock emEdit emSign emExportWithLock emImportWithUnlock emChangeVersionNote emOpenForModify emChangeLifeStage emDelete emCreateVersion emImport emUnlockExportedWithLock emStart emAbort emReInit emMarkAsReaded emMarkAsUnreaded emPerform emAccept emResume emChangeRights emEditRoute emEditObserver emRecoveryFromLocalCopy emChangeWorkAccessType emChangeEncodeTypeToCertificate emChangeEncodeTypeToPassword emChangeEncodeTypeToNone emChangeEncodeTypeToCertificatePassword emChangeStandardRoute emGetText emOpenForView emMoveToStorage emCreateObject emChangeVersionHidden emDeleteVersion emChangeLifeCycleStage emApprovingSign emExport emContinue emLockFromEdit emUnLockForEdit emLockForServer emUnlockFromServer emDelegateAccessRights emReEncode ecotFile ecotProcess eaGet eaCopy eaCreate eaCreateStandardRoute edltAll edltNothing edltQuery essmText essmCard esvtLast esvtLastActive esvtSpecified edsfExecutive edsfArchive edstSQLServer edstFile edvstNone edvstEDocumentVersionCopy edvstFile edvstTemplate edvstScannedFile vsDefault vsDesign vsActive vsObsolete etNone etCertificate etPassword etCertificatePassword ecException ecWarning ecInformation estAll estApprovingOnly evtLast evtLastActive evtQuery fdtString fdtNumeric fdtInteger fdtDate fdtText fdtUnknown fdtWideString fdtLargeInteger ftInbox ftOutbox ftFavorites ftCommonFolder ftUserFolder ftComponents ftQuickLaunch ftShortcuts ftSearch grhAuto grhX1 grhX2 grhX3 hltText hltRTF hltHTML iffBMP iffJPEG iffMultiPageTIFF iffSinglePageTIFF iffTIFF iffPNG im8bGrayscale im24bRGB im1bMonochrome itBMP itJPEG itWMF itPNG ikhInformation ikhWarning ikhError ikhNoIcon icUnknown icScript icFunction icIntegratedReport icAnalyticReport icDataSetEventHandler icActionHandler icFormEventHandler icLookUpEventHandler icRequisiteChangeEventHandler icBeforeSearchEventHandler icRoleCalculation icSelectRouteEventHandler icBlockPropertyCalculation icBlockQueryParamsEventHandler icChangeSearchResultEventHandler icBlockEventHandler icSubTaskInitEventHandler icEDocDataSetEventHandler icEDocLookUpEventHandler icEDocActionHandler icEDocFormEventHandler icEDocRequisiteChangeEventHandler icStructuredConversionRule icStructuredConversionEventBefore icStructuredConversionEventAfter icWizardEventHandler icWizardFinishEventHandler icWizardStepEventHandler icWizardStepFinishEventHandler icWizardActionEnableEventHandler icWizardActionExecuteEventHandler icCreateJobsHandler icCreateNoticesHandler icBeforeLookUpEventHandler icAfterLookUpEventHandler icTaskAbortEventHandler icWorkflowBlockActionHandler icDialogDataSetEventHandler icDialogActionHandler icDialogLookUpEventHandler icDialogRequisiteChangeEventHandler icDialogFormEventHandler icDialogValidCloseEventHandler icBlockFormEventHandler icTaskFormEventHandler icReferenceMethod icEDocMethod icDialogMethod icProcessMessageHandler isShow isHide isByUserSettings jkJob jkNotice jkControlJob jtInner jtLeft jtRight jtFull jtCross lbpAbove lbpBelow lbpLeft lbpRight eltPerConnection eltPerUser sfcUndefined sfcBlack sfcGreen sfcRed sfcBlue sfcOrange sfcLilac sfsItalic sfsStrikeout sfsNormal ldctStandardRoute ldctWizard ldctScript ldctFunction ldctRouteBlock ldctIntegratedReport ldctAnalyticReport ldctReferenceType ldctEDocumentType ldctDialog ldctServerEvents mrcrtNone mrcrtUser mrcrtMaximal mrcrtCustom vtEqual vtGreaterOrEqual vtLessOrEqual vtRange rdYesterday rdToday rdTomorrow rdThisWeek rdThisMonth rdThisYear rdNextMonth rdNextWeek rdLastWeek rdLastMonth rdWindow rdFile rdPrinter rdtString rdtNumeric rdtInteger rdtDate rdtReference rdtAccount rdtText rdtPick rdtUnknown rdtLargeInteger rdtDocument reOnChange reOnChangeValues ttGlobal ttLocal ttUser ttSystem ssmBrowse ssmSelect ssmMultiSelect ssmBrowseModal smSelect smLike smCard stNone stAuthenticating stApproving sctString sctStream sstAnsiSort sstNaturalSort svtEqual svtContain soatString soatNumeric soatInteger soatDatetime soatReferenceRecord soatText soatPick soatBoolean soatEDocument soatAccount soatIntegerCollection soatNumericCollection soatStringCollection soatPickCollection soatDatetimeCollection soatBooleanCollection soatReferenceRecordCollection soatEDocumentCollection soatAccountCollection soatContents soatUnknown tarAbortByUser tarAbortByWorkflowException tvtAllWords tvtExactPhrase tvtAnyWord usNone usCompleted usRedSquare usBlueSquare usYellowSquare usGreenSquare usOrangeSquare usPurpleSquare usFollowUp utUnknown utUser utDeveloper utAdministrator utSystemDeveloper utDisconnected btAnd btDetailAnd btOr btNotOr btOnly vmView vmSelect vmNavigation vsmSingle vsmMultiple vsmMultipleCheck vsmNoSelection wfatPrevious wfatNext wfatCancel wfatFinish wfepUndefined wfepText3 wfepText6 wfepText9 wfepSpinEdit wfepDropDown wfepRadioGroup wfepFlag wfepText12 wfepText15 wfepText18 wfepText21 wfepText24 wfepText27 wfepText30 wfepRadioGroupColumn1 wfepRadioGroupColumn2 wfepRadioGroupColumn3 wfetQueryParameter wfetText wfetDelimiter wfetLabel wptString wptInteger wptNumeric wptBoolean wptDateTime wptPick wptText wptUser wptUserList wptEDocumentInfo wptEDocumentInfoList wptReferenceRecordInfo wptReferenceRecordInfoList wptFolderInfo wptTaskInfo wptContents wptFileName wptDate wsrComplete wsrGoNext wsrGoPrevious wsrCustom wsrCancel wsrGoFinal wstForm wstEDocument wstTaskCard wstReferenceRecordCard wstFinal waAll waPerformers waManual wsbStart wsbFinish wsbNotice wsbStep wsbDecision wsbWait wsbMonitor wsbScript wsbConnector wsbSubTask wsbLifeCycleStage wsbPause wdtInteger wdtFloat wdtString wdtPick wdtDateTime wdtBoolean wdtTask wdtJob wdtFolder wdtEDocument wdtReferenceRecord wdtUser wdtGroup wdtRole wdtIntegerCollection wdtFloatCollection wdtStringCollection wdtPickCollection wdtDateTimeCollection wdtBooleanCollection wdtTaskCollection wdtJobCollection wdtFolderCollection wdtEDocumentCollection wdtReferenceRecordCollection wdtUserCollection wdtGroupCollection wdtRoleCollection wdtContents wdtUserList wdtSearchDescription wdtDeadLine wdtPickSet wdtAccountCollection wiLow wiNormal wiHigh wrtSoft wrtHard wsInit wsRunning wsDone wsControlled wsAborted wsContinued wtmFull wtmFromCurrent wtmOnlyCurrent ",
 class:"AltState Application CallType ComponentTokens CreatedJobs CreatedNotices ControlState DialogResult Dialogs EDocuments EDocumentVersionSource Folders GlobalIDs Job Jobs InputValue LookUpReference LookUpRequisiteNames LookUpSearch Object ParentComponent Processes References Requisite ReportName Reports Result Scripts Searches SelectedAttachments SelectedItems SelectMode Sender ServerEvents ServiceFactory ShiftState SubTask SystemDialogs Tasks Wizard Wizards Work \u0412\u044b\u0437\u043e\u0432\u0421\u043f\u043e\u0441\u043e\u0431 \u0418\u043c\u044f\u041e\u0442\u0447\u0435\u0442\u0430 \u0420\u0435\u043a\u0432\u0417\u043d\u0430\u0447 ",
-literal:"null true false nil "};a={begin:"\\.\\s*"+a.UNDERSCORE_IDENT_RE,keywords:e,relevance:0};var f={className:"type",begin:":[ \\t]*("+"IApplication IAccessRights IAccountRepository IAccountSelectionRestrictions IAction IActionList IAdministrationHistoryDescription IAnchors IApplication IArchiveInfo IAttachment IAttachmentList ICheckListBox ICheckPointedList IColumn IComponent IComponentDescription IComponentToken IComponentTokenFactory IComponentTokenInfo ICompRecordInfo IConnection IContents IControl IControlJob IControlJobInfo IControlList ICrypto ICrypto2 ICustomJob ICustomJobInfo ICustomListBox ICustomObjectWizardStep ICustomWork ICustomWorkInfo IDataSet IDataSetAccessInfo IDataSigner IDateCriterion IDateRequisite IDateRequisiteDescription IDateValue IDeaAccessRights IDeaObjectInfo IDevelopmentComponentLock IDialog IDialogFactory IDialogPickRequisiteItems IDialogsFactory IDICSFactory IDocRequisite IDocumentInfo IDualListDialog IECertificate IECertificateInfo IECertificates IEditControl IEditorForm IEdmsExplorer IEdmsObject IEdmsObjectDescription IEdmsObjectFactory IEdmsObjectInfo IEDocument IEDocumentAccessRights IEDocumentDescription IEDocumentEditor IEDocumentFactory IEDocumentInfo IEDocumentStorage IEDocumentVersion IEDocumentVersionListDialog IEDocumentVersionSource IEDocumentWizardStep IEDocVerSignature IEDocVersionState IEnabledMode IEncodeProvider IEncrypter IEvent IEventList IException IExternalEvents IExternalHandler IFactory IField IFileDialog IFolder IFolderDescription IFolderDialog IFolderFactory IFolderInfo IForEach IForm IFormTitle IFormWizardStep IGlobalIDFactory IGlobalIDInfo IGrid IHasher IHistoryDescription IHyperLinkControl IImageButton IImageControl IInnerPanel IInplaceHint IIntegerCriterion IIntegerList IIntegerRequisite IIntegerValue IISBLEditorForm IJob IJobDescription IJobFactory IJobForm IJobInfo ILabelControl ILargeIntegerCriterion ILargeIntegerRequisite ILargeIntegerValue ILicenseInfo ILifeCycleStage IList IListBox ILocalIDInfo ILocalization ILock IMemoryDataSet IMessagingFactory IMetadataRepository INotice INoticeInfo INumericCriterion INumericRequisite INumericValue IObject IObjectDescription IObjectImporter IObjectInfo IObserver IPanelGroup IPickCriterion IPickProperty IPickRequisite IPickRequisiteDescription IPickRequisiteItem IPickRequisiteItems IPickValue IPrivilege IPrivilegeList IProcess IProcessFactory IProcessMessage IProgress IProperty IPropertyChangeEvent IQuery IReference IReferenceCriterion IReferenceEnabledMode IReferenceFactory IReferenceHistoryDescription IReferenceInfo IReferenceRecordCardWizardStep IReferenceRequisiteDescription IReferencesFactory IReferenceValue IRefRequisite IReport IReportFactory IRequisite IRequisiteDescription IRequisiteDescriptionList IRequisiteFactory IRichEdit IRouteStep IRule IRuleList ISchemeBlock IScript IScriptFactory ISearchCriteria ISearchCriterion ISearchDescription ISearchFactory ISearchFolderInfo ISearchForObjectDescription ISearchResultRestrictions ISecuredContext ISelectDialog IServerEvent IServerEventFactory IServiceDialog IServiceFactory ISignature ISignProvider ISignProvider2 ISignProvider3 ISimpleCriterion IStringCriterion IStringList IStringRequisite IStringRequisiteDescription IStringValue ISystemDialogsFactory ISystemInfo ITabSheet ITask ITaskAbortReasonInfo ITaskCardWizardStep ITaskDescription ITaskFactory ITaskInfo ITaskRoute ITextCriterion ITextRequisite ITextValue ITreeListSelectDialog IUser IUserList IValue IView IWebBrowserControl IWizard IWizardAction IWizardFactory IWizardFormElement IWizardParam IWizardPickParam IWizardReferenceParam IWizardStep IWorkAccessRights IWorkDescription IWorkflowAskableParam IWorkflowAskableParams IWorkflowBlock IWorkflowBlockResult IWorkflowEnabledMode IWorkflowParam IWorkflowPickParam IWorkflowReferenceParam IWorkState IWorkTreeCustomNode IWorkTreeJobNode IWorkTreeTaskNode IXMLEditorForm SBCrypto".replace(/\s/g,
-"|")+")",end:"[ \\t]*=",excludeEnd:!0},h={className:"variable",keywords:e,begin:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_!][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*",relevance:0,contains:[f,a]};return{name:"ISBL",aliases:["isbl"],case_insensitive:!0,keywords:e,illegal:"\\$|\\?|%|,|;$|~|#|@|</",contains:[{className:"function",begin:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*\\(",end:"\\)$",returnBegin:!0,keywords:e,illegal:"[\\[\\]\\|\\$\\?%,~#@]",
-contains:[{className:"title",keywords:{$pattern:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_!][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*",built_in:"AddSubString AdjustLineBreaks AmountInWords Analysis ArrayDimCount ArrayHighBound ArrayLowBound ArrayOf ArrayReDim Assert Assigned BeginOfMonth BeginOfPeriod BuildProfilingOperationAnalysis CallProcedure CanReadFile CArrayElement CDataSetRequisite ChangeDate ChangeReferenceDataset Char CharPos CheckParam CheckParamValue CompareStrings ConstantExists ControlState ConvertDateStr Copy CopyFile CreateArray CreateCachedReference CreateConnection CreateDialog CreateDualListDialog CreateEditor CreateException CreateFile CreateFolderDialog CreateInputDialog CreateLinkFile CreateList CreateLock CreateMemoryDataSet CreateObject CreateOpenDialog CreateProgress CreateQuery CreateReference CreateReport CreateSaveDialog CreateScript CreateSQLPivotFunction CreateStringList CreateTreeListSelectDialog CSelectSQL CSQL CSubString CurrentUserID CurrentUserName CurrentVersion DataSetLocateEx DateDiff DateTimeDiff DateToStr DayOfWeek DeleteFile DirectoryExists DisableCheckAccessRights DisableCheckFullShowingRestriction DisableMassTaskSendingRestrictions DropTable DupeString EditText EnableCheckAccessRights EnableCheckFullShowingRestriction EnableMassTaskSendingRestrictions EndOfMonth EndOfPeriod ExceptionExists ExceptionsOff ExceptionsOn Execute ExecuteProcess Exit ExpandEnvironmentVariables ExtractFileDrive ExtractFileExt ExtractFileName ExtractFilePath ExtractParams FileExists FileSize FindFile FindSubString FirmContext ForceDirectories Format FormatDate FormatNumeric FormatSQLDate FormatString FreeException GetComponent GetComponentLaunchParam GetConstant GetLastException GetReferenceRecord GetRefTypeByRefID GetTableID GetTempFolder IfThen In IndexOf InputDialog InputDialogEx InteractiveMode IsFileLocked IsGraphicFile IsNumeric Length LoadString LoadStringFmt LocalTimeToUTC LowerCase Max MessageBox MessageBoxEx MimeDecodeBinary MimeDecodeString MimeEncodeBinary MimeEncodeString Min MoneyInWords MoveFile NewID Now OpenFile Ord Precision Raise ReadCertificateFromFile ReadFile ReferenceCodeByID ReferenceNumber ReferenceRequisiteMode ReferenceRequisiteValue RegionDateSettings RegionNumberSettings RegionTimeSettings RegRead RegWrite RenameFile Replace Round SelectServerCode SelectSQL ServerDateTime SetConstant SetManagedFolderFieldsState ShowConstantsInputDialog ShowMessage Sleep Split SQL SQL2XLSTAB SQLProfilingSendReport StrToDate SubString SubStringCount SystemSetting Time TimeDiff Today Transliterate Trim UpperCase UserStatus UTCToLocalTime ValidateXML VarIsClear VarIsEmpty VarIsNull WorkTimeDiff WriteFile WriteFileEx WriteObjectHistory \u0410\u043d\u0430\u043b\u0438\u0437 \u0411\u0430\u0437\u0430\u0414\u0430\u043d\u043d\u044b\u0445 \u0411\u043b\u043e\u043a\u0415\u0441\u0442\u044c \u0411\u043b\u043e\u043a\u0415\u0441\u0442\u044c\u0420\u0430\u0441\u0448 \u0411\u043b\u043e\u043a\u0418\u043d\u0444\u043e \u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c \u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c\u0420\u0430\u0441\u0448 \u0411\u043b\u043e\u043a\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0412\u0432\u043e\u0434 \u0412\u0432\u043e\u0434\u041c\u0435\u043d\u044e \u0412\u0435\u0434\u0421 \u0412\u0435\u0434\u0421\u043f\u0440 \u0412\u0435\u0440\u0445\u043d\u044f\u044f\u0413\u0440\u0430\u043d\u0438\u0446\u0430\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u0412\u043d\u0435\u0448\u041f\u0440\u043e\u0433\u0440 \u0412\u043e\u0441\u0441\u0442 \u0412\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f\u041f\u0430\u043f\u043a\u0430 \u0412\u0440\u0435\u043c\u044f \u0412\u044b\u0431\u043e\u0440SQL \u0412\u044b\u0431\u0440\u0430\u0442\u044c\u0417\u0430\u043f\u0438\u0441\u044c \u0412\u044b\u0434\u0435\u043b\u0438\u0442\u044c\u0421\u0442\u0440 \u0412\u044b\u0437\u0432\u0430\u0442\u044c \u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0412\u044b\u043f\u041f\u0440\u043e\u0433\u0440 \u0413\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u0438\u0439\u0424\u0430\u0439\u043b \u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0414\u0430\u0442\u0430\u0412\u0440\u0435\u043c\u044f\u0421\u0435\u0440\u0432 \u0414\u0435\u043d\u044c\u041d\u0435\u0434\u0435\u043b\u0438 \u0414\u0438\u0430\u043b\u043e\u0433\u0414\u0430\u041d\u0435\u0442 \u0414\u043b\u0438\u043d\u0430\u0421\u0442\u0440 \u0414\u043e\u0431\u041f\u043e\u0434\u0441\u0442\u0440 \u0415\u041f\u0443\u0441\u0442\u043e \u0415\u0441\u043b\u0438\u0422\u043e \u0415\u0427\u0438\u0441\u043b\u043e \u0417\u0430\u043c\u041f\u043e\u0434\u0441\u0442\u0440 \u0417\u0430\u043f\u0438\u0441\u044c\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0430 \u0417\u043d\u0430\u0447\u041f\u043e\u043b\u044f\u0421\u043f\u0440 \u0418\u0414\u0422\u0438\u043f\u0421\u043f\u0440 \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u0414\u0438\u0441\u043a \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u0418\u043c\u044f\u0424\u0430\u0439\u043b\u0430 \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u041f\u0443\u0442\u044c \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0418\u0437\u043c\u0414\u0430\u0442 \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c\u0420\u0430\u0437\u043c\u0435\u0440\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u0418\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u0439\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u0418\u043c\u044f\u041e\u0440\u0433 \u0418\u043c\u044f\u041f\u043e\u043b\u044f\u0421\u043f\u0440 \u0418\u043d\u0434\u0435\u043a\u0441 \u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0417\u0430\u043a\u0440\u044b\u0442\u044c \u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0428\u0430\u0433 \u0418\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439\u0420\u0435\u0436\u0438\u043c \u0418\u0442\u043e\u0433\u0422\u0431\u043b\u0421\u043f\u0440 \u041a\u043e\u0434\u0412\u0438\u0434\u0412\u0435\u0434\u0421\u043f\u0440 \u041a\u043e\u0434\u0412\u0438\u0434\u0421\u043f\u0440\u041f\u043e\u0418\u0414 \u041a\u043e\u0434\u041f\u043eAnalit \u041a\u043e\u0434\u0421\u0438\u043c\u0432\u043e\u043b\u0430 \u041a\u043e\u0434\u0421\u043f\u0440 \u041a\u043e\u043b\u041f\u043e\u0434\u0441\u0442\u0440 \u041a\u043e\u043b\u041f\u0440\u043e\u043f \u041a\u043e\u043d\u041c\u0435\u0441 \u041a\u043e\u043d\u0441\u0442 \u041a\u043e\u043d\u0441\u0442\u0415\u0441\u0442\u044c \u041a\u043e\u043d\u0441\u0442\u0417\u043d\u0430\u0447 \u041a\u043e\u043d\u0422\u0440\u0430\u043d \u041a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0424\u0430\u0439\u043b \u041a\u043e\u043f\u0438\u044f\u0421\u0442\u0440 \u041a\u041f\u0435\u0440\u0438\u043e\u0434 \u041a\u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u041c\u0430\u043a\u0441 \u041c\u0430\u043a\u0441\u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u041c\u0430\u0441\u0441\u0438\u0432 \u041c\u0435\u043d\u044e \u041c\u0435\u043d\u044e\u0420\u0430\u0441\u0448 \u041c\u0438\u043d \u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445\u041d\u0430\u0439\u0442\u0438\u0420\u0430\u0441\u0448 \u041d\u0430\u0438\u043c\u0412\u0438\u0434\u0421\u043f\u0440 \u041d\u0430\u0438\u043c\u041f\u043eAnalit \u041d\u0430\u0438\u043c\u0421\u043f\u0440 \u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c\u041f\u0435\u0440\u0435\u0432\u043e\u0434\u044b\u0421\u0442\u0440\u043e\u043a \u041d\u0430\u0447\u041c\u0435\u0441 \u041d\u0430\u0447\u0422\u0440\u0430\u043d \u041d\u0438\u0436\u043d\u044f\u044f\u0413\u0440\u0430\u043d\u0438\u0446\u0430\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u041d\u043e\u043c\u0435\u0440\u0421\u043f\u0440 \u041d\u041f\u0435\u0440\u0438\u043e\u0434 \u041e\u043a\u043d\u043e \u041e\u043a\u0440 \u041e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u041e\u0442\u043b\u0418\u043d\u0444\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u041e\u0442\u043b\u0418\u043d\u0444\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u041e\u0442\u0447\u0435\u0442 \u041e\u0442\u0447\u0435\u0442\u0410\u043d\u0430\u043b \u041e\u0442\u0447\u0435\u0442\u0418\u043d\u0442 \u041f\u0430\u043f\u043a\u0430\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u041f\u0430\u0443\u0437\u0430 \u041f\u0412\u044b\u0431\u043e\u0440SQL \u041f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u044c\u0424\u0430\u0439\u043b \u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u041f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c\u0424\u0430\u0439\u043b \u041f\u043e\u0434\u0441\u0442\u0440 \u041f\u043e\u0438\u0441\u043a\u041f\u043e\u0434\u0441\u0442\u0440 \u041f\u043e\u0438\u0441\u043a\u0421\u0442\u0440 \u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0418\u0414\u0422\u0430\u0431\u043b\u0438\u0446\u044b \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0418\u0414 \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0418\u043c\u044f \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0421\u0442\u0430\u0442\u0443\u0441 \u041f\u0440\u0435\u0440\u0432\u0430\u0442\u044c \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0417\u043d\u0430\u0447 \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\u0423\u0441\u043b\u043e\u0432\u0438\u0435 \u0420\u0430\u0437\u0431\u0421\u0442\u0440 \u0420\u0430\u0437\u043d\u0412\u0440\u0435\u043c\u044f \u0420\u0430\u0437\u043d\u0414\u0430\u0442 \u0420\u0430\u0437\u043d\u0414\u0430\u0442\u0430\u0412\u0440\u0435\u043c\u044f \u0420\u0430\u0437\u043d\u0420\u0430\u0431\u0412\u0440\u0435\u043c\u044f \u0420\u0435\u0433\u0423\u0441\u0442\u0412\u0440\u0435\u043c \u0420\u0435\u0433\u0423\u0441\u0442\u0414\u0430\u0442 \u0420\u0435\u0433\u0423\u0441\u0442\u0427\u0441\u043b \u0420\u0435\u0434\u0422\u0435\u043a\u0441\u0442 \u0420\u0435\u0435\u0441\u0442\u0440\u0417\u0430\u043f\u0438\u0441\u044c \u0420\u0435\u0435\u0441\u0442\u0440\u0421\u043f\u0438\u0441\u043e\u043a\u0418\u043c\u0435\u043d\u041f\u0430\u0440\u0430\u043c \u0420\u0435\u0435\u0441\u0442\u0440\u0427\u0442\u0435\u043d\u0438\u0435 \u0420\u0435\u043a\u0432\u0421\u043f\u0440 \u0420\u0435\u043a\u0432\u0421\u043f\u0440\u041f\u0440 \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0421\u0435\u0439\u0447\u0430\u0441 \u0421\u0435\u0440\u0432\u0435\u0440 \u0421\u0435\u0440\u0432\u0435\u0440\u041f\u0440\u043e\u0446\u0435\u0441\u0441\u0418\u0414 \u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0424\u0430\u0439\u043b\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0421\u0436\u041f\u0440\u043e\u0431 \u0421\u0438\u043c\u0432\u043e\u043b \u0421\u0438\u0441\u0442\u0435\u043c\u0430\u0414\u0438\u0440\u0435\u043a\u0442\u0443\u043c\u041a\u043e\u0434 \u0421\u0438\u0441\u0442\u0435\u043c\u0430\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0421\u0438\u0441\u0442\u0435\u043c\u0430\u041a\u043e\u0434 \u0421\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435\u0417\u0430\u043a\u0440\u044b\u0442\u044c \u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u0412\u044b\u0431\u043e\u0440\u0430\u0418\u0437\u0414\u0432\u0443\u0445\u0421\u043f\u0438\u0441\u043a\u043e\u0432 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u0412\u044b\u0431\u043e\u0440\u0430\u041f\u0430\u043f\u043a\u0438 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u044f\u0424\u0430\u0439\u043b\u0430 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u0421\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f\u0424\u0430\u0439\u043b\u0430 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0417\u0430\u043f\u0440\u043e\u0441 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041c\u0430\u0441\u0441\u0438\u0432 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041e\u0431\u044a\u0435\u043a\u0442 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041e\u0442\u0447\u0435\u0442 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041f\u0430\u043f\u043a\u0443 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0420\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043f\u0438\u0441\u043e\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043f\u0438\u0441\u043e\u043a\u0421\u0442\u0440\u043e\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u0421\u043e\u0437\u0434\u0421\u043f\u0440 \u0421\u043e\u0441\u0442\u0421\u043f\u0440 \u0421\u043e\u0445\u0440 \u0421\u043e\u0445\u0440\u0421\u043f\u0440 \u0421\u043f\u0438\u0441\u043e\u043a\u0421\u0438\u0441\u0442\u0435\u043c \u0421\u043f\u0440 \u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0415\u0441\u0442\u044c \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c\u0420\u0430\u0441\u0448 \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0421\u043f\u0440\u0418\u0437\u043c\u041d\u0430\u0431\u0414\u0430\u043d \u0421\u043f\u0440\u041a\u043e\u0434 \u0421\u043f\u0440\u041d\u043e\u043c\u0435\u0440 \u0421\u043f\u0440\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0421\u043f\u0440\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0421\u043f\u0440\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0421\u043f\u0440\u041f\u0430\u0440\u0430\u043c \u0421\u043f\u0440\u041f\u043e\u043b\u0435\u0417\u043d\u0430\u0447 \u0421\u043f\u0440\u041f\u043e\u043b\u0435\u0418\u043c\u044f \u0421\u043f\u0440\u0420\u0435\u043a\u0432 \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u0412\u0432\u0435\u0434\u0417\u043d \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u041d\u043e\u0432\u044b\u0435 \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u041f\u0440 \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u041f\u0440\u0435\u0434\u0417\u043d \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u0420\u0435\u0436\u0438\u043c \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u0422\u0438\u043f\u0422\u0435\u043a\u0441\u0442 \u0421\u043f\u0440\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0421\u043f\u0440\u0421\u043e\u0441\u0442 \u0421\u043f\u0440\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0421\u043f\u0440\u0422\u0431\u043b\u0418\u0442\u043e\u0433 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041a\u043e\u043b \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041c\u0430\u043a\u0441 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041c\u0438\u043d \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041f\u0440\u0435\u0434 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u0421\u043b\u0435\u0434 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u0421\u043e\u0437\u0434 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u0423\u0434 \u0421\u043f\u0440\u0422\u0435\u043a\u041f\u0440\u0435\u0434\u0441\u0442 \u0421\u043f\u0440\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0421\u0440\u0430\u0432\u043d\u0438\u0442\u044c\u0421\u0442\u0440 \u0421\u0442\u0440\u0412\u0435\u0440\u0445\u0420\u0435\u0433\u0438\u0441\u0442\u0440 \u0421\u0442\u0440\u041d\u0438\u0436\u043d\u0420\u0435\u0433\u0438\u0441\u0442\u0440 \u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u0421\u0443\u043c\u041f\u0440\u043e\u043f \u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439\u041f\u0430\u0440\u0430\u043c \u0422\u0435\u043a\u0412\u0435\u0440\u0441\u0438\u044f \u0422\u0435\u043a\u041e\u0440\u0433 \u0422\u043e\u0447\u043d \u0422\u0440\u0430\u043d \u0422\u0440\u0430\u043d\u0441\u043b\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044f \u0423\u0434\u0430\u043b\u0438\u0442\u044c\u0422\u0430\u0431\u043b\u0438\u0446\u0443 \u0423\u0434\u0430\u043b\u0438\u0442\u044c\u0424\u0430\u0439\u043b \u0423\u0434\u0421\u043f\u0440 \u0423\u0434\u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u0423\u0441\u0442 \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438\u041a\u043e\u043d\u0441\u0442\u0430\u043d\u0442 \u0424\u0430\u0439\u043b\u0410\u0442\u0440\u0438\u0431\u0443\u0442\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0410\u0442\u0440\u0438\u0431\u0443\u0442\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0424\u0430\u0439\u043b\u0412\u0440\u0435\u043c\u044f \u0424\u0430\u0439\u043b\u0412\u0440\u0435\u043c\u044f\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0424\u0430\u0439\u043b\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0417\u0430\u043d\u044f\u0442 \u0424\u0430\u0439\u043b\u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0418\u0441\u043a\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041c\u043e\u0436\u043d\u043e\u0427\u0438\u0442\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0435\u0440\u0435\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0424\u0430\u0439\u043b\u0420\u0430\u0437\u043c\u0435\u0440 \u0424\u0430\u0439\u043b\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0421\u0441\u044b\u043b\u043a\u0430\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0424\u0430\u0439\u043b\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0424\u043c\u0442SQL\u0414\u0430\u0442 \u0424\u043c\u0442\u0414\u0430\u0442 \u0424\u043c\u0442\u0421\u0442\u0440 \u0424\u043c\u0442\u0427\u0441\u043b \u0424\u043e\u0440\u043c\u0430\u0442 \u0426\u041c\u0430\u0441\u0441\u0438\u0432\u042d\u043b\u0435\u043c\u0435\u043d\u0442 \u0426\u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442 \u0426\u041f\u043e\u0434\u0441\u0442\u0440 "},
-begin:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*\\(",end:"\\(",returnBegin:!0,excludeEnd:!0},a,h,c,b,d]},f,a,h,c,b,d]}}}());
-hljs.registerLanguage("java",function(){return function(a){var b={className:"meta",begin:"@[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*",contains:[{begin:/\(/,end:/\)/,contains:["self"]}]};return{name:"Java",aliases:["jsp"],keywords:"false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",illegal:/<\/|#/,
-contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(<[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(\\s*,\\s*[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*)*>)?\\s+)+"+
-a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:"false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,
-contains:[a.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:"false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",relevance:0,contains:[b,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,
-a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0},b]}}}());
-hljs.registerLanguage("javascript",function(){function a(a){for(var b=[],c=0;c<arguments.length;++c)b[c-0]=arguments[c];return b.map(function(a){return a&&a.source||a}).join("")}var b="as in of if for while finally var new function do return void else break catch instanceof with throw case default try switch continue typeof delete let yield const class debugger async await static import from export extends".split(" "),c="true false null undefined NaN Infinity".split(" "),d=[].concat("setInterval setTimeout clearInterval clearTimeout require exports eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape".split(" "),
-"arguments this super console window document localStorage module global".split(" "),"Intl DataView Number Math Date String RegExp Object Function Boolean Error Symbol Set Map WeakSet WeakMap Proxy Reflect JSON Promise Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Float32Array Array Uint8Array Uint8ClampedArray ArrayBuffer".split(" "),"EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError".split(" "));return function(e){var f=/<[A-Za-z0-9\\._:-]+/,
-h=/\/[A-Za-z0-9\\._:-]+>|\/>/,g={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:b.join(" "),literal:c.join(" "),built_in:d.join(" ")},k={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:e.C_NUMBER_RE+"n?"}],relevance:0},l={className:"subst",begin:"\\$\\{",end:"\\}",keywords:g,contains:[]},m={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,l],subLanguage:"xml"}},p={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,
-l],subLanguage:"css"}},q={className:"string",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE,l]};l.contains=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,m,p,q,k,e.REGEXP_MODE];l=l.contains.concat([{begin:/\(/,end:/\)/,contains:["self"].concat(l.contains,[e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE])},e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE]);var r={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:l};return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:g,
-contains:[e.SHEBANG({binary:"node",relevance:5}),{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,m,p,q,e.C_LINE_COMMENT_MODE,e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:"[A-Za-z$_][0-9A-Za-z$_]*(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),e.C_BLOCK_COMMENT_MODE,k,{begin:a(/[{,\n]\s*/,
-a("(?=",a(/(((\/\/.*)|(\/\*(.|\n)*\*\/))\s*)*/,"[A-Za-z$_][0-9A-Za-z$_]*\\s*:"),")")),relevance:0,contains:[{className:"attr",begin:"[A-Za-z$_][0-9A-Za-z$_]*"+a("(?=","\\s*:",")"),relevance:0}]},{begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+e.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",
-variants:[{begin:e.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:g,contains:l}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:"<>",end:"</>"},{begin:f,end:h}],subLanguage:"xml",contains:[{begin:f,end:h,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),
-r],illegal:/\[|%/},{begin:/\$[(.]/},e.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0},{begin:"(get|set)\\s+(?=[A-Za-z$_][0-9A-Za-z$_]*\\()",end:/{/,keywords:"get set",contains:[e.inherit(e.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),{begin:/\(\)/},r]}],illegal:/#(?!!)/}}}());
-hljs.registerLanguage("jboss-cli",function(){return function(a){return{name:"JBoss CLI",aliases:["wildfly-cli"],keywords:{$pattern:"[a-z-]+",keyword:"alias batch cd clear command connect connection-factory connection-info data-source deploy deployment-info deployment-overlay echo echo-dmr help history if jdbc-driver-info jms-queue|20 jms-topic|20 ls patch pwd quit read-attribute read-operation reload rollout-plan run-batch set shutdown try unalias undeploy unset version xa-data-source",literal:"true false"},
-contains:[a.HASH_COMMENT_MODE,a.QUOTE_STRING_MODE,{className:"params",begin:/--[\w\-=\/]+/},{className:"function",begin:/:[\w\-.]+/,relevance:0},{className:"string",begin:/\B(([\/.])[\w\-.\/=]+)+/},{className:"params",begin:/\(/,end:/\)/,contains:[{begin:/[\w-]+ *=/,returnBegin:!0,relevance:0,contains:[{className:"attr",begin:/[\w-]+/}]}],relevance:0}]}}}());
-hljs.registerLanguage("json",function(){return function(a){var b={literal:"true false null"},c=[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],d=[a.QUOTE_STRING_MODE,a.C_NUMBER_MODE],e={end:",",endsWithParent:!0,excludeEnd:!0,contains:d,keywords:b},f={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE],illegal:"\\n"},a.inherit(e,{begin:/:/})].concat(c),illegal:"\\S"};a={begin:"\\[",end:"\\]",contains:[a.inherit(e)],illegal:"\\S"};d.push(f,a);c.forEach(function(a){d.push(a)});
-return{name:"JSON",contains:d,keywords:b,illegal:"\\S"}}}());
-hljs.registerLanguage("julia",function(){return function(a){var b={$pattern:"[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*",keyword:"in isa where baremodule begin break catch ccall const continue do else elseif end export false finally for function global if import importall let local macro module quote return true try using while type immutable abstract bitstype typealias ",literal:"true false ARGS C_NULL DevNull ENDIAN_BOM ENV I Inf Inf16 Inf32 Inf64 InsertionSort JULIA_HOME LOAD_PATH MergeSort NaN NaN16 NaN32 NaN64 PROGRAM_FILE QuickSort RoundDown RoundFromZero RoundNearest RoundNearestTiesAway RoundNearestTiesUp RoundToZero RoundUp STDERR STDIN STDOUT VERSION catalan e|0 eu|0 eulergamma golden im nothing pi \u03b3 \u03c0 \u03c6 ",built_in:"ANY AbstractArray AbstractChannel AbstractFloat AbstractMatrix AbstractRNG AbstractSerializer AbstractSet AbstractSparseArray AbstractSparseMatrix AbstractSparseVector AbstractString AbstractUnitRange AbstractVecOrMat AbstractVector Any ArgumentError Array AssertionError Associative Base64DecodePipe Base64EncodePipe Bidiagonal BigFloat BigInt BitArray BitMatrix BitVector Bool BoundsError BufferStream CachingPool CapturedException CartesianIndex CartesianRange Cchar Cdouble Cfloat Channel Char Cint Cintmax_t Clong Clonglong ClusterManager Cmd CodeInfo Colon Complex Complex128 Complex32 Complex64 CompositeException Condition ConjArray ConjMatrix ConjVector Cptrdiff_t Cshort Csize_t Cssize_t Cstring Cuchar Cuint Cuintmax_t Culong Culonglong Cushort Cwchar_t Cwstring DataType Date DateFormat DateTime DenseArray DenseMatrix DenseVecOrMat DenseVector Diagonal Dict DimensionMismatch Dims DirectIndexString Display DivideError DomainError EOFError EachLine Enum Enumerate ErrorException Exception ExponentialBackOff Expr Factorization FileMonitor Float16 Float32 Float64 Function Future GlobalRef GotoNode HTML Hermitian IO IOBuffer IOContext IOStream IPAddr IPv4 IPv6 IndexCartesian IndexLinear IndexStyle InexactError InitError Int Int128 Int16 Int32 Int64 Int8 IntSet Integer InterruptException InvalidStateException Irrational KeyError LabelNode LinSpace LineNumberNode LoadError LowerTriangular MIME Matrix MersenneTwister Method MethodError MethodTable Module NTuple NewvarNode NullException Nullable Number ObjectIdDict OrdinalRange OutOfMemoryError OverflowError Pair ParseError PartialQuickSort PermutedDimsArray Pipe PollingFileWatcher ProcessExitedException Ptr QuoteNode RandomDevice Range RangeIndex Rational RawFD ReadOnlyMemoryError Real ReentrantLock Ref Regex RegexMatch RemoteChannel RemoteException RevString RoundingMode RowVector SSAValue SegmentationFault SerializationState Set SharedArray SharedMatrix SharedVector Signed SimpleVector Slot SlotNumber SparseMatrixCSC SparseVector StackFrame StackOverflowError StackTrace StepRange StepRangeLen StridedArray StridedMatrix StridedVecOrMat StridedVector String SubArray SubString SymTridiagonal Symbol Symmetric SystemError TCPSocket Task Text TextDisplay Timer Tridiagonal Tuple Type TypeError TypeMapEntry TypeMapLevel TypeName TypeVar TypedSlot UDPSocket UInt UInt128 UInt16 UInt32 UInt64 UInt8 UndefRefError UndefVarError UnicodeError UniformScaling Union UnionAll UnitRange Unsigned UpperTriangular Val Vararg VecElement VecOrMat Vector VersionNumber Void WeakKeyDict WeakRef WorkerConfig WorkerPool "},
-c={keywords:b,illegal:/<\//};b={className:"subst",begin:/\$\(/,end:/\)/,keywords:b};var d={className:"variable",begin:"\\$[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*"},e={className:"string",contains:[a.BACKSLASH_ESCAPE,b,d],variants:[{begin:/\w*"""/,end:/"""\w*/,relevance:10},{begin:/\w*"/,end:/"\w*/}]};d={className:"string",contains:[a.BACKSLASH_ESCAPE,b,d],begin:"`",end:"`"};c.name="Julia";c.contains=[{className:"number",begin:/(\b0x[\d_]*(\.[\d_]*)?|0x\.\d[\d_]*)p[-+]?\d+|\b0[box][a-fA-F0-9][a-fA-F0-9_]*|(\b\d[\d_]*(\.[\d_]*)?|\.\d[\d_]*)([eEfF][-+]?\d+)?/,
-relevance:0},{className:"string",begin:/'(.|\\[xXuU][a-zA-Z0-9]+)'/},e,d,{className:"meta",begin:"@[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*"},{className:"comment",variants:[{begin:"#=",end:"=#",relevance:10},{begin:"#",end:"$"}]},a.HASH_COMMENT_MODE,{className:"keyword",begin:"\\b(((abstract|primitive)\\s+)type|(mutable\\s+)?struct)\\b"},{begin:/<:/}];b.contains=c.contains;return c}}());
-hljs.registerLanguage("julia-repl",function(){return function(a){return{name:"Julia REPL",contains:[{className:"meta",begin:/^julia>/,relevance:10,starts:{end:/^(?![ ]{6})/,subLanguage:"julia"},aliases:["jldoctest"]}]}}}());
-hljs.registerLanguage("kotlin",function(){return function(a){var b={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual trait volatile transient native default",built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",
-literal:"true false null"},c={className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"@"},d={className:"subst",begin:"\\${",end:"}",contains:[a.C_NUMBER_MODE]},e={className:"variable",begin:"\\$"+a.UNDERSCORE_IDENT_RE};e={className:"string",variants:[{begin:'"""',end:'"""(?=[^"])',contains:[e,d]},{begin:"'",end:"'",illegal:/\n/,contains:[a.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[a.BACKSLASH_ESCAPE,e,d]}]};d.contains.push(e);d={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+
-a.UNDERSCORE_IDENT_RE+")?"};var f={className:"meta",begin:"@"+a.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[a.inherit(e,{className:"meta-string"})]}]},h=a.COMMENT("/\\*","\\*/",{contains:[a.C_BLOCK_COMMENT_MODE]}),g={variants:[{className:"type",begin:a.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,contains:[]}]};g.variants[1].contains=[g];g.variants[1].contains=[g];return{name:"Kotlin",aliases:["kt"],keywords:b,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",
-begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,h,{className:"keyword",begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",begin:/@\w+/}]}},c,d,f,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:b,illegal:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,relevance:5,contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"type",begin:/</,end:/>/,keywords:"reified",relevance:0},
-{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,endsWithParent:!0,contains:[g,a.C_LINE_COMMENT_MODE,h],relevance:0},a.C_LINE_COMMENT_MODE,h,d,f,e,a.C_NUMBER_MODE]},h]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},a.UNDERSCORE_TITLE_MODE,{className:"type",begin:/</,end:/>/,excludeBegin:!0,excludeEnd:!0,
-relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},d,f]},e,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0}]}}}());
-hljs.registerLanguage("lasso",function(){return function(a){var b={$pattern:"[a-zA-Z_][\\w.]*|&[lg]t;",literal:"true false none minimal full all void and or not bw nbw ew new cn ncn lt lte gt gte eq neq rx nrx ft",built_in:"array date decimal duration integer map pair string tag xml null boolean bytes keyword list locale queue set stack staticarray local var variable global data self inherited currentcapture givenblock",keyword:"cache database_names database_schemanames database_tablenames define_tag define_type email_batch encode_set html_comment handle handle_error header if inline iterate ljax_target link link_currentaction link_currentgroup link_currentrecord link_detail link_firstgroup link_firstrecord link_lastgroup link_lastrecord link_nextgroup link_nextrecord link_prevgroup link_prevrecord log loop namespace_using output_none portal private protect records referer referrer repeating resultset rows search_args search_arguments select sort_args sort_arguments thread_atomic value_list while abort case else fail_if fail_ifnot fail if_empty if_false if_null if_true loop_abort loop_continue loop_count params params_up return return_value run_children soap_definetag soap_lastrequest soap_lastresponse tag_name ascending average by define descending do equals frozen group handle_failure import in into join let match max min on order parent protected provide public require returnhome skip split_thread sum take thread to trait type where with yield yieldhome"},
-c=a.COMMENT("\x3c!--","--\x3e",{relevance:0}),d={className:"meta",begin:"\\[noprocess\\]",starts:{end:"\\[/noprocess\\]",returnEnd:!0,contains:[c]}},e={className:"meta",begin:"\\[/noprocess|<\\?(lasso(script)?|=)"};a=[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.inherit(a.C_NUMBER_MODE,{begin:a.C_NUMBER_RE+"|(-?infinity|NaN)\\b"}),a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"string",begin:"`",end:"`"},{variants:[{begin:"[#$][a-zA-Z_][\\w.]*"},
-{begin:"#",end:"\\d+",illegal:"\\W"}]},{className:"type",begin:"::\\s*",end:"[a-zA-Z_][\\w.]*",illegal:"\\W"},{className:"params",variants:[{begin:"-(?!infinity)[a-zA-Z_][\\w.]*",relevance:0},{begin:"(\\.\\.\\.)"}]},{begin:/(->|\.)\s*/,relevance:0,contains:[{className:"symbol",begin:"'[a-zA-Z_][\\w.]*'"}]},{className:"class",beginKeywords:"define",returnEnd:!0,end:"\\(|=>",contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_][\\w.]*(=(?!>))?|[-+*/%](?!>)"})]}];return{name:"Lasso",aliases:["ls","lassoscript"],
-case_insensitive:!0,keywords:b,contains:[{className:"meta",begin:"\\]|\\?>",relevance:0,starts:{end:"\\[|<\\?(lasso(script)?|=)",returnEnd:!0,relevance:0,contains:[c]}},d,e,{className:"meta",begin:"\\[no_square_brackets",starts:{end:"\\[/no_square_brackets\\]",keywords:b,contains:[{className:"meta",begin:"\\]|\\?>",relevance:0,starts:{end:"\\[noprocess\\]|<\\?(lasso(script)?|=)",returnEnd:!0,contains:[c]}},d,e].concat(a)}},{className:"meta",begin:"\\[",relevance:0},{className:"meta",begin:"^#!",end:"lasso9$",
-relevance:10}].concat(a)}}}());
-hljs.registerLanguage("latex",function(){return function(a){var b={className:"tag",begin:/\\/,relevance:0,contains:[{className:"name",variants:[{begin:/[a-zA-Z\u0430-\u044f\u0410-\u042f]+[*]?/},{begin:/[^a-zA-Z\u0430-\u044f\u0410-\u042f0-9]/}],starts:{endsWithParent:!0,relevance:0,contains:[{className:"string",variants:[{begin:/\[/,end:/\]/},{begin:/\{/,end:/\}/}]},{begin:/\s*=\s*/,endsWithParent:!0,relevance:0,contains:[{className:"number",begin:/-?\d*\.?\d+(pt|pc|mm|cm|in|dd|cc|ex|em)?/}]}]}}]};return{name:"LaTeX",
-aliases:["tex"],contains:[b,{className:"formula",contains:[b],relevance:0,variants:[{begin:/\$\$/,end:/\$\$/},{begin:/\$/,end:/\$/}]},a.COMMENT("%","$",{relevance:0})]}}}());hljs.registerLanguage("ldif",function(){return function(a){return{name:"LDIF",contains:[{className:"attribute",begin:"^dn",end:": ",excludeEnd:!0,starts:{end:"$",relevance:0},relevance:10},{className:"attribute",begin:"^\\w",end:": ",excludeEnd:!0,starts:{end:"$",relevance:0}},{className:"literal",begin:"^-",end:"$"},a.HASH_COMMENT_MODE]}}}());
-hljs.registerLanguage("leaf",function(){return function(a){return{name:"Leaf",contains:[{className:"function",begin:"#+[A-Za-z_0-9]*\\(",end:" {",returnBegin:!0,excludeEnd:!0,contains:[{className:"keyword",begin:"#+"},{className:"title",begin:"[A-Za-z_][A-Za-z_0-9]*"},{className:"params",begin:"\\(",end:"\\)",endsParent:!0,contains:[{className:"string",begin:'"',end:'"'},{className:"variable",begin:"[A-Za-z_][A-Za-z_0-9]*"}]}]}]}}}());
-hljs.registerLanguage("less",function(){return function(a){var b=[],c=[],d=function(a){return{className:"string",begin:"~?"+a+".*?"+a}},e=function(a,b,c){return{className:a,begin:b,relevance:c}},f={begin:"\\(",end:"\\)",contains:c,relevance:0};c.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d("'"),d('"'),a.CSS_NUMBER_MODE,{begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",excludeEnd:!0}},e("number","#[0-9A-Fa-f]+\\b"),f,e("variable","@@?[\\w-]+",10),e("variable","@{[\\w-]+}"),
-e("built_in","~?`[^`]*?`"),{className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0},{className:"meta",begin:"!important"});f=c.concat({begin:"{",end:"}",contains:b});var h={beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"}].concat(c)};d={begin:"([\\w-]+|@{[\\w-]+})\\s*:",returnBegin:!0,end:"[;}]",relevance:0,contains:[{className:"attribute",begin:"([\\w-]+|@{[\\w-]+})",end:":",excludeEnd:!0,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:c}}]};
-c={className:"keyword",begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{end:"[;{}]",returnEnd:!0,contains:c,relevance:0}};var g={className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:f}};e={variants:[{begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:"([\\w-]+|@{[\\w-]+})",end:"{"}],returnBegin:!0,returnEnd:!0,illegal:"[<='$\"]",relevance:0,contains:[a.C_LINE_COMMENT_MODE,
-a.C_BLOCK_COMMENT_MODE,h,e("keyword","all\\b"),e("variable","@{[\\w-]+}"),e("selector-tag","([\\w-]+|@{[\\w-]+})%?",0),e("selector-id","#([\\w-]+|@{[\\w-]+})"),e("selector-class","\\.([\\w-]+|@{[\\w-]+})",0),e("selector-tag","&",0),{className:"selector-attr",begin:"\\[",end:"\\]"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9_\-\+\(\)"'.]+/},{begin:"\\(",end:"\\)",contains:f},{begin:"!important"}]};b.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,g,d,e);return{name:"Less",case_insensitive:!0,
-illegal:"[=>'/<($\"]",contains:b}}}());
-hljs.registerLanguage("lisp",function(){return function(a){var b={className:"literal",begin:"\\b(t{1}|nil)\\b"},c={className:"number",variants:[{begin:"(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{begin:"#(o|O)[0-7]+(/[0-7]+)?"},{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},{begin:"#(c|C)\\((\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)? +(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",end:"\\)"}]},
-d=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),e=a.COMMENT(";","$",{relevance:0}),f={begin:"\\*",end:"\\*"},h={className:"symbol",begin:"[:&][a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},g={begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*",relevance:0},k={contains:[c,d,f,h,{begin:"\\(",end:"\\)",contains:["self",b,d,c,g]},g],variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{name:"quote"}},{begin:"'\\|[^]*?\\|"}]},
-l={variants:[{begin:"'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"#'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*(::[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*)*"}]},m={begin:"\\(\\s*",end:"\\)"},p={endsWithParent:!0,relevance:0};m.contains=[{className:"name",variants:[{begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"\\|[^]*?\\|"}]},p];p.contains=
-[k,l,m,b,c,d,e,f,h,{begin:"\\|[^]*?\\|"},g];return{name:"Lisp",illegal:/\S/,contains:[c,a.SHEBANG(),b,d,e,k,l,m,g]}}}());
-hljs.registerLanguage("livecodeserver",function(){return function(a){var b={className:"variable",variants:[{begin:"\\b([gtps][A-Z]{1}[a-zA-Z0-9]*)(\\[.+\\])?(?:\\s*?)"},{begin:"\\$_[A-Z]+"}],relevance:0},c=[a.C_BLOCK_COMMENT_MODE,a.HASH_COMMENT_MODE,a.COMMENT("--","$"),a.COMMENT("[^:]//","$")],d=a.inherit(a.TITLE_MODE,{variants:[{begin:"\\b_*rig[A-Z]+[A-Za-z0-9_\\-]*"},{begin:"\\b_[a-z0-9\\-]+"}]}),e=a.inherit(a.TITLE_MODE,{begin:"\\b([A-Za-z0-9_\\-]+)\\b"});return{name:"LiveCode",case_insensitive:!1,
-keywords:{keyword:"$_COOKIE $_FILES $_GET $_GET_BINARY $_GET_RAW $_POST $_POST_BINARY $_POST_RAW $_SESSION $_SERVER codepoint codepoints segment segments codeunit codeunits sentence sentences trueWord trueWords paragraph after byte bytes english the until http forever descending using line real8 with seventh for stdout finally element word words fourth before black ninth sixth characters chars stderr uInt1 uInt1s uInt2 uInt2s stdin string lines relative rel any fifth items from middle mid at else of catch then third it file milliseconds seconds second secs sec int1 int1s int4 int4s internet int2 int2s normal text item last long detailed effective uInt4 uInt4s repeat end repeat URL in try into switch to words https token binfile each tenth as ticks tick system real4 by dateItems without char character ascending eighth whole dateTime numeric short first ftp integer abbreviated abbr abbrev private case while if div mod wrap and or bitAnd bitNot bitOr bitXor among not in a an within contains ends with begins the keys of keys",
+literal:"null true false nil "},I={begin:"\\.\\s*"+S.UNDERSCORE_IDENT_RE,
+keywords:C,relevance:0},N={className:"type",
+begin:":[ \\t]*(IApplication|IAccessRights|IAccountRepository|IAccountSelectionRestrictions|IAction|IActionList|IAdministrationHistoryDescription|IAnchors|IApplication|IArchiveInfo|IAttachment|IAttachmentList|ICheckListBox|ICheckPointedList|IColumn|IComponent|IComponentDescription|IComponentToken|IComponentTokenFactory|IComponentTokenInfo|ICompRecordInfo|IConnection|IContents|IControl|IControlJob|IControlJobInfo|IControlList|ICrypto|ICrypto2|ICustomJob|ICustomJobInfo|ICustomListBox|ICustomObjectWizardStep|ICustomWork|ICustomWorkInfo|IDataSet|IDataSetAccessInfo|IDataSigner|IDateCriterion|IDateRequisite|IDateRequisiteDescription|IDateValue|IDeaAccessRights|IDeaObjectInfo|IDevelopmentComponentLock|IDialog|IDialogFactory|IDialogPickRequisiteItems|IDialogsFactory|IDICSFactory|IDocRequisite|IDocumentInfo|IDualListDialog|IECertificate|IECertificateInfo|IECertificates|IEditControl|IEditorForm|IEdmsExplorer|IEdmsObject|IEdmsObjectDescription|IEdmsObjectFactory|IEdmsObjectInfo|IEDocument|IEDocumentAccessRights|IEDocumentDescription|IEDocumentEditor|IEDocumentFactory|IEDocumentInfo|IEDocumentStorage|IEDocumentVersion|IEDocumentVersionListDialog|IEDocumentVersionSource|IEDocumentWizardStep|IEDocVerSignature|IEDocVersionState|IEnabledMode|IEncodeProvider|IEncrypter|IEvent|IEventList|IException|IExternalEvents|IExternalHandler|IFactory|IField|IFileDialog|IFolder|IFolderDescription|IFolderDialog|IFolderFactory|IFolderInfo|IForEach|IForm|IFormTitle|IFormWizardStep|IGlobalIDFactory|IGlobalIDInfo|IGrid|IHasher|IHistoryDescription|IHyperLinkControl|IImageButton|IImageControl|IInnerPanel|IInplaceHint|IIntegerCriterion|IIntegerList|IIntegerRequisite|IIntegerValue|IISBLEditorForm|IJob|IJobDescription|IJobFactory|IJobForm|IJobInfo|ILabelControl|ILargeIntegerCriterion|ILargeIntegerRequisite|ILargeIntegerValue|ILicenseInfo|ILifeCycleStage|IList|IListBox|ILocalIDInfo|ILocalization|ILock|IMemoryDataSet|IMessagingFactory|IMetadataRepository|INotice|INoticeInfo|INumericCriterion|INumericRequisite|INumericValue|IObject|IObjectDescription|IObjectImporter|IObjectInfo|IObserver|IPanelGroup|IPickCriterion|IPickProperty|IPickRequisite|IPickRequisiteDescription|IPickRequisiteItem|IPickRequisiteItems|IPickValue|IPrivilege|IPrivilegeList|IProcess|IProcessFactory|IProcessMessage|IProgress|IProperty|IPropertyChangeEvent|IQuery|IReference|IReferenceCriterion|IReferenceEnabledMode|IReferenceFactory|IReferenceHistoryDescription|IReferenceInfo|IReferenceRecordCardWizardStep|IReferenceRequisiteDescription|IReferencesFactory|IReferenceValue|IRefRequisite|IReport|IReportFactory|IRequisite|IRequisiteDescription|IRequisiteDescriptionList|IRequisiteFactory|IRichEdit|IRouteStep|IRule|IRuleList|ISchemeBlock|IScript|IScriptFactory|ISearchCriteria|ISearchCriterion|ISearchDescription|ISearchFactory|ISearchFolderInfo|ISearchForObjectDescription|ISearchResultRestrictions|ISecuredContext|ISelectDialog|IServerEvent|IServerEventFactory|IServiceDialog|IServiceFactory|ISignature|ISignProvider|ISignProvider2|ISignProvider3|ISimpleCriterion|IStringCriterion|IStringList|IStringRequisite|IStringRequisiteDescription|IStringValue|ISystemDialogsFactory|ISystemInfo|ITabSheet|ITask|ITaskAbortReasonInfo|ITaskCardWizardStep|ITaskDescription|ITaskFactory|ITaskInfo|ITaskRoute|ITextCriterion|ITextRequisite|ITextValue|ITreeListSelectDialog|IUser|IUserList|IValue|IView|IWebBrowserControl|IWizard|IWizardAction|IWizardFactory|IWizardFormElement|IWizardParam|IWizardPickParam|IWizardReferenceParam|IWizardStep|IWorkAccessRights|IWorkDescription|IWorkflowAskableParam|IWorkflowAskableParams|IWorkflowBlock|IWorkflowBlockResult|IWorkflowEnabledMode|IWorkflowParam|IWorkflowPickParam|IWorkflowReferenceParam|IWorkState|IWorkTreeCustomNode|IWorkTreeJobNode|IWorkTreeTaskNode|IXMLEditorForm|SBCrypto)",
+end:"[ \\t]*=",excludeEnd:!0},A={className:"variable",keywords:C,begin:E,
+relevance:0,contains:[N,I]
+},e="[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*\\("
+;return{name:"ISBL",case_insensitive:!0,keywords:C,
+illegal:"\\$|\\?|%|,|;$|~|#|@|</",contains:[{className:"function",begin:e,
+end:"\\)$",returnBegin:!0,keywords:C,illegal:"[\\[\\]\\|\\$\\?%,~#@]",
+contains:[{className:"title",keywords:{$pattern:E,
+built_in:"AddSubString AdjustLineBreaks AmountInWords Analysis ArrayDimCount ArrayHighBound ArrayLowBound ArrayOf ArrayReDim Assert Assigned BeginOfMonth BeginOfPeriod BuildProfilingOperationAnalysis CallProcedure CanReadFile CArrayElement CDataSetRequisite ChangeDate ChangeReferenceDataset Char CharPos CheckParam CheckParamValue CompareStrings ConstantExists ControlState ConvertDateStr Copy CopyFile CreateArray CreateCachedReference CreateConnection CreateDialog CreateDualListDialog CreateEditor CreateException CreateFile CreateFolderDialog CreateInputDialog CreateLinkFile CreateList CreateLock CreateMemoryDataSet CreateObject CreateOpenDialog CreateProgress CreateQuery CreateReference CreateReport CreateSaveDialog CreateScript CreateSQLPivotFunction CreateStringList CreateTreeListSelectDialog CSelectSQL CSQL CSubString CurrentUserID CurrentUserName CurrentVersion DataSetLocateEx DateDiff DateTimeDiff DateToStr DayOfWeek DeleteFile DirectoryExists DisableCheckAccessRights DisableCheckFullShowingRestriction DisableMassTaskSendingRestrictions DropTable DupeString EditText EnableCheckAccessRights EnableCheckFullShowingRestriction EnableMassTaskSendingRestrictions EndOfMonth EndOfPeriod ExceptionExists ExceptionsOff ExceptionsOn Execute ExecuteProcess Exit ExpandEnvironmentVariables ExtractFileDrive ExtractFileExt ExtractFileName ExtractFilePath ExtractParams FileExists FileSize FindFile FindSubString FirmContext ForceDirectories Format FormatDate FormatNumeric FormatSQLDate FormatString FreeException GetComponent GetComponentLaunchParam GetConstant GetLastException GetReferenceRecord GetRefTypeByRefID GetTableID GetTempFolder IfThen In IndexOf InputDialog InputDialogEx InteractiveMode IsFileLocked IsGraphicFile IsNumeric Length LoadString LoadStringFmt LocalTimeToUTC LowerCase Max MessageBox MessageBoxEx MimeDecodeBinary MimeDecodeString MimeEncodeBinary MimeEncodeString Min MoneyInWords MoveFile NewID Now OpenFile Ord Precision Raise ReadCertificateFromFile ReadFile ReferenceCodeByID ReferenceNumber ReferenceRequisiteMode ReferenceRequisiteValue RegionDateSettings RegionNumberSettings RegionTimeSettings RegRead RegWrite RenameFile Replace Round SelectServerCode SelectSQL ServerDateTime SetConstant SetManagedFolderFieldsState ShowConstantsInputDialog ShowMessage Sleep Split SQL SQL2XLSTAB SQLProfilingSendReport StrToDate SubString SubStringCount SystemSetting Time TimeDiff Today Transliterate Trim UpperCase UserStatus UTCToLocalTime ValidateXML VarIsClear VarIsEmpty VarIsNull WorkTimeDiff WriteFile WriteFileEx WriteObjectHistory \u0410\u043d\u0430\u043b\u0438\u0437 \u0411\u0430\u0437\u0430\u0414\u0430\u043d\u043d\u044b\u0445 \u0411\u043b\u043e\u043a\u0415\u0441\u0442\u044c \u0411\u043b\u043e\u043a\u0415\u0441\u0442\u044c\u0420\u0430\u0441\u0448 \u0411\u043b\u043e\u043a\u0418\u043d\u0444\u043e \u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c \u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c\u0420\u0430\u0441\u0448 \u0411\u043b\u043e\u043a\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0412\u0432\u043e\u0434 \u0412\u0432\u043e\u0434\u041c\u0435\u043d\u044e \u0412\u0435\u0434\u0421 \u0412\u0435\u0434\u0421\u043f\u0440 \u0412\u0435\u0440\u0445\u043d\u044f\u044f\u0413\u0440\u0430\u043d\u0438\u0446\u0430\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u0412\u043d\u0435\u0448\u041f\u0440\u043e\u0433\u0440 \u0412\u043e\u0441\u0441\u0442 \u0412\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f\u041f\u0430\u043f\u043a\u0430 \u0412\u0440\u0435\u043c\u044f \u0412\u044b\u0431\u043e\u0440SQL \u0412\u044b\u0431\u0440\u0430\u0442\u044c\u0417\u0430\u043f\u0438\u0441\u044c \u0412\u044b\u0434\u0435\u043b\u0438\u0442\u044c\u0421\u0442\u0440 \u0412\u044b\u0437\u0432\u0430\u0442\u044c \u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0412\u044b\u043f\u041f\u0440\u043e\u0433\u0440 \u0413\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u0438\u0439\u0424\u0430\u0439\u043b \u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0414\u0430\u0442\u0430\u0412\u0440\u0435\u043c\u044f\u0421\u0435\u0440\u0432 \u0414\u0435\u043d\u044c\u041d\u0435\u0434\u0435\u043b\u0438 \u0414\u0438\u0430\u043b\u043e\u0433\u0414\u0430\u041d\u0435\u0442 \u0414\u043b\u0438\u043d\u0430\u0421\u0442\u0440 \u0414\u043e\u0431\u041f\u043e\u0434\u0441\u0442\u0440 \u0415\u041f\u0443\u0441\u0442\u043e \u0415\u0441\u043b\u0438\u0422\u043e \u0415\u0427\u0438\u0441\u043b\u043e \u0417\u0430\u043c\u041f\u043e\u0434\u0441\u0442\u0440 \u0417\u0430\u043f\u0438\u0441\u044c\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0430 \u0417\u043d\u0430\u0447\u041f\u043e\u043b\u044f\u0421\u043f\u0440 \u0418\u0414\u0422\u0438\u043f\u0421\u043f\u0440 \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u0414\u0438\u0441\u043a \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u0418\u043c\u044f\u0424\u0430\u0439\u043b\u0430 \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u041f\u0443\u0442\u044c \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0418\u0437\u043c\u0414\u0430\u0442 \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c\u0420\u0430\u0437\u043c\u0435\u0440\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u0418\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u0439\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u0418\u043c\u044f\u041e\u0440\u0433 \u0418\u043c\u044f\u041f\u043e\u043b\u044f\u0421\u043f\u0440 \u0418\u043d\u0434\u0435\u043a\u0441 \u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0417\u0430\u043a\u0440\u044b\u0442\u044c \u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0428\u0430\u0433 \u0418\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439\u0420\u0435\u0436\u0438\u043c \u0418\u0442\u043e\u0433\u0422\u0431\u043b\u0421\u043f\u0440 \u041a\u043e\u0434\u0412\u0438\u0434\u0412\u0435\u0434\u0421\u043f\u0440 \u041a\u043e\u0434\u0412\u0438\u0434\u0421\u043f\u0440\u041f\u043e\u0418\u0414 \u041a\u043e\u0434\u041f\u043eAnalit \u041a\u043e\u0434\u0421\u0438\u043c\u0432\u043e\u043b\u0430 \u041a\u043e\u0434\u0421\u043f\u0440 \u041a\u043e\u043b\u041f\u043e\u0434\u0441\u0442\u0440 \u041a\u043e\u043b\u041f\u0440\u043e\u043f \u041a\u043e\u043d\u041c\u0435\u0441 \u041a\u043e\u043d\u0441\u0442 \u041a\u043e\u043d\u0441\u0442\u0415\u0441\u0442\u044c \u041a\u043e\u043d\u0441\u0442\u0417\u043d\u0430\u0447 \u041a\u043e\u043d\u0422\u0440\u0430\u043d \u041a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0424\u0430\u0439\u043b \u041a\u043e\u043f\u0438\u044f\u0421\u0442\u0440 \u041a\u041f\u0435\u0440\u0438\u043e\u0434 \u041a\u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u041c\u0430\u043a\u0441 \u041c\u0430\u043a\u0441\u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u041c\u0430\u0441\u0441\u0438\u0432 \u041c\u0435\u043d\u044e \u041c\u0435\u043d\u044e\u0420\u0430\u0441\u0448 \u041c\u0438\u043d \u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445\u041d\u0430\u0439\u0442\u0438\u0420\u0430\u0441\u0448 \u041d\u0430\u0438\u043c\u0412\u0438\u0434\u0421\u043f\u0440 \u041d\u0430\u0438\u043c\u041f\u043eAnalit \u041d\u0430\u0438\u043c\u0421\u043f\u0440 \u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c\u041f\u0435\u0440\u0435\u0432\u043e\u0434\u044b\u0421\u0442\u0440\u043e\u043a \u041d\u0430\u0447\u041c\u0435\u0441 \u041d\u0430\u0447\u0422\u0440\u0430\u043d \u041d\u0438\u0436\u043d\u044f\u044f\u0413\u0440\u0430\u043d\u0438\u0446\u0430\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u041d\u043e\u043c\u0435\u0440\u0421\u043f\u0440 \u041d\u041f\u0435\u0440\u0438\u043e\u0434 \u041e\u043a\u043d\u043e \u041e\u043a\u0440 \u041e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u041e\u0442\u043b\u0418\u043d\u0444\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u041e\u0442\u043b\u0418\u043d\u0444\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u041e\u0442\u0447\u0435\u0442 \u041e\u0442\u0447\u0435\u0442\u0410\u043d\u0430\u043b \u041e\u0442\u0447\u0435\u0442\u0418\u043d\u0442 \u041f\u0430\u043f\u043a\u0430\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u041f\u0430\u0443\u0437\u0430 \u041f\u0412\u044b\u0431\u043e\u0440SQL \u041f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u044c\u0424\u0430\u0439\u043b \u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u041f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c\u0424\u0430\u0439\u043b \u041f\u043e\u0434\u0441\u0442\u0440 \u041f\u043e\u0438\u0441\u043a\u041f\u043e\u0434\u0441\u0442\u0440 \u041f\u043e\u0438\u0441\u043a\u0421\u0442\u0440 \u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0418\u0414\u0422\u0430\u0431\u043b\u0438\u0446\u044b \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0418\u0414 \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0418\u043c\u044f \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0421\u0442\u0430\u0442\u0443\u0441 \u041f\u0440\u0435\u0440\u0432\u0430\u0442\u044c \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0417\u043d\u0430\u0447 \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\u0423\u0441\u043b\u043e\u0432\u0438\u0435 \u0420\u0430\u0437\u0431\u0421\u0442\u0440 \u0420\u0430\u0437\u043d\u0412\u0440\u0435\u043c\u044f \u0420\u0430\u0437\u043d\u0414\u0430\u0442 \u0420\u0430\u0437\u043d\u0414\u0430\u0442\u0430\u0412\u0440\u0435\u043c\u044f \u0420\u0430\u0437\u043d\u0420\u0430\u0431\u0412\u0440\u0435\u043c\u044f \u0420\u0435\u0433\u0423\u0441\u0442\u0412\u0440\u0435\u043c \u0420\u0435\u0433\u0423\u0441\u0442\u0414\u0430\u0442 \u0420\u0435\u0433\u0423\u0441\u0442\u0427\u0441\u043b \u0420\u0435\u0434\u0422\u0435\u043a\u0441\u0442 \u0420\u0435\u0435\u0441\u0442\u0440\u0417\u0430\u043f\u0438\u0441\u044c \u0420\u0435\u0435\u0441\u0442\u0440\u0421\u043f\u0438\u0441\u043e\u043a\u0418\u043c\u0435\u043d\u041f\u0430\u0440\u0430\u043c \u0420\u0435\u0435\u0441\u0442\u0440\u0427\u0442\u0435\u043d\u0438\u0435 \u0420\u0435\u043a\u0432\u0421\u043f\u0440 \u0420\u0435\u043a\u0432\u0421\u043f\u0440\u041f\u0440 \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0421\u0435\u0439\u0447\u0430\u0441 \u0421\u0435\u0440\u0432\u0435\u0440 \u0421\u0435\u0440\u0432\u0435\u0440\u041f\u0440\u043e\u0446\u0435\u0441\u0441\u0418\u0414 \u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0424\u0430\u0439\u043b\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0421\u0436\u041f\u0440\u043e\u0431 \u0421\u0438\u043c\u0432\u043e\u043b \u0421\u0438\u0441\u0442\u0435\u043c\u0430\u0414\u0438\u0440\u0435\u043a\u0442\u0443\u043c\u041a\u043e\u0434 \u0421\u0438\u0441\u0442\u0435\u043c\u0430\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0421\u0438\u0441\u0442\u0435\u043c\u0430\u041a\u043e\u0434 \u0421\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435\u0417\u0430\u043a\u0440\u044b\u0442\u044c \u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u0412\u044b\u0431\u043e\u0440\u0430\u0418\u0437\u0414\u0432\u0443\u0445\u0421\u043f\u0438\u0441\u043a\u043e\u0432 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u0412\u044b\u0431\u043e\u0440\u0430\u041f\u0430\u043f\u043a\u0438 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u044f\u0424\u0430\u0439\u043b\u0430 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u0421\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f\u0424\u0430\u0439\u043b\u0430 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0417\u0430\u043f\u0440\u043e\u0441 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041c\u0430\u0441\u0441\u0438\u0432 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041e\u0431\u044a\u0435\u043a\u0442 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041e\u0442\u0447\u0435\u0442 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041f\u0430\u043f\u043a\u0443 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0420\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043f\u0438\u0441\u043e\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043f\u0438\u0441\u043e\u043a\u0421\u0442\u0440\u043e\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u0421\u043e\u0437\u0434\u0421\u043f\u0440 \u0421\u043e\u0441\u0442\u0421\u043f\u0440 \u0421\u043e\u0445\u0440 \u0421\u043e\u0445\u0440\u0421\u043f\u0440 \u0421\u043f\u0438\u0441\u043e\u043a\u0421\u0438\u0441\u0442\u0435\u043c \u0421\u043f\u0440 \u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0415\u0441\u0442\u044c \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c\u0420\u0430\u0441\u0448 \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0421\u043f\u0440\u0418\u0437\u043c\u041d\u0430\u0431\u0414\u0430\u043d \u0421\u043f\u0440\u041a\u043e\u0434 \u0421\u043f\u0440\u041d\u043e\u043c\u0435\u0440 \u0421\u043f\u0440\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0421\u043f\u0440\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0421\u043f\u0440\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0421\u043f\u0440\u041f\u0430\u0440\u0430\u043c \u0421\u043f\u0440\u041f\u043e\u043b\u0435\u0417\u043d\u0430\u0447 \u0421\u043f\u0440\u041f\u043e\u043b\u0435\u0418\u043c\u044f \u0421\u043f\u0440\u0420\u0435\u043a\u0432 \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u0412\u0432\u0435\u0434\u0417\u043d \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u041d\u043e\u0432\u044b\u0435 \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u041f\u0440 \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u041f\u0440\u0435\u0434\u0417\u043d \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u0420\u0435\u0436\u0438\u043c \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u0422\u0438\u043f\u0422\u0435\u043a\u0441\u0442 \u0421\u043f\u0440\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0421\u043f\u0440\u0421\u043e\u0441\u0442 \u0421\u043f\u0440\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0421\u043f\u0440\u0422\u0431\u043b\u0418\u0442\u043e\u0433 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041a\u043e\u043b \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041c\u0430\u043a\u0441 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041c\u0438\u043d \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041f\u0440\u0435\u0434 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u0421\u043b\u0435\u0434 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u0421\u043e\u0437\u0434 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u0423\u0434 \u0421\u043f\u0440\u0422\u0435\u043a\u041f\u0440\u0435\u0434\u0441\u0442 \u0421\u043f\u0440\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0421\u0440\u0430\u0432\u043d\u0438\u0442\u044c\u0421\u0442\u0440 \u0421\u0442\u0440\u0412\u0435\u0440\u0445\u0420\u0435\u0433\u0438\u0441\u0442\u0440 \u0421\u0442\u0440\u041d\u0438\u0436\u043d\u0420\u0435\u0433\u0438\u0441\u0442\u0440 \u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u0421\u0443\u043c\u041f\u0440\u043e\u043f \u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439\u041f\u0430\u0440\u0430\u043c \u0422\u0435\u043a\u0412\u0435\u0440\u0441\u0438\u044f \u0422\u0435\u043a\u041e\u0440\u0433 \u0422\u043e\u0447\u043d \u0422\u0440\u0430\u043d \u0422\u0440\u0430\u043d\u0441\u043b\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044f \u0423\u0434\u0430\u043b\u0438\u0442\u044c\u0422\u0430\u0431\u043b\u0438\u0446\u0443 \u0423\u0434\u0430\u043b\u0438\u0442\u044c\u0424\u0430\u0439\u043b \u0423\u0434\u0421\u043f\u0440 \u0423\u0434\u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u0423\u0441\u0442 \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438\u041a\u043e\u043d\u0441\u0442\u0430\u043d\u0442 \u0424\u0430\u0439\u043b\u0410\u0442\u0440\u0438\u0431\u0443\u0442\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0410\u0442\u0440\u0438\u0431\u0443\u0442\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0424\u0430\u0439\u043b\u0412\u0440\u0435\u043c\u044f \u0424\u0430\u0439\u043b\u0412\u0440\u0435\u043c\u044f\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0424\u0430\u0439\u043b\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0417\u0430\u043d\u044f\u0442 \u0424\u0430\u0439\u043b\u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0418\u0441\u043a\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041c\u043e\u0436\u043d\u043e\u0427\u0438\u0442\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0435\u0440\u0435\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0424\u0430\u0439\u043b\u0420\u0430\u0437\u043c\u0435\u0440 \u0424\u0430\u0439\u043b\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0421\u0441\u044b\u043b\u043a\u0430\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0424\u0430\u0439\u043b\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0424\u043c\u0442SQL\u0414\u0430\u0442 \u0424\u043c\u0442\u0414\u0430\u0442 \u0424\u043c\u0442\u0421\u0442\u0440 \u0424\u043c\u0442\u0427\u0441\u043b \u0424\u043e\u0440\u043c\u0430\u0442 \u0426\u041c\u0430\u0441\u0441\u0438\u0432\u042d\u043b\u0435\u043c\u0435\u043d\u0442 \u0426\u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442 \u0426\u041f\u043e\u0434\u0441\u0442\u0440 "
+},begin:e,end:"\\(",returnBegin:!0,excludeEnd:!0},I,A,T,_,O]},N,I,A,T,_,O]}}
+})());
+hljs.registerLanguage("java",(()=>{"use strict"
+;var e="\\.([0-9](_*[0-9])*)",n="[0-9a-fA-F](_*[0-9a-fA-F])*",a={
+className:"number",variants:[{
+begin:`(\\b([0-9](_*[0-9])*)((${e})|\\.)?|(${e}))[eE][+-]?([0-9](_*[0-9])*)[fFdD]?\\b`
+},{begin:`\\b([0-9](_*[0-9])*)((${e})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{
+begin:`(${e})[fFdD]?\\b`},{begin:"\\b([0-9](_*[0-9])*)[fFdD]\\b"},{
+begin:`\\b0[xX]((${n})\\.?|(${n})?\\.(${n}))[pP][+-]?([0-9](_*[0-9])*)[fFdD]?\\b`
+},{begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${n})[lL]?\\b`},{
+begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}],
+relevance:0};return e=>{
+var n="false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",s={
+className:"meta",begin:"@[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",
+contains:[{begin:/\(/,end:/\)/,contains:["self"]}]};const r=a;return{
+name:"Java",aliases:["jsp"],keywords:n,illegal:/<\/|#/,
+contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,
+relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{
+begin:/import java\.[a-z]+\./,keywords:"import",relevance:2
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{
+className:"class",beginKeywords:"class interface enum",end:/[{;=]/,
+excludeEnd:!0,relevance:1,keywords:"class interface enum",illegal:/[:"\[\]]/,
+contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{
+beginKeywords:"new throw return else",relevance:0},{className:"class",
+begin:"record\\s+"+e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,excludeEnd:!0,
+end:/[{;=]/,keywords:n,contains:[{beginKeywords:"record"},{
+begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,
+contains:[e.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,
+keywords:n,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE]
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"function",
+begin:"([\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*(<[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*(\\s*,\\s*[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*)*>)?\\s+)+"+e.UNDERSCORE_IDENT_RE+"\\s*\\(",
+returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:n,contains:[{
+begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,
+contains:[e.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,
+keywords:n,relevance:0,
+contains:[s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,r,e.C_BLOCK_COMMENT_MODE]
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},r,s]}}})());
+hljs.registerLanguage("javascript",(()=>{"use strict"
+;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],s=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"])
+;function r(e){return t("(?=",e,")")}function t(...e){return e.map((e=>{
+return(n=e)?"string"==typeof n?n:n.source:null;var n})).join("")}return i=>{
+const c=e,o={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,
+isTrulyOpeningTag:(e,n)=>{const a=e[0].length+e.index,s=e.input[a]
+;"<"!==s?">"===s&&(((e,{after:n})=>{const a="</"+e[0].slice(1)
+;return-1!==e.input.indexOf(a,n)})(e,{after:a
+})||n.ignoreMatch()):n.ignoreMatch()}},l={$pattern:e,keyword:n,literal:a,
+built_in:s},g="\\.([0-9](_?[0-9])*)",b="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",d={
+className:"number",variants:[{
+begin:`(\\b(${b})((${g})|\\.)?|(${g}))[eE][+-]?([0-9](_?[0-9])*)\\b`},{
+begin:`\\b(${b})\\b((${g})\\b|\\.)?|(${g})\\b`},{
+begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{
+begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{
+begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{
+begin:"\\b0[0-7]+n?\\b"}],relevance:0},E={className:"subst",begin:"\\$\\{",
+end:"\\}",keywords:l,contains:[]},u={begin:"html`",end:"",starts:{end:"`",
+returnEnd:!1,contains:[i.BACKSLASH_ESCAPE,E],subLanguage:"xml"}},_={
+begin:"css`",end:"",starts:{end:"`",returnEnd:!1,
+contains:[i.BACKSLASH_ESCAPE,E],subLanguage:"css"}},m={className:"string",
+begin:"`",end:"`",contains:[i.BACKSLASH_ESCAPE,E]},y={className:"comment",
+variants:[i.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{
+className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",
+end:"\\}",relevance:0},{className:"variable",begin:c+"(?=\\s*(-)|$)",
+endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]
+}),i.C_BLOCK_COMMENT_MODE,i.C_LINE_COMMENT_MODE]
+},N=[i.APOS_STRING_MODE,i.QUOTE_STRING_MODE,u,_,m,d,i.REGEXP_MODE]
+;E.contains=N.concat({begin:/\{/,end:/\}/,keywords:l,contains:["self"].concat(N)
+});const A=[].concat(y,E.contains),f=A.concat([{begin:/\(/,end:/\)/,keywords:l,
+contains:["self"].concat(A)}]),p={className:"params",begin:/\(/,end:/\)/,
+excludeBegin:!0,excludeEnd:!0,keywords:l,contains:f};return{name:"Javascript",
+aliases:["js","jsx","mjs","cjs"],keywords:l,exports:{PARAMS_CONTAINS:f},
+illegal:/#(?![$_A-z])/,contains:[i.SHEBANG({label:"shebang",binary:"node",
+relevance:5}),{label:"use_strict",className:"meta",relevance:10,
+begin:/^\s*['"]use (strict|asm)['"]/
+},i.APOS_STRING_MODE,i.QUOTE_STRING_MODE,u,_,m,y,d,{
+begin:t(/[{,\n]\s*/,r(t(/(((\/\/.*$)|(\/\*(\*[^/]|[^*])*\*\/))\s*)*/,c+"\\s*:"))),
+relevance:0,contains:[{className:"attr",begin:c+r("\\s*:"),relevance:0}]},{
+begin:"("+i.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",
+keywords:"return throw case",contains:[y,i.REGEXP_MODE,{className:"function",
+begin:"(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+i.UNDERSCORE_IDENT_RE+")\\s*=>",
+returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{
+begin:i.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0
+},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:l,contains:f}]}]
+},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{
+variants:[{begin:"<>",end:"</>"},{begin:o.begin,"on:begin":o.isTrulyOpeningTag,
+end:o.end}],subLanguage:"xml",contains:[{begin:o.begin,end:o.end,skip:!0,
+contains:["self"]}]}],relevance:0},{className:"function",
+beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:l,
+contains:["self",i.inherit(i.TITLE_MODE,{begin:c}),p],illegal:/%/},{
+beginKeywords:"while if switch catch for"},{className:"function",
+begin:i.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",
+returnBegin:!0,contains:[p,i.inherit(i.TITLE_MODE,{begin:c})]},{variants:[{
+begin:"\\."+c},{begin:"\\$"+c}],relevance:0},{className:"class",
+beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{
+beginKeywords:"extends"},i.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,
+end:/[{;]/,excludeEnd:!0,contains:[i.inherit(i.TITLE_MODE,{begin:c}),"self",p]
+},{begin:"(get|set)\\s+(?="+c+"\\()",end:/\{/,keywords:"get set",
+contains:[i.inherit(i.TITLE_MODE,{begin:c}),{begin:/\(\)/},p]},{begin:/\$[(.]/}]
+}}})());
+hljs.registerLanguage("jboss-cli",(()=>{"use strict";return e=>({
+name:"JBoss CLI",aliases:["wildfly-cli"],keywords:{$pattern:"[a-z-]+",
+keyword:"alias batch cd clear command connect connection-factory connection-info data-source deploy deployment-info deployment-overlay echo echo-dmr help history if jdbc-driver-info jms-queue|20 jms-topic|20 ls patch pwd quit read-attribute read-operation reload rollout-plan run-batch set shutdown try unalias undeploy unset version xa-data-source",
+literal:"true false"},contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,{
+className:"params",begin:/--[\w\-=\/]+/},{className:"function",
+begin:/:[\w\-.]+/,relevance:0},{className:"string",begin:/\B([\/.])[\w\-.\/=]+/
+},{className:"params",begin:/\(/,end:/\)/,contains:[{begin:/[\w-]+ *=/,
+returnBegin:!0,relevance:0,contains:[{className:"attr",begin:/[\w-]+/}]}],
+relevance:0}]})})());
+hljs.registerLanguage("json",(()=>{"use strict";return n=>{const e={
+literal:"true false null"
+},i=[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE],a=[n.QUOTE_STRING_MODE,n.C_NUMBER_MODE],l={
+end:",",endsWithParent:!0,excludeEnd:!0,contains:a,keywords:e},t={begin:/\{/,
+end:/\}/,contains:[{className:"attr",begin:/"/,end:/"/,
+contains:[n.BACKSLASH_ESCAPE],illegal:"\\n"},n.inherit(l,{begin:/:/
+})].concat(i),illegal:"\\S"},s={begin:"\\[",end:"\\]",contains:[n.inherit(l)],
+illegal:"\\S"};return a.push(t,s),i.forEach((n=>{a.push(n)})),{name:"JSON",
+contains:a,keywords:e,illegal:"\\S"}}})());
+hljs.registerLanguage("julia",(()=>{"use strict";return e=>{
+var r="[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*",t={$pattern:r,
+keyword:["baremodule","begin","break","catch","ccall","const","continue","do","else","elseif","end","export","false","finally","for","function","global","if","import","in","isa","let","local","macro","module","quote","return","true","try","using","where","while"],
+literal:["ARGS","C_NULL","DEPOT_PATH","ENDIAN_BOM","ENV","Inf","Inf16","Inf32","Inf64","InsertionSort","LOAD_PATH","MergeSort","NaN","NaN16","NaN32","NaN64","PROGRAM_FILE","QuickSort","RoundDown","RoundFromZero","RoundNearest","RoundNearestTiesAway","RoundNearestTiesUp","RoundToZero","RoundUp","VERSION|0","devnull","false","im","missing","nothing","pi","stderr","stdin","stdout","true","undef","\u03c0","\u212f"],
+built_in:["AbstractArray","AbstractChannel","AbstractChar","AbstractDict","AbstractDisplay","AbstractFloat","AbstractIrrational","AbstractMatrix","AbstractRange","AbstractSet","AbstractString","AbstractUnitRange","AbstractVecOrMat","AbstractVector","Any","ArgumentError","Array","AssertionError","BigFloat","BigInt","BitArray","BitMatrix","BitSet","BitVector","Bool","BoundsError","CapturedException","CartesianIndex","CartesianIndices","Cchar","Cdouble","Cfloat","Channel","Char","Cint","Cintmax_t","Clong","Clonglong","Cmd","Colon","Complex","ComplexF16","ComplexF32","ComplexF64","CompositeException","Condition","Cptrdiff_t","Cshort","Csize_t","Cssize_t","Cstring","Cuchar","Cuint","Cuintmax_t","Culong","Culonglong","Cushort","Cvoid","Cwchar_t","Cwstring","DataType","DenseArray","DenseMatrix","DenseVecOrMat","DenseVector","Dict","DimensionMismatch","Dims","DivideError","DomainError","EOFError","Enum","ErrorException","Exception","ExponentialBackOff","Expr","Float16","Float32","Float64","Function","GlobalRef","HTML","IO","IOBuffer","IOContext","IOStream","IdDict","IndexCartesian","IndexLinear","IndexStyle","InexactError","InitError","Int","Int128","Int16","Int32","Int64","Int8","Integer","InterruptException","InvalidStateException","Irrational","KeyError","LinRange","LineNumberNode","LinearIndices","LoadError","MIME","Matrix","Method","MethodError","Missing","MissingException","Module","NTuple","NamedTuple","Nothing","Number","OrdinalRange","OutOfMemoryError","OverflowError","Pair","PartialQuickSort","PermutedDimsArray","Pipe","ProcessFailedException","Ptr","QuoteNode","Rational","RawFD","ReadOnlyMemoryError","Real","ReentrantLock","Ref","Regex","RegexMatch","RoundingMode","SegmentationFault","Set","Signed","Some","StackOverflowError","StepRange","StepRangeLen","StridedArray","StridedMatrix","StridedVecOrMat","StridedVector","String","StringIndexError","SubArray","SubString","SubstitutionString","Symbol","SystemError","Task","TaskFailedException","Text","TextDisplay","Timer","Tuple","Type","TypeError","TypeVar","UInt","UInt128","UInt16","UInt32","UInt64","UInt8","UndefInitializer","UndefKeywordError","UndefRefError","UndefVarError","Union","UnionAll","UnitRange","Unsigned","Val","Vararg","VecElement","VecOrMat","Vector","VersionNumber","WeakKeyDict","WeakRef"]
+},n={keywords:t,illegal:/<\//},a={className:"subst",begin:/\$\(/,end:/\)/,
+keywords:t},i={className:"variable",begin:"\\$"+r},o={className:"string",
+contains:[e.BACKSLASH_ESCAPE,a,i],variants:[{begin:/\w*"""/,end:/"""\w*/,
+relevance:10},{begin:/\w*"/,end:/"\w*/}]},s={className:"string",
+contains:[e.BACKSLASH_ESCAPE,a,i],begin:"`",end:"`"},l={className:"meta",
+begin:"@"+r};return n.name="Julia",n.contains=[{className:"number",
+begin:/(\b0x[\d_]*(\.[\d_]*)?|0x\.\d[\d_]*)p[-+]?\d+|\b0[box][a-fA-F0-9][a-fA-F0-9_]*|(\b\d[\d_]*(\.[\d_]*)?|\.\d[\d_]*)([eEfF][-+]?\d+)?/,
+relevance:0},{className:"string",begin:/'(.|\\[xXuU][a-zA-Z0-9]+)'/},o,s,l,{
+className:"comment",variants:[{begin:"#=",end:"=#",relevance:10},{begin:"#",
+end:"$"}]},e.HASH_COMMENT_MODE,{className:"keyword",
+begin:"\\b(((abstract|primitive)\\s+)type|(mutable\\s+)?struct)\\b"},{begin:/<:/
+}],a.contains=n.contains,n}})());
+hljs.registerLanguage("julia-repl",(()=>{"use strict";return a=>({
+name:"Julia REPL",contains:[{className:"meta",begin:/^julia>/,relevance:10,
+starts:{end:/^(?![ ]{6})/,subLanguage:"julia"},aliases:["jldoctest"]}]})})());
+hljs.registerLanguage("kotlin",(()=>{"use strict"
+;var e="\\.([0-9](_*[0-9])*)",n="[0-9a-fA-F](_*[0-9a-fA-F])*",a={
+className:"number",variants:[{
+begin:`(\\b([0-9](_*[0-9])*)((${e})|\\.)?|(${e}))[eE][+-]?([0-9](_*[0-9])*)[fFdD]?\\b`
+},{begin:`\\b([0-9](_*[0-9])*)((${e})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{
+begin:`(${e})[fFdD]?\\b`},{begin:"\\b([0-9](_*[0-9])*)[fFdD]\\b"},{
+begin:`\\b0[xX]((${n})\\.?|(${n})?\\.(${n}))[pP][+-]?([0-9](_*[0-9])*)[fFdD]?\\b`
+},{begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${n})[lL]?\\b`},{
+begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}],
+relevance:0};return e=>{const n={
+keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual",
+built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",
+literal:"true false null"},i={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@"
+},s={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},t={
+className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string",
+variants:[{begin:'"""',end:'"""(?=[^"])',contains:[t,s]},{begin:"'",end:"'",
+illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,
+contains:[e.BACKSLASH_ESCAPE,t,s]}]};s.contains.push(r);const l={
+className:"meta",
+begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?"
+},c={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,
+end:/\)/,contains:[e.inherit(r,{className:"meta-string"})]}]
+},o=a,b=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),E={
+variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,
+contains:[]}]},d=E;return d.variants[1].contains=[E],E.variants[1].contains=[d],
+{name:"Kotlin",aliases:["kt","kts"],keywords:n,
+contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",
+begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,b,{className:"keyword",
+begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",
+begin:/@\w+/}]}},i,l,c,{className:"function",beginKeywords:"fun",end:"[(]|$",
+returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{
+begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,
+contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin:/</,end:/>/,
+keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,
+endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,
+endsWithParent:!0,contains:[E,e.C_LINE_COMMENT_MODE,b],relevance:0
+},e.C_LINE_COMMENT_MODE,b,l,c,r,e.C_NUMBER_MODE]},b]},{className:"class",
+beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,
+illegal:"extends implements",contains:[{
+beginKeywords:"public protected internal private constructor"
+},e.UNDERSCORE_TITLE_MODE,{className:"type",begin:/</,end:/>/,excludeBegin:!0,
+excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,
+excludeBegin:!0,returnEnd:!0},l,c]},r,{className:"meta",begin:"^#!/usr/bin/env",
+end:"$",illegal:"\n"},o]}}})());
+hljs.registerLanguage("lasso",(()=>{"use strict";return e=>{
+const a="<\\?(lasso(script)?|=)",n="\\]|\\?>",r={
+$pattern:"[a-zA-Z_][\\w.]*|&[lg]t;",
+literal:"true false none minimal full all void and or not bw nbw ew new cn ncn lt lte gt gte eq neq rx nrx ft",
+built_in:"array date decimal duration integer map pair string tag xml null boolean bytes keyword list locale queue set stack staticarray local var variable global data self inherited currentcapture givenblock",
+keyword:"cache database_names database_schemanames database_tablenames define_tag define_type email_batch encode_set html_comment handle handle_error header if inline iterate ljax_target link link_currentaction link_currentgroup link_currentrecord link_detail link_firstgroup link_firstrecord link_lastgroup link_lastrecord link_nextgroup link_nextrecord link_prevgroup link_prevrecord log loop namespace_using output_none portal private protect records referer referrer repeating resultset rows search_args search_arguments select sort_args sort_arguments thread_atomic value_list while abort case else fail_if fail_ifnot fail if_empty if_false if_null if_true loop_abort loop_continue loop_count params params_up return return_value run_children soap_definetag soap_lastrequest soap_lastresponse tag_name ascending average by define descending do equals frozen group handle_failure import in into join let match max min on order parent protected provide public require returnhome skip split_thread sum take thread to trait type where with yield yieldhome"
+},t=e.COMMENT("\x3c!--","--\x3e",{relevance:0}),s={className:"meta",
+begin:"\\[noprocess\\]",starts:{end:"\\[/noprocess\\]",returnEnd:!0,contains:[t]
+}},i={className:"meta",begin:"\\[/noprocess|"+a
+},l=[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.inherit(e.C_NUMBER_MODE,{
+begin:e.C_NUMBER_RE+"|(-?infinity|NaN)\\b"}),e.inherit(e.APOS_STRING_MODE,{
+illegal:null}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null}),{
+className:"string",begin:"`",end:"`"},{variants:[{begin:"[#$][a-zA-Z_][\\w.]*"
+},{begin:"#",end:"\\d+",illegal:"\\W"}]},{className:"type",begin:"::\\s*",
+end:"[a-zA-Z_][\\w.]*",illegal:"\\W"},{className:"params",variants:[{
+begin:"-(?!infinity)[a-zA-Z_][\\w.]*",relevance:0},{begin:"(\\.\\.\\.)"}]},{
+begin:/(->|\.)\s*/,relevance:0,contains:[{className:"symbol",
+begin:"'[a-zA-Z_][\\w.]*'"}]},{className:"class",beginKeywords:"define",
+returnEnd:!0,end:"\\(|=>",contains:[e.inherit(e.TITLE_MODE,{
+begin:"[a-zA-Z_][\\w.]*(=(?!>))?|[-+*/%](?!>)"})]}];return{name:"Lasso",
+aliases:["ls","lassoscript"],case_insensitive:!0,keywords:r,contains:[{
+className:"meta",begin:n,relevance:0,starts:{end:"\\[|"+a,returnEnd:!0,
+relevance:0,contains:[t]}},s,i,{className:"meta",begin:"\\[no_square_brackets",
+starts:{end:"\\[/no_square_brackets\\]",keywords:r,contains:[{className:"meta",
+begin:n,relevance:0,starts:{end:"\\[noprocess\\]|"+a,returnEnd:!0,contains:[t]}
+},s,i].concat(l)}},{className:"meta",begin:"\\[",relevance:0},{className:"meta",
+begin:"^#!",end:"lasso9$",relevance:10}].concat(l)}}})());
+hljs.registerLanguage("latex",(()=>{"use strict";return e=>{const n=[{
+begin:/\^{6}[0-9a-f]{6}/},{begin:/\^{5}[0-9a-f]{5}/},{begin:/\^{4}[0-9a-f]{4}/
+},{begin:/\^{3}[0-9a-f]{3}/},{begin:/\^{2}[0-9a-f]{2}/},{
+begin:/\^{2}[\u0000-\u007f]/}],a=[{className:"keyword",begin:/\\/,relevance:0,
+contains:[{endsParent:!0,begin:((...e)=>"("+e.map((e=>{
+return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("|")+")")(...["(?:NeedsTeXFormat|RequirePackage|GetIdInfo)","Provides(?:Expl)?(?:Package|Class|File)","(?:DeclareOption|ProcessOptions)","(?:documentclass|usepackage|input|include)","makeat(?:letter|other)","ExplSyntax(?:On|Off)","(?:new|renew|provide)?command","(?:re)newenvironment","(?:New|Renew|Provide|Declare)(?:Expandable)?DocumentCommand","(?:New|Renew|Provide|Declare)DocumentEnvironment","(?:(?:e|g|x)?def|let)","(?:begin|end)","(?:part|chapter|(?:sub){0,2}section|(?:sub)?paragraph)","caption","(?:label|(?:eq|page|name)?ref|(?:paren|foot|super)?cite)","(?:alpha|beta|[Gg]amma|[Dd]elta|(?:var)?epsilon|zeta|eta|[Tt]heta|vartheta)","(?:iota|(?:var)?kappa|[Ll]ambda|mu|nu|[Xx]i|[Pp]i|varpi|(?:var)rho)","(?:[Ss]igma|varsigma|tau|[Uu]psilon|[Pp]hi|varphi|chi|[Pp]si|[Oo]mega)","(?:frac|sum|prod|lim|infty|times|sqrt|leq|geq|left|right|middle|[bB]igg?)","(?:[lr]angle|q?quad|[lcvdi]?dots|d?dot|hat|tilde|bar)"].map((e=>e+"(?![a-zA-Z@:_])")))
+},{endsParent:!0,
+begin:RegExp(["(?:__)?[a-zA-Z]{2,}_[a-zA-Z](?:_?[a-zA-Z])+:[a-zA-Z]*","[lgc]__?[a-zA-Z](?:_?[a-zA-Z])*_[a-zA-Z]{2,}","[qs]__?[a-zA-Z](?:_?[a-zA-Z])+","use(?:_i)?:[a-zA-Z]*","(?:else|fi|or):","(?:if|cs|exp):w","(?:hbox|vbox):n","::[a-zA-Z]_unbraced","::[a-zA-Z:]"].map((e=>e+"(?![a-zA-Z:_])")).join("|"))
+},{endsParent:!0,variants:n},{endsParent:!0,relevance:0,variants:[{
+begin:/[a-zA-Z@]+/},{begin:/[^a-zA-Z@]?/}]}]},{className:"params",relevance:0,
+begin:/#+\d?/},{variants:n},{className:"built_in",relevance:0,begin:/[$&^_]/},{
+className:"meta",begin:"% !TeX",end:"$",relevance:10},e.COMMENT("%","$",{
+relevance:0})],i={begin:/\{/,end:/\}/,relevance:0,contains:["self",...a]
+},t=e.inherit(i,{relevance:0,endsParent:!0,contains:[i,...a]}),r={begin:/\[/,
+end:/\]/,endsParent:!0,relevance:0,contains:[i,...a]},s={begin:/\s+/,relevance:0
+},c=[t],l=[r],o=(e,n)=>({contains:[s],starts:{relevance:0,contains:e,starts:n}
+}),d=(e,n)=>({begin:"\\\\"+e+"(?![a-zA-Z@:_])",keywords:{$pattern:/\\[a-zA-Z]+/,
+keyword:"\\"+e},relevance:0,contains:[s],starts:n}),g=(n,a)=>e.inherit({
+begin:"\\\\begin(?=[ \t]*(\\r?\\n[ \t]*)?\\{"+n+"\\})",keywords:{
+$pattern:/\\[a-zA-Z]+/,keyword:"\\begin"},relevance:0
+},o(c,a)),m=(n="string")=>e.END_SAME_AS_BEGIN({className:n,begin:/(.|\r?\n)/,
+end:/(.|\r?\n)/,excludeBegin:!0,excludeEnd:!0,endsParent:!0}),b=e=>({
+className:"string",end:"(?=\\\\end\\{"+e+"\\})"}),p=(e="string")=>({relevance:0,
+begin:/\{/,starts:{endsParent:!0,contains:[{className:e,end:/(?=\})/,
+endsParent:!0,contains:[{begin:/\{/,end:/\}/,relevance:0,contains:["self"]}]}]}
+});return{name:"LaTeX",aliases:["TeX"],
+contains:[...["verb","lstinline"].map((e=>d(e,{contains:[m()]}))),d("mint",o(c,{
+contains:[m()]})),d("mintinline",o(c,{contains:[p(),m()]})),d("url",{
+contains:[p("link"),p("link")]}),d("hyperref",{contains:[p("link")]
+}),d("href",o(l,{contains:[p("link")]
+})),...[].concat(...["","\\*"].map((e=>[g("verbatim"+e,b("verbatim"+e)),g("filecontents"+e,o(c,b("filecontents"+e))),...["","B","L"].map((n=>g(n+"Verbatim"+e,o(l,b(n+"Verbatim"+e)))))]))),g("minted",o(l,o(c,b("minted")))),...a]
+}}})());
+hljs.registerLanguage("ldif",(()=>{"use strict";return e=>({name:"LDIF",
+contains:[{className:"attribute",begin:"^dn",end:": ",excludeEnd:!0,starts:{
+end:"$",relevance:0},relevance:10},{className:"attribute",begin:"^\\w",end:": ",
+excludeEnd:!0,starts:{end:"$",relevance:0}},{className:"literal",begin:"^-",
+end:"$"},e.HASH_COMMENT_MODE]})})());
+hljs.registerLanguage("leaf",(()=>{"use strict";return e=>({name:"Leaf",
+contains:[{className:"function",begin:"#+[A-Za-z_0-9]*\\(",end:/ \{/,
+returnBegin:!0,excludeEnd:!0,contains:[{className:"keyword",begin:"#+"},{
+className:"title",begin:"[A-Za-z_][A-Za-z_0-9]*"},{className:"params",
+begin:"\\(",end:"\\)",endsParent:!0,contains:[{className:"string",begin:'"',
+end:'"'},{className:"variable",begin:"[A-Za-z_][A-Za-z_0-9]*"}]}]}]})})());
+hljs.registerLanguage("less",(()=>{"use strict"
+;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],t=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],i=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],o=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],n=["align-content","align-items","align-self","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","auto","backface-visibility","background","background-attachment","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","clear","clip","clip-path","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","content","counter-increment","counter-reset","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-variant-ligatures","font-variation-settings","font-weight","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inherit","initial","justify-content","left","letter-spacing","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marks","mask","max-height","max-width","min-height","min-width","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","perspective","perspective-origin","pointer-events","position","quotes","resize","right","src","tab-size","table-layout","text-align","text-align-last","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-indent","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","white-space","widows","width","word-break","word-spacing","word-wrap","z-index"].reverse(),r=i.concat(o)
+;return a=>{const s=(e=>({IMPORTANT:{className:"meta",begin:"!important"},
+HEXCOLOR:{className:"number",begin:"#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})"},
+ATTRIBUTE_SELECTOR_MODE:{className:"selector-attr",begin:/\[/,end:/\]/,
+illegal:"$",contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]}
+}))(a),l=r,d="([\\w-]+|@\\{[\\w-]+\\})",c=[],g=[],b=e=>({className:"string",
+begin:"~?"+e+".*?"+e}),m=(e,t,i)=>({className:e,begin:t,relevance:i}),u={
+$pattern:/[a-z-]+/,keyword:"and or not only",attribute:t.join(" ")},p={
+begin:"\\(",end:"\\)",contains:g,keywords:u,relevance:0}
+;g.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b("'"),b('"'),a.CSS_NUMBER_MODE,{
+begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",
+excludeEnd:!0}
+},s.HEXCOLOR,p,m("variable","@@?[\\w-]+",10),m("variable","@\\{[\\w-]+\\}"),m("built_in","~?`[^`]*?`"),{
+className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0
+},s.IMPORTANT);const f=g.concat({begin:/\{/,end:/\}/,contains:c}),h={
+beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"
+}].concat(g)},w={begin:d+"\\s*:",returnBegin:!0,end:/[;}]/,relevance:0,
+contains:[{begin:/-(webkit|moz|ms|o)-/},{className:"attribute",
+begin:"\\b("+n.join("|")+")\\b",end:/(?=:)/,starts:{endsWithParent:!0,
+illegal:"[<=$]",relevance:0,contains:g}}]},v={className:"keyword",
+begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",
+starts:{end:"[;{}]",keywords:u,returnEnd:!0,contains:g,relevance:0}},y={
+className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{
+begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:f}},k={variants:[{
+begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:d,end:/\{/}],returnBegin:!0,
+returnEnd:!0,illegal:"[<='$\"]",relevance:0,
+contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,h,m("keyword","all\\b"),m("variable","@\\{[\\w-]+\\}"),{
+begin:"\\b("+e.join("|")+")\\b",className:"selector-tag"
+},m("selector-tag",d+"%?",0),m("selector-id","#"+d),m("selector-class","\\."+d,0),m("selector-tag","&",0),s.ATTRIBUTE_SELECTOR_MODE,{
+className:"selector-pseudo",begin:":("+i.join("|")+")"},{
+className:"selector-pseudo",begin:"::("+o.join("|")+")"},{begin:"\\(",end:"\\)",
+contains:f},{begin:"!important"}]},E={begin:`[\\w-]+:(:)?(${l.join("|")})`,
+returnBegin:!0,contains:[k]}
+;return c.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,v,y,E,w,k),{
+name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:c}}})());
+hljs.registerLanguage("lisp",(()=>{"use strict";return e=>{
+var n="[a-zA-Z_\\-+\\*\\/<=>&#][a-zA-Z0-9_\\-+*\\/<=>&#!]*",a="\\|[^]*?\\|",i="(-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|-)?\\d+)?",s={
+className:"literal",begin:"\\b(t{1}|nil)\\b"},l={className:"number",variants:[{
+begin:i,relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{
+begin:"#(o|O)[0-7]+(/[0-7]+)?"},{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},{
+begin:"#(c|C)\\("+i+" +"+i,end:"\\)"}]},b=e.inherit(e.QUOTE_STRING_MODE,{
+illegal:null}),g=e.COMMENT(";","$",{relevance:0}),r={begin:"\\*",end:"\\*"},t={
+className:"symbol",begin:"[:&]"+n},c={begin:n,relevance:0},d={begin:a},o={
+contains:[l,b,r,t,{begin:"\\(",end:"\\)",contains:["self",s,b,l,c]},c],
+variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{
+name:"quote"}},{begin:"'"+a}]},v={variants:[{begin:"'"+n},{
+begin:"#'"+n+"(::"+n+")*"}]},m={begin:"\\(\\s*",end:"\\)"},u={endsWithParent:!0,
+relevance:0};return m.contains=[{className:"name",variants:[{begin:n,relevance:0
+},{begin:a}]},u],u.contains=[o,v,m,s,l,b,g,r,t,d,c],{name:"Lisp",illegal:/\S/,
+contains:[l,e.SHEBANG(),s,b,g,o,v,m,c]}}})());
+hljs.registerLanguage("livecodeserver",(()=>{"use strict";return e=>{const r={
+className:"variable",variants:[{
+begin:"\\b([gtps][A-Z]{1}[a-zA-Z0-9]*)(\\[.+\\])?(?:\\s*?)"},{begin:"\\$_[A-Z]+"
+}],relevance:0
+},t=[e.C_BLOCK_COMMENT_MODE,e.HASH_COMMENT_MODE,e.COMMENT("--","$"),e.COMMENT("[^:]//","$")],a=e.inherit(e.TITLE_MODE,{
+variants:[{begin:"\\b_*rig[A-Z][A-Za-z0-9_\\-]*"},{begin:"\\b_[a-z0-9\\-]+"}]
+}),o=e.inherit(e.TITLE_MODE,{begin:"\\b([A-Za-z0-9_\\-]+)\\b"});return{
+name:"LiveCode",case_insensitive:!1,keywords:{
+keyword:"$_COOKIE $_FILES $_GET $_GET_BINARY $_GET_RAW $_POST $_POST_BINARY $_POST_RAW $_SESSION $_SERVER codepoint codepoints segment segments codeunit codeunits sentence sentences trueWord trueWords paragraph after byte bytes english the until http forever descending using line real8 with seventh for stdout finally element word words fourth before black ninth sixth characters chars stderr uInt1 uInt1s uInt2 uInt2s stdin string lines relative rel any fifth items from middle mid at else of catch then third it file milliseconds seconds second secs sec int1 int1s int4 int4s internet int2 int2s normal text item last long detailed effective uInt4 uInt4s repeat end repeat URL in try into switch to words https token binfile each tenth as ticks tick system real4 by dateItems without char character ascending eighth whole dateTime numeric short first ftp integer abbreviated abbr abbrev private case while if div mod wrap and or bitAnd bitNot bitOr bitXor among not in a an within contains ends with begins the keys of keys",
 literal:"SIX TEN FORMFEED NINE ZERO NONE SPACE FOUR FALSE COLON CRLF PI COMMA ENDOFFILE EOF EIGHT FIVE QUOTE EMPTY ONE TRUE RETURN CR LINEFEED RIGHT BACKSLASH NULL SEVEN TAB THREE TWO six ten formfeed nine zero none space four false colon crlf pi comma endoffile eof eight five quote empty one true return cr linefeed right backslash null seven tab three two RIVERSION RISTATE FILE_READ_MODE FILE_WRITE_MODE FILE_WRITE_MODE DIR_WRITE_MODE FILE_READ_UMASK FILE_WRITE_UMASK DIR_READ_UMASK DIR_WRITE_UMASK",
-built_in:"put abs acos aliasReference annuity arrayDecode arrayEncode asin atan atan2 average avg avgDev base64Decode base64Encode baseConvert binaryDecode binaryEncode byteOffset byteToNum cachedURL cachedURLs charToNum cipherNames codepointOffset codepointProperty codepointToNum codeunitOffset commandNames compound compress constantNames cos date dateFormat decompress difference directories diskSpace DNSServers exp exp1 exp2 exp10 extents files flushEvents folders format functionNames geometricMean global globals hasMemory harmonicMean hostAddress hostAddressToName hostName hostNameToAddress isNumber ISOToMac itemOffset keys len length libURLErrorData libUrlFormData libURLftpCommand libURLLastHTTPHeaders libURLLastRHHeaders libUrlMultipartFormAddPart libUrlMultipartFormData libURLVersion lineOffset ln ln1 localNames log log2 log10 longFilePath lower macToISO matchChunk matchText matrixMultiply max md5Digest median merge messageAuthenticationCode messageDigest millisec millisecs millisecond milliseconds min monthNames nativeCharToNum normalizeText num number numToByte numToChar numToCodepoint numToNativeChar offset open openfiles openProcesses openProcessIDs openSockets paragraphOffset paramCount param params peerAddress pendingMessages platform popStdDev populationStandardDeviation populationVariance popVariance processID random randomBytes replaceText result revCreateXMLTree revCreateXMLTreeFromFile revCurrentRecord revCurrentRecordIsFirst revCurrentRecordIsLast revDatabaseColumnCount revDatabaseColumnIsNull revDatabaseColumnLengths revDatabaseColumnNames revDatabaseColumnNamed revDatabaseColumnNumbered revDatabaseColumnTypes revDatabaseConnectResult revDatabaseCursors revDatabaseID revDatabaseTableNames revDatabaseType revDataFromQuery revdb_closeCursor revdb_columnbynumber revdb_columncount revdb_columnisnull revdb_columnlengths revdb_columnnames revdb_columntypes revdb_commit revdb_connect revdb_connections revdb_connectionerr revdb_currentrecord revdb_cursorconnection revdb_cursorerr revdb_cursors revdb_dbtype revdb_disconnect revdb_execute revdb_iseof revdb_isbof revdb_movefirst revdb_movelast revdb_movenext revdb_moveprev revdb_query revdb_querylist revdb_recordcount revdb_rollback revdb_tablenames revGetDatabaseDriverPath revNumberOfRecords revOpenDatabase revOpenDatabases revQueryDatabase revQueryDatabaseBlob revQueryResult revQueryIsAtStart revQueryIsAtEnd revUnixFromMacPath revXMLAttribute revXMLAttributes revXMLAttributeValues revXMLChildContents revXMLChildNames revXMLCreateTreeFromFileWithNamespaces revXMLCreateTreeWithNamespaces revXMLDataFromXPathQuery revXMLEvaluateXPath revXMLFirstChild revXMLMatchingNode revXMLNextSibling revXMLNodeContents revXMLNumberOfChildren revXMLParent revXMLPreviousSibling revXMLRootNode revXMLRPC_CreateRequest revXMLRPC_Documents revXMLRPC_Error revXMLRPC_GetHost revXMLRPC_GetMethod revXMLRPC_GetParam revXMLText revXMLRPC_Execute revXMLRPC_GetParamCount revXMLRPC_GetParamNode revXMLRPC_GetParamType revXMLRPC_GetPath revXMLRPC_GetPort revXMLRPC_GetProtocol revXMLRPC_GetRequest revXMLRPC_GetResponse revXMLRPC_GetSocket revXMLTree revXMLTrees revXMLValidateDTD revZipDescribeItem revZipEnumerateItems revZipOpenArchives round sampVariance sec secs seconds sentenceOffset sha1Digest shell shortFilePath sin specialFolderPath sqrt standardDeviation statRound stdDev sum sysError systemVersion tan tempName textDecode textEncode tick ticks time to tokenOffset toLower toUpper transpose truewordOffset trunc uniDecode uniEncode upper URLDecode URLEncode URLStatus uuid value variableNames variance version waitDepth weekdayNames wordOffset xsltApplyStylesheet xsltApplyStylesheetFromFile xsltLoadStylesheet xsltLoadStylesheetFromFile add breakpoint cancel clear local variable file word line folder directory URL close socket process combine constant convert create new alias folder directory decrypt delete variable word line folder directory URL dispatch divide do encrypt filter get include intersect kill libURLDownloadToFile libURLFollowHttpRedirects libURLftpUpload libURLftpUploadFile libURLresetAll libUrlSetAuthCallback libURLSetDriver libURLSetCustomHTTPHeaders libUrlSetExpect100 libURLSetFTPListCommand libURLSetFTPMode libURLSetFTPStopTime libURLSetStatusCallback load extension loadedExtensions multiply socket prepare process post seek rel relative read from process rename replace require resetAll resolve revAddXMLNode revAppendXML revCloseCursor revCloseDatabase revCommitDatabase revCopyFile revCopyFolder revCopyXMLNode revDeleteFolder revDeleteXMLNode revDeleteAllXMLTrees revDeleteXMLTree revExecuteSQL revGoURL revInsertXMLNode revMoveFolder revMoveToFirstRecord revMoveToLastRecord revMoveToNextRecord revMoveToPreviousRecord revMoveToRecord revMoveXMLNode revPutIntoXMLNode revRollBackDatabase revSetDatabaseDriverPath revSetXMLAttribute revXMLRPC_AddParam revXMLRPC_DeleteAllDocuments revXMLAddDTD revXMLRPC_Free revXMLRPC_FreeAll revXMLRPC_DeleteDocument revXMLRPC_DeleteParam revXMLRPC_SetHost revXMLRPC_SetMethod revXMLRPC_SetPort revXMLRPC_SetProtocol revXMLRPC_SetSocket revZipAddItemWithData revZipAddItemWithFile revZipAddUncompressedItemWithData revZipAddUncompressedItemWithFile revZipCancel revZipCloseArchive revZipDeleteItem revZipExtractItemToFile revZipExtractItemToVariable revZipSetProgressCallback revZipRenameItem revZipReplaceItemWithData revZipReplaceItemWithFile revZipOpenArchive send set sort split start stop subtract symmetric union unload vectorDotProduct wait write"},
-contains:[b,{className:"keyword",begin:"\\bend\\sif\\b"},{className:"function",beginKeywords:"function",end:"$",contains:[b,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE,d]},{className:"function",begin:"\\bend\\s+",end:"$",keywords:"end",contains:[e,d],relevance:0},{beginKeywords:"command on",end:"$",contains:[b,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE,d]},{className:"meta",variants:[{begin:"<\\?(rev|lc|livecode)",relevance:10},
-{begin:"<\\?"},{begin:"\\?>"}]},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE,d].concat(c),illegal:";$|^\\[|^=|&|{"}}}());
-hljs.registerLanguage("livescript",function(){var a="as in of if for while finally var new function do return void else break catch instanceof with throw case default try switch continue typeof delete let yield const class debugger async await static import from export extends".split(" "),b="true false null undefined NaN Infinity".split(" "),c=[].concat("setInterval setTimeout clearInterval clearTimeout require exports eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape".split(" "),"arguments this super console window document localStorage module global".split(" "),
-"Intl DataView Number Math Date String RegExp Object Function Boolean Error Symbol Set Map WeakSet WeakMap Proxy Reflect JSON Promise Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Float32Array Array Uint8Array Uint8ClampedArray ArrayBuffer".split(" "),"EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError".split(" "));return function(d){var e={keyword:a.concat("then unless until loop of by when and or is isnt not it that otherwise from to til fallthrough case enum native list map __hasProp __extends __slice __bind __indexOf".split(" ")).join(" "),
-literal:b.concat("yes no on off it that void".split(" ")).join(" "),built_in:c.concat(["npm","print"]).join(" ")},f=d.inherit(d.TITLE_MODE,{begin:"[A-Za-z$_](?:-[0-9A-Za-z$_]|[0-9A-Za-z$_])*"}),h={className:"subst",begin:/#\{/,end:/}/,keywords:e},g={className:"subst",begin:/#[A-Za-z$_]/,end:/(?:\-[0-9A-Za-z$_]|[0-9A-Za-z$_])*/,keywords:e};g=[d.BINARY_NUMBER_MODE,{className:"number",begin:"(\\b0[xX][a-fA-F0-9_]+)|(\\b\\d(\\d|_\\d)*(\\.(\\d(\\d|_\\d)*)?)?(_*[eE]([-+]\\d(_\\d|\\d)*)?)?[_a-z]*)",relevance:0,
-starts:{end:"(\\s*/)?",relevance:0}},{className:"string",variants:[{begin:/'''/,end:/'''/,contains:[d.BACKSLASH_ESCAPE]},{begin:/'/,end:/'/,contains:[d.BACKSLASH_ESCAPE]},{begin:/"""/,end:/"""/,contains:[d.BACKSLASH_ESCAPE,h,g]},{begin:/"/,end:/"/,contains:[d.BACKSLASH_ESCAPE,h,g]},{begin:/\\/,end:/(\s|$)/,excludeEnd:!0}]},{className:"regexp",variants:[{begin:"//",end:"//[gim]*",contains:[h,d.HASH_COMMENT_MODE]},{begin:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W)/}]},{begin:"@[A-Za-z$_](?:-[0-9A-Za-z$_]|[0-9A-Za-z$_])*"},
-{begin:"``",end:"``",excludeBegin:!0,excludeEnd:!0,subLanguage:"javascript"}];h.contains=g;h={className:"params",begin:"\\(",returnBegin:!0,contains:[{begin:/\(/,end:/\)/,keywords:e,contains:["self"].concat(g)}]};return{name:"LiveScript",aliases:["ls"],keywords:e,illegal:/\/\*/,contains:g.concat([d.COMMENT("\\/\\*","\\*\\/"),d.HASH_COMMENT_MODE,{begin:"(#=>|=>|\\|>>|-?->|\\!->)"},{className:"function",contains:[f,h],returnBegin:!0,variants:[{begin:"([A-Za-z$_](?:-[0-9A-Za-z$_]|[0-9A-Za-z$_])*\\s*(?:=|:=)\\s*)?(\\(.*\\))?\\s*\\B\\->\\*?",
-end:"\\->\\*?"},{begin:"([A-Za-z$_](?:-[0-9A-Za-z$_]|[0-9A-Za-z$_])*\\s*(?:=|:=)\\s*)?!?(\\(.*\\))?\\s*\\B[-~]{1,2}>\\*?",end:"[-~]{1,2}>\\*?"},{begin:"([A-Za-z$_](?:-[0-9A-Za-z$_]|[0-9A-Za-z$_])*\\s*(?:=|:=)\\s*)?(\\(.*\\))?\\s*\\B!?[-~]{1,2}>\\*?",end:"!?[-~]{1,2}>\\*?"}]},{className:"class",beginKeywords:"class",end:"$",illegal:/[:="\[\]]/,contains:[{beginKeywords:"extends",endsWithParent:!0,illegal:/[:="\[\]]/,contains:[f]},f]},{begin:"[A-Za-z$_](?:-[0-9A-Za-z$_]|[0-9A-Za-z$_])*:",end:":",returnBegin:!0,
-returnEnd:!0,relevance:0}])}}}());
-hljs.registerLanguage("llvm",function(){return function(a){return{name:"LLVM IR",keywords:"begin end true false declare define global constant private linker_private internal available_externally linkonce linkonce_odr weak weak_odr appending dllimport dllexport common default hidden protected extern_weak external thread_local zeroinitializer undef null to tail target triple datalayout volatile nuw nsw nnan ninf nsz arcp fast exact inbounds align addrspace section alias module asm sideeffect gc dbg linker_private_weak attributes blockaddress initialexec localdynamic localexec prefix unnamed_addr ccc fastcc coldcc x86_stdcallcc x86_fastcallcc arm_apcscc arm_aapcscc arm_aapcs_vfpcc ptx_device ptx_kernel intel_ocl_bicc msp430_intrcc spir_func spir_kernel x86_64_sysvcc x86_64_win64cc x86_thiscallcc cc c signext zeroext inreg sret nounwind noreturn noalias nocapture byval nest readnone readonly inlinehint noinline alwaysinline optsize ssp sspreq noredzone noimplicitfloat naked builtin cold nobuiltin noduplicate nonlazybind optnone returns_twice sanitize_address sanitize_memory sanitize_thread sspstrong uwtable returned type opaque eq ne slt sgt sle sge ult ugt ule uge oeq one olt ogt ole oge ord uno ueq une x acq_rel acquire alignstack atomic catch cleanup filter inteldialect max min monotonic nand personality release seq_cst singlethread umax umin unordered xchg add fadd sub fsub mul fmul udiv sdiv fdiv urem srem frem shl lshr ashr and or xor icmp fcmp phi call trunc zext sext fptrunc fpext uitofp sitofp fptoui fptosi inttoptr ptrtoint bitcast addrspacecast select va_arg ret br switch invoke unwind unreachable indirectbr landingpad resume malloc alloca free load store getelementptr extractelement insertelement shufflevector getresult extractvalue insertvalue atomicrmw cmpxchg fence argmemonly double",contains:[{className:"keyword",
-begin:"i\\d+"},a.COMMENT(";","\\n",{relevance:0}),a.QUOTE_STRING_MODE,{className:"string",variants:[{begin:'"',end:'[^\\\\]"'}],relevance:0},{className:"title",variants:[{begin:"@([-a-zA-Z$._][\\w\\-$.]*)"},{begin:"@\\d+"},{begin:"!([-a-zA-Z$._][\\w\\-$.]*)"},{begin:"!\\d+([-a-zA-Z$._][\\w\\-$.]*)"}]},{className:"symbol",variants:[{begin:"%([-a-zA-Z$._][\\w\\-$.]*)"},{begin:"%\\d+"},{begin:"#\\d+"}]},{className:"number",variants:[{begin:"0[xX][a-fA-F0-9]+"},{begin:"-?\\d+(?:[.]\\d+)?(?:[eE][-+]?\\d+(?:[.]\\d+)?)?"}],
-relevance:0}]}}}());
-hljs.registerLanguage("lsl",function(){return function(a){var b={className:"number",begin:a.C_NUMBER_RE};return{name:"LSL (Linden Scripting Language)",illegal:":",contains:[{className:"string",begin:'"',end:'"',contains:[{className:"subst",begin:/\\[tn"\\]/}]},{className:"comment",variants:[a.COMMENT("//","$"),a.COMMENT("/\\*","\\*/")],relevance:0},b,{className:"section",variants:[{begin:"\\b(?:state|default)\\b"},{begin:"\\b(?:state_(?:entry|exit)|touch(?:_(?:start|end))?|(?:land_)?collision(?:_(?:start|end))?|timer|listen|(?:no_)?sensor|control|(?:not_)?at_(?:rot_)?target|money|email|experience_permissions(?:_denied)?|run_time_permissions|changed|attach|dataserver|moving_(?:start|end)|link_message|(?:on|object)_rez|remote_data|http_re(?:sponse|quest)|path_update|transaction_result)\\b"}]},{className:"built_in",
-begin:"\\b(?:ll(?:AgentInExperience|(?:Create|DataSize|Delete|KeyCount|Keys|Read|Update)KeyValue|GetExperience(?:Details|ErrorMessage)|ReturnObjectsBy(?:ID|Owner)|Json(?:2List|[GS]etValue|ValueType)|Sin|Cos|Tan|Atan2|Sqrt|Pow|Abs|Fabs|Frand|Floor|Ceil|Round|Vec(?:Mag|Norm|Dist)|Rot(?:Between|2(?:Euler|Fwd|Left|Up))|(?:Euler|Axes)2Rot|Whisper|(?:Region|Owner)?Say|Shout|Listen(?:Control|Remove)?|Sensor(?:Repeat|Remove)?|Detected(?:Name|Key|Owner|Type|Pos|Vel|Grab|Rot|Group|LinkNumber)|Die|Ground|Wind|(?:[GS]et)(?:AnimationOverride|MemoryLimit|PrimMediaParams|ParcelMusicURL|Object(?:Desc|Name)|PhysicsMaterial|Status|Scale|Color|Alpha|Texture|Pos|Rot|Force|Torque)|ResetAnimationOverride|(?:Scale|Offset|Rotate)Texture|(?:Rot)?Target(?:Remove)?|(?:Stop)?MoveToTarget|Apply(?:Rotational)?Impulse|Set(?:KeyframedMotion|ContentType|RegionPos|(?:Angular)?Velocity|Buoyancy|HoverHeight|ForceAndTorque|TimerEvent|ScriptState|Damage|TextureAnim|Sound(?:Queueing|Radius)|Vehicle(?:Type|(?:Float|Vector|Rotation)Param)|(?:Touch|Sit)?Text|Camera(?:Eye|At)Offset|PrimitiveParams|ClickAction|Link(?:Alpha|Color|PrimitiveParams(?:Fast)?|Texture(?:Anim)?|Camera|Media)|RemoteScriptAccessPin|PayPrice|LocalRot)|ScaleByFactor|Get(?:(?:Max|Min)ScaleFactor|ClosestNavPoint|StaticPath|SimStats|Env|PrimitiveParams|Link(?:PrimitiveParams|Number(?:OfSides)?|Key|Name|Media)|HTTPHeader|FreeURLs|Object(?:Details|PermMask|PrimCount)|Parcel(?:MaxPrims|Details|Prim(?:Count|Owners))|Attached(?:List)?|(?:SPMax|Free|Used)Memory|Region(?:Name|TimeDilation|FPS|Corner|AgentCount)|Root(?:Position|Rotation)|UnixTime|(?:Parcel|Region)Flags|(?:Wall|GMT)clock|SimulatorHostname|BoundingBox|GeometricCenter|Creator|NumberOf(?:Prims|NotecardLines|Sides)|Animation(?:List)?|(?:Camera|Local)(?:Pos|Rot)|Vel|Accel|Omega|Time(?:stamp|OfDay)|(?:Object|CenterOf)?Mass|MassMKS|Energy|Owner|(?:Owner)?Key|SunDirection|Texture(?:Offset|Scale|Rot)|Inventory(?:Number|Name|Key|Type|Creator|PermMask)|Permissions(?:Key)?|StartParameter|List(?:Length|EntryType)|Date|Agent(?:Size|Info|Language|List)|LandOwnerAt|NotecardLine|Script(?:Name|State))|(?:Get|Reset|GetAndReset)Time|PlaySound(?:Slave)?|LoopSound(?:Master|Slave)?|(?:Trigger|Stop|Preload)Sound|(?:(?:Get|Delete)Sub|Insert)String|To(?:Upper|Lower)|Give(?:InventoryList|Money)|RezObject|(?:Stop)?LookAt|Sleep|CollisionFilter|(?:Take|Release)Controls|DetachFromAvatar|AttachToAvatar(?:Temp)?|InstantMessage|(?:GetNext)?Email|StopHover|MinEventDelay|RotLookAt|String(?:Length|Trim)|(?:Start|Stop)Animation|TargetOmega|Request(?:Experience)?Permissions|(?:Create|Break)Link|BreakAllLinks|(?:Give|Remove)Inventory|Water|PassTouches|Request(?:Agent|Inventory)Data|TeleportAgent(?:Home|GlobalCoords)?|ModifyLand|CollisionSound|ResetScript|MessageLinked|PushObject|PassCollisions|AxisAngle2Rot|Rot2(?:Axis|Angle)|A(?:cos|sin)|AngleBetween|AllowInventoryDrop|SubStringIndex|List2(?:CSV|Integer|Json|Float|String|Key|Vector|Rot|List(?:Strided)?)|DeleteSubList|List(?:Statistics|Sort|Randomize|(?:Insert|Find|Replace)List)|EdgeOfWorld|AdjustSoundVolume|Key2Name|TriggerSoundLimited|EjectFromLand|(?:CSV|ParseString)2List|OverMyLand|SameGroup|UnSit|Ground(?:Slope|Normal|Contour)|GroundRepel|(?:Set|Remove)VehicleFlags|SitOnLink|(?:AvatarOn)?(?:Link)?SitTarget|Script(?:Danger|Profiler)|Dialog|VolumeDetect|ResetOtherScript|RemoteLoadScriptPin|(?:Open|Close)RemoteDataChannel|SendRemoteData|RemoteDataReply|(?:Integer|String)ToBase64|XorBase64|Log(?:10)?|Base64To(?:String|Integer)|ParseStringKeepNulls|RezAtRoot|RequestSimulatorData|ForceMouselook|(?:Load|Release|(?:E|Une)scape)URL|ParcelMedia(?:CommandList|Query)|ModPow|MapDestination|(?:RemoveFrom|AddTo|Reset)Land(?:Pass|Ban)List|(?:Set|Clear)CameraParams|HTTP(?:Request|Response)|TextBox|DetectedTouch(?:UV|Face|Pos|(?:N|Bin)ormal|ST)|(?:MD5|SHA1|DumpList2)String|Request(?:Secure)?URL|Clear(?:Prim|Link)Media|(?:Link)?ParticleSystem|(?:Get|Request)(?:Username|DisplayName)|RegionSayTo|CastRay|GenerateKey|TransferLindenDollars|ManageEstateAccess|(?:Create|Delete)Character|ExecCharacterCmd|Evade|FleeFrom|NavigateTo|PatrolPoints|Pursue|UpdateCharacter|WanderWithin))\\b"},
-{className:"literal",variants:[{begin:"\\b(?:PI|TWO_PI|PI_BY_TWO|DEG_TO_RAD|RAD_TO_DEG|SQRT2)\\b"},{begin:"\\b(?:XP_ERROR_(?:EXPERIENCES_DISABLED|EXPERIENCE_(?:DISABLED|SUSPENDED)|INVALID_(?:EXPERIENCE|PARAMETERS)|KEY_NOT_FOUND|MATURITY_EXCEEDED|NONE|NOT_(?:FOUND|PERMITTED(?:_LAND)?)|NO_EXPERIENCE|QUOTA_EXCEEDED|RETRY_UPDATE|STORAGE_EXCEPTION|STORE_DISABLED|THROTTLED|UNKNOWN_ERROR)|JSON_APPEND|STATUS_(?:PHYSICS|ROTATE_[XYZ]|PHANTOM|SANDBOX|BLOCK_GRAB(?:_OBJECT)?|(?:DIE|RETURN)_AT_EDGE|CAST_SHADOWS|OK|MALFORMED_PARAMS|TYPE_MISMATCH|BOUNDS_ERROR|NOT_(?:FOUND|SUPPORTED)|INTERNAL_ERROR|WHITELIST_FAILED)|AGENT(?:_(?:BY_(?:LEGACY_|USER)NAME|FLYING|ATTACHMENTS|SCRIPTED|MOUSELOOK|SITTING|ON_OBJECT|AWAY|WALKING|IN_AIR|TYPING|CROUCHING|BUSY|ALWAYS_RUN|AUTOPILOT|LIST_(?:PARCEL(?:_OWNER)?|REGION)))?|CAMERA_(?:PITCH|DISTANCE|BEHINDNESS_(?:ANGLE|LAG)|(?:FOCUS|POSITION)(?:_(?:THRESHOLD|LOCKED|LAG))?|FOCUS_OFFSET|ACTIVE)|ANIM_ON|LOOP|REVERSE|PING_PONG|SMOOTH|ROTATE|SCALE|ALL_SIDES|LINK_(?:ROOT|SET|ALL_(?:OTHERS|CHILDREN)|THIS)|ACTIVE|PASS(?:IVE|_(?:ALWAYS|IF_NOT_HANDLED|NEVER))|SCRIPTED|CONTROL_(?:FWD|BACK|(?:ROT_)?(?:LEFT|RIGHT)|UP|DOWN|(?:ML_)?LBUTTON)|PERMISSION_(?:RETURN_OBJECTS|DEBIT|OVERRIDE_ANIMATIONS|SILENT_ESTATE_MANAGEMENT|TAKE_CONTROLS|TRIGGER_ANIMATION|ATTACH|CHANGE_LINKS|(?:CONTROL|TRACK)_CAMERA|TELEPORT)|INVENTORY_(?:TEXTURE|SOUND|OBJECT|SCRIPT|LANDMARK|CLOTHING|NOTECARD|BODYPART|ANIMATION|GESTURE|ALL|NONE)|CHANGED_(?:INVENTORY|COLOR|SHAPE|SCALE|TEXTURE|LINK|ALLOWED_DROP|OWNER|REGION(?:_START)?|TELEPORT|MEDIA)|OBJECT_(?:CLICK_ACTION|HOVER_HEIGHT|LAST_OWNER_ID|(?:PHYSICS|SERVER|STREAMING)_COST|UNKNOWN_DETAIL|CHARACTER_TIME|PHANTOM|PHYSICS|TEMP_(?:ATTACHED|ON_REZ)|NAME|DESC|POS|PRIM_(?:COUNT|EQUIVALENCE)|RETURN_(?:PARCEL(?:_OWNER)?|REGION)|REZZER_KEY|ROO?T|VELOCITY|OMEGA|OWNER|GROUP(?:_TAG)?|CREATOR|ATTACHED_(?:POINT|SLOTS_AVAILABLE)|RENDER_WEIGHT|(?:BODY_SHAPE|PATHFINDING)_TYPE|(?:RUNNING|TOTAL)_SCRIPT_COUNT|TOTAL_INVENTORY_COUNT|SCRIPT_(?:MEMORY|TIME))|TYPE_(?:INTEGER|FLOAT|STRING|KEY|VECTOR|ROTATION|INVALID)|(?:DEBUG|PUBLIC)_CHANNEL|ATTACH_(?:AVATAR_CENTER|CHEST|HEAD|BACK|PELVIS|MOUTH|CHIN|NECK|NOSE|BELLY|[LR](?:SHOULDER|HAND|FOOT|EAR|EYE|[UL](?:ARM|LEG)|HIP)|(?:LEFT|RIGHT)_PEC|HUD_(?:CENTER_[12]|TOP_(?:RIGHT|CENTER|LEFT)|BOTTOM(?:_(?:RIGHT|LEFT))?)|[LR]HAND_RING1|TAIL_(?:BASE|TIP)|[LR]WING|FACE_(?:JAW|[LR]EAR|[LR]EYE|TOUNGE)|GROIN|HIND_[LR]FOOT)|LAND_(?:LEVEL|RAISE|LOWER|SMOOTH|NOISE|REVERT)|DATA_(?:ONLINE|NAME|BORN|SIM_(?:POS|STATUS|RATING)|PAYINFO)|PAYMENT_INFO_(?:ON_FILE|USED)|REMOTE_DATA_(?:CHANNEL|REQUEST|REPLY)|PSYS_(?:PART_(?:BF_(?:ZERO|ONE(?:_MINUS_(?:DEST_COLOR|SOURCE_(ALPHA|COLOR)))?|DEST_COLOR|SOURCE_(ALPHA|COLOR))|BLEND_FUNC_(DEST|SOURCE)|FLAGS|(?:START|END)_(?:COLOR|ALPHA|SCALE|GLOW)|MAX_AGE|(?:RIBBON|WIND|INTERP_(?:COLOR|SCALE)|BOUNCE|FOLLOW_(?:SRC|VELOCITY)|TARGET_(?:POS|LINEAR)|EMISSIVE)_MASK)|SRC_(?:MAX_AGE|PATTERN|ANGLE_(?:BEGIN|END)|BURST_(?:RATE|PART_COUNT|RADIUS|SPEED_(?:MIN|MAX))|ACCEL|TEXTURE|TARGET_KEY|OMEGA|PATTERN_(?:DROP|EXPLODE|ANGLE(?:_CONE(?:_EMPTY)?)?)))|VEHICLE_(?:REFERENCE_FRAME|TYPE_(?:NONE|SLED|CAR|BOAT|AIRPLANE|BALLOON)|(?:LINEAR|ANGULAR)_(?:FRICTION_TIMESCALE|MOTOR_DIRECTION)|LINEAR_MOTOR_OFFSET|HOVER_(?:HEIGHT|EFFICIENCY|TIMESCALE)|BUOYANCY|(?:LINEAR|ANGULAR)_(?:DEFLECTION_(?:EFFICIENCY|TIMESCALE)|MOTOR_(?:DECAY_)?TIMESCALE)|VERTICAL_ATTRACTION_(?:EFFICIENCY|TIMESCALE)|BANKING_(?:EFFICIENCY|MIX|TIMESCALE)|FLAG_(?:NO_DEFLECTION_UP|LIMIT_(?:ROLL_ONLY|MOTOR_UP)|HOVER_(?:(?:WATER|TERRAIN|UP)_ONLY|GLOBAL_HEIGHT)|MOUSELOOK_(?:STEER|BANK)|CAMERA_DECOUPLED))|PRIM_(?:ALLOW_UNSIT|ALPHA_MODE(?:_(?:BLEND|EMISSIVE|MASK|NONE))?|NORMAL|SPECULAR|TYPE(?:_(?:BOX|CYLINDER|PRISM|SPHERE|TORUS|TUBE|RING|SCULPT))?|HOLE_(?:DEFAULT|CIRCLE|SQUARE|TRIANGLE)|MATERIAL(?:_(?:STONE|METAL|GLASS|WOOD|FLESH|PLASTIC|RUBBER))?|SHINY_(?:NONE|LOW|MEDIUM|HIGH)|BUMP_(?:NONE|BRIGHT|DARK|WOOD|BARK|BRICKS|CHECKER|CONCRETE|TILE|STONE|DISKS|GRAVEL|BLOBS|SIDING|LARGETILE|STUCCO|SUCTION|WEAVE)|TEXGEN_(?:DEFAULT|PLANAR)|SCRIPTED_SIT_ONLY|SCULPT_(?:TYPE_(?:SPHERE|TORUS|PLANE|CYLINDER|MASK)|FLAG_(?:MIRROR|INVERT))|PHYSICS(?:_(?:SHAPE_(?:CONVEX|NONE|PRIM|TYPE)))?|(?:POS|ROT)_LOCAL|SLICE|TEXT|FLEXIBLE|POINT_LIGHT|TEMP_ON_REZ|PHANTOM|POSITION|SIT_TARGET|SIZE|ROTATION|TEXTURE|NAME|OMEGA|DESC|LINK_TARGET|COLOR|BUMP_SHINY|FULLBRIGHT|TEXGEN|GLOW|MEDIA_(?:ALT_IMAGE_ENABLE|CONTROLS|(?:CURRENT|HOME)_URL|AUTO_(?:LOOP|PLAY|SCALE|ZOOM)|FIRST_CLICK_INTERACT|(?:WIDTH|HEIGHT)_PIXELS|WHITELIST(?:_ENABLE)?|PERMS_(?:INTERACT|CONTROL)|PARAM_MAX|CONTROLS_(?:STANDARD|MINI)|PERM_(?:NONE|OWNER|GROUP|ANYONE)|MAX_(?:URL_LENGTH|WHITELIST_(?:SIZE|COUNT)|(?:WIDTH|HEIGHT)_PIXELS)))|MASK_(?:BASE|OWNER|GROUP|EVERYONE|NEXT)|PERM_(?:TRANSFER|MODIFY|COPY|MOVE|ALL)|PARCEL_(?:MEDIA_COMMAND_(?:STOP|PAUSE|PLAY|LOOP|TEXTURE|URL|TIME|AGENT|UNLOAD|AUTO_ALIGN|TYPE|SIZE|DESC|LOOP_SET)|FLAG_(?:ALLOW_(?:FLY|(?:GROUP_)?SCRIPTS|LANDMARK|TERRAFORM|DAMAGE|CREATE_(?:GROUP_)?OBJECTS)|USE_(?:ACCESS_(?:GROUP|LIST)|BAN_LIST|LAND_PASS_LIST)|LOCAL_SOUND_ONLY|RESTRICT_PUSHOBJECT|ALLOW_(?:GROUP|ALL)_OBJECT_ENTRY)|COUNT_(?:TOTAL|OWNER|GROUP|OTHER|SELECTED|TEMP)|DETAILS_(?:NAME|DESC|OWNER|GROUP|AREA|ID|SEE_AVATARS))|LIST_STAT_(?:MAX|MIN|MEAN|MEDIAN|STD_DEV|SUM(?:_SQUARES)?|NUM_COUNT|GEOMETRIC_MEAN|RANGE)|PAY_(?:HIDE|DEFAULT)|REGION_FLAG_(?:ALLOW_DAMAGE|FIXED_SUN|BLOCK_TERRAFORM|SANDBOX|DISABLE_(?:COLLISIONS|PHYSICS)|BLOCK_FLY|ALLOW_DIRECT_TELEPORT|RESTRICT_PUSHOBJECT)|HTTP_(?:METHOD|MIMETYPE|BODY_(?:MAXLENGTH|TRUNCATED)|CUSTOM_HEADER|PRAGMA_NO_CACHE|VERBOSE_THROTTLE|VERIFY_CERT)|SIT_(?:INVALID_(?:AGENT|LINK_OBJECT)|NO(?:T_EXPERIENCE|_(?:ACCESS|EXPERIENCE_PERMISSION|SIT_TARGET)))|STRING_(?:TRIM(?:_(?:HEAD|TAIL))?)|CLICK_ACTION_(?:NONE|TOUCH|SIT|BUY|PAY|OPEN(?:_MEDIA)?|PLAY|ZOOM)|TOUCH_INVALID_FACE|PROFILE_(?:NONE|SCRIPT_MEMORY)|RC_(?:DATA_FLAGS|DETECT_PHANTOM|GET_(?:LINK_NUM|NORMAL|ROOT_KEY)|MAX_HITS|REJECT_(?:TYPES|AGENTS|(?:NON)?PHYSICAL|LAND))|RCERR_(?:CAST_TIME_EXCEEDED|SIM_PERF_LOW|UNKNOWN)|ESTATE_ACCESS_(?:ALLOWED_(?:AGENT|GROUP)_(?:ADD|REMOVE)|BANNED_AGENT_(?:ADD|REMOVE))|DENSITY|FRICTION|RESTITUTION|GRAVITY_MULTIPLIER|KFM_(?:COMMAND|CMD_(?:PLAY|STOP|PAUSE)|MODE|FORWARD|LOOP|PING_PONG|REVERSE|DATA|ROTATION|TRANSLATION)|ERR_(?:GENERIC|PARCEL_PERMISSIONS|MALFORMED_PARAMS|RUNTIME_PERMISSIONS|THROTTLED)|CHARACTER_(?:CMD_(?:(?:SMOOTH_)?STOP|JUMP)|DESIRED_(?:TURN_)?SPEED|RADIUS|STAY_WITHIN_PARCEL|LENGTH|ORIENTATION|ACCOUNT_FOR_SKIPPED_FRAMES|AVOIDANCE_MODE|TYPE(?:_(?:[ABCD]|NONE))?|MAX_(?:DECEL|TURN_RADIUS|(?:ACCEL|SPEED)))|PURSUIT_(?:OFFSET|FUZZ_FACTOR|GOAL_TOLERANCE|INTERCEPT)|REQUIRE_LINE_OF_SIGHT|FORCE_DIRECT_PATH|VERTICAL|HORIZONTAL|AVOID_(?:CHARACTERS|DYNAMIC_OBSTACLES|NONE)|PU_(?:EVADE_(?:HIDDEN|SPOTTED)|FAILURE_(?:DYNAMIC_PATHFINDING_DISABLED|INVALID_(?:GOAL|START)|NO_(?:NAVMESH|VALID_DESTINATION)|OTHER|TARGET_GONE|(?:PARCEL_)?UNREACHABLE)|(?:GOAL|SLOWDOWN_DISTANCE)_REACHED)|TRAVERSAL_TYPE(?:_(?:FAST|NONE|SLOW))?|CONTENT_TYPE_(?:ATOM|FORM|HTML|JSON|LLSD|RSS|TEXT|XHTML|XML)|GCNP_(?:RADIUS|STATIC)|(?:PATROL|WANDER)_PAUSE_AT_WAYPOINTS|OPT_(?:AVATAR|CHARACTER|EXCLUSION_VOLUME|LEGACY_LINKSET|MATERIAL_VOLUME|OTHER|STATIC_OBSTACLE|WALKABLE)|SIM_STAT_PCT_CHARS_STEPPED)\\b"},
-{begin:"\\b(?:FALSE|TRUE)\\b"},{begin:"\\b(?:ZERO_ROTATION)\\b"},{begin:"\\b(?:EOF|JSON_(?:ARRAY|DELETE|FALSE|INVALID|NULL|NUMBER|OBJECT|STRING|TRUE)|NULL_KEY|TEXTURE_(?:BLANK|DEFAULT|MEDIA|PLYWOOD|TRANSPARENT)|URL_REQUEST_(?:GRANTED|DENIED))\\b"},{begin:"\\b(?:ZERO_VECTOR|TOUCH_INVALID_(?:TEXCOORD|VECTOR))\\b"}]},{className:"type",begin:"\\b(?:integer|float|string|key|vector|quaternion|rotation|list)\\b"}]}}}());
-hljs.registerLanguage("lua",function(){return function(a){var b={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},c=[a.COMMENT("--(?!\\[=*\\[)","$"),a.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[b],relevance:10})];return{name:"Lua",keywords:{$pattern:a.UNDERSCORE_IDENT_RE,literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},
-contains:c.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[a.inherit(a.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:c}].concat(c)},a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[b],relevance:5}])}}}());
-hljs.registerLanguage("makefile",function(){return function(a){var b={className:"variable",variants:[{begin:"\\$\\("+a.UNDERSCORE_IDENT_RE+"\\)",contains:[a.BACKSLASH_ESCAPE]},{begin:/\$[@%<?\^\+\*]/}]};return{name:"Makefile",aliases:["mk","mak"],keywords:{$pattern:/[\w-]+/,keyword:"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath"},contains:[a.HASH_COMMENT_MODE,b,{className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,
-b]},{className:"variable",begin:/\$\([\w-]+\s/,end:/\)/,keywords:{built_in:"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value"},contains:[b]},{begin:"^"+a.UNDERSCORE_IDENT_RE+"\\s*(?=[:+?]?=)"},{className:"meta",begin:/^\.PHONY:/,end:/$/,keywords:{$pattern:/[\.\w]+/,"meta-keyword":".PHONY"}},{className:"section",
-begin:/^[^\s]+:/,end:/$/,contains:[b]}]}}}());
-hljs.registerLanguage("mathematica",function(){return function(a){return{name:"Mathematica",aliases:["mma","wl"],keywords:{$pattern:"(\\$|\\b)"+a.IDENT_RE+"\\b",keyword:"AASTriangle AbelianGroup Abort AbortKernels AbortProtect AbortScheduledTask Above Abs AbsArg AbsArgPlot Absolute AbsoluteCorrelation AbsoluteCorrelationFunction AbsoluteCurrentValue AbsoluteDashing AbsoluteFileName AbsoluteOptions AbsolutePointSize AbsoluteThickness AbsoluteTime AbsoluteTiming AcceptanceThreshold AccountingForm Accumulate Accuracy AccuracyGoal ActionDelay ActionMenu ActionMenuBox ActionMenuBoxOptions Activate Active ActiveClassification ActiveClassificationObject ActiveItem ActivePrediction ActivePredictionObject ActiveStyle AcyclicGraphQ AddOnHelpPath AddSides AddTo AddToSearchIndex AddUsers AdjacencyGraph AdjacencyList AdjacencyMatrix AdjustmentBox AdjustmentBoxOptions AdjustTimeSeriesForecast AdministrativeDivisionData AffineHalfSpace AffineSpace AffineStateSpaceModel AffineTransform After AggregatedEntityClass AggregationLayer AircraftData AirportData AirPressureData AirTemperatureData AiryAi AiryAiPrime AiryAiZero AiryBi AiryBiPrime AiryBiZero AlgebraicIntegerQ AlgebraicNumber AlgebraicNumberDenominator AlgebraicNumberNorm AlgebraicNumberPolynomial AlgebraicNumberTrace AlgebraicRules AlgebraicRulesData Algebraics AlgebraicUnitQ Alignment AlignmentMarker AlignmentPoint All AllowAdultContent AllowedCloudExtraParameters AllowedCloudParameterExtensions AllowedDimensions AllowedFrequencyRange AllowedHeads AllowGroupClose AllowIncomplete AllowInlineCells AllowKernelInitialization AllowLooseGrammar AllowReverseGroupClose AllowScriptLevelChange AllTrue Alphabet AlphabeticOrder AlphabeticSort AlphaChannel AlternateImage AlternatingFactorial AlternatingGroup AlternativeHypothesis Alternatives AltitudeMethod AmbientLight AmbiguityFunction AmbiguityList Analytic AnatomyData AnatomyForm AnatomyPlot3D AnatomySkinStyle AnatomyStyling AnchoredSearch And AndersonDarlingTest AngerJ AngleBisector AngleBracket AnglePath AnglePath3D AngleVector AngularGauge Animate AnimationCycleOffset AnimationCycleRepetitions AnimationDirection AnimationDisplayTime AnimationRate AnimationRepetitions AnimationRunning AnimationRunTime AnimationTimeIndex Animator AnimatorBox AnimatorBoxOptions AnimatorElements Annotate Annotation AnnotationDelete AnnotationNames AnnotationRules AnnotationValue Annuity AnnuityDue Annulus AnomalyDetection AnomalyDetectorFunction Anonymous Antialiasing AntihermitianMatrixQ Antisymmetric AntisymmetricMatrixQ Antonyms AnyOrder AnySubset AnyTrue Apart ApartSquareFree APIFunction Appearance AppearanceElements AppearanceRules AppellF1 Append AppendCheck AppendLayer AppendTo ApplicationIdentificationKey Apply ApplySides ArcCos ArcCosh ArcCot ArcCoth ArcCsc ArcCsch ArcCurvature ARCHProcess ArcLength ArcSec ArcSech ArcSin ArcSinDistribution ArcSinh ArcTan ArcTanh Area Arg ArgMax ArgMin ArgumentCountQ ARIMAProcess ArithmeticGeometricMean ARMAProcess Around AroundReplace ARProcess Array ArrayComponents ArrayDepth ArrayFilter ArrayFlatten ArrayMesh ArrayPad ArrayPlot ArrayQ ArrayResample ArrayReshape ArrayRules Arrays Arrow Arrow3DBox ArrowBox Arrowheads ASATriangle Ask AskAppend AskConfirm AskDisplay AskedQ AskedValue AskFunction AskState AskTemplateDisplay AspectRatio AspectRatioFixed Assert AssociateTo Association AssociationFormat AssociationMap AssociationQ AssociationThread AssumeDeterministic Assuming Assumptions AstronomicalData AsymptoticDSolveValue AsymptoticEqual AsymptoticEquivalent AsymptoticGreater AsymptoticGreaterEqual AsymptoticIntegrate AsymptoticLess AsymptoticLessEqual AsymptoticOutputTracker AsymptoticRSolveValue AsymptoticSolve AsymptoticSum Asynchronous AsynchronousTaskObject AsynchronousTasks Atom AtomCoordinates AtomCount AtomDiagramCoordinates AtomList AtomQ AttentionLayer Attributes Audio AudioAmplify AudioAnnotate AudioAnnotationLookup AudioBlockMap AudioCapture AudioChannelAssignment AudioChannelCombine AudioChannelMix AudioChannels AudioChannelSeparate AudioData AudioDelay AudioDelete AudioDevice AudioDistance AudioFade AudioFrequencyShift AudioGenerator AudioIdentify AudioInputDevice AudioInsert AudioIntervals AudioJoin AudioLabel AudioLength AudioLocalMeasurements AudioLooping AudioLoudness AudioMeasurements AudioNormalize AudioOutputDevice AudioOverlay AudioPad AudioPan AudioPartition AudioPause AudioPitchShift AudioPlay AudioPlot AudioQ AudioRecord AudioReplace AudioResample AudioReverb AudioSampleRate AudioSpectralMap AudioSpectralTransformation AudioSplit AudioStop AudioStream AudioStreams AudioTimeStretch AudioTrim AudioType AugmentedPolyhedron AugmentedSymmetricPolynomial Authenticate Authentication AuthenticationDialog AutoAction Autocomplete AutocompletionFunction AutoCopy AutocorrelationTest AutoDelete AutoEvaluateEvents AutoGeneratedPackage AutoIndent AutoIndentSpacings AutoItalicWords AutoloadPath AutoMatch Automatic AutomaticImageSize AutoMultiplicationSymbol AutoNumberFormatting AutoOpenNotebooks AutoOpenPalettes AutoQuoteCharacters AutoRefreshed AutoRemove AutorunSequencing AutoScaling AutoScroll AutoSpacing AutoStyleOptions AutoStyleWords AutoSubmitting Axes AxesEdge AxesLabel AxesOrigin AxesStyle AxiomaticTheory Axis BabyMonsterGroupB Back Background BackgroundAppearance BackgroundTasksSettings Backslash Backsubstitution Backward Ball Band BandpassFilter BandstopFilter BarabasiAlbertGraphDistribution BarChart BarChart3D BarcodeImage BarcodeRecognize BaringhausHenzeTest BarLegend BarlowProschanImportance BarnesG BarOrigin BarSpacing BartlettHannWindow BartlettWindow BaseDecode BaseEncode BaseForm Baseline BaselinePosition BaseStyle BasicRecurrentLayer BatchNormalizationLayer BatchSize BatesDistribution BattleLemarieWavelet BayesianMaximization BayesianMaximizationObject BayesianMinimization BayesianMinimizationObject Because BeckmannDistribution Beep Before Begin BeginDialogPacket BeginFrontEndInteractionPacket BeginPackage BellB BellY Below BenfordDistribution BeniniDistribution BenktanderGibratDistribution BenktanderWeibullDistribution BernoulliB BernoulliDistribution BernoulliGraphDistribution BernoulliProcess BernsteinBasis BesselFilterModel BesselI BesselJ BesselJZero BesselK BesselY BesselYZero Beta BetaBinomialDistribution BetaDistribution BetaNegativeBinomialDistribution BetaPrimeDistribution BetaRegularized Between BetweennessCentrality BeveledPolyhedron BezierCurve BezierCurve3DBox BezierCurve3DBoxOptions BezierCurveBox BezierCurveBoxOptions BezierFunction BilateralFilter Binarize BinaryDeserialize BinaryDistance BinaryFormat BinaryImageQ BinaryRead BinaryReadList BinarySerialize BinaryWrite BinCounts BinLists Binomial BinomialDistribution BinomialProcess BinormalDistribution BiorthogonalSplineWavelet BipartiteGraphQ BiquadraticFilterModel BirnbaumImportance BirnbaumSaundersDistribution BitAnd BitClear BitGet BitLength BitNot BitOr BitSet BitShiftLeft BitShiftRight BitXor BiweightLocation BiweightMidvariance Black BlackmanHarrisWindow BlackmanNuttallWindow BlackmanWindow Blank BlankForm BlankNullSequence BlankSequence Blend Block BlockchainAddressData BlockchainBase BlockchainBlockData BlockchainContractValue BlockchainData BlockchainGet BlockchainKeyEncode BlockchainPut BlockchainTokenData BlockchainTransaction BlockchainTransactionData BlockchainTransactionSign BlockchainTransactionSubmit BlockMap BlockRandom BlomqvistBeta BlomqvistBetaTest Blue Blur BodePlot BohmanWindow Bold Bond BondCount BondList BondQ Bookmarks Boole BooleanConsecutiveFunction BooleanConvert BooleanCountingFunction BooleanFunction BooleanGraph BooleanMaxterms BooleanMinimize BooleanMinterms BooleanQ BooleanRegion Booleans BooleanStrings BooleanTable BooleanVariables BorderDimensions BorelTannerDistribution Bottom BottomHatTransform BoundaryDiscretizeGraphics BoundaryDiscretizeRegion BoundaryMesh BoundaryMeshRegion BoundaryMeshRegionQ BoundaryStyle BoundedRegionQ BoundingRegion Bounds Box BoxBaselineShift BoxData BoxDimensions Boxed Boxes BoxForm BoxFormFormatTypes BoxFrame BoxID BoxMargins BoxMatrix BoxObject BoxRatios BoxRotation BoxRotationPoint BoxStyle BoxWhiskerChart Bra BracketingBar BraKet BrayCurtisDistance BreadthFirstScan Break BridgeData BrightnessEqualize BroadcastStationData Brown BrownForsytheTest BrownianBridgeProcess BrowserCategory BSplineBasis BSplineCurve BSplineCurve3DBox BSplineCurve3DBoxOptions BSplineCurveBox BSplineCurveBoxOptions BSplineFunction BSplineSurface BSplineSurface3DBox BSplineSurface3DBoxOptions BubbleChart BubbleChart3D BubbleScale BubbleSizes BuildingData BulletGauge BusinessDayQ ButterflyGraph ButterworthFilterModel Button ButtonBar ButtonBox ButtonBoxOptions ButtonCell ButtonContents ButtonData ButtonEvaluator ButtonExpandable ButtonFrame ButtonFunction ButtonMargins ButtonMinHeight ButtonNote ButtonNotebook ButtonSource ButtonStyle ButtonStyleMenuListing Byte ByteArray ByteArrayFormat ByteArrayQ ByteArrayToString ByteCount ByteOrdering C CachedValue CacheGraphics CachePersistence CalendarConvert CalendarData CalendarType Callout CalloutMarker CalloutStyle CallPacket CanberraDistance Cancel CancelButton CandlestickChart CanonicalGraph CanonicalizePolygon CanonicalizePolyhedron CanonicalName CanonicalWarpingCorrespondence CanonicalWarpingDistance CantorMesh CantorStaircase Cap CapForm CapitalDifferentialD Capitalize CapsuleShape CaptureRunning CardinalBSplineBasis CarlemanLinearize CarmichaelLambda CaseOrdering Cases CaseSensitive Cashflow Casoratian Catalan CatalanNumber Catch Catenate CatenateLayer CauchyDistribution CauchyWindow CayleyGraph CDF CDFDeploy CDFInformation CDFWavelet Ceiling CelestialSystem Cell CellAutoOverwrite CellBaseline CellBoundingBox CellBracketOptions CellChangeTimes CellContents CellContext CellDingbat CellDynamicExpression CellEditDuplicate CellElementsBoundingBox CellElementSpacings CellEpilog CellEvaluationDuplicate CellEvaluationFunction CellEvaluationLanguage CellEventActions CellFrame CellFrameColor CellFrameLabelMargins CellFrameLabels CellFrameMargins CellGroup CellGroupData CellGrouping CellGroupingRules CellHorizontalScrolling CellID CellLabel CellLabelAutoDelete CellLabelMargins CellLabelPositioning CellLabelStyle CellLabelTemplate CellMargins CellObject CellOpen CellPrint CellProlog Cells CellSize CellStyle CellTags CellularAutomaton CensoredDistribution Censoring Center CenterArray CenterDot CentralFeature CentralMoment CentralMomentGeneratingFunction Cepstrogram CepstrogramArray CepstrumArray CForm ChampernowneNumber ChangeOptions ChannelBase ChannelBrokerAction ChannelDatabin ChannelHistoryLength ChannelListen ChannelListener ChannelListeners ChannelListenerWait ChannelObject ChannelPreSendFunction ChannelReceiverFunction ChannelSend ChannelSubscribers ChanVeseBinarize Character CharacterCounts CharacterEncoding CharacterEncodingsPath CharacteristicFunction CharacteristicPolynomial CharacterName CharacterRange Characters ChartBaseStyle ChartElementData ChartElementDataFunction ChartElementFunction ChartElements ChartLabels ChartLayout ChartLegends ChartStyle Chebyshev1FilterModel Chebyshev2FilterModel ChebyshevDistance ChebyshevT ChebyshevU Check CheckAbort CheckAll Checkbox CheckboxBar CheckboxBox CheckboxBoxOptions ChemicalData ChessboardDistance ChiDistribution ChineseRemainder ChiSquareDistribution ChoiceButtons ChoiceDialog CholeskyDecomposition Chop ChromaticityPlot ChromaticityPlot3D ChromaticPolynomial Circle CircleBox CircleDot CircleMinus CirclePlus CirclePoints CircleThrough CircleTimes CirculantGraph CircularOrthogonalMatrixDistribution CircularQuaternionMatrixDistribution CircularRealMatrixDistribution CircularSymplecticMatrixDistribution CircularUnitaryMatrixDistribution Circumsphere CityData ClassifierFunction ClassifierInformation ClassifierMeasurements ClassifierMeasurementsObject Classify ClassPriors Clear ClearAll ClearAttributes ClearCookies ClearPermissions ClearSystemCache ClebschGordan ClickPane Clip ClipboardNotebook ClipFill ClippingStyle ClipPlanes ClipPlanesStyle ClipRange Clock ClockGauge ClockwiseContourIntegral Close Closed CloseKernels ClosenessCentrality Closing ClosingAutoSave ClosingEvent CloudAccountData CloudBase CloudConnect CloudDeploy CloudDirectory CloudDisconnect CloudEvaluate CloudExport CloudExpression CloudExpressions CloudFunction CloudGet CloudImport CloudLoggingData CloudObject CloudObjectInformation CloudObjectInformationData CloudObjectNameFormat CloudObjects CloudObjectURLType CloudPublish CloudPut CloudRenderingMethod CloudSave CloudShare CloudSubmit CloudSymbol CloudUnshare ClusterClassify ClusterDissimilarityFunction ClusteringComponents ClusteringTree CMYKColor Coarse CodeAssistOptions Coefficient CoefficientArrays CoefficientDomain CoefficientList CoefficientRules CoifletWavelet Collect Colon ColonForm ColorBalance ColorCombine ColorConvert ColorCoverage ColorData ColorDataFunction ColorDetect ColorDistance ColorFunction ColorFunctionScaling Colorize ColorNegate ColorOutput ColorProfileData ColorQ ColorQuantize ColorReplace ColorRules ColorSelectorSettings ColorSeparate ColorSetter ColorSetterBox ColorSetterBoxOptions ColorSlider ColorsNear ColorSpace ColorToneMapping Column ColumnAlignments ColumnBackgrounds ColumnForm ColumnLines ColumnsEqual ColumnSpacings ColumnWidths CombinedEntityClass CombinerFunction CometData CommonDefaultFormatTypes Commonest CommonestFilter CommonName CommonUnits CommunityBoundaryStyle CommunityGraphPlot CommunityLabels CommunityRegionStyle CompanyData CompatibleUnitQ CompilationOptions CompilationTarget Compile Compiled CompiledCodeFunction CompiledFunction CompilerOptions Complement CompleteGraph CompleteGraphQ CompleteKaryTree CompletionsListPacket Complex Complexes ComplexExpand ComplexInfinity ComplexityFunction ComplexListPlot ComplexPlot ComplexPlot3D ComponentMeasurements ComponentwiseContextMenu Compose ComposeList ComposeSeries CompositeQ Composition CompoundElement CompoundExpression CompoundPoissonDistribution CompoundPoissonProcess CompoundRenewalProcess Compress CompressedData ComputeUncertainty Condition ConditionalExpression Conditioned Cone ConeBox ConfidenceLevel ConfidenceRange ConfidenceTransform ConfigurationPath ConformAudio ConformImages Congruent ConicHullRegion ConicHullRegion3DBox ConicHullRegionBox ConicOptimization Conjugate ConjugateTranspose Conjunction Connect ConnectedComponents ConnectedGraphComponents ConnectedGraphQ ConnectedMeshComponents ConnectedMoleculeComponents ConnectedMoleculeQ ConnectionSettings ConnectLibraryCallbackFunction ConnectSystemModelComponents ConnesWindow ConoverTest ConsoleMessage ConsoleMessagePacket ConsolePrint Constant ConstantArray ConstantArrayLayer ConstantImage ConstantPlusLayer ConstantRegionQ Constants ConstantTimesLayer ConstellationData ConstrainedMax ConstrainedMin Construct Containing ContainsAll ContainsAny ContainsExactly ContainsNone ContainsOnly ContentFieldOptions ContentLocationFunction ContentObject ContentPadding ContentsBoundingBox ContentSelectable ContentSize Context ContextMenu Contexts ContextToFileName Continuation Continue ContinuedFraction ContinuedFractionK ContinuousAction ContinuousMarkovProcess ContinuousTask ContinuousTimeModelQ ContinuousWaveletData ContinuousWaveletTransform ContourDetect ContourGraphics ContourIntegral ContourLabels ContourLines ContourPlot ContourPlot3D Contours ContourShading ContourSmoothing ContourStyle ContraharmonicMean ContrastiveLossLayer Control ControlActive ControlAlignment ControlGroupContentsBox ControllabilityGramian ControllabilityMatrix ControllableDecomposition ControllableModelQ ControllerDuration ControllerInformation ControllerInformationData ControllerLinking ControllerManipulate ControllerMethod ControllerPath ControllerState ControlPlacement ControlsRendering ControlType Convergents ConversionOptions ConversionRules ConvertToBitmapPacket ConvertToPostScript ConvertToPostScriptPacket ConvexHullMesh ConvexPolygonQ ConvexPolyhedronQ ConvolutionLayer Convolve ConwayGroupCo1 ConwayGroupCo2 ConwayGroupCo3 CookieFunction Cookies CoordinateBoundingBox CoordinateBoundingBoxArray CoordinateBounds CoordinateBoundsArray CoordinateChartData CoordinatesToolOptions CoordinateTransform CoordinateTransformData CoprimeQ Coproduct CopulaDistribution Copyable CopyDatabin CopyDirectory CopyFile CopyTag CopyToClipboard CornerFilter CornerNeighbors Correlation CorrelationDistance CorrelationFunction CorrelationTest Cos Cosh CoshIntegral CosineDistance CosineWindow CosIntegral Cot Coth Count CountDistinct CountDistinctBy CounterAssignments CounterBox CounterBoxOptions CounterClockwiseContourIntegral CounterEvaluator CounterFunction CounterIncrements CounterStyle CounterStyleMenuListing CountRoots CountryData Counts CountsBy Covariance CovarianceEstimatorFunction CovarianceFunction CoxianDistribution CoxIngersollRossProcess CoxModel CoxModelFit CramerVonMisesTest CreateArchive CreateCellID CreateChannel CreateCloudExpression CreateDatabin CreateDataSystemModel CreateDialog CreateDirectory CreateDocument CreateFile CreateIntermediateDirectories CreateManagedLibraryExpression CreateNotebook CreatePalette CreatePalettePacket CreatePermissionsGroup CreateScheduledTask CreateSearchIndex CreateSystemModel CreateTemporary CreateUUID CreateWindow CriterionFunction CriticalityFailureImportance CriticalitySuccessImportance CriticalSection Cross CrossEntropyLossLayer CrossingCount CrossingDetect CrossingPolygon CrossMatrix Csc Csch CTCLossLayer Cube CubeRoot Cubics Cuboid CuboidBox Cumulant CumulantGeneratingFunction Cup CupCap Curl CurlyDoubleQuote CurlyQuote CurrencyConvert CurrentDate CurrentImage CurrentlySpeakingPacket CurrentNotebookImage CurrentScreenImage CurrentValue Curry CurvatureFlowFilter CurveClosed Cyan CycleGraph CycleIndexPolynomial Cycles CyclicGroup Cyclotomic Cylinder CylinderBox CylindricalDecomposition D DagumDistribution DamData DamerauLevenshteinDistance DampingFactor Darker Dashed Dashing DatabaseConnect DatabaseDisconnect DatabaseReference Databin DatabinAdd DatabinRemove Databins DatabinUpload DataCompression DataDistribution DataRange DataReversed Dataset Date DateBounds Dated DateDelimiters DateDifference DatedUnit DateFormat DateFunction DateHistogram DateList DateListLogPlot DateListPlot DateListStepPlot DateObject DateObjectQ DateOverlapsQ DatePattern DatePlus DateRange DateReduction DateString DateTicksFormat DateValue DateWithinQ DaubechiesWavelet DavisDistribution DawsonF DayCount DayCountConvention DayHemisphere DaylightQ DayMatchQ DayName DayNightTerminator DayPlus DayRange DayRound DeBruijnGraph DeBruijnSequence Debug DebugTag Decapitalize Decimal DecimalForm DeclareKnownSymbols DeclarePackage Decompose DeconvolutionLayer Decrement Decrypt DecryptFile DedekindEta DeepSpaceProbeData Default DefaultAxesStyle DefaultBaseStyle DefaultBoxStyle DefaultButton DefaultColor DefaultControlPlacement DefaultDuplicateCellStyle DefaultDuration DefaultElement DefaultFaceGridsStyle DefaultFieldHintStyle DefaultFont DefaultFontProperties DefaultFormatType DefaultFormatTypeForStyle DefaultFrameStyle DefaultFrameTicksStyle DefaultGridLinesStyle DefaultInlineFormatType DefaultInputFormatType DefaultLabelStyle DefaultMenuStyle DefaultNaturalLanguage DefaultNewCellStyle DefaultNewInlineCellStyle DefaultNotebook DefaultOptions DefaultOutputFormatType DefaultPrintPrecision DefaultStyle DefaultStyleDefinitions DefaultTextFormatType DefaultTextInlineFormatType DefaultTicksStyle DefaultTooltipStyle DefaultValue DefaultValues Defer DefineExternal DefineInputStreamMethod DefineOutputStreamMethod DefineResourceFunction Definition Degree DegreeCentrality DegreeGraphDistribution DegreeLexicographic DegreeReverseLexicographic DEigensystem DEigenvalues Deinitialization Del DelaunayMesh Delayed Deletable Delete DeleteAnomalies DeleteBorderComponents DeleteCases DeleteChannel DeleteCloudExpression DeleteContents DeleteDirectory DeleteDuplicates DeleteDuplicatesBy DeleteFile DeleteMissing DeleteObject DeletePermissionsKey DeleteSearchIndex DeleteSmallComponents DeleteStopwords DeleteWithContents DeletionWarning DelimitedArray DelimitedSequence Delimiter DelimiterFlashTime DelimiterMatching Delimiters DeliveryFunction Dendrogram Denominator DensityGraphics DensityHistogram DensityPlot DensityPlot3D DependentVariables Deploy Deployed Depth DepthFirstScan Derivative DerivativeFilter DerivedKey DescriptorStateSpace DesignMatrix DestroyAfterEvaluation Det DeviceClose DeviceConfigure DeviceExecute DeviceExecuteAsynchronous DeviceObject DeviceOpen DeviceOpenQ DeviceRead DeviceReadBuffer DeviceReadLatest DeviceReadList DeviceReadTimeSeries Devices DeviceStreams DeviceWrite DeviceWriteBuffer DGaussianWavelet DiacriticalPositioning Diagonal DiagonalizableMatrixQ DiagonalMatrix DiagonalMatrixQ Dialog DialogIndent DialogInput DialogLevel DialogNotebook DialogProlog DialogReturn DialogSymbols Diamond DiamondMatrix DiceDissimilarity DictionaryLookup DictionaryWordQ DifferenceDelta DifferenceOrder DifferenceQuotient DifferenceRoot DifferenceRootReduce Differences DifferentialD DifferentialRoot DifferentialRootReduce DifferentiatorFilter DigitalSignature DigitBlock DigitBlockMinimum DigitCharacter DigitCount DigitQ DihedralAngle DihedralGroup Dilation DimensionalCombinations DimensionalMeshComponents DimensionReduce DimensionReducerFunction DimensionReduction Dimensions DiracComb DiracDelta DirectedEdge DirectedEdges DirectedGraph DirectedGraphQ DirectedInfinity Direction Directive Directory DirectoryName DirectoryQ DirectoryStack DirichletBeta DirichletCharacter DirichletCondition DirichletConvolve DirichletDistribution DirichletEta DirichletL DirichletLambda DirichletTransform DirichletWindow DisableConsolePrintPacket DisableFormatting DiscreteChirpZTransform DiscreteConvolve DiscreteDelta DiscreteHadamardTransform DiscreteIndicator DiscreteLimit DiscreteLQEstimatorGains DiscreteLQRegulatorGains DiscreteLyapunovSolve DiscreteMarkovProcess DiscreteMaxLimit DiscreteMinLimit DiscretePlot DiscretePlot3D DiscreteRatio DiscreteRiccatiSolve DiscreteShift DiscreteTimeModelQ DiscreteUniformDistribution DiscreteVariables DiscreteWaveletData DiscreteWaveletPacketTransform DiscreteWaveletTransform DiscretizeGraphics DiscretizeRegion Discriminant DisjointQ Disjunction Disk DiskBox DiskMatrix DiskSegment Dispatch DispatchQ DispersionEstimatorFunction Display DisplayAllSteps DisplayEndPacket DisplayFlushImagePacket DisplayForm DisplayFunction DisplayPacket DisplayRules DisplaySetSizePacket DisplayString DisplayTemporary DisplayWith DisplayWithRef DisplayWithVariable DistanceFunction DistanceMatrix DistanceTransform Distribute Distributed DistributedContexts DistributeDefinitions DistributionChart DistributionDomain DistributionFitTest DistributionParameterAssumptions DistributionParameterQ Dithering Div Divergence Divide DivideBy Dividers DivideSides Divisible Divisors DivisorSigma DivisorSum DMSList DMSString Do DockedCells DocumentGenerator DocumentGeneratorInformation DocumentGeneratorInformationData DocumentGenerators DocumentNotebook DocumentWeightingRules Dodecahedron DomainRegistrationInformation DominantColors DOSTextFormat Dot DotDashed DotEqual DotLayer DotPlusLayer Dotted DoubleBracketingBar DoubleContourIntegral DoubleDownArrow DoubleLeftArrow DoubleLeftRightArrow DoubleLeftTee DoubleLongLeftArrow DoubleLongLeftRightArrow DoubleLongRightArrow DoubleRightArrow DoubleRightTee DoubleUpArrow DoubleUpDownArrow DoubleVerticalBar DoublyInfinite Down DownArrow DownArrowBar DownArrowUpArrow DownLeftRightVector DownLeftTeeVector DownLeftVector DownLeftVectorBar DownRightTeeVector DownRightVector DownRightVectorBar Downsample DownTee DownTeeArrow DownValues DragAndDrop DrawEdges DrawFrontFaces DrawHighlighted Drop DropoutLayer DSolve DSolveValue Dt DualLinearProgramming DualPolyhedron DualSystemsModel DumpGet DumpSave DuplicateFreeQ Duration Dynamic DynamicBox DynamicBoxOptions DynamicEvaluationTimeout DynamicGeoGraphics DynamicImage DynamicLocation DynamicModule DynamicModuleBox DynamicModuleBoxOptions DynamicModuleParent DynamicModuleValues DynamicName DynamicNamespace DynamicReference DynamicSetting DynamicUpdating DynamicWrapper DynamicWrapperBox DynamicWrapperBoxOptions E EarthImpactData EarthquakeData EccentricityCentrality Echo EchoFunction EclipseType EdgeAdd EdgeBetweennessCentrality EdgeCapacity EdgeCapForm EdgeColor EdgeConnectivity EdgeContract EdgeCost EdgeCount EdgeCoverQ EdgeCycleMatrix EdgeDashing EdgeDelete EdgeDetect EdgeForm EdgeIndex EdgeJoinForm EdgeLabeling EdgeLabels EdgeLabelStyle EdgeList EdgeOpacity EdgeQ EdgeRenderingFunction EdgeRules EdgeShapeFunction EdgeStyle EdgeThickness EdgeWeight EdgeWeightedGraphQ Editable EditButtonSettings EditCellTagsSettings EditDistance EffectiveInterest Eigensystem Eigenvalues EigenvectorCentrality Eigenvectors Element ElementData ElementwiseLayer ElidedForms Eliminate EliminationOrder Ellipsoid EllipticE EllipticExp EllipticExpPrime EllipticF EllipticFilterModel EllipticK EllipticLog EllipticNomeQ EllipticPi EllipticReducedHalfPeriods EllipticTheta EllipticThetaPrime EmbedCode EmbeddedHTML EmbeddedService EmbeddingLayer EmbeddingObject EmitSound EmphasizeSyntaxErrors EmpiricalDistribution Empty EmptyGraphQ EmptyRegion EnableConsolePrintPacket Enabled Encode Encrypt EncryptedObject EncryptFile End EndAdd EndDialogPacket EndFrontEndInteractionPacket EndOfBuffer EndOfFile EndOfLine EndOfString EndPackage EngineEnvironment EngineeringForm Enter EnterExpressionPacket EnterTextPacket Entity EntityClass EntityClassList EntityCopies EntityFunction EntityGroup EntityInstance EntityList EntityPrefetch EntityProperties EntityProperty EntityPropertyClass EntityRegister EntityStore EntityStores EntityTypeName EntityUnregister EntityValue Entropy EntropyFilter Environment Epilog EpilogFunction Equal EqualColumns EqualRows EqualTilde EqualTo EquatedTo Equilibrium EquirippleFilterKernel Equivalent Erf Erfc Erfi ErlangB ErlangC ErlangDistribution Erosion ErrorBox ErrorBoxOptions ErrorNorm ErrorPacket ErrorsDialogSettings EscapeRadius EstimatedBackground EstimatedDistribution EstimatedProcess EstimatorGains EstimatorRegulator EuclideanDistance EulerAngles EulerCharacteristic EulerE EulerGamma EulerianGraphQ EulerMatrix EulerPhi Evaluatable Evaluate Evaluated EvaluatePacket EvaluateScheduledTask EvaluationBox EvaluationCell EvaluationCompletionAction EvaluationData EvaluationElements EvaluationEnvironment EvaluationMode EvaluationMonitor EvaluationNotebook EvaluationObject EvaluationOrder Evaluator EvaluatorNames EvenQ EventData EventEvaluator EventHandler EventHandlerTag EventLabels EventSeries ExactBlackmanWindow ExactNumberQ ExactRootIsolation ExampleData Except ExcludedForms ExcludedLines ExcludedPhysicalQuantities ExcludePods Exclusions ExclusionsStyle Exists Exit ExitDialog ExoplanetData Exp Expand ExpandAll ExpandDenominator ExpandFileName ExpandNumerator Expectation ExpectationE ExpectedValue ExpGammaDistribution ExpIntegralE ExpIntegralEi ExpirationDate Exponent ExponentFunction ExponentialDistribution ExponentialFamily ExponentialGeneratingFunction ExponentialMovingAverage ExponentialPowerDistribution ExponentPosition ExponentStep Export ExportAutoReplacements ExportByteArray ExportForm ExportPacket ExportString Expression ExpressionCell ExpressionPacket ExpressionUUID ExpToTrig ExtendedEntityClass ExtendedGCD Extension ExtentElementFunction ExtentMarkers ExtentSize ExternalBundle ExternalCall ExternalDataCharacterEncoding ExternalEvaluate ExternalFunction ExternalFunctionName ExternalObject ExternalOptions ExternalSessionObject ExternalSessions ExternalTypeSignature ExternalValue Extract ExtractArchive ExtractLayer ExtremeValueDistribution FaceForm FaceGrids FaceGridsStyle FacialFeatures Factor FactorComplete Factorial Factorial2 FactorialMoment FactorialMomentGeneratingFunction FactorialPower FactorInteger FactorList FactorSquareFree FactorSquareFreeList FactorTerms FactorTermsList Fail Failure FailureAction FailureDistribution FailureQ False FareySequence FARIMAProcess FeatureDistance FeatureExtract FeatureExtraction FeatureExtractor FeatureExtractorFunction FeatureNames FeatureNearest FeatureSpacePlot FeatureSpacePlot3D FeatureTypes FEDisableConsolePrintPacket FeedbackLinearize FeedbackSector FeedbackSectorStyle FeedbackType FEEnableConsolePrintPacket FetalGrowthData Fibonacci Fibonorial FieldCompletionFunction FieldHint FieldHintStyle FieldMasked FieldSize File FileBaseName FileByteCount FileConvert FileDate FileExistsQ FileExtension FileFormat FileHandler FileHash FileInformation FileName FileNameDepth FileNameDialogSettings FileNameDrop FileNameForms FileNameJoin FileNames FileNameSetter FileNameSplit FileNameTake FilePrint FileSize FileSystemMap FileSystemScan FileTemplate FileTemplateApply FileType FilledCurve FilledCurveBox FilledCurveBoxOptions Filling FillingStyle FillingTransform FilteredEntityClass FilterRules FinancialBond FinancialData FinancialDerivative FinancialIndicator Find FindAnomalies FindArgMax FindArgMin FindChannels FindClique FindClusters FindCookies FindCurvePath FindCycle FindDevices FindDistribution FindDistributionParameters FindDivisions FindEdgeCover FindEdgeCut FindEdgeIndependentPaths FindEquationalProof FindEulerianCycle FindExternalEvaluators FindFaces FindFile FindFit FindFormula FindFundamentalCycles FindGeneratingFunction FindGeoLocation FindGeometricConjectures FindGeometricTransform FindGraphCommunities FindGraphIsomorphism FindGraphPartition FindHamiltonianCycle FindHamiltonianPath FindHiddenMarkovStates FindIndependentEdgeSet FindIndependentVertexSet FindInstance FindIntegerNullVector FindKClan FindKClique FindKClub FindKPlex FindLibrary FindLinearRecurrence FindList FindMatchingColor FindMaximum FindMaximumFlow FindMaxValue FindMeshDefects FindMinimum FindMinimumCostFlow FindMinimumCut FindMinValue FindMoleculeSubstructure FindPath FindPeaks FindPermutation FindPostmanTour FindProcessParameters FindRepeat FindRoot FindSequenceFunction FindSettings FindShortestPath FindShortestTour FindSpanningTree FindSystemModelEquilibrium FindTextualAnswer FindThreshold FindTransientRepeat FindVertexCover FindVertexCut FindVertexIndependentPaths Fine FinishDynamic FiniteAbelianGroupCount FiniteGroupCount FiniteGroupData First FirstCase FirstPassageTimeDistribution FirstPosition FischerGroupFi22 FischerGroupFi23 FischerGroupFi24Prime FisherHypergeometricDistribution FisherRatioTest FisherZDistribution Fit FitAll FitRegularization FittedModel FixedOrder FixedPoint FixedPointList FlashSelection Flat Flatten FlattenAt FlattenLayer FlatTopWindow FlipView Floor FlowPolynomial FlushPrintOutputPacket Fold FoldList FoldPair FoldPairList FollowRedirects Font FontColor FontFamily FontForm FontName FontOpacity FontPostScriptName FontProperties FontReencoding FontSize FontSlant FontSubstitutions FontTracking FontVariations FontWeight For ForAll Format FormatRules FormatType FormatTypeAutoConvert FormatValues FormBox FormBoxOptions FormControl FormFunction FormLayoutFunction FormObject FormPage FormTheme FormulaData FormulaLookup FortranForm Forward ForwardBackward Fourier FourierCoefficient FourierCosCoefficient FourierCosSeries FourierCosTransform FourierDCT FourierDCTFilter FourierDCTMatrix FourierDST FourierDSTMatrix FourierMatrix FourierParameters FourierSequenceTransform FourierSeries FourierSinCoefficient FourierSinSeries FourierSinTransform FourierTransform FourierTrigSeries FractionalBrownianMotionProcess FractionalGaussianNoiseProcess FractionalPart FractionBox FractionBoxOptions FractionLine Frame FrameBox FrameBoxOptions Framed FrameInset FrameLabel Frameless FrameMargins FrameRate FrameStyle FrameTicks FrameTicksStyle FRatioDistribution FrechetDistribution FreeQ FrenetSerretSystem FrequencySamplingFilterKernel FresnelC FresnelF FresnelG FresnelS Friday FrobeniusNumber FrobeniusSolve FromAbsoluteTime FromCharacterCode FromCoefficientRules FromContinuedFraction FromDate FromDigits FromDMS FromEntity FromJulianDate FromLetterNumber FromPolarCoordinates FromRomanNumeral FromSphericalCoordinates FromUnixTime Front FrontEndDynamicExpression FrontEndEventActions FrontEndExecute FrontEndObject FrontEndResource FrontEndResourceString FrontEndStackSize FrontEndToken FrontEndTokenExecute FrontEndValueCache FrontEndVersion FrontFaceColor FrontFaceOpacity Full FullAxes FullDefinition FullForm FullGraphics FullInformationOutputRegulator FullOptions FullRegion FullSimplify Function FunctionCompile FunctionCompileExport FunctionCompileExportByteArray FunctionCompileExportLibrary FunctionCompileExportString FunctionDomain FunctionExpand FunctionInterpolation FunctionPeriod FunctionRange FunctionSpace FussellVeselyImportanceGaborFilter GaborMatrix GaborWavelet GainMargins GainPhaseMargins GalaxyData GalleryView Gamma GammaDistribution GammaRegularized GapPenalty GARCHProcess GatedRecurrentLayer Gather GatherBy GaugeFaceElementFunction GaugeFaceStyle GaugeFrameElementFunction GaugeFrameSize GaugeFrameStyle GaugeLabels GaugeMarkers GaugeStyle GaussianFilter GaussianIntegers GaussianMatrix GaussianOrthogonalMatrixDistribution GaussianSymplecticMatrixDistribution GaussianUnitaryMatrixDistribution GaussianWindow GCD GegenbauerC General GeneralizedLinearModelFit GenerateAsymmetricKeyPair GenerateConditions GeneratedCell GeneratedDocumentBinding GenerateDerivedKey GenerateDigitalSignature GenerateDocument GeneratedParameters GeneratedQuantityMagnitudes GenerateHTTPResponse GenerateSecuredAuthenticationKey GenerateSymmetricKey GeneratingFunction GeneratorDescription GeneratorHistoryLength GeneratorOutputType Generic GenericCylindricalDecomposition GenomeData GenomeLookup GeoAntipode GeoArea GeoArraySize GeoBackground GeoBoundingBox GeoBounds GeoBoundsRegion GeoBubbleChart GeoCenter GeoCircle GeodesicClosing GeodesicDilation GeodesicErosion GeodesicOpening GeoDestination GeodesyData GeoDirection GeoDisk GeoDisplacement GeoDistance GeoDistanceList GeoElevationData GeoEntities GeoGraphics GeogravityModelData GeoGridDirectionDifference GeoGridLines GeoGridLinesStyle GeoGridPosition GeoGridRange GeoGridRangePadding GeoGridUnitArea GeoGridUnitDistance GeoGridVector GeoGroup GeoHemisphere GeoHemisphereBoundary GeoHistogram GeoIdentify GeoImage GeoLabels GeoLength GeoListPlot GeoLocation GeologicalPeriodData GeomagneticModelData GeoMarker GeometricAssertion GeometricBrownianMotionProcess GeometricDistribution GeometricMean GeometricMeanFilter GeometricScene GeometricTransformation GeometricTransformation3DBox GeometricTransformation3DBoxOptions GeometricTransformationBox GeometricTransformationBoxOptions GeoModel GeoNearest GeoPath GeoPosition GeoPositionENU GeoPositionXYZ GeoProjection GeoProjectionData GeoRange GeoRangePadding GeoRegionValuePlot GeoResolution GeoScaleBar GeoServer GeoSmoothHistogram GeoStreamPlot GeoStyling GeoStylingImageFunction GeoVariant GeoVector GeoVectorENU GeoVectorPlot GeoVectorXYZ GeoVisibleRegion GeoVisibleRegionBoundary GeoWithinQ GeoZoomLevel GestureHandler GestureHandlerTag Get GetBoundingBoxSizePacket GetContext GetEnvironment GetFileName GetFrontEndOptionsDataPacket GetLinebreakInformationPacket GetMenusPacket GetPageBreakInformationPacket Glaisher GlobalClusteringCoefficient GlobalPreferences GlobalSession Glow GoldenAngle GoldenRatio GompertzMakehamDistribution GoodmanKruskalGamma GoodmanKruskalGammaTest Goto Grad Gradient GradientFilter GradientOrientationFilter GrammarApply GrammarRules GrammarToken Graph Graph3D GraphAssortativity GraphAutomorphismGroup GraphCenter GraphComplement GraphData GraphDensity GraphDiameter GraphDifference GraphDisjointUnion GraphDistance GraphDistanceMatrix GraphElementData GraphEmbedding GraphHighlight GraphHighlightStyle GraphHub Graphics Graphics3D Graphics3DBox Graphics3DBoxOptions GraphicsArray GraphicsBaseline GraphicsBox GraphicsBoxOptions GraphicsColor GraphicsColumn GraphicsComplex GraphicsComplex3DBox GraphicsComplex3DBoxOptions GraphicsComplexBox GraphicsComplexBoxOptions GraphicsContents GraphicsData GraphicsGrid GraphicsGridBox GraphicsGroup GraphicsGroup3DBox GraphicsGroup3DBoxOptions GraphicsGroupBox GraphicsGroupBoxOptions GraphicsGrouping GraphicsHighlightColor GraphicsRow GraphicsSpacing GraphicsStyle GraphIntersection GraphLayout GraphLinkEfficiency GraphPeriphery GraphPlot GraphPlot3D GraphPower GraphPropertyDistribution GraphQ GraphRadius GraphReciprocity GraphRoot GraphStyle GraphUnion Gray GrayLevel Greater GreaterEqual GreaterEqualLess GreaterEqualThan GreaterFullEqual GreaterGreater GreaterLess GreaterSlantEqual GreaterThan GreaterTilde Green GreenFunction Grid GridBaseline GridBox GridBoxAlignment GridBoxBackground GridBoxDividers GridBoxFrame GridBoxItemSize GridBoxItemStyle GridBoxOptions GridBoxSpacings GridCreationSettings GridDefaultElement GridElementStyleOptions GridFrame GridFrameMargins GridGraph GridLines GridLinesStyle GroebnerBasis GroupActionBase GroupBy GroupCentralizer GroupElementFromWord GroupElementPosition GroupElementQ GroupElements GroupElementToWord GroupGenerators Groupings GroupMultiplicationTable GroupOrbits GroupOrder GroupPageBreakWithin GroupSetwiseStabilizer GroupStabilizer GroupStabilizerChain GroupTogetherGrouping GroupTogetherNestedGrouping GrowCutComponents Gudermannian GuidedFilter GumbelDistribution HaarWavelet HadamardMatrix HalfLine HalfNormalDistribution HalfPlane HalfSpace HamiltonianGraphQ HammingDistance HammingWindow HandlerFunctions HandlerFunctionsKeys HankelH1 HankelH2 HankelMatrix HankelTransform HannPoissonWindow HannWindow HaradaNortonGroupHN HararyGraph HarmonicMean HarmonicMeanFilter HarmonicNumber Hash Haversine HazardFunction Head HeadCompose HeaderLines Heads HeavisideLambda HeavisidePi HeavisideTheta HeldGroupHe HeldPart HelpBrowserLookup HelpBrowserNotebook HelpBrowserSettings Here HermiteDecomposition HermiteH HermitianMatrixQ HessenbergDecomposition Hessian HexadecimalCharacter Hexahedron HexahedronBox HexahedronBoxOptions HiddenMarkovProcess HiddenSurface Highlighted HighlightGraph HighlightImage HighlightMesh HighpassFilter HigmanSimsGroupHS HilbertCurve HilbertFilter HilbertMatrix Histogram Histogram3D HistogramDistribution HistogramList HistogramTransform HistogramTransformInterpolation HistoricalPeriodData HitMissTransform HITSCentrality HjorthDistribution HodgeDual HoeffdingD HoeffdingDTest Hold HoldAll HoldAllComplete HoldComplete HoldFirst HoldForm HoldPattern HoldRest HolidayCalendar HomeDirectory HomePage Horizontal HorizontalForm HorizontalGauge HorizontalScrollPosition HornerForm HostLookup HotellingTSquareDistribution HoytDistribution HTMLSave HTTPErrorResponse HTTPRedirect HTTPRequest HTTPRequestData HTTPResponse Hue HumanGrowthData HumpDownHump HumpEqual HurwitzLerchPhi HurwitzZeta HyperbolicDistribution HypercubeGraph HyperexponentialDistribution Hyperfactorial Hypergeometric0F1 Hypergeometric0F1Regularized Hypergeometric1F1 Hypergeometric1F1Regularized Hypergeometric2F1 Hypergeometric2F1Regularized HypergeometricDistribution HypergeometricPFQ HypergeometricPFQRegularized HypergeometricU Hyperlink HyperlinkCreationSettings Hyperplane Hyphenation HyphenationOptions HypoexponentialDistribution HypothesisTestData I IconData Iconize IconizedObject IconRules Icosahedron Identity IdentityMatrix If IgnoreCase IgnoreDiacritics IgnorePunctuation IgnoreSpellCheck IgnoringInactive Im Image Image3D Image3DProjection Image3DSlices ImageAccumulate ImageAdd ImageAdjust ImageAlign ImageApply ImageApplyIndexed ImageAspectRatio ImageAssemble ImageAugmentationLayer ImageBoundingBoxes ImageCache ImageCacheValid ImageCapture ImageCaptureFunction ImageCases ImageChannels ImageClip ImageCollage ImageColorSpace ImageCompose ImageContainsQ ImageContents ImageConvolve ImageCooccurrence ImageCorners ImageCorrelate ImageCorrespondingPoints ImageCrop ImageData ImageDeconvolve ImageDemosaic ImageDifference ImageDimensions ImageDisplacements ImageDistance ImageEffect ImageExposureCombine ImageFeatureTrack ImageFileApply ImageFileFilter ImageFileScan ImageFilter ImageFocusCombine ImageForestingComponents ImageFormattingWidth ImageForwardTransformation ImageGraphics ImageHistogram ImageIdentify ImageInstanceQ ImageKeypoints ImageLevels ImageLines ImageMargins ImageMarker ImageMarkers ImageMeasurements ImageMesh ImageMultiply ImageOffset ImagePad ImagePadding ImagePartition ImagePeriodogram ImagePerspectiveTransformation ImagePosition ImagePreviewFunction ImagePyramid ImagePyramidApply ImageQ ImageRangeCache ImageRecolor ImageReflect ImageRegion ImageResize ImageResolution ImageRestyle ImageRotate ImageRotated ImageSaliencyFilter ImageScaled ImageScan ImageSize ImageSizeAction ImageSizeCache ImageSizeMultipliers ImageSizeRaw ImageSubtract ImageTake ImageTransformation ImageTrim ImageType ImageValue ImageValuePositions ImagingDevice ImplicitRegion Implies Import ImportAutoReplacements ImportByteArray ImportOptions ImportString ImprovementImportance In Inactivate Inactive IncidenceGraph IncidenceList IncidenceMatrix IncludeAromaticBonds IncludeConstantBasis IncludeDefinitions IncludeDirectories IncludeFileExtension IncludeGeneratorTasks IncludeHydrogens IncludeInflections IncludeMetaInformation IncludePods IncludeQuantities IncludeRelatedTables IncludeSingularTerm IncludeWindowTimes Increment IndefiniteMatrixQ Indent IndentingNewlineSpacings IndentMaxFraction IndependenceTest IndependentEdgeSetQ IndependentPhysicalQuantity IndependentUnit IndependentUnitDimension IndependentVertexSetQ Indeterminate IndeterminateThreshold IndexCreationOptions Indexed IndexGraph IndexTag Inequality InexactNumberQ InexactNumbers InfiniteLine InfinitePlane Infinity Infix InflationAdjust InflationMethod Information InformationData InformationDataGrid Inherited InheritScope InhomogeneousPoissonProcess InitialEvaluationHistory Initialization InitializationCell InitializationCellEvaluation InitializationCellWarning InitializationObjects InitializationValue Initialize InitialSeeding InlineCounterAssignments InlineCounterIncrements InlineRules Inner InnerPolygon InnerPolyhedron Inpaint Input InputAliases InputAssumptions InputAutoReplacements InputField InputFieldBox InputFieldBoxOptions InputForm InputGrouping InputNamePacket InputNotebook InputPacket InputSettings InputStream InputString InputStringPacket InputToBoxFormPacket Insert InsertionFunction InsertionPointObject InsertLinebreaks InsertResults Inset Inset3DBox Inset3DBoxOptions InsetBox InsetBoxOptions Insphere Install InstallService InstanceNormalizationLayer InString Integer IntegerDigits IntegerExponent IntegerLength IntegerName IntegerPart IntegerPartitions IntegerQ IntegerReverse Integers IntegerString Integral Integrate Interactive InteractiveTradingChart Interlaced Interleaving InternallyBalancedDecomposition InterpolatingFunction InterpolatingPolynomial Interpolation InterpolationOrder InterpolationPoints InterpolationPrecision Interpretation InterpretationBox InterpretationBoxOptions InterpretationFunction Interpreter InterpretTemplate InterquartileRange Interrupt InterruptSettings IntersectingQ Intersection Interval IntervalIntersection IntervalMarkers IntervalMarkersStyle IntervalMemberQ IntervalSlider IntervalUnion Into Inverse InverseBetaRegularized InverseCDF InverseChiSquareDistribution InverseContinuousWaveletTransform InverseDistanceTransform InverseEllipticNomeQ InverseErf InverseErfc InverseFourier InverseFourierCosTransform InverseFourierSequenceTransform InverseFourierSinTransform InverseFourierTransform InverseFunction InverseFunctions InverseGammaDistribution InverseGammaRegularized InverseGaussianDistribution InverseGudermannian InverseHankelTransform InverseHaversine InverseImagePyramid InverseJacobiCD InverseJacobiCN InverseJacobiCS InverseJacobiDC InverseJacobiDN InverseJacobiDS InverseJacobiNC InverseJacobiND InverseJacobiNS InverseJacobiSC InverseJacobiSD InverseJacobiSN InverseLaplaceTransform InverseMellinTransform InversePermutation InverseRadon InverseRadonTransform InverseSeries InverseShortTimeFourier InverseSpectrogram InverseSurvivalFunction InverseTransformedRegion InverseWaveletTransform InverseWeierstrassP InverseWishartMatrixDistribution InverseZTransform Invisible InvisibleApplication InvisibleTimes IPAddress IrreduciblePolynomialQ IslandData IsolatingInterval IsomorphicGraphQ IsotopeData Italic Item ItemAspectRatio ItemBox ItemBoxOptions ItemSize ItemStyle ItoProcess JaccardDissimilarity JacobiAmplitude Jacobian JacobiCD JacobiCN JacobiCS JacobiDC JacobiDN JacobiDS JacobiNC JacobiND JacobiNS JacobiP JacobiSC JacobiSD JacobiSN JacobiSymbol JacobiZeta JankoGroupJ1 JankoGroupJ2 JankoGroupJ3 JankoGroupJ4 JarqueBeraALMTest JohnsonDistribution Join JoinAcross Joined JoinedCurve JoinedCurveBox JoinedCurveBoxOptions JoinForm JordanDecomposition JordanModelDecomposition JulianDate JuliaSetBoettcher JuliaSetIterationCount JuliaSetPlot JuliaSetPoints K KagiChart KaiserBesselWindow KaiserWindow KalmanEstimator KalmanFilter KarhunenLoeveDecomposition KaryTree KatzCentrality KCoreComponents KDistribution KEdgeConnectedComponents KEdgeConnectedGraphQ KelvinBei KelvinBer KelvinKei KelvinKer KendallTau KendallTauTest KernelExecute KernelFunction KernelMixtureDistribution Kernels Ket Key KeyCollisionFunction KeyComplement KeyDrop KeyDropFrom KeyExistsQ KeyFreeQ KeyIntersection KeyMap KeyMemberQ KeypointStrength Keys KeySelect KeySort KeySortBy KeyTake KeyUnion KeyValueMap KeyValuePattern Khinchin KillProcess KirchhoffGraph KirchhoffMatrix KleinInvariantJ KnapsackSolve KnightTourGraph KnotData KnownUnitQ KochCurve KolmogorovSmirnovTest KroneckerDelta KroneckerModelDecomposition KroneckerProduct KroneckerSymbol KuiperTest KumaraswamyDistribution Kurtosis KuwaharaFilter KVertexConnectedComponents KVertexConnectedGraphQ LABColor Label Labeled LabeledSlider LabelingFunction LabelingSize LabelStyle LabelVisibility LaguerreL LakeData LambdaComponents LambertW LaminaData LanczosWindow LandauDistribution Language LanguageCategory LanguageData LanguageIdentify LanguageOptions LaplaceDistribution LaplaceTransform Laplacian LaplacianFilter LaplacianGaussianFilter Large Larger Last Latitude LatitudeLongitude LatticeData LatticeReduce Launch LaunchKernels LayeredGraphPlot LayerSizeFunction LayoutInformation LCHColor LCM LeaderSize LeafCount LeapYearQ LearnDistribution LearnedDistribution LearningRate LearningRateMultipliers LeastSquares LeastSquaresFilterKernel Left LeftArrow LeftArrowBar LeftArrowRightArrow LeftDownTeeVector LeftDownVector LeftDownVectorBar LeftRightArrow LeftRightVector LeftTee LeftTeeArrow LeftTeeVector LeftTriangle LeftTriangleBar LeftTriangleEqual LeftUpDownVector LeftUpTeeVector LeftUpVector LeftUpVectorBar LeftVector LeftVectorBar LegendAppearance Legended LegendFunction LegendLabel LegendLayout LegendMargins LegendMarkers LegendMarkerSize LegendreP LegendreQ LegendreType Length LengthWhile LerchPhi Less LessEqual LessEqualGreater LessEqualThan LessFullEqual LessGreater LessLess LessSlantEqual LessThan LessTilde LetterCharacter LetterCounts LetterNumber LetterQ Level LeveneTest LeviCivitaTensor LevyDistribution Lexicographic LibraryDataType LibraryFunction LibraryFunctionError LibraryFunctionInformation LibraryFunctionLoad LibraryFunctionUnload LibraryLoad LibraryUnload LicenseID LiftingFilterData LiftingWaveletTransform LightBlue LightBrown LightCyan Lighter LightGray LightGreen Lighting LightingAngle LightMagenta LightOrange LightPink LightPurple LightRed LightSources LightYellow Likelihood Limit LimitsPositioning LimitsPositioningTokens LindleyDistribution Line Line3DBox Line3DBoxOptions LinearFilter LinearFractionalOptimization LinearFractionalTransform LinearGradientImage LinearizingTransformationData LinearLayer LinearModelFit LinearOffsetFunction LinearOptimization LinearProgramming LinearRecurrence LinearSolve LinearSolveFunction LineBox LineBoxOptions LineBreak LinebreakAdjustments LineBreakChart LinebreakSemicolonWeighting LineBreakWithin LineColor LineGraph LineIndent LineIndentMaxFraction LineIntegralConvolutionPlot LineIntegralConvolutionScale LineLegend LineOpacity LineSpacing LineWrapParts LinkActivate LinkClose LinkConnect LinkConnectedQ LinkCreate LinkError LinkFlush LinkFunction LinkHost LinkInterrupt LinkLaunch LinkMode LinkObject LinkOpen LinkOptions LinkPatterns LinkProtocol LinkRankCentrality LinkRead LinkReadHeld LinkReadyQ Links LinkService LinkWrite LinkWriteHeld LiouvilleLambda List Listable ListAnimate ListContourPlot ListContourPlot3D ListConvolve ListCorrelate ListCurvePathPlot ListDeconvolve ListDensityPlot ListDensityPlot3D Listen ListFormat ListFourierSequenceTransform ListInterpolation ListLineIntegralConvolutionPlot ListLinePlot ListLogLinearPlot ListLogLogPlot ListLogPlot ListPicker ListPickerBox ListPickerBoxBackground ListPickerBoxOptions ListPlay ListPlot ListPlot3D ListPointPlot3D ListPolarPlot ListQ ListSliceContourPlot3D ListSliceDensityPlot3D ListSliceVectorPlot3D ListStepPlot ListStreamDensityPlot ListStreamPlot ListSurfacePlot3D ListVectorDensityPlot ListVectorPlot ListVectorPlot3D ListZTransform Literal LiteralSearch LocalAdaptiveBinarize LocalCache LocalClusteringCoefficient LocalizeDefinitions LocalizeVariables LocalObject LocalObjects LocalResponseNormalizationLayer LocalSubmit LocalSymbol LocalTime LocalTimeZone LocationEquivalenceTest LocationTest Locator LocatorAutoCreate LocatorBox LocatorBoxOptions LocatorCentering LocatorPane LocatorPaneBox LocatorPaneBoxOptions LocatorRegion Locked Log Log10 Log2 LogBarnesG LogGamma LogGammaDistribution LogicalExpand LogIntegral LogisticDistribution LogisticSigmoid LogitModelFit LogLikelihood LogLinearPlot LogLogisticDistribution LogLogPlot LogMultinormalDistribution LogNormalDistribution LogPlot LogRankTest LogSeriesDistribution LongEqual Longest LongestCommonSequence LongestCommonSequencePositions LongestCommonSubsequence LongestCommonSubsequencePositions LongestMatch LongestOrderedSequence LongForm Longitude LongLeftArrow LongLeftRightArrow LongRightArrow LongShortTermMemoryLayer Lookup Loopback LoopFreeGraphQ LossFunction LowerCaseQ LowerLeftArrow LowerRightArrow LowerTriangularize LowerTriangularMatrixQ LowpassFilter LQEstimatorGains LQGRegulator LQOutputRegulatorGains LQRegulatorGains LUBackSubstitution LucasL LuccioSamiComponents LUDecomposition LunarEclipse LUVColor LyapunovSolve LyonsGroupLy MachineID MachineName MachineNumberQ MachinePrecision MacintoshSystemPageSetup Magenta Magnification Magnify MailAddressValidation MailExecute MailFolder MailItem MailReceiverFunction MailResponseFunction MailSearch MailServerConnect MailServerConnection MailSettings MainSolve MaintainDynamicCaches Majority MakeBoxes MakeExpression MakeRules ManagedLibraryExpressionID ManagedLibraryExpressionQ MandelbrotSetBoettcher MandelbrotSetDistance MandelbrotSetIterationCount MandelbrotSetMemberQ MandelbrotSetPlot MangoldtLambda ManhattanDistance Manipulate Manipulator MannedSpaceMissionData MannWhitneyTest MantissaExponent Manual Map MapAll MapAt MapIndexed MAProcess MapThread MarchenkoPasturDistribution MarcumQ MardiaCombinedTest MardiaKurtosisTest MardiaSkewnessTest MarginalDistribution MarkovProcessProperties Masking MatchingDissimilarity MatchLocalNameQ MatchLocalNames MatchQ Material MathematicalFunctionData MathematicaNotation MathieuC MathieuCharacteristicA MathieuCharacteristicB MathieuCharacteristicExponent MathieuCPrime MathieuGroupM11 MathieuGroupM12 MathieuGroupM22 MathieuGroupM23 MathieuGroupM24 MathieuS MathieuSPrime MathMLForm MathMLText Matrices MatrixExp MatrixForm MatrixFunction MatrixLog MatrixNormalDistribution MatrixPlot MatrixPower MatrixPropertyDistribution MatrixQ MatrixRank MatrixTDistribution Max MaxBend MaxCellMeasure MaxColorDistance MaxDetect MaxDuration MaxExtraBandwidths MaxExtraConditions MaxFeatureDisplacement MaxFeatures MaxFilter MaximalBy Maximize MaxItems MaxIterations MaxLimit MaxMemoryUsed MaxMixtureKernels MaxOverlapFraction MaxPlotPoints MaxPoints MaxRecursion MaxStableDistribution MaxStepFraction MaxSteps MaxStepSize MaxTrainingRounds MaxValue MaxwellDistribution MaxWordGap McLaughlinGroupMcL Mean MeanAbsoluteLossLayer MeanAround MeanClusteringCoefficient MeanDegreeConnectivity MeanDeviation MeanFilter MeanGraphDistance MeanNeighborDegree MeanShift MeanShiftFilter MeanSquaredLossLayer Median MedianDeviation MedianFilter MedicalTestData Medium MeijerG MeijerGReduce MeixnerDistribution MellinConvolve MellinTransform MemberQ MemoryAvailable MemoryConstrained MemoryConstraint MemoryInUse MengerMesh Menu MenuAppearance MenuCommandKey MenuEvaluator MenuItem MenuList MenuPacket MenuSortingValue MenuStyle MenuView Merge MergeDifferences MergingFunction MersennePrimeExponent MersennePrimeExponentQ Mesh MeshCellCentroid MeshCellCount MeshCellHighlight MeshCellIndex MeshCellLabel MeshCellMarker MeshCellMeasure MeshCellQuality MeshCells MeshCellShapeFunction MeshCellStyle MeshCoordinates MeshFunctions MeshPrimitives MeshQualityGoal MeshRange MeshRefinementFunction MeshRegion MeshRegionQ MeshShading MeshStyle Message MessageDialog MessageList MessageName MessageObject MessageOptions MessagePacket Messages MessagesNotebook MetaCharacters MetaInformation MeteorShowerData Method MethodOptions MexicanHatWavelet MeyerWavelet Midpoint Min MinColorDistance MinDetect MineralData MinFilter MinimalBy MinimalPolynomial MinimalStateSpaceModel Minimize MinimumTimeIncrement MinIntervalSize MinkowskiQuestionMark MinLimit MinMax MinorPlanetData Minors MinRecursion MinSize MinStableDistribution Minus MinusPlus MinValue Missing MissingBehavior MissingDataMethod MissingDataRules MissingQ MissingString MissingStyle MissingValuePattern MittagLefflerE MixedFractionParts MixedGraphQ MixedMagnitude MixedRadix MixedRadixQuantity MixedUnit MixtureDistribution Mod Modal Mode Modular ModularInverse ModularLambda Module Modulus MoebiusMu Molecule MoleculeContainsQ MoleculeEquivalentQ MoleculeGraph MoleculeModify MoleculePattern MoleculePlot MoleculePlot3D MoleculeProperty MoleculeQ MoleculeValue Moment Momentary MomentConvert MomentEvaluate MomentGeneratingFunction MomentOfInertia Monday Monitor MonomialList MonomialOrder MonsterGroupM MoonPhase MoonPosition MorletWavelet MorphologicalBinarize MorphologicalBranchPoints MorphologicalComponents MorphologicalEulerNumber MorphologicalGraph MorphologicalPerimeter MorphologicalTransform MortalityData Most MountainData MouseAnnotation MouseAppearance MouseAppearanceTag MouseButtons Mouseover MousePointerNote MousePosition MovieData MovingAverage MovingMap MovingMedian MoyalDistribution Multicolumn MultiedgeStyle MultigraphQ MultilaunchWarning MultiLetterItalics MultiLetterStyle MultilineFunction Multinomial MultinomialDistribution MultinormalDistribution MultiplicativeOrder Multiplicity MultiplySides Multiselection MultivariateHypergeometricDistribution MultivariatePoissonDistribution MultivariateTDistribution N NakagamiDistribution NameQ Names NamespaceBox NamespaceBoxOptions Nand NArgMax NArgMin NBernoulliB NBodySimulation NBodySimulationData NCache NDEigensystem NDEigenvalues NDSolve NDSolveValue Nearest NearestFunction NearestNeighborGraph NearestTo NebulaData NeedCurrentFrontEndPackagePacket NeedCurrentFrontEndSymbolsPacket NeedlemanWunschSimilarity Needs Negative NegativeBinomialDistribution NegativeDefiniteMatrixQ NegativeIntegers NegativeMultinomialDistribution NegativeRationals NegativeReals NegativeSemidefiniteMatrixQ NeighborhoodData NeighborhoodGraph Nest NestedGreaterGreater NestedLessLess NestedScriptRules NestGraph NestList NestWhile NestWhileList NetAppend NetBidirectionalOperator NetChain NetDecoder NetDelete NetDrop NetEncoder NetEvaluationMode NetExtract NetFlatten NetFoldOperator NetGraph NetInformation NetInitialize NetInsert NetInsertSharedArrays NetJoin NetMapOperator NetMapThreadOperator NetMeasurements NetModel NetNestOperator NetPairEmbeddingOperator NetPort NetPortGradient NetPrepend NetRename NetReplace NetReplacePart NetSharedArray NetStateObject NetTake NetTrain NetTrainResultsObject NetworkPacketCapture NetworkPacketRecording NetworkPacketRecordingDuring NetworkPacketTrace NeumannValue NevilleThetaC NevilleThetaD NevilleThetaN NevilleThetaS NewPrimitiveStyle NExpectation Next NextCell NextDate NextPrime NextScheduledTaskTime NHoldAll NHoldFirst NHoldRest NicholsGridLines NicholsPlot NightHemisphere NIntegrate NMaximize NMaxValue NMinimize NMinValue NominalVariables NonAssociative NoncentralBetaDistribution NoncentralChiSquareDistribution NoncentralFRatioDistribution NoncentralStudentTDistribution NonCommutativeMultiply NonConstants NondimensionalizationTransform None NoneTrue NonlinearModelFit NonlinearStateSpaceModel NonlocalMeansFilter NonNegative NonNegativeIntegers NonNegativeRationals NonNegativeReals NonPositive NonPositiveIntegers NonPositiveRationals NonPositiveReals Nor NorlundB Norm Normal NormalDistribution NormalGrouping NormalizationLayer Normalize Normalized NormalizedSquaredEuclideanDistance NormalMatrixQ NormalsFunction NormFunction Not NotCongruent NotCupCap NotDoubleVerticalBar Notebook NotebookApply NotebookAutoSave NotebookClose NotebookConvertSettings NotebookCreate NotebookCreateReturnObject NotebookDefault NotebookDelete NotebookDirectory NotebookDynamicExpression NotebookEvaluate NotebookEventActions NotebookFileName NotebookFind NotebookFindReturnObject NotebookGet NotebookGetLayoutInformationPacket NotebookGetMisspellingsPacket NotebookImport NotebookInformation NotebookInterfaceObject NotebookLocate NotebookObject NotebookOpen NotebookOpenReturnObject NotebookPath NotebookPrint NotebookPut NotebookPutReturnObject NotebookRead NotebookResetGeneratedCells Notebooks NotebookSave NotebookSaveAs NotebookSelection NotebookSetupLayoutInformationPacket NotebooksMenu NotebookTemplate NotebookWrite NotElement NotEqualTilde NotExists NotGreater NotGreaterEqual NotGreaterFullEqual NotGreaterGreater NotGreaterLess NotGreaterSlantEqual NotGreaterTilde Nothing NotHumpDownHump NotHumpEqual NotificationFunction NotLeftTriangle NotLeftTriangleBar NotLeftTriangleEqual NotLess NotLessEqual NotLessFullEqual NotLessGreater NotLessLess NotLessSlantEqual NotLessTilde NotNestedGreaterGreater NotNestedLessLess NotPrecedes NotPrecedesEqual NotPrecedesSlantEqual NotPrecedesTilde NotReverseElement NotRightTriangle NotRightTriangleBar NotRightTriangleEqual NotSquareSubset NotSquareSubsetEqual NotSquareSuperset NotSquareSupersetEqual NotSubset NotSubsetEqual NotSucceeds NotSucceedsEqual NotSucceedsSlantEqual NotSucceedsTilde NotSuperset NotSupersetEqual NotTilde NotTildeEqual NotTildeFullEqual NotTildeTilde NotVerticalBar Now NoWhitespace NProbability NProduct NProductFactors NRoots NSolve NSum NSumTerms NuclearExplosionData NuclearReactorData Null NullRecords NullSpace NullWords Number NumberCompose NumberDecompose NumberExpand NumberFieldClassNumber NumberFieldDiscriminant NumberFieldFundamentalUnits NumberFieldIntegralBasis NumberFieldNormRepresentatives NumberFieldRegulator NumberFieldRootsOfUnity NumberFieldSignature NumberForm NumberFormat NumberLinePlot NumberMarks NumberMultiplier NumberPadding NumberPoint NumberQ NumberSeparator NumberSigns NumberString Numerator NumeratorDenominator NumericalOrder NumericalSort NumericArray NumericArrayQ NumericArrayType NumericFunction NumericQ NuttallWindow NValues NyquistGridLines NyquistPlot O ObservabilityGramian ObservabilityMatrix ObservableDecomposition ObservableModelQ OceanData Octahedron OddQ Off Offset OLEData On ONanGroupON Once OneIdentity Opacity OpacityFunction OpacityFunctionScaling Open OpenAppend Opener OpenerBox OpenerBoxOptions OpenerView OpenFunctionInspectorPacket Opening OpenRead OpenSpecialOptions OpenTemporary OpenWrite Operate OperatingSystem OptimumFlowData Optional OptionalElement OptionInspectorSettings OptionQ Options OptionsPacket OptionsPattern OptionValue OptionValueBox OptionValueBoxOptions Or Orange Order OrderDistribution OrderedQ Ordering OrderingBy OrderingLayer Orderless OrderlessPatternSequence OrnsteinUhlenbeckProcess Orthogonalize OrthogonalMatrixQ Out Outer OuterPolygon OuterPolyhedron OutputAutoOverwrite OutputControllabilityMatrix OutputControllableModelQ OutputForm OutputFormData OutputGrouping OutputMathEditExpression OutputNamePacket OutputResponse OutputSizeLimit OutputStream Over OverBar OverDot Overflow OverHat Overlaps Overlay OverlayBox OverlayBoxOptions Overscript OverscriptBox OverscriptBoxOptions OverTilde OverVector OverwriteTarget OwenT OwnValues Package PackingMethod PaddedForm Padding PaddingLayer PaddingSize PadeApproximant PadLeft PadRight PageBreakAbove PageBreakBelow PageBreakWithin PageFooterLines PageFooters PageHeaderLines PageHeaders PageHeight PageRankCentrality PageTheme PageWidth Pagination PairedBarChart PairedHistogram PairedSmoothHistogram PairedTTest PairedZTest PaletteNotebook PalettePath PalindromeQ Pane PaneBox PaneBoxOptions Panel PanelBox PanelBoxOptions Paneled PaneSelector PaneSelectorBox PaneSelectorBoxOptions PaperWidth ParabolicCylinderD ParagraphIndent ParagraphSpacing ParallelArray ParallelCombine ParallelDo Parallelepiped ParallelEvaluate Parallelization Parallelize ParallelMap ParallelNeeds Parallelogram ParallelProduct ParallelSubmit ParallelSum ParallelTable ParallelTry Parameter ParameterEstimator ParameterMixtureDistribution ParameterVariables ParametricFunction ParametricNDSolve ParametricNDSolveValue ParametricPlot ParametricPlot3D ParametricRegion ParentBox ParentCell ParentConnect ParentDirectory ParentForm Parenthesize ParentList ParentNotebook ParetoDistribution ParetoPickandsDistribution ParkData Part PartBehavior PartialCorrelationFunction PartialD ParticleAcceleratorData ParticleData Partition PartitionGranularity PartitionsP PartitionsQ PartLayer PartOfSpeech PartProtection ParzenWindow PascalDistribution PassEventsDown PassEventsUp Paste PasteAutoQuoteCharacters PasteBoxFormInlineCells PasteButton Path PathGraph PathGraphQ Pattern PatternSequence PatternTest PauliMatrix PaulWavelet Pause PausedTime PDF PeakDetect PeanoCurve PearsonChiSquareTest PearsonCorrelationTest PearsonDistribution PercentForm PerfectNumber PerfectNumberQ PerformanceGoal Perimeter PeriodicBoundaryCondition PeriodicInterpolation Periodogram PeriodogramArray Permanent Permissions PermissionsGroup PermissionsGroupMemberQ PermissionsGroups PermissionsKey PermissionsKeys PermutationCycles PermutationCyclesQ PermutationGroup PermutationLength PermutationList PermutationListQ PermutationMax PermutationMin PermutationOrder PermutationPower PermutationProduct PermutationReplace Permutations PermutationSupport Permute PeronaMalikFilter Perpendicular PerpendicularBisector PersistenceLocation PersistenceTime PersistentObject PersistentObjects PersistentValue PersonData PERTDistribution PetersenGraph PhaseMargins PhaseRange PhysicalSystemData Pi Pick PIDData PIDDerivativeFilter PIDFeedforward PIDTune Piecewise PiecewiseExpand PieChart PieChart3D PillaiTrace PillaiTraceTest PingTime Pink PitchRecognize Pivoting PixelConstrained PixelValue PixelValuePositions Placed Placeholder PlaceholderReplace Plain PlanarAngle PlanarGraph PlanarGraphQ PlanckRadiationLaw PlaneCurveData PlanetaryMoonData PlanetData PlantData Play PlayRange Plot Plot3D Plot3Matrix PlotDivision PlotJoined PlotLabel PlotLabels PlotLayout PlotLegends PlotMarkers PlotPoints PlotRange PlotRangeClipping PlotRangeClipPlanesStyle PlotRangePadding PlotRegion PlotStyle PlotTheme Pluralize Plus PlusMinus Pochhammer PodStates PodWidth Point Point3DBox Point3DBoxOptions PointBox PointBoxOptions PointFigureChart PointLegend PointSize PoissonConsulDistribution PoissonDistribution PoissonProcess PoissonWindow PolarAxes PolarAxesOrigin PolarGridLines PolarPlot PolarTicks PoleZeroMarkers PolyaAeppliDistribution PolyGamma Polygon Polygon3DBox Polygon3DBoxOptions PolygonalNumber PolygonAngle PolygonBox PolygonBoxOptions PolygonCoordinates PolygonDecomposition PolygonHoleScale PolygonIntersections PolygonScale Polyhedron PolyhedronAngle PolyhedronCoordinates PolyhedronData PolyhedronDecomposition PolyhedronGenus PolyLog PolynomialExtendedGCD PolynomialForm PolynomialGCD PolynomialLCM PolynomialMod PolynomialQ PolynomialQuotient PolynomialQuotientRemainder PolynomialReduce PolynomialRemainder Polynomials PoolingLayer PopupMenu PopupMenuBox PopupMenuBoxOptions PopupView PopupWindow Position PositionIndex Positive PositiveDefiniteMatrixQ PositiveIntegers PositiveRationals PositiveReals PositiveSemidefiniteMatrixQ PossibleZeroQ Postfix PostScript Power PowerDistribution PowerExpand PowerMod PowerModList PowerRange PowerSpectralDensity PowersRepresentations PowerSymmetricPolynomial Precedence PrecedenceForm Precedes PrecedesEqual PrecedesSlantEqual PrecedesTilde Precision PrecisionGoal PreDecrement Predict PredictionRoot PredictorFunction PredictorInformation PredictorMeasurements PredictorMeasurementsObject PreemptProtect PreferencesPath Prefix PreIncrement Prepend PrependLayer PrependTo PreprocessingRules PreserveColor PreserveImageOptions Previous PreviousCell PreviousDate PriceGraphDistribution PrimaryPlaceholder Prime PrimeNu PrimeOmega PrimePi PrimePowerQ PrimeQ Primes PrimeZetaP PrimitivePolynomialQ PrimitiveRoot PrimitiveRootList PrincipalComponents PrincipalValue Print PrintableASCIIQ PrintAction PrintForm PrintingCopies PrintingOptions PrintingPageRange PrintingStartingPageNumber PrintingStyleEnvironment Printout3D Printout3DPreviewer PrintPrecision PrintTemporary Prism PrismBox PrismBoxOptions PrivateCellOptions PrivateEvaluationOptions PrivateFontOptions PrivateFrontEndOptions PrivateKey PrivateNotebookOptions PrivatePaths Probability ProbabilityDistribution ProbabilityPlot ProbabilityPr ProbabilityScalePlot ProbitModelFit ProcessConnection ProcessDirectory ProcessEnvironment Processes ProcessEstimator ProcessInformation ProcessObject ProcessParameterAssumptions ProcessParameterQ ProcessStateDomain ProcessStatus ProcessTimeDomain Product ProductDistribution ProductLog ProgressIndicator ProgressIndicatorBox ProgressIndicatorBoxOptions Projection Prolog PromptForm ProofObject Properties Property PropertyList PropertyValue Proportion Proportional Protect Protected ProteinData Pruning PseudoInverse PsychrometricPropertyData PublicKey PublisherID PulsarData PunctuationCharacter Purple Put PutAppend Pyramid PyramidBox PyramidBoxOptions QBinomial QFactorial QGamma QHypergeometricPFQ QnDispersion QPochhammer QPolyGamma QRDecomposition QuadraticIrrationalQ QuadraticOptimization Quantile QuantilePlot Quantity QuantityArray QuantityDistribution QuantityForm QuantityMagnitude QuantityQ QuantityUnit QuantityVariable QuantityVariableCanonicalUnit QuantityVariableDimensions QuantityVariableIdentifier QuantityVariablePhysicalQuantity Quartics QuartileDeviation Quartiles QuartileSkewness Query QueueingNetworkProcess QueueingProcess QueueProperties Quiet Quit Quotient QuotientRemainder RadialGradientImage RadialityCentrality RadicalBox RadicalBoxOptions RadioButton RadioButtonBar RadioButtonBox RadioButtonBoxOptions Radon RadonTransform RamanujanTau RamanujanTauL RamanujanTauTheta RamanujanTauZ Ramp Random RandomChoice RandomColor RandomComplex RandomEntity RandomFunction RandomGeoPosition RandomGraph RandomImage RandomInstance RandomInteger RandomPermutation RandomPoint RandomPolygon RandomPolyhedron RandomPrime RandomReal RandomSample RandomSeed RandomSeeding RandomVariate RandomWalkProcess RandomWord Range RangeFilter RangeSpecification RankedMax RankedMin RarerProbability Raster Raster3D Raster3DBox Raster3DBoxOptions RasterArray RasterBox RasterBoxOptions Rasterize RasterSize Rational RationalFunctions Rationalize Rationals Ratios RawArray RawBoxes RawData RawMedium RayleighDistribution Re Read ReadByteArray ReadLine ReadList ReadProtected ReadString Real RealAbs RealBlockDiagonalForm RealDigits RealExponent Reals RealSign Reap RecognitionPrior RecognitionThreshold Record RecordLists RecordSeparators Rectangle RectangleBox RectangleBoxOptions RectangleChart RectangleChart3D RectangularRepeatingElement RecurrenceFilter RecurrenceTable RecurringDigitsForm Red Reduce RefBox ReferenceLineStyle ReferenceMarkers ReferenceMarkerStyle Refine ReflectionMatrix ReflectionTransform Refresh RefreshRate Region RegionBinarize RegionBoundary RegionBounds RegionCentroid RegionDifference RegionDimension RegionDisjoint RegionDistance RegionDistanceFunction RegionEmbeddingDimension RegionEqual RegionFunction RegionImage RegionIntersection RegionMeasure RegionMember RegionMemberFunction RegionMoment RegionNearest RegionNearestFunction RegionPlot RegionPlot3D RegionProduct RegionQ RegionResize RegionSize RegionSymmetricDifference RegionUnion RegionWithin RegisterExternalEvaluator RegularExpression Regularization RegularlySampledQ RegularPolygon ReIm ReImLabels ReImPlot ReImStyle Reinstall RelationalDatabase RelationGraph Release ReleaseHold ReliabilityDistribution ReliefImage ReliefPlot RemoteAuthorizationCaching RemoteConnect RemoteConnectionObject RemoteFile RemoteRun RemoteRunProcess Remove RemoveAlphaChannel RemoveAsynchronousTask RemoveAudioStream RemoveBackground RemoveChannelListener RemoveChannelSubscribers Removed RemoveDiacritics RemoveInputStreamMethod RemoveOutputStreamMethod RemoveProperty RemoveScheduledTask RemoveUsers RenameDirectory RenameFile RenderAll RenderingOptions RenewalProcess RenkoChart RepairMesh Repeated RepeatedNull RepeatedString RepeatedTiming RepeatingElement Replace ReplaceAll ReplaceHeldPart ReplaceImageValue ReplaceList ReplacePart ReplacePixelValue ReplaceRepeated ReplicateLayer RequiredPhysicalQuantities Resampling ResamplingAlgorithmData ResamplingMethod Rescale RescalingTransform ResetDirectory ResetMenusPacket ResetScheduledTask ReshapeLayer Residue ResizeLayer Resolve ResourceAcquire ResourceData ResourceFunction ResourceObject ResourceRegister ResourceRemove ResourceSearch ResourceSubmissionObject ResourceSubmit ResourceSystemBase ResourceUpdate ResponseForm Rest RestartInterval Restricted Resultant ResumePacket Return ReturnEntersInput ReturnExpressionPacket ReturnInputFormPacket ReturnPacket ReturnReceiptFunction ReturnTextPacket Reverse ReverseBiorthogonalSplineWavelet ReverseElement ReverseEquilibrium ReverseGraph ReverseSort ReverseSortBy ReverseUpEquilibrium RevolutionAxis RevolutionPlot3D RGBColor RiccatiSolve RiceDistribution RidgeFilter RiemannR RiemannSiegelTheta RiemannSiegelZ RiemannXi Riffle Right RightArrow RightArrowBar RightArrowLeftArrow RightComposition RightCosetRepresentative RightDownTeeVector RightDownVector RightDownVectorBar RightTee RightTeeArrow RightTeeVector RightTriangle RightTriangleBar RightTriangleEqual RightUpDownVector RightUpTeeVector RightUpVector RightUpVectorBar RightVector RightVectorBar RiskAchievementImportance RiskReductionImportance RogersTanimotoDissimilarity RollPitchYawAngles RollPitchYawMatrix RomanNumeral Root RootApproximant RootIntervals RootLocusPlot RootMeanSquare RootOfUnityQ RootReduce Roots RootSum Rotate RotateLabel RotateLeft RotateRight RotationAction RotationBox RotationBoxOptions RotationMatrix RotationTransform Round RoundImplies RoundingRadius Row RowAlignments RowBackgrounds RowBox RowHeights RowLines RowMinHeight RowReduce RowsEqual RowSpacings RSolve RSolveValue RudinShapiro RudvalisGroupRu Rule RuleCondition RuleDelayed RuleForm RulePlot RulerUnits Run RunProcess RunScheduledTask RunThrough RuntimeAttributes RuntimeOptions RussellRaoDissimilarity SameQ SameTest SampledEntityClass SampleDepth SampledSoundFunction SampledSoundList SampleRate SamplingPeriod SARIMAProcess SARMAProcess SASTriangle SatelliteData SatisfiabilityCount SatisfiabilityInstances SatisfiableQ Saturday Save Saveable SaveAutoDelete SaveConnection SaveDefinitions SavitzkyGolayMatrix SawtoothWave Scale Scaled ScaleDivisions ScaledMousePosition ScaleOrigin ScalePadding ScaleRanges ScaleRangeStyle ScalingFunctions ScalingMatrix ScalingTransform Scan ScheduledTask ScheduledTaskActiveQ ScheduledTaskInformation ScheduledTaskInformationData ScheduledTaskObject ScheduledTasks SchurDecomposition ScientificForm ScientificNotationThreshold ScorerGi ScorerGiPrime ScorerHi ScorerHiPrime ScreenRectangle ScreenStyleEnvironment ScriptBaselineShifts ScriptForm ScriptLevel ScriptMinSize ScriptRules ScriptSizeMultipliers Scrollbars ScrollingOptions ScrollPosition SearchAdjustment SearchIndexObject SearchIndices SearchQueryString SearchResultObject Sec Sech SechDistribution SecondOrderConeOptimization SectionGrouping SectorChart SectorChart3D SectorOrigin SectorSpacing SecuredAuthenticationKey SecuredAuthenticationKeys SeedRandom Select Selectable SelectComponents SelectedCells SelectedNotebook SelectFirst Selection SelectionAnimate SelectionCell SelectionCellCreateCell SelectionCellDefaultStyle SelectionCellParentStyle SelectionCreateCell SelectionDebuggerTag SelectionDuplicateCell SelectionEvaluate SelectionEvaluateCreateCell SelectionMove SelectionPlaceholder SelectionSetStyle SelectWithContents SelfLoops SelfLoopStyle SemanticImport SemanticImportString SemanticInterpretation SemialgebraicComponentInstances SemidefiniteOptimization SendMail SendMessage Sequence SequenceAlignment SequenceAttentionLayer SequenceCases SequenceCount SequenceFold SequenceFoldList SequenceForm SequenceHold SequenceLastLayer SequenceMostLayer SequencePosition SequencePredict SequencePredictorFunction SequenceReplace SequenceRestLayer SequenceReverseLayer SequenceSplit Series SeriesCoefficient SeriesData ServiceConnect ServiceDisconnect ServiceExecute ServiceObject ServiceRequest ServiceResponse ServiceSubmit SessionSubmit SessionTime Set SetAccuracy SetAlphaChannel SetAttributes Setbacks SetBoxFormNamesPacket SetCloudDirectory SetCookies SetDelayed SetDirectory SetEnvironment SetEvaluationNotebook SetFileDate SetFileLoadingContext SetNotebookStatusLine SetOptions SetOptionsPacket SetPermissions SetPrecision SetProperty SetSecuredAuthenticationKey SetSelectedNotebook SetSharedFunction SetSharedVariable SetSpeechParametersPacket SetStreamPosition SetSystemModel SetSystemOptions Setter SetterBar SetterBox SetterBoxOptions Setting SetUsers SetValue Shading Shallow ShannonWavelet ShapiroWilkTest Share SharingList Sharpen ShearingMatrix ShearingTransform ShellRegion ShenCastanMatrix ShiftedGompertzDistribution ShiftRegisterSequence Short ShortDownArrow Shortest ShortestMatch ShortestPathFunction ShortLeftArrow ShortRightArrow ShortTimeFourier ShortTimeFourierData ShortUpArrow Show ShowAutoConvert ShowAutoSpellCheck ShowAutoStyles ShowCellBracket ShowCellLabel ShowCellTags ShowClosedCellArea ShowCodeAssist ShowContents ShowControls ShowCursorTracker ShowGroupOpenCloseIcon ShowGroupOpener ShowInvisibleCharacters ShowPageBreaks ShowPredictiveInterface ShowSelection ShowShortBoxForm ShowSpecialCharacters ShowStringCharacters ShowSyntaxStyles ShrinkingDelay ShrinkWrapBoundingBox SiderealTime SiegelTheta SiegelTukeyTest SierpinskiCurve SierpinskiMesh Sign Signature SignedRankTest SignedRegionDistance SignificanceLevel SignPadding SignTest SimilarityRules SimpleGraph SimpleGraphQ SimplePolygonQ SimplePolyhedronQ Simplex Simplify Sin Sinc SinghMaddalaDistribution SingleEvaluation SingleLetterItalics SingleLetterStyle SingularValueDecomposition SingularValueList SingularValuePlot SingularValues Sinh SinhIntegral SinIntegral SixJSymbol Skeleton SkeletonTransform SkellamDistribution Skewness SkewNormalDistribution SkinStyle Skip SliceContourPlot3D SliceDensityPlot3D SliceDistribution SliceVectorPlot3D Slider Slider2D Slider2DBox Slider2DBoxOptions SliderBox SliderBoxOptions SlideView Slot SlotSequence Small SmallCircle Smaller SmithDecomposition SmithDelayCompensator SmithWatermanSimilarity SmoothDensityHistogram SmoothHistogram SmoothHistogram3D SmoothKernelDistribution SnDispersion Snippet SnubPolyhedron SocialMediaData Socket SocketConnect SocketListen SocketListener SocketObject SocketOpen SocketReadMessage SocketReadyQ Sockets SocketWaitAll SocketWaitNext SoftmaxLayer SokalSneathDissimilarity SolarEclipse SolarSystemFeatureData SolidAngle SolidData SolidRegionQ Solve SolveAlways SolveDelayed Sort SortBy SortedBy SortedEntityClass Sound SoundAndGraphics SoundNote SoundVolume SourceLink Sow Space SpaceCurveData SpaceForm Spacer Spacings Span SpanAdjustments SpanCharacterRounding SpanFromAbove SpanFromBoth SpanFromLeft SpanLineThickness SpanMaxSize SpanMinSize SpanningCharacters SpanSymmetric SparseArray SpatialGraphDistribution SpatialMedian SpatialTransformationLayer Speak SpeakTextPacket SpearmanRankTest SpearmanRho SpeciesData SpecificityGoal SpectralLineData Spectrogram SpectrogramArray Specularity SpeechRecognize SpeechSynthesize SpellingCorrection SpellingCorrectionList SpellingDictionaries SpellingDictionariesPath SpellingOptions SpellingSuggestionsPacket Sphere SphereBox SpherePoints SphericalBesselJ SphericalBesselY SphericalHankelH1 SphericalHankelH2 SphericalHarmonicY SphericalPlot3D SphericalRegion SphericalShell SpheroidalEigenvalue SpheroidalJoiningFactor SpheroidalPS SpheroidalPSPrime SpheroidalQS SpheroidalQSPrime SpheroidalRadialFactor SpheroidalS1 SpheroidalS1Prime SpheroidalS2 SpheroidalS2Prime Splice SplicedDistribution SplineClosed SplineDegree SplineKnots SplineWeights Split SplitBy SpokenString Sqrt SqrtBox SqrtBoxOptions Square SquaredEuclideanDistance SquareFreeQ SquareIntersection SquareMatrixQ SquareRepeatingElement SquaresR SquareSubset SquareSubsetEqual SquareSuperset SquareSupersetEqual SquareUnion SquareWave SSSTriangle StabilityMargins StabilityMarginsStyle StableDistribution Stack StackBegin StackComplete StackedDateListPlot StackedListPlot StackInhibit StadiumShape StandardAtmosphereData StandardDeviation StandardDeviationFilter StandardForm Standardize Standardized StandardOceanData StandbyDistribution Star StarClusterData StarData StarGraph StartAsynchronousTask StartExternalSession StartingStepSize StartOfLine StartOfString StartProcess StartScheduledTask StartupSound StartWebSession StateDimensions StateFeedbackGains StateOutputEstimator StateResponse StateSpaceModel StateSpaceRealization StateSpaceTransform StateTransformationLinearize StationaryDistribution StationaryWaveletPacketTransform StationaryWaveletTransform StatusArea StatusCentrality StepMonitor StereochemistryElements StieltjesGamma StirlingS1 StirlingS2 StopAsynchronousTask StoppingPowerData StopScheduledTask StrataVariables StratonovichProcess StreamColorFunction StreamColorFunctionScaling StreamDensityPlot StreamMarkers StreamPlot StreamPoints StreamPosition Streams StreamScale StreamStyle String StringBreak StringByteCount StringCases StringContainsQ StringCount StringDelete StringDrop StringEndsQ StringExpression StringExtract StringForm StringFormat StringFreeQ StringInsert StringJoin StringLength StringMatchQ StringPadLeft StringPadRight StringPart StringPartition StringPosition StringQ StringRepeat StringReplace StringReplaceList StringReplacePart StringReverse StringRiffle StringRotateLeft StringRotateRight StringSkeleton StringSplit StringStartsQ StringTake StringTemplate StringToByteArray StringToStream StringTrim StripBoxes StripOnInput StripWrapperBoxes StrokeForm StructuralImportance StructuredArray StructuredSelection StruveH StruveL Stub StudentTDistribution Style StyleBox StyleBoxAutoDelete StyleData StyleDefinitions StyleForm StyleHints StyleKeyMapping StyleMenuListing StyleNameDialogSettings StyleNames StylePrint StyleSheetPath Subdivide Subfactorial Subgraph SubMinus SubPlus SubresultantPolynomialRemainders SubresultantPolynomials Subresultants Subscript SubscriptBox SubscriptBoxOptions Subscripted Subsequences Subset SubsetEqual SubsetMap SubsetQ Subsets SubStar SubstitutionSystem Subsuperscript SubsuperscriptBox SubsuperscriptBoxOptions Subtract SubtractFrom SubtractSides SubValues Succeeds SucceedsEqual SucceedsSlantEqual SucceedsTilde Success SuchThat Sum SumConvergence SummationLayer Sunday SunPosition Sunrise Sunset SuperDagger SuperMinus SupernovaData SuperPlus Superscript SuperscriptBox SuperscriptBoxOptions Superset SupersetEqual SuperStar Surd SurdForm SurfaceArea SurfaceColor SurfaceData SurfaceGraphics SurvivalDistribution SurvivalFunction SurvivalModel SurvivalModelFit SuspendPacket SuzukiDistribution SuzukiGroupSuz SwatchLegend Switch Symbol SymbolName SymletWavelet Symmetric SymmetricGroup SymmetricKey SymmetricMatrixQ SymmetricPolynomial SymmetricReduction Symmetrize SymmetrizedArray SymmetrizedArrayRules SymmetrizedDependentComponents SymmetrizedIndependentComponents SymmetrizedReplacePart SynchronousInitialization SynchronousUpdating Synonyms Syntax SyntaxForm SyntaxInformation SyntaxLength SyntaxPacket SyntaxQ SynthesizeMissingValues SystemDialogInput SystemException SystemGet SystemHelpPath SystemInformation SystemInformationData SystemInstall SystemModel SystemModeler SystemModelExamples SystemModelLinearize SystemModelParametricSimulate SystemModelPlot SystemModelProgressReporting SystemModelReliability SystemModels SystemModelSimulate SystemModelSimulateSensitivity SystemModelSimulationData SystemOpen SystemOptions SystemProcessData SystemProcesses SystemsConnectionsModel SystemsModelDelay SystemsModelDelayApproximate SystemsModelDelete SystemsModelDimensions SystemsModelExtract SystemsModelFeedbackConnect SystemsModelLabels SystemsModelLinearity SystemsModelMerge SystemsModelOrder SystemsModelParallelConnect SystemsModelSeriesConnect SystemsModelStateFeedbackConnect SystemsModelVectorRelativeOrders SystemStub SystemTest Tab TabFilling Table TableAlignments TableDepth TableDirections TableForm TableHeadings TableSpacing TableView TableViewBox TableViewBoxBackground TableViewBoxOptions TabSpacings TabView TabViewBox TabViewBoxOptions TagBox TagBoxNote TagBoxOptions TaggingRules TagSet TagSetDelayed TagStyle TagUnset Take TakeDrop TakeLargest TakeLargestBy TakeList TakeSmallest TakeSmallestBy TakeWhile Tally Tan Tanh TargetDevice TargetFunctions TargetSystem TargetUnits TaskAbort TaskExecute TaskObject TaskRemove TaskResume Tasks TaskSuspend TaskWait TautologyQ TelegraphProcess TemplateApply TemplateArgBox TemplateBox TemplateBoxOptions TemplateEvaluate TemplateExpression TemplateIf TemplateObject TemplateSequence TemplateSlot TemplateSlotSequence TemplateUnevaluated TemplateVerbatim TemplateWith TemporalData TemporalRegularity Temporary TemporaryVariable TensorContract TensorDimensions TensorExpand TensorProduct TensorQ TensorRank TensorReduce TensorSymmetry TensorTranspose TensorWedge TestID TestReport TestReportObject TestResultObject Tetrahedron TetrahedronBox TetrahedronBoxOptions TeXForm TeXSave Text Text3DBox Text3DBoxOptions TextAlignment TextBand TextBoundingBox TextBox TextCases TextCell TextClipboardType TextContents TextData TextElement TextForm TextGrid TextJustification TextLine TextPacket TextParagraph TextPosition TextRecognize TextSearch TextSearchReport TextSentences TextString TextStructure TextStyle TextTranslation Texture TextureCoordinateFunction TextureCoordinateScaling TextWords Therefore ThermodynamicData ThermometerGauge Thick Thickness Thin Thinning ThisLink ThompsonGroupTh Thread ThreadingLayer ThreeJSymbol Threshold Through Throw ThueMorse Thumbnail Thursday Ticks TicksStyle TideData Tilde TildeEqual TildeFullEqual TildeTilde TimeConstrained TimeConstraint TimeDirection TimeFormat TimeGoal TimelinePlot TimeObject TimeObjectQ Times TimesBy TimeSeries TimeSeriesAggregate TimeSeriesForecast TimeSeriesInsert TimeSeriesInvertibility TimeSeriesMap TimeSeriesMapThread TimeSeriesModel TimeSeriesModelFit TimeSeriesResample TimeSeriesRescale TimeSeriesShift TimeSeriesThread TimeSeriesWindow TimeUsed TimeValue TimeWarpingCorrespondence TimeWarpingDistance TimeZone TimeZoneConvert TimeZoneOffset Timing Tiny TitleGrouping TitsGroupT ToBoxes ToCharacterCode ToColor ToContinuousTimeModel ToDate Today ToDiscreteTimeModel ToEntity ToeplitzMatrix ToExpression ToFileName Together Toggle ToggleFalse Toggler TogglerBar TogglerBox TogglerBoxOptions ToHeldExpression ToInvertibleTimeSeries TokenWords Tolerance ToLowerCase Tomorrow ToNumberField TooBig Tooltip TooltipBox TooltipBoxOptions TooltipDelay TooltipStyle Top TopHatTransform ToPolarCoordinates TopologicalSort ToRadicals ToRules ToSphericalCoordinates ToString Total TotalHeight TotalLayer TotalVariationFilter TotalWidth TouchPosition TouchscreenAutoZoom TouchscreenControlPlacement ToUpperCase Tr Trace TraceAbove TraceAction TraceBackward TraceDepth TraceDialog TraceForward TraceInternal TraceLevel TraceOff TraceOn TraceOriginal TracePrint TraceScan TrackedSymbols TrackingFunction TracyWidomDistribution TradingChart TraditionalForm TraditionalFunctionNotation TraditionalNotation TraditionalOrder TrainingProgressCheckpointing TrainingProgressFunction TrainingProgressMeasurements TrainingProgressReporting TrainingStoppingCriterion TransferFunctionCancel TransferFunctionExpand TransferFunctionFactor TransferFunctionModel TransferFunctionPoles TransferFunctionTransform TransferFunctionZeros TransformationClass TransformationFunction TransformationFunctions TransformationMatrix TransformedDistribution TransformedField TransformedProcess TransformedRegion TransitionDirection TransitionDuration TransitionEffect TransitiveClosureGraph TransitiveReductionGraph Translate TranslationOptions TranslationTransform Transliterate Transparent TransparentColor Transpose TransposeLayer TrapSelection TravelDirections TravelDirectionsData TravelDistance TravelDistanceList TravelMethod TravelTime TreeForm TreeGraph TreeGraphQ TreePlot TrendStyle Triangle TriangleCenter TriangleConstruct TriangleMeasurement TriangleWave TriangularDistribution TriangulateMesh Trig TrigExpand TrigFactor TrigFactorList Trigger TrigReduce TrigToExp TrimmedMean TrimmedVariance TropicalStormData True TrueQ TruncatedDistribution TruncatedPolyhedron TsallisQExponentialDistribution TsallisQGaussianDistribution TTest Tube TubeBezierCurveBox TubeBezierCurveBoxOptions TubeBox TubeBoxOptions TubeBSplineCurveBox TubeBSplineCurveBoxOptions Tuesday TukeyLambdaDistribution TukeyWindow TunnelData Tuples TuranGraph TuringMachine TuttePolynomial TwoWayRule Typed TypeSpecifier UnateQ Uncompress UnconstrainedParameters Undefined UnderBar Underflow Underlined Underoverscript UnderoverscriptBox UnderoverscriptBoxOptions Underscript UnderscriptBox UnderscriptBoxOptions UnderseaFeatureData UndirectedEdge UndirectedGraph UndirectedGraphQ UndoOptions UndoTrackedVariables Unequal UnequalTo Unevaluated UniformDistribution UniformGraphDistribution UniformPolyhedron UniformSumDistribution Uninstall Union UnionPlus Unique UnitaryMatrixQ UnitBox UnitConvert UnitDimensions Unitize UnitRootTest UnitSimplify UnitStep UnitSystem UnitTriangle UnitVector UnitVectorLayer UnityDimensions UniverseModelData UniversityData UnixTime Unprotect UnregisterExternalEvaluator UnsameQ UnsavedVariables Unset UnsetShared UntrackedVariables Up UpArrow UpArrowBar UpArrowDownArrow Update UpdateDynamicObjects UpdateDynamicObjectsSynchronous UpdateInterval UpdateSearchIndex UpDownArrow UpEquilibrium UpperCaseQ UpperLeftArrow UpperRightArrow UpperTriangularize UpperTriangularMatrixQ Upsample UpSet UpSetDelayed UpTee UpTeeArrow UpTo UpValues URL URLBuild URLDecode URLDispatcher URLDownload URLDownloadSubmit URLEncode URLExecute URLExpand URLFetch URLFetchAsynchronous URLParse URLQueryDecode URLQueryEncode URLRead URLResponseTime URLSave URLSaveAsynchronous URLShorten URLSubmit UseGraphicsRange UserDefinedWavelet Using UsingFrontEnd UtilityFunction V2Get ValenceErrorHandling ValidationLength ValidationSet Value ValueBox ValueBoxOptions ValueDimensions ValueForm ValuePreprocessingFunction ValueQ Values ValuesData Variables Variance VarianceEquivalenceTest VarianceEstimatorFunction VarianceGammaDistribution VarianceTest VectorAngle VectorAround VectorColorFunction VectorColorFunctionScaling VectorDensityPlot VectorGlyphData VectorGreater VectorGreaterEqual VectorLess VectorLessEqual VectorMarkers VectorPlot VectorPlot3D VectorPoints VectorQ Vectors VectorScale VectorStyle Vee Verbatim Verbose VerboseConvertToPostScriptPacket VerificationTest VerifyConvergence VerifyDerivedKey VerifyDigitalSignature VerifyInterpretation VerifySecurityCertificates VerifySolutions VerifyTestAssumptions Version VersionNumber VertexAdd VertexCapacity VertexColors VertexComponent VertexConnectivity VertexContract VertexCoordinateRules VertexCoordinates VertexCorrelationSimilarity VertexCosineSimilarity VertexCount VertexCoverQ VertexDataCoordinates VertexDegree VertexDelete VertexDiceSimilarity VertexEccentricity VertexInComponent VertexInDegree VertexIndex VertexJaccardSimilarity VertexLabeling VertexLabels VertexLabelStyle VertexList VertexNormals VertexOutComponent VertexOutDegree VertexQ VertexRenderingFunction VertexReplace VertexShape VertexShapeFunction VertexSize VertexStyle VertexTextureCoordinates VertexWeight VertexWeightedGraphQ Vertical VerticalBar VerticalForm VerticalGauge VerticalSeparator VerticalSlider VerticalTilde ViewAngle ViewCenter ViewMatrix ViewPoint ViewPointSelectorSettings ViewPort ViewProjection ViewRange ViewVector ViewVertical VirtualGroupData Visible VisibleCell VoiceStyleData VoigtDistribution VolcanoData Volume VonMisesDistribution VoronoiMesh WaitAll WaitAsynchronousTask WaitNext WaitUntil WakebyDistribution WalleniusHypergeometricDistribution WaringYuleDistribution WarpingCorrespondence WarpingDistance WatershedComponents WatsonUSquareTest WattsStrogatzGraphDistribution WaveletBestBasis WaveletFilterCoefficients WaveletImagePlot WaveletListPlot WaveletMapIndexed WaveletMatrixPlot WaveletPhi WaveletPsi WaveletScale WaveletScalogram WaveletThreshold WeaklyConnectedComponents WeaklyConnectedGraphComponents WeaklyConnectedGraphQ WeakStationarity WeatherData WeatherForecastData WebAudioSearch WebElementObject WeberE WebExecute WebImage WebImageSearch WebSearch WebSessionObject WebSessions WebWindowObject Wedge Wednesday WeibullDistribution WeierstrassE1 WeierstrassE2 WeierstrassE3 WeierstrassEta1 WeierstrassEta2 WeierstrassEta3 WeierstrassHalfPeriods WeierstrassHalfPeriodW1 WeierstrassHalfPeriodW2 WeierstrassHalfPeriodW3 WeierstrassInvariantG2 WeierstrassInvariantG3 WeierstrassInvariants WeierstrassP WeierstrassPPrime WeierstrassSigma WeierstrassZeta WeightedAdjacencyGraph WeightedAdjacencyMatrix WeightedData WeightedGraphQ Weights WelchWindow WheelGraph WhenEvent Which While White WhiteNoiseProcess WhitePoint Whitespace WhitespaceCharacter WhittakerM WhittakerW WienerFilter WienerProcess WignerD WignerSemicircleDistribution WikipediaData WikipediaSearch WilksW WilksWTest WindDirectionData WindingCount WindingPolygon WindowClickSelect WindowElements WindowFloating WindowFrame WindowFrameElements WindowMargins WindowMovable WindowOpacity WindowPersistentStyles WindowSelected WindowSize WindowStatusArea WindowTitle WindowToolbars WindowWidth WindSpeedData WindVectorData WinsorizedMean WinsorizedVariance WishartMatrixDistribution With WolframAlpha WolframAlphaDate WolframAlphaQuantity WolframAlphaResult WolframLanguageData Word WordBoundary WordCharacter WordCloud WordCount WordCounts WordData WordDefinition WordFrequency WordFrequencyData WordList WordOrientation WordSearch WordSelectionFunction WordSeparators WordSpacings WordStem WordTranslation WorkingPrecision WrapAround Write WriteLine WriteString Wronskian XMLElement XMLObject XMLTemplate Xnor Xor XYZColor Yellow Yesterday YuleDissimilarity ZernikeR ZeroSymmetric ZeroTest ZeroWidthTimes Zeta ZetaZero ZIPCodeData ZipfDistribution ZoomCenter ZoomFactor ZTest ZTransform $Aborted $ActivationGroupID $ActivationKey $ActivationUserRegistered $AddOnsDirectory $AllowExternalChannelFunctions $AssertFunction $Assumptions $AsynchronousTask $AudioInputDevices $AudioOutputDevices $BaseDirectory $BatchInput $BatchOutput $BlockchainBase $BoxForms $ByteOrdering $CacheBaseDirectory $Canceled $ChannelBase $CharacterEncoding $CharacterEncodings $CloudBase $CloudConnected $CloudCreditsAvailable $CloudEvaluation $CloudExpressionBase $CloudObjectNameFormat $CloudObjectURLType $CloudRootDirectory $CloudSymbolBase $CloudUserID $CloudUserUUID $CloudVersion $CloudVersionNumber $CloudWolframEngineVersionNumber $CommandLine $CompilationTarget $ConditionHold $ConfiguredKernels $Context $ContextPath $ControlActiveSetting $Cookies $CookieStore $CreationDate $CurrentLink $CurrentTask $CurrentWebSession $DateStringFormat $DefaultAudioInputDevice $DefaultAudioOutputDevice $DefaultFont $DefaultFrontEnd $DefaultImagingDevice $DefaultLocalBase $DefaultMailbox $DefaultNetworkInterface $DefaultPath $Display $DisplayFunction $DistributedContexts $DynamicEvaluation $Echo $EmbedCodeEnvironments $EmbeddableServices $EntityStores $Epilog $EvaluationCloudBase $EvaluationCloudObject $EvaluationEnvironment $ExportFormats $Failed $FinancialDataSource $FontFamilies $FormatType $FrontEnd $FrontEndSession $GeoEntityTypes $GeoLocation $GeoLocationCity $GeoLocationCountry $GeoLocationPrecision $GeoLocationSource $HistoryLength $HomeDirectory $HTMLExportRules $HTTPCookies $HTTPRequest $IgnoreEOF $ImageFormattingWidth $ImagingDevice $ImagingDevices $ImportFormats $IncomingMailSettings $InitialDirectory $Initialization $InitializationContexts $Input $InputFileName $InputStreamMethods $Inspector $InstallationDate $InstallationDirectory $InterfaceEnvironment $InterpreterTypes $IterationLimit $KernelCount $KernelID $Language $LaunchDirectory $LibraryPath $LicenseExpirationDate $LicenseID $LicenseProcesses $LicenseServer $LicenseSubprocesses $LicenseType $Line $Linked $LinkSupported $LoadedFiles $LocalBase $LocalSymbolBase $MachineAddresses $MachineDomain $MachineDomains $MachineEpsilon $MachineID $MachineName $MachinePrecision $MachineType $MaxExtraPrecision $MaxLicenseProcesses $MaxLicenseSubprocesses $MaxMachineNumber $MaxNumber $MaxPiecewiseCases $MaxPrecision $MaxRootDegree $MessageGroups $MessageList $MessagePrePrint $Messages $MinMachineNumber $MinNumber $MinorReleaseNumber $MinPrecision $MobilePhone $ModuleNumber $NetworkConnected $NetworkInterfaces $NetworkLicense $NewMessage $NewSymbol $Notebooks $NoValue $NumberMarks $Off $OperatingSystem $Output $OutputForms $OutputSizeLimit $OutputStreamMethods $Packages $ParentLink $ParentProcessID $PasswordFile $PatchLevelID $Path $PathnameSeparator $PerformanceGoal $Permissions $PermissionsGroupBase $PersistenceBase $PersistencePath $PipeSupported $PlotTheme $Post $Pre $PreferencesDirectory $PreInitialization $PrePrint $PreRead $PrintForms $PrintLiteral $Printout3DPreviewer $ProcessID $ProcessorCount $ProcessorType $ProductInformation $ProgramName $PublisherID $RandomState $RecursionLimit $RegisteredDeviceClasses $RegisteredUserName $ReleaseNumber $RequesterAddress $RequesterWolframID $RequesterWolframUUID $ResourceSystemBase $RootDirectory $ScheduledTask $ScriptCommandLine $ScriptInputString $SecuredAuthenticationKeyTokens $ServiceCreditsAvailable $Services $SessionID $SetParentLink $SharedFunctions $SharedVariables $SoundDisplay $SoundDisplayFunction $SourceLink $SSHAuthentication $SummaryBoxDataSizeLimit $SuppressInputFormHeads $SynchronousEvaluation $SyntaxHandler $System $SystemCharacterEncoding $SystemID $SystemMemory $SystemShell $SystemTimeZone $SystemWordLength $TemplatePath $TemporaryDirectory $TemporaryPrefix $TestFileName $TextStyle $TimedOut $TimeUnit $TimeZone $TimeZoneEntity $TopDirectory $TraceOff $TraceOn $TracePattern $TracePostAction $TracePreAction $UnitSystem $Urgent $UserAddOnsDirectory $UserAgentLanguages $UserAgentMachine $UserAgentName $UserAgentOperatingSystem $UserAgentString $UserAgentVersion $UserBaseDirectory $UserDocumentsDirectory $Username $UserName $UserURLBase $Version $VersionNumber $VoiceStyles $WolframID $WolframUUID"},contains:[a.COMMENT("\\(\\*",
-"\\*\\)",{contains:["self"]}),a.QUOTE_STRING_MODE,a.C_NUMBER_MODE]}}}());
-hljs.registerLanguage("matlab",function(){return function(a){var b={relevance:0,contains:[{begin:"('|\\.')+"}]};return{name:"Matlab",keywords:{keyword:"break case catch classdef continue else elseif end enumerated events for function global if methods otherwise parfor persistent properties return spmd switch try while",built_in:"sin sind sinh asin asind asinh cos cosd cosh acos acosd acosh tan tand tanh atan atand atan2 atanh sec secd sech asec asecd asech csc cscd csch acsc acscd acsch cot cotd coth acot acotd acoth hypot exp expm1 log log1p log10 log2 pow2 realpow reallog realsqrt sqrt nthroot nextpow2 abs angle complex conj imag real unwrap isreal cplxpair fix floor ceil round mod rem sign airy besselj bessely besselh besseli besselk beta betainc betaln ellipj ellipke erf erfc erfcx erfinv expint gamma gammainc gammaln psi legendre cross dot factor isprime primes gcd lcm rat rats perms nchoosek factorial cart2sph cart2pol pol2cart sph2cart hsv2rgb rgb2hsv zeros ones eye repmat rand randn linspace logspace freqspace meshgrid accumarray size length ndims numel disp isempty isequal isequalwithequalnans cat reshape diag blkdiag tril triu fliplr flipud flipdim rot90 find sub2ind ind2sub bsxfun ndgrid permute ipermute shiftdim circshift squeeze isscalar isvector ans eps realmax realmin pi i inf nan isnan isinf isfinite j why compan gallery hadamard hankel hilb invhilb magic pascal rosser toeplitz vander wilkinson max min nanmax nanmin mean nanmean type table readtable writetable sortrows sort figure plot plot3 scatter scatter3 cellfun legend intersect ismember procrustes hold num2cell "},illegal:'(//|"|#|/\\*|\\s+/\\w+)',
-contains:[{className:"function",beginKeywords:"function",end:"$",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",variants:[{begin:"\\(",end:"\\)"},{begin:"\\[",end:"\\]"}]}]},{className:"built_in",begin:/true|false/,relevance:0,starts:b},{begin:"[a-zA-Z][a-zA-Z_0-9]*('|\\.')+",relevance:0},{className:"number",begin:a.C_NUMBER_RE,relevance:0,starts:b},{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]},{begin:/\]|}|\)/,relevance:0,starts:b},{className:"string",begin:'"',
-end:'"',contains:[a.BACKSLASH_ESCAPE,{begin:'""'}],starts:b},a.COMMENT("^\\s*\\%\\{\\s*$","^\\s*\\%\\}\\s*$"),a.COMMENT("\\%","$")]}}}());
-hljs.registerLanguage("maxima",function(){return function(a){return{name:"Maxima",keywords:{$pattern:"[A-Za-z_%][0-9A-Za-z_%]*",keyword:"if then else elseif for thru do while unless step in and or not",literal:"true false unknown inf minf ind und %e %i %pi %phi %gamma",built_in:" abasep abs absint absolute_real_time acos acosh acot acoth acsc acsch activate addcol add_edge add_edges addmatrices addrow add_vertex add_vertices adjacency_matrix adjoin adjoint af agd airy airy_ai airy_bi airy_dai airy_dbi algsys alg_type alias allroots alphacharp alphanumericp amortization %and annuity_fv annuity_pv antid antidiff AntiDifference append appendfile apply apply1 apply2 applyb1 apropos args arit_amortization arithmetic arithsum array arrayapply arrayinfo arraymake arraysetapply ascii asec asech asin asinh askinteger asksign assoc assoc_legendre_p assoc_legendre_q assume assume_external_byte_order asympa at atan atan2 atanh atensimp atom atvalue augcoefmatrix augmented_lagrangian_method av average_degree backtrace bars barsplot barsplot_description base64 base64_decode bashindices batch batchload bc2 bdvac belln benefit_cost bern bernpoly bernstein_approx bernstein_expand bernstein_poly bessel bessel_i bessel_j bessel_k bessel_simplify bessel_y beta beta_incomplete beta_incomplete_generalized beta_incomplete_regularized bezout bfallroots bffac bf_find_root bf_fmin_cobyla bfhzeta bfloat bfloatp bfpsi bfpsi0 bfzeta biconnected_components bimetric binomial bipartition block blockmatrixp bode_gain bode_phase bothcoef box boxplot boxplot_description break bug_report build_info|10 buildq build_sample burn cabs canform canten cardinality carg cartan cartesian_product catch cauchy_matrix cbffac cdf_bernoulli cdf_beta cdf_binomial cdf_cauchy cdf_chi2 cdf_continuous_uniform cdf_discrete_uniform cdf_exp cdf_f cdf_gamma cdf_general_finite_discrete cdf_geometric cdf_gumbel cdf_hypergeometric cdf_laplace cdf_logistic cdf_lognormal cdf_negative_binomial cdf_noncentral_chi2 cdf_noncentral_student_t cdf_normal cdf_pareto cdf_poisson cdf_rank_sum cdf_rayleigh cdf_signed_rank cdf_student_t cdf_weibull cdisplay ceiling central_moment cequal cequalignore cf cfdisrep cfexpand cgeodesic cgreaterp cgreaterpignore changename changevar chaosgame charat charfun charfun2 charlist charp charpoly chdir chebyshev_t chebyshev_u checkdiv check_overlaps chinese cholesky christof chromatic_index chromatic_number cint circulant_graph clear_edge_weight clear_rules clear_vertex_label clebsch_gordan clebsch_graph clessp clesspignore close closefile cmetric coeff coefmatrix cograd col collapse collectterms columnop columnspace columnswap columnvector combination combine comp2pui compare compfile compile compile_file complement_graph complete_bipartite_graph complete_graph complex_number_p components compose_functions concan concat conjugate conmetderiv connected_components connect_vertices cons constant constantp constituent constvalue cont2part content continuous_freq contortion contour_plot contract contract_edge contragrad contrib_ode convert coord copy copy_file copy_graph copylist copymatrix cor cos cosh cot coth cov cov1 covdiff covect covers crc24sum create_graph create_list csc csch csetup cspline ctaylor ct_coordsys ctransform ctranspose cube_graph cuboctahedron_graph cunlisp cv cycle_digraph cycle_graph cylindrical days360 dblint deactivate declare declare_constvalue declare_dimensions declare_fundamental_dimensions declare_fundamental_units declare_qty declare_translated declare_unit_conversion declare_units declare_weights decsym defcon define define_alt_display define_variable defint defmatch defrule defstruct deftaylor degree_sequence del delete deleten delta demo demoivre denom depends derivdegree derivlist describe desolve determinant dfloat dgauss_a dgauss_b dgeev dgemm dgeqrf dgesv dgesvd diag diagmatrix diag_matrix diagmatrixp diameter diff digitcharp dimacs_export dimacs_import dimension dimensionless dimensions dimensions_as_list direct directory discrete_freq disjoin disjointp disolate disp dispcon dispform dispfun dispJordan display disprule dispterms distrib divide divisors divsum dkummer_m dkummer_u dlange dodecahedron_graph dotproduct dotsimp dpart draw draw2d draw3d drawdf draw_file draw_graph dscalar echelon edge_coloring edge_connectivity edges eigens_by_jacobi eigenvalues eigenvectors eighth einstein eivals eivects elapsed_real_time elapsed_run_time ele2comp ele2polynome ele2pui elem elementp elevation_grid elim elim_allbut eliminate eliminate_using ellipse elliptic_e elliptic_ec elliptic_eu elliptic_f elliptic_kc elliptic_pi ematrix empty_graph emptyp endcons entermatrix entertensor entier equal equalp equiv_classes erf erfc erf_generalized erfi errcatch error errormsg errors euler ev eval_string evenp every evolution evolution2d evundiff example exp expand expandwrt expandwrt_factored expint expintegral_chi expintegral_ci expintegral_e expintegral_e1 expintegral_ei expintegral_e_simplify expintegral_li expintegral_shi expintegral_si explicit explose exponentialize express expt exsec extdiff extract_linear_equations extremal_subset ezgcd %f f90 facsum factcomb factor factorfacsum factorial factorout factorsum facts fast_central_elements fast_linsolve fasttimes featurep fernfale fft fib fibtophi fifth filename_merge file_search file_type fillarray findde find_root find_root_abs find_root_error find_root_rel first fix flatten flength float floatnump floor flower_snark flush flush1deriv flushd flushnd flush_output fmin_cobyla forget fortran fourcos fourexpand fourier fourier_elim fourint fourintcos fourintsin foursimp foursin fourth fposition frame_bracket freeof freshline fresnel_c fresnel_s from_adjacency_matrix frucht_graph full_listify fullmap fullmapl fullratsimp fullratsubst fullsetify funcsolve fundamental_dimensions fundamental_units fundef funmake funp fv g0 g1 gamma gamma_greek gamma_incomplete gamma_incomplete_generalized gamma_incomplete_regularized gauss gauss_a gauss_b gaussprob gcd gcdex gcdivide gcfac gcfactor gd generalized_lambert_w genfact gen_laguerre genmatrix gensym geo_amortization geo_annuity_fv geo_annuity_pv geomap geometric geometric_mean geosum get getcurrentdirectory get_edge_weight getenv get_lu_factors get_output_stream_string get_pixel get_plot_option get_tex_environment get_tex_environment_default get_vertex_label gfactor gfactorsum ggf girth global_variances gn gnuplot_close gnuplot_replot gnuplot_reset gnuplot_restart gnuplot_start go Gosper GosperSum gr2d gr3d gradef gramschmidt graph6_decode graph6_encode graph6_export graph6_import graph_center graph_charpoly graph_eigenvalues graph_flow graph_order graph_periphery graph_product graph_size graph_union great_rhombicosidodecahedron_graph great_rhombicuboctahedron_graph grid_graph grind grobner_basis grotzch_graph hamilton_cycle hamilton_path hankel hankel_1 hankel_2 harmonic harmonic_mean hav heawood_graph hermite hessian hgfred hilbertmap hilbert_matrix hipow histogram histogram_description hodge horner hypergeometric i0 i1 %ibes ic1 ic2 ic_convert ichr1 ichr2 icosahedron_graph icosidodecahedron_graph icurvature ident identfor identity idiff idim idummy ieqn %if ifactors iframes ifs igcdex igeodesic_coords ilt image imagpart imetric implicit implicit_derivative implicit_plot indexed_tensor indices induced_subgraph inferencep inference_result infix info_display init_atensor init_ctensor in_neighbors innerproduct inpart inprod inrt integerp integer_partitions integrate intersect intersection intervalp intopois intosum invariant1 invariant2 inverse_fft inverse_jacobi_cd inverse_jacobi_cn inverse_jacobi_cs inverse_jacobi_dc inverse_jacobi_dn inverse_jacobi_ds inverse_jacobi_nc inverse_jacobi_nd inverse_jacobi_ns inverse_jacobi_sc inverse_jacobi_sd inverse_jacobi_sn invert invert_by_adjoint invert_by_lu inv_mod irr is is_biconnected is_bipartite is_connected is_digraph is_edge_in_graph is_graph is_graph_or_digraph ishow is_isomorphic isolate isomorphism is_planar isqrt isreal_p is_sconnected is_tree is_vertex_in_graph items_inference %j j0 j1 jacobi jacobian jacobi_cd jacobi_cn jacobi_cs jacobi_dc jacobi_dn jacobi_ds jacobi_nc jacobi_nd jacobi_ns jacobi_p jacobi_sc jacobi_sd jacobi_sn JF jn join jordan julia julia_set julia_sin %k kdels kdelta kill killcontext kostka kron_delta kronecker_product kummer_m kummer_u kurtosis kurtosis_bernoulli kurtosis_beta kurtosis_binomial kurtosis_chi2 kurtosis_continuous_uniform kurtosis_discrete_uniform kurtosis_exp kurtosis_f kurtosis_gamma kurtosis_general_finite_discrete kurtosis_geometric kurtosis_gumbel kurtosis_hypergeometric kurtosis_laplace kurtosis_logistic kurtosis_lognormal kurtosis_negative_binomial kurtosis_noncentral_chi2 kurtosis_noncentral_student_t kurtosis_normal kurtosis_pareto kurtosis_poisson kurtosis_rayleigh kurtosis_student_t kurtosis_weibull label labels lagrange laguerre lambda lambert_w laplace laplacian_matrix last lbfgs lc2kdt lcharp lc_l lcm lc_u ldefint ldisp ldisplay legendre_p legendre_q leinstein length let letrules letsimp levi_civita lfreeof lgtreillis lhs li liediff limit Lindstedt linear linearinterpol linear_program linear_regression line_graph linsolve listarray list_correlations listify list_matrix_entries list_nc_monomials listoftens listofvars listp lmax lmin load loadfile local locate_matrix_entry log logcontract log_gamma lopow lorentz_gauge lowercasep lpart lratsubst lreduce lriemann lsquares_estimates lsquares_estimates_approximate lsquares_estimates_exact lsquares_mse lsquares_residual_mse lsquares_residuals lsum ltreillis lu_backsub lucas lu_factor %m macroexpand macroexpand1 make_array makebox makefact makegamma make_graph make_level_picture makelist makeOrders make_poly_continent make_poly_country make_polygon make_random_state make_rgb_picture makeset make_string_input_stream make_string_output_stream make_transform mandelbrot mandelbrot_set map mapatom maplist matchdeclare matchfix mat_cond mat_fullunblocker mat_function mathml_display mat_norm matrix matrixmap matrixp matrix_size mattrace mat_trace mat_unblocker max max_clique max_degree max_flow maximize_lp max_independent_set max_matching maybe md5sum mean mean_bernoulli mean_beta mean_binomial mean_chi2 mean_continuous_uniform mean_deviation mean_discrete_uniform mean_exp mean_f mean_gamma mean_general_finite_discrete mean_geometric mean_gumbel mean_hypergeometric mean_laplace mean_logistic mean_lognormal mean_negative_binomial mean_noncentral_chi2 mean_noncentral_student_t mean_normal mean_pareto mean_poisson mean_rayleigh mean_student_t mean_weibull median median_deviation member mesh metricexpandall mgf1_sha1 min min_degree min_edge_cut minfactorial minimalPoly minimize_lp minimum_spanning_tree minor minpack_lsquares minpack_solve min_vertex_cover min_vertex_cut mkdir mnewton mod mode_declare mode_identity ModeMatrix moebius mon2schur mono monomial_dimensions multibernstein_poly multi_display_for_texinfo multi_elem multinomial multinomial_coeff multi_orbit multiplot_mode multi_pui multsym multthru mycielski_graph nary natural_unit nc_degree ncexpt ncharpoly negative_picture neighbors new newcontext newdet new_graph newline newton new_variable next_prime nicedummies niceindices ninth nofix nonarray noncentral_moment nonmetricity nonnegintegerp nonscalarp nonzeroandfreeof notequal nounify nptetrad npv nroots nterms ntermst nthroot nullity nullspace num numbered_boundaries numberp number_to_octets num_distinct_partitions numerval numfactor num_partitions nusum nzeta nzetai nzetar octets_to_number octets_to_oid odd_girth oddp ode2 ode_check odelin oid_to_octets op opena opena_binary openr openr_binary openw openw_binary operatorp opsubst optimize %or orbit orbits ordergreat ordergreatp orderless orderlessp orthogonal_complement orthopoly_recur orthopoly_weight outermap out_neighbors outofpois pade parabolic_cylinder_d parametric parametric_surface parg parGosper parse_string parse_timedate part part2cont partfrac partition partition_set partpol path_digraph path_graph pathname_directory pathname_name pathname_type pdf_bernoulli pdf_beta pdf_binomial pdf_cauchy pdf_chi2 pdf_continuous_uniform pdf_discrete_uniform pdf_exp pdf_f pdf_gamma pdf_general_finite_discrete pdf_geometric pdf_gumbel pdf_hypergeometric pdf_laplace pdf_logistic pdf_lognormal pdf_negative_binomial pdf_noncentral_chi2 pdf_noncentral_student_t pdf_normal pdf_pareto pdf_poisson pdf_rank_sum pdf_rayleigh pdf_signed_rank pdf_student_t pdf_weibull pearson_skewness permanent permut permutation permutations petersen_graph petrov pickapart picture_equalp picturep piechart piechart_description planar_embedding playback plog plot2d plot3d plotdf ploteq plsquares pochhammer points poisdiff poisexpt poisint poismap poisplus poissimp poissubst poistimes poistrim polar polarform polartorect polar_to_xy poly_add poly_buchberger poly_buchberger_criterion poly_colon_ideal poly_content polydecomp poly_depends_p poly_elimination_ideal poly_exact_divide poly_expand poly_expt poly_gcd polygon poly_grobner poly_grobner_equal poly_grobner_member poly_grobner_subsetp poly_ideal_intersection poly_ideal_polysaturation poly_ideal_polysaturation1 poly_ideal_saturation poly_ideal_saturation1 poly_lcm poly_minimization polymod poly_multiply polynome2ele polynomialp poly_normal_form poly_normalize poly_normalize_list poly_polysaturation_extension poly_primitive_part poly_pseudo_divide poly_reduced_grobner poly_reduction poly_saturation_extension poly_s_polynomial poly_subtract polytocompanion pop postfix potential power_mod powerseries powerset prefix prev_prime primep primes principal_components print printf printfile print_graph printpois printprops prodrac product properties propvars psi psubst ptriangularize pui pui2comp pui2ele pui2polynome pui_direct puireduc push put pv qput qrange qty quad_control quad_qag quad_qagi quad_qagp quad_qags quad_qawc quad_qawf quad_qawo quad_qaws quadrilateral quantile quantile_bernoulli quantile_beta quantile_binomial quantile_cauchy quantile_chi2 quantile_continuous_uniform quantile_discrete_uniform quantile_exp quantile_f quantile_gamma quantile_general_finite_discrete quantile_geometric quantile_gumbel quantile_hypergeometric quantile_laplace quantile_logistic quantile_lognormal quantile_negative_binomial quantile_noncentral_chi2 quantile_noncentral_student_t quantile_normal quantile_pareto quantile_poisson quantile_rayleigh quantile_student_t quantile_weibull quartile_skewness quit qunit quotient racah_v racah_w radcan radius random random_bernoulli random_beta random_binomial random_bipartite_graph random_cauchy random_chi2 random_continuous_uniform random_digraph random_discrete_uniform random_exp random_f random_gamma random_general_finite_discrete random_geometric random_graph random_graph1 random_gumbel random_hypergeometric random_laplace random_logistic random_lognormal random_negative_binomial random_network random_noncentral_chi2 random_noncentral_student_t random_normal random_pareto random_permutation random_poisson random_rayleigh random_regular_graph random_student_t random_tournament random_tree random_weibull range rank rat ratcoef ratdenom ratdiff ratdisrep ratexpand ratinterpol rational rationalize ratnumer ratnump ratp ratsimp ratsubst ratvars ratweight read read_array read_binary_array read_binary_list read_binary_matrix readbyte readchar read_hashed_array readline read_list read_matrix read_nested_list readonly read_xpm real_imagpart_to_conjugate realpart realroots rearray rectangle rectform rectform_log_if_constant recttopolar rediff reduce_consts reduce_order region region_boundaries region_boundaries_plus rem remainder remarray rembox remcomps remcon remcoord remfun remfunction remlet remove remove_constvalue remove_dimensions remove_edge remove_fundamental_dimensions remove_fundamental_units remove_plot_option remove_vertex rempart remrule remsym remvalue rename rename_file reset reset_displays residue resolvante resolvante_alternee1 resolvante_bipartite resolvante_diedrale resolvante_klein resolvante_klein3 resolvante_produit_sym resolvante_unitaire resolvante_vierer rest resultant return reveal reverse revert revert2 rgb2level rhs ricci riemann rinvariant risch rk rmdir rncombine romberg room rootscontract round row rowop rowswap rreduce run_testsuite %s save saving scalarp scaled_bessel_i scaled_bessel_i0 scaled_bessel_i1 scalefactors scanmap scatterplot scatterplot_description scene schur2comp sconcat scopy scsimp scurvature sdowncase sec sech second sequal sequalignore set_alt_display setdifference set_draw_defaults set_edge_weight setelmx setequalp setify setp set_partitions set_plot_option set_prompt set_random_state set_tex_environment set_tex_environment_default setunits setup_autoload set_up_dot_simplifications set_vertex_label seventh sexplode sf sha1sum sha256sum shortest_path shortest_weighted_path show showcomps showratvars sierpinskiale sierpinskimap sign signum similaritytransform simp_inequality simplify_sum simplode simpmetderiv simtran sin sinh sinsert sinvertcase sixth skewness skewness_bernoulli skewness_beta skewness_binomial skewness_chi2 skewness_continuous_uniform skewness_discrete_uniform skewness_exp skewness_f skewness_gamma skewness_general_finite_discrete skewness_geometric skewness_gumbel skewness_hypergeometric skewness_laplace skewness_logistic skewness_lognormal skewness_negative_binomial skewness_noncentral_chi2 skewness_noncentral_student_t skewness_normal skewness_pareto skewness_poisson skewness_rayleigh skewness_student_t skewness_weibull slength smake small_rhombicosidodecahedron_graph small_rhombicuboctahedron_graph smax smin smismatch snowmap snub_cube_graph snub_dodecahedron_graph solve solve_rec solve_rec_rat some somrac sort sparse6_decode sparse6_encode sparse6_export sparse6_import specint spherical spherical_bessel_j spherical_bessel_y spherical_hankel1 spherical_hankel2 spherical_harmonic spherical_to_xyz splice split sposition sprint sqfr sqrt sqrtdenest sremove sremovefirst sreverse ssearch ssort sstatus ssubst ssubstfirst staircase standardize standardize_inverse_trig starplot starplot_description status std std1 std_bernoulli std_beta std_binomial std_chi2 std_continuous_uniform std_discrete_uniform std_exp std_f std_gamma std_general_finite_discrete std_geometric std_gumbel std_hypergeometric std_laplace std_logistic std_lognormal std_negative_binomial std_noncentral_chi2 std_noncentral_student_t std_normal std_pareto std_poisson std_rayleigh std_student_t std_weibull stemplot stirling stirling1 stirling2 strim striml strimr string stringout stringp strong_components struve_h struve_l sublis sublist sublist_indices submatrix subsample subset subsetp subst substinpart subst_parallel substpart substring subvar subvarp sum sumcontract summand_to_rec supcase supcontext symbolp symmdifference symmetricp system take_channel take_inference tan tanh taylor taylorinfo taylorp taylor_simplifier taytorat tcl_output tcontract tellrat tellsimp tellsimpafter tentex tenth test_mean test_means_difference test_normality test_proportion test_proportions_difference test_rank_sum test_sign test_signed_rank test_variance test_variance_ratio tex tex1 tex_display texput %th third throw time timedate timer timer_info tldefint tlimit todd_coxeter toeplitz tokens to_lisp topological_sort to_poly to_poly_solve totaldisrep totalfourier totient tpartpol trace tracematrix trace_options transform_sample translate translate_file transpose treefale tree_reduce treillis treinat triangle triangularize trigexpand trigrat trigreduce trigsimp trunc truncate truncated_cube_graph truncated_dodecahedron_graph truncated_icosahedron_graph truncated_tetrahedron_graph tr_warnings_get tube tutte_graph ueivects uforget ultraspherical underlying_graph undiff union unique uniteigenvectors unitp units unit_step unitvector unorder unsum untellrat untimer untrace uppercasep uricci uriemann uvect vandermonde_matrix var var1 var_bernoulli var_beta var_binomial var_chi2 var_continuous_uniform var_discrete_uniform var_exp var_f var_gamma var_general_finite_discrete var_geometric var_gumbel var_hypergeometric var_laplace var_logistic var_lognormal var_negative_binomial var_noncentral_chi2 var_noncentral_student_t var_normal var_pareto var_poisson var_rayleigh var_student_t var_weibull vector vectorpotential vectorsimp verbify vers vertex_coloring vertex_connectivity vertex_degree vertex_distance vertex_eccentricity vertex_in_degree vertex_out_degree vertices vertices_to_cycle vertices_to_path %w weyl wheel_graph wiener_index wigner_3j wigner_6j wigner_9j with_stdout write_binary_data writebyte write_data writefile wronskian xreduce xthru %y Zeilberger zeroequiv zerofor zeromatrix zeromatrixp zeta zgeev zheev zlange zn_add_table zn_carmichael_lambda zn_characteristic_factors zn_determinant zn_factor_generators zn_invert_by_lu zn_log zn_mult_table absboxchar activecontexts adapt_depth additive adim aform algebraic algepsilon algexact aliases allbut all_dotsimp_denoms allocation allsym alphabetic animation antisymmetric arrays askexp assume_pos assume_pos_pred assumescalar asymbol atomgrad atrig1 axes axis_3d axis_bottom axis_left axis_right axis_top azimuth background background_color backsubst berlefact bernstein_explicit besselexpand beta_args_sum_to_integer beta_expand bftorat bftrunc bindtest border boundaries_array box boxchar breakup %c capping cauchysum cbrange cbtics center cflength cframe_flag cnonmet_flag color color_bar color_bar_tics colorbox columns commutative complex cone context contexts contour contour_levels cosnpiflag ctaypov ctaypt ctayswitch ctayvar ct_coords ctorsion_flag ctrgsimp cube current_let_rule_package cylinder data_file_name debugmode decreasing default_let_rule_package delay dependencies derivabbrev derivsubst detout diagmetric diff dim dimensions dispflag display2d|10 display_format_internal distribute_over doallmxops domain domxexpt domxmxops domxnctimes dontfactor doscmxops doscmxplus dot0nscsimp dot0simp dot1simp dotassoc dotconstrules dotdistrib dotexptsimp dotident dotscrules draw_graph_program draw_realpart edge_color edge_coloring edge_partition edge_type edge_width %edispflag elevation %emode endphi endtheta engineering_format_floats enhanced3d %enumer epsilon_lp erfflag erf_representation errormsg error_size error_syms error_type %e_to_numlog eval even evenfun evflag evfun ev_point expandwrt_denom expintexpand expintrep expon expop exptdispflag exptisolate exptsubst facexpand facsum_combine factlim factorflag factorial_expand factors_only fb feature features file_name file_output_append file_search_demo file_search_lisp file_search_maxima|10 file_search_tests file_search_usage file_type_lisp file_type_maxima|10 fill_color fill_density filled_func fixed_vertices flipflag float2bf font font_size fortindent fortspaces fpprec fpprintprec functions gamma_expand gammalim gdet genindex gensumnum GGFCFMAX GGFINFINITY globalsolve gnuplot_command gnuplot_curve_styles gnuplot_curve_titles gnuplot_default_term_command gnuplot_dumb_term_command gnuplot_file_args gnuplot_file_name gnuplot_out_file gnuplot_pdf_term_command gnuplot_pm3d gnuplot_png_term_command gnuplot_postamble gnuplot_preamble gnuplot_ps_term_command gnuplot_svg_term_command gnuplot_term gnuplot_view_args Gosper_in_Zeilberger gradefs grid grid2d grind halfangles head_angle head_both head_length head_type height hypergeometric_representation %iargs ibase icc1 icc2 icounter idummyx ieqnprint ifb ifc1 ifc2 ifg ifgi ifr iframe_bracket_form ifri igeowedge_flag ikt1 ikt2 imaginary inchar increasing infeval infinity inflag infolists inm inmc1 inmc2 intanalysis integer integervalued integrate_use_rootsof integration_constant integration_constant_counter interpolate_color intfaclim ip_grid ip_grid_in irrational isolate_wrt_times iterations itr julia_parameter %k1 %k2 keepfloat key key_pos kinvariant kt label label_alignment label_orientation labels lassociative lbfgs_ncorrections lbfgs_nfeval_max leftjust legend letrat let_rule_packages lfg lg lhospitallim limsubst linear linear_solver linechar linel|10 linenum line_type linewidth line_width linsolve_params linsolvewarn lispdisp listarith listconstvars listdummyvars lmxchar load_pathname loadprint logabs logarc logcb logconcoeffp logexpand lognegint logsimp logx logx_secondary logy logy_secondary logz lriem m1pbranch macroexpansion macros mainvar manual_demo maperror mapprint matrix_element_add matrix_element_mult matrix_element_transpose maxapplydepth maxapplyheight maxima_tempdir|10 maxima_userdir|10 maxnegex MAX_ORD maxposex maxpsifracdenom maxpsifracnum maxpsinegint maxpsiposint maxtayorder mesh_lines_color method mod_big_prime mode_check_errorp mode_checkp mode_check_warnp mod_test mod_threshold modular_linear_solver modulus multiplicative multiplicities myoptions nary negdistrib negsumdispflag newline newtonepsilon newtonmaxiter nextlayerfactor niceindicespref nm nmc noeval nolabels nonegative_lp noninteger nonscalar noun noundisp nouns np npi nticks ntrig numer numer_pbranch obase odd oddfun opacity opproperties opsubst optimprefix optionset orientation origin orthopoly_returns_intervals outative outchar packagefile palette partswitch pdf_file pfeformat phiresolution %piargs piece pivot_count_sx pivot_max_sx plot_format plot_options plot_realpart png_file pochhammer_max_index points pointsize point_size points_joined point_type poislim poisson poly_coefficient_ring poly_elimination_order polyfactor poly_grobner_algorithm poly_grobner_debug poly_monomial_order poly_primary_elimination_order poly_return_term_list poly_secondary_elimination_order poly_top_reduction_only posfun position powerdisp pred prederror primep_number_of_tests product_use_gamma program programmode promote_float_to_bigfloat prompt proportional_axes props psexpand ps_file radexpand radius radsubstflag rassociative ratalgdenom ratchristof ratdenomdivide rateinstein ratepsilon ratfac rational ratmx ratprint ratriemann ratsimpexpons ratvarswitch ratweights ratweyl ratwtlvl real realonly redraw refcheck resolution restart resultant ric riem rmxchar %rnum_list rombergabs rombergit rombergmin rombergtol rootsconmode rootsepsilon run_viewer same_xy same_xyz savedef savefactors scalar scalarmatrixp scale scale_lp setcheck setcheckbreak setval show_edge_color show_edges show_edge_type show_edge_width show_id show_label showtime show_vertex_color show_vertex_size show_vertex_type show_vertices show_weight simp simplified_output simplify_products simpproduct simpsum sinnpiflag solvedecomposes solveexplicit solvefactors solvenullwarn solveradcan solvetrigwarn space sparse sphere spring_embedding_depth sqrtdispflag stardisp startphi starttheta stats_numer stringdisp structures style sublis_apply_lambda subnumsimp sumexpand sumsplitfact surface surface_hide svg_file symmetric tab taylordepth taylor_logexpand taylor_order_coefficients taylor_truncate_polynomials tensorkill terminal testsuite_files thetaresolution timer_devalue title tlimswitch tr track transcompile transform transform_xy translate_fast_arrays transparent transrun tr_array_as_ref tr_bound_function_applyp tr_file_tty_messagesp tr_float_can_branch_complex tr_function_call_default trigexpandplus trigexpandtimes triginverses trigsign trivial_solutions tr_numer tr_optimize_max_loop tr_semicompile tr_state_vars tr_warn_bad_function_calls tr_warn_fexpr tr_warn_meval tr_warn_mode tr_warn_undeclared tr_warn_undefined_variable tstep ttyoff tube_extremes ufg ug %unitexpand unit_vectors uric uriem use_fast_arrays user_preamble usersetunits values vect_cross verbose vertex_color vertex_coloring vertex_partition vertex_size vertex_type view warnings weyl width windowname windowtitle wired_surface wireframe xaxis xaxis_color xaxis_secondary xaxis_type xaxis_width xlabel xlabel_secondary xlength xrange xrange_secondary xtics xtics_axis xtics_rotate xtics_rotate_secondary xtics_secondary xtics_secondary_axis xu_grid x_voxel xy_file xyplane xy_scale yaxis yaxis_color yaxis_secondary yaxis_type yaxis_width ylabel ylabel_secondary ylength yrange yrange_secondary ytics ytics_axis ytics_rotate ytics_rotate_secondary ytics_secondary ytics_secondary_axis yv_grid y_voxel yx_ratio zaxis zaxis_color zaxis_type zaxis_width zeroa zerob zerobern zeta%pi zlabel zlabel_rotate zlength zmin zn_primroot_limit zn_primroot_pretest",symbol:"_ __ %|0 %%|0"},
-contains:[{className:"comment",begin:"/\\*",end:"\\*/",contains:["self"]},a.QUOTE_STRING_MODE,{className:"number",relevance:0,variants:[{begin:"\\b(\\d+|\\d+\\.|\\.\\d+|\\d+\\.\\d+)[Ee][-+]?\\d+\\b"},{begin:"\\b(\\d+|\\d+\\.|\\.\\d+|\\d+\\.\\d+)[Bb][-+]?\\d+\\b",relevance:10},{begin:"\\b(\\.\\d+|\\d+\\.\\d+)\\b"},{begin:"\\b(\\d+|0[0-9A-Za-z]+)\\.?\\b"}]}],illegal:/@/}}}());
-hljs.registerLanguage("mel",function(){return function(a){return{name:"MEL",keywords:"int float string vector matrix if else switch case default while do for in break continue global proc return about abs addAttr addAttributeEditorNodeHelp addDynamic addNewShelfTab addPP addPanelCategory addPrefixToName advanceToNextDrivenKey affectedNet affects aimConstraint air alias aliasAttr align alignCtx alignCurve alignSurface allViewFit ambientLight angle angleBetween animCone animCurveEditor animDisplay animView annotate appendStringArray applicationName applyAttrPreset applyTake arcLenDimContext arcLengthDimension arclen arrayMapper art3dPaintCtx artAttrCtx artAttrPaintVertexCtx artAttrSkinPaintCtx artAttrTool artBuildPaintMenu artFluidAttrCtx artPuttyCtx artSelectCtx artSetPaintCtx artUserPaintCtx assignCommand assignInputDevice assignViewportFactories attachCurve attachDeviceAttr attachSurface attrColorSliderGrp attrCompatibility attrControlGrp attrEnumOptionMenu attrEnumOptionMenuGrp attrFieldGrp attrFieldSliderGrp attrNavigationControlGrp attrPresetEditWin attributeExists attributeInfo attributeMenu attributeQuery autoKeyframe autoPlace bakeClip bakeFluidShading bakePartialHistory bakeResults bakeSimulation basename basenameEx batchRender bessel bevel bevelPlus binMembership bindSkin blend2 blendShape blendShapeEditor blendShapePanel blendTwoAttr blindDataType boneLattice boundary boxDollyCtx boxZoomCtx bufferCurve buildBookmarkMenu buildKeyframeMenu button buttonManip CBG cacheFile cacheFileCombine cacheFileMerge cacheFileTrack camera cameraView canCreateManip canvas capitalizeString catch catchQuiet ceil changeSubdivComponentDisplayLevel changeSubdivRegion channelBox character characterMap characterOutlineEditor characterize chdir checkBox checkBoxGrp checkDefaultRenderGlobals choice circle circularFillet clamp clear clearCache clip clipEditor clipEditorCurrentTimeCtx clipSchedule clipSchedulerOutliner clipTrimBefore closeCurve closeSurface cluster cmdFileOutput cmdScrollFieldExecuter cmdScrollFieldReporter cmdShell coarsenSubdivSelectionList collision color colorAtPoint colorEditor colorIndex colorIndexSliderGrp colorSliderButtonGrp colorSliderGrp columnLayout commandEcho commandLine commandPort compactHairSystem componentEditor compositingInterop computePolysetVolume condition cone confirmDialog connectAttr connectControl connectDynamic connectJoint connectionInfo constrain constrainValue constructionHistory container containsMultibyte contextInfo control convertFromOldLayers convertIffToPsd convertLightmap convertSolidTx convertTessellation convertUnit copyArray copyFlexor copyKey copySkinWeights cos cpButton cpCache cpClothSet cpCollision cpConstraint cpConvClothToMesh cpForces cpGetSolverAttr cpPanel cpProperty cpRigidCollisionFilter cpSeam cpSetEdit cpSetSolverAttr cpSolver cpSolverTypes cpTool cpUpdateClothUVs createDisplayLayer createDrawCtx createEditor createLayeredPsdFile createMotionField createNewShelf createNode createRenderLayer createSubdivRegion cross crossProduct ctxAbort ctxCompletion ctxEditMode ctxTraverse currentCtx currentTime currentTimeCtx currentUnit curve curveAddPtCtx curveCVCtx curveEPCtx curveEditorCtx curveIntersect curveMoveEPCtx curveOnSurface curveSketchCtx cutKey cycleCheck cylinder dagPose date defaultLightListCheckBox defaultNavigation defineDataServer defineVirtualDevice deformer deg_to_rad delete deleteAttr deleteShadingGroupsAndMaterials deleteShelfTab deleteUI deleteUnusedBrushes delrandstr detachCurve detachDeviceAttr detachSurface deviceEditor devicePanel dgInfo dgdirty dgeval dgtimer dimWhen directKeyCtx directionalLight dirmap dirname disable disconnectAttr disconnectJoint diskCache displacementToPoly displayAffected displayColor displayCull displayLevelOfDetail displayPref displayRGBColor displaySmoothness displayStats displayString displaySurface distanceDimContext distanceDimension doBlur dolly dollyCtx dopeSheetEditor dot dotProduct doubleProfileBirailSurface drag dragAttrContext draggerContext dropoffLocator duplicate duplicateCurve duplicateSurface dynCache dynControl dynExport dynExpression dynGlobals dynPaintEditor dynParticleCtx dynPref dynRelEdPanel dynRelEditor dynamicLoad editAttrLimits editDisplayLayerGlobals editDisplayLayerMembers editRenderLayerAdjustment editRenderLayerGlobals editRenderLayerMembers editor editorTemplate effector emit emitter enableDevice encodeString endString endsWith env equivalent equivalentTol erf error eval evalDeferred evalEcho event exactWorldBoundingBox exclusiveLightCheckBox exec executeForEachObject exists exp expression expressionEditorListen extendCurve extendSurface extrude fcheck fclose feof fflush fgetline fgetword file fileBrowserDialog fileDialog fileExtension fileInfo filetest filletCurve filter filterCurve filterExpand filterStudioImport findAllIntersections findAnimCurves findKeyframe findMenuItem findRelatedSkinCluster finder firstParentOf fitBspline flexor floatEq floatField floatFieldGrp floatScrollBar floatSlider floatSlider2 floatSliderButtonGrp floatSliderGrp floor flow fluidCacheInfo fluidEmitter fluidVoxelInfo flushUndo fmod fontDialog fopen formLayout format fprint frameLayout fread freeFormFillet frewind fromNativePath fwrite gamma gauss geometryConstraint getApplicationVersionAsFloat getAttr getClassification getDefaultBrush getFileList getFluidAttr getInputDeviceRange getMayaPanelTypes getModifiers getPanel getParticleAttr getPluginResource getenv getpid glRender glRenderEditor globalStitch gmatch goal gotoBindPose grabColor gradientControl gradientControlNoAttr graphDollyCtx graphSelectContext graphTrackCtx gravity grid gridLayout group groupObjectsByName HfAddAttractorToAS HfAssignAS HfBuildEqualMap HfBuildFurFiles HfBuildFurImages HfCancelAFR HfConnectASToHF HfCreateAttractor HfDeleteAS HfEditAS HfPerformCreateAS HfRemoveAttractorFromAS HfSelectAttached HfSelectAttractors HfUnAssignAS hardenPointCurve hardware hardwareRenderPanel headsUpDisplay headsUpMessage help helpLine hermite hide hilite hitTest hotBox hotkey hotkeyCheck hsv_to_rgb hudButton hudSlider hudSliderButton hwReflectionMap hwRender hwRenderLoad hyperGraph hyperPanel hyperShade hypot iconTextButton iconTextCheckBox iconTextRadioButton iconTextRadioCollection iconTextScrollList iconTextStaticLabel ikHandle ikHandleCtx ikHandleDisplayScale ikSolver ikSplineHandleCtx ikSystem ikSystemInfo ikfkDisplayMethod illustratorCurves image imfPlugins inheritTransform insertJoint insertJointCtx insertKeyCtx insertKnotCurve insertKnotSurface instance instanceable instancer intField intFieldGrp intScrollBar intSlider intSliderGrp interToUI internalVar intersect iprEngine isAnimCurve isConnected isDirty isParentOf isSameObject isTrue isValidObjectName isValidString isValidUiName isolateSelect itemFilter itemFilterAttr itemFilterRender itemFilterType joint jointCluster jointCtx jointDisplayScale jointLattice keyTangent keyframe keyframeOutliner keyframeRegionCurrentTimeCtx keyframeRegionDirectKeyCtx keyframeRegionDollyCtx keyframeRegionInsertKeyCtx keyframeRegionMoveKeyCtx keyframeRegionScaleKeyCtx keyframeRegionSelectKeyCtx keyframeRegionSetKeyCtx keyframeRegionTrackCtx keyframeStats lassoContext lattice latticeDeformKeyCtx launch launchImageEditor layerButton layeredShaderPort layeredTexturePort layout layoutDialog lightList lightListEditor lightListPanel lightlink lineIntersection linearPrecision linstep listAnimatable listAttr listCameras listConnections listDeviceAttachments listHistory listInputDeviceAxes listInputDeviceButtons listInputDevices listMenuAnnotation listNodeTypes listPanelCategories listRelatives listSets listTransforms listUnselected listerEditor loadFluid loadNewShelf loadPlugin loadPluginLanguageResources loadPrefObjects localizedPanelLabel lockNode loft log longNameOf lookThru ls lsThroughFilter lsType lsUI Mayatomr mag makeIdentity makeLive makePaintable makeRoll makeSingleSurface makeTubeOn makebot manipMoveContext manipMoveLimitsCtx manipOptions manipRotateContext manipRotateLimitsCtx manipScaleContext manipScaleLimitsCtx marker match max memory menu menuBarLayout menuEditor menuItem menuItemToShelf menuSet menuSetPref messageLine min minimizeApp mirrorJoint modelCurrentTimeCtx modelEditor modelPanel mouse movIn movOut move moveIKtoFK moveKeyCtx moveVertexAlongDirection multiProfileBirailSurface mute nParticle nameCommand nameField namespace namespaceInfo newPanelItems newton nodeCast nodeIconButton nodeOutliner nodePreset nodeType noise nonLinear normalConstraint normalize nurbsBoolean nurbsCopyUVSet nurbsCube nurbsEditUV nurbsPlane nurbsSelect nurbsSquare nurbsToPoly nurbsToPolygonsPref nurbsToSubdiv nurbsToSubdivPref nurbsUVSet nurbsViewDirectionVector objExists objectCenter objectLayer objectType objectTypeUI obsoleteProc oceanNurbsPreviewPlane offsetCurve offsetCurveOnSurface offsetSurface openGLExtension openMayaPref optionMenu optionMenuGrp optionVar orbit orbitCtx orientConstraint outlinerEditor outlinerPanel overrideModifier paintEffectsDisplay pairBlend palettePort paneLayout panel panelConfiguration panelHistory paramDimContext paramDimension paramLocator parent parentConstraint particle particleExists particleInstancer particleRenderInfo partition pasteKey pathAnimation pause pclose percent performanceOptions pfxstrokes pickWalk picture pixelMove planarSrf plane play playbackOptions playblast plugAttr plugNode pluginInfo pluginResourceUtil pointConstraint pointCurveConstraint pointLight pointMatrixMult pointOnCurve pointOnSurface pointPosition poleVectorConstraint polyAppend polyAppendFacetCtx polyAppendVertex polyAutoProjection polyAverageNormal polyAverageVertex polyBevel polyBlendColor polyBlindData polyBoolOp polyBridgeEdge polyCacheMonitor polyCheck polyChipOff polyClipboard polyCloseBorder polyCollapseEdge polyCollapseFacet polyColorBlindData polyColorDel polyColorPerVertex polyColorSet polyCompare polyCone polyCopyUV polyCrease polyCreaseCtx polyCreateFacet polyCreateFacetCtx polyCube polyCut polyCutCtx polyCylinder polyCylindricalProjection polyDelEdge polyDelFacet polyDelVertex polyDuplicateAndConnect polyDuplicateEdge polyEditUV polyEditUVShell polyEvaluate polyExtrudeEdge polyExtrudeFacet polyExtrudeVertex polyFlipEdge polyFlipUV polyForceUV polyGeoSampler polyHelix polyInfo polyInstallAction polyLayoutUV polyListComponentConversion polyMapCut polyMapDel polyMapSew polyMapSewMove polyMergeEdge polyMergeEdgeCtx polyMergeFacet polyMergeFacetCtx polyMergeUV polyMergeVertex polyMirrorFace polyMoveEdge polyMoveFacet polyMoveFacetUV polyMoveUV polyMoveVertex polyNormal polyNormalPerVertex polyNormalizeUV polyOptUvs polyOptions polyOutput polyPipe polyPlanarProjection polyPlane polyPlatonicSolid polyPoke polyPrimitive polyPrism polyProjection polyPyramid polyQuad polyQueryBlindData polyReduce polySelect polySelectConstraint polySelectConstraintMonitor polySelectCtx polySelectEditCtx polySeparate polySetToFaceNormal polySewEdge polyShortestPathCtx polySmooth polySoftEdge polySphere polySphericalProjection polySplit polySplitCtx polySplitEdge polySplitRing polySplitVertex polyStraightenUVBorder polySubdivideEdge polySubdivideFacet polyToSubdiv polyTorus polyTransfer polyTriangulate polyUVSet polyUnite polyWedgeFace popen popupMenu pose pow preloadRefEd print progressBar progressWindow projFileViewer projectCurve projectTangent projectionContext projectionManip promptDialog propModCtx propMove psdChannelOutliner psdEditTextureFile psdExport psdTextureFile putenv pwd python querySubdiv quit rad_to_deg radial radioButton radioButtonGrp radioCollection radioMenuItemCollection rampColorPort rand randomizeFollicles randstate rangeControl readTake rebuildCurve rebuildSurface recordAttr recordDevice redo reference referenceEdit referenceQuery refineSubdivSelectionList refresh refreshAE registerPluginResource rehash reloadImage removeJoint removeMultiInstance removePanelCategory rename renameAttr renameSelectionList renameUI render renderGlobalsNode renderInfo renderLayerButton renderLayerParent renderLayerPostProcess renderLayerUnparent renderManip renderPartition renderQualityNode renderSettings renderThumbnailUpdate renderWindowEditor renderWindowSelectContext renderer reorder reorderDeformers requires reroot resampleFluid resetAE resetPfxToPolyCamera resetTool resolutionNode retarget reverseCurve reverseSurface revolve rgb_to_hsv rigidBody rigidSolver roll rollCtx rootOf rot rotate rotationInterpolation roundConstantRadius rowColumnLayout rowLayout runTimeCommand runup sampleImage saveAllShelves saveAttrPreset saveFluid saveImage saveInitialState saveMenu savePrefObjects savePrefs saveShelf saveToolSettings scale scaleBrushBrightness scaleComponents scaleConstraint scaleKey scaleKeyCtx sceneEditor sceneUIReplacement scmh scriptCtx scriptEditorInfo scriptJob scriptNode scriptTable scriptToShelf scriptedPanel scriptedPanelType scrollField scrollLayout sculpt searchPathArray seed selLoadSettings select selectContext selectCurveCV selectKey selectKeyCtx selectKeyframeRegionCtx selectMode selectPref selectPriority selectType selectedNodes selectionConnection separator setAttr setAttrEnumResource setAttrMapping setAttrNiceNameResource setConstraintRestPosition setDefaultShadingGroup setDrivenKeyframe setDynamic setEditCtx setEditor setFluidAttr setFocus setInfinity setInputDeviceMapping setKeyCtx setKeyPath setKeyframe setKeyframeBlendshapeTargetWts setMenuMode setNodeNiceNameResource setNodeTypeFlag setParent setParticleAttr setPfxToPolyCamera setPluginResource setProject setStampDensity setStartupMessage setState setToolTo setUITemplate setXformManip sets shadingConnection shadingGeometryRelCtx shadingLightRelCtx shadingNetworkCompare shadingNode shapeCompare shelfButton shelfLayout shelfTabLayout shellField shortNameOf showHelp showHidden showManipCtx showSelectionInTitle showShadingGroupAttrEditor showWindow sign simplify sin singleProfileBirailSurface size sizeBytes skinCluster skinPercent smoothCurve smoothTangentSurface smoothstep snap2to2 snapKey snapMode snapTogetherCtx snapshot soft softMod softModCtx sort sound soundControl source spaceLocator sphere sphrand spotLight spotLightPreviewPort spreadSheetEditor spring sqrt squareSurface srtContext stackTrace startString startsWith stitchAndExplodeShell stitchSurface stitchSurfacePoints strcmp stringArrayCatenate stringArrayContains stringArrayCount stringArrayInsertAtIndex stringArrayIntersector stringArrayRemove stringArrayRemoveAtIndex stringArrayRemoveDuplicates stringArrayRemoveExact stringArrayToString stringToStringArray strip stripPrefixFromName stroke subdAutoProjection subdCleanTopology subdCollapse subdDuplicateAndConnect subdEditUV subdListComponentConversion subdMapCut subdMapSewMove subdMatchTopology subdMirror subdToBlind subdToPoly subdTransferUVsToCache subdiv subdivCrease subdivDisplaySmoothness substitute substituteAllString substituteGeometry substring surface surfaceSampler surfaceShaderList swatchDisplayPort switchTable symbolButton symbolCheckBox sysFile system tabLayout tan tangentConstraint texLatticeDeformContext texManipContext texMoveContext texMoveUVShellContext texRotateContext texScaleContext texSelectContext texSelectShortestPathCtx texSmudgeUVContext texWinToolCtx text textCurves textField textFieldButtonGrp textFieldGrp textManip textScrollList textToShelf textureDisplacePlane textureHairColor texturePlacementContext textureWindow threadCount threePointArcCtx timeControl timePort timerX toNativePath toggle toggleAxis toggleWindowVisibility tokenize tokenizeList tolerance tolower toolButton toolCollection toolDropped toolHasOptions toolPropertyWindow torus toupper trace track trackCtx transferAttributes transformCompare transformLimits translator trim trunc truncateFluidCache truncateHairCache tumble tumbleCtx turbulence twoPointArcCtx uiRes uiTemplate unassignInputDevice undo undoInfo ungroup uniform unit unloadPlugin untangleUV untitledFileName untrim upAxis updateAE userCtx uvLink uvSnapshot validateShelfName vectorize view2dToolCtx viewCamera viewClipPlane viewFit viewHeadOn viewLookAt viewManip viewPlace viewSet visor volumeAxis vortex waitCursor warning webBrowser webBrowserPrefs whatIs window windowPref wire wireContext workspace wrinkle wrinkleContext writeTake xbmLangPathList xform",illegal:"</",
-contains:[a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},{begin:"[\\$\\%\\@](\\^\\w\\b|#\\w+|[^\\s\\w{]|{\\w+}|\\w+)"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}}}());
-hljs.registerLanguage("mercury",function(){return function(a){var b=a.COMMENT("%","$"),c=a.inherit(a.APOS_STRING_MODE,{relevance:0}),d=a.inherit(a.QUOTE_STRING_MODE,{relevance:0});d.contains=d.contains.slice();d.contains.push({className:"subst",begin:"\\\\[abfnrtv]\\|\\\\x[0-9a-fA-F]*\\\\\\|%[-+# *.0-9]*[dioxXucsfeEgGp]",relevance:0});return{name:"Mercury",aliases:["m","moo"],keywords:{keyword:"module use_module import_module include_module end_module initialise mutable initialize finalize finalise interface implementation pred mode func type inst solver any_pred any_func is semidet det nondet multi erroneous failure cc_nondet cc_multi typeclass instance where pragma promise external trace atomic or_else require_complete_switch require_det require_semidet require_multi require_nondet require_cc_multi require_cc_nondet require_erroneous require_failure",
+built_in:"put abs acos aliasReference annuity arrayDecode arrayEncode asin atan atan2 average avg avgDev base64Decode base64Encode baseConvert binaryDecode binaryEncode byteOffset byteToNum cachedURL cachedURLs charToNum cipherNames codepointOffset codepointProperty codepointToNum codeunitOffset commandNames compound compress constantNames cos date dateFormat decompress difference directories diskSpace DNSServers exp exp1 exp2 exp10 extents files flushEvents folders format functionNames geometricMean global globals hasMemory harmonicMean hostAddress hostAddressToName hostName hostNameToAddress isNumber ISOToMac itemOffset keys len length libURLErrorData libUrlFormData libURLftpCommand libURLLastHTTPHeaders libURLLastRHHeaders libUrlMultipartFormAddPart libUrlMultipartFormData libURLVersion lineOffset ln ln1 localNames log log2 log10 longFilePath lower macToISO matchChunk matchText matrixMultiply max md5Digest median merge messageAuthenticationCode messageDigest millisec millisecs millisecond milliseconds min monthNames nativeCharToNum normalizeText num number numToByte numToChar numToCodepoint numToNativeChar offset open openfiles openProcesses openProcessIDs openSockets paragraphOffset paramCount param params peerAddress pendingMessages platform popStdDev populationStandardDeviation populationVariance popVariance processID random randomBytes replaceText result revCreateXMLTree revCreateXMLTreeFromFile revCurrentRecord revCurrentRecordIsFirst revCurrentRecordIsLast revDatabaseColumnCount revDatabaseColumnIsNull revDatabaseColumnLengths revDatabaseColumnNames revDatabaseColumnNamed revDatabaseColumnNumbered revDatabaseColumnTypes revDatabaseConnectResult revDatabaseCursors revDatabaseID revDatabaseTableNames revDatabaseType revDataFromQuery revdb_closeCursor revdb_columnbynumber revdb_columncount revdb_columnisnull revdb_columnlengths revdb_columnnames revdb_columntypes revdb_commit revdb_connect revdb_connections revdb_connectionerr revdb_currentrecord revdb_cursorconnection revdb_cursorerr revdb_cursors revdb_dbtype revdb_disconnect revdb_execute revdb_iseof revdb_isbof revdb_movefirst revdb_movelast revdb_movenext revdb_moveprev revdb_query revdb_querylist revdb_recordcount revdb_rollback revdb_tablenames revGetDatabaseDriverPath revNumberOfRecords revOpenDatabase revOpenDatabases revQueryDatabase revQueryDatabaseBlob revQueryResult revQueryIsAtStart revQueryIsAtEnd revUnixFromMacPath revXMLAttribute revXMLAttributes revXMLAttributeValues revXMLChildContents revXMLChildNames revXMLCreateTreeFromFileWithNamespaces revXMLCreateTreeWithNamespaces revXMLDataFromXPathQuery revXMLEvaluateXPath revXMLFirstChild revXMLMatchingNode revXMLNextSibling revXMLNodeContents revXMLNumberOfChildren revXMLParent revXMLPreviousSibling revXMLRootNode revXMLRPC_CreateRequest revXMLRPC_Documents revXMLRPC_Error revXMLRPC_GetHost revXMLRPC_GetMethod revXMLRPC_GetParam revXMLText revXMLRPC_Execute revXMLRPC_GetParamCount revXMLRPC_GetParamNode revXMLRPC_GetParamType revXMLRPC_GetPath revXMLRPC_GetPort revXMLRPC_GetProtocol revXMLRPC_GetRequest revXMLRPC_GetResponse revXMLRPC_GetSocket revXMLTree revXMLTrees revXMLValidateDTD revZipDescribeItem revZipEnumerateItems revZipOpenArchives round sampVariance sec secs seconds sentenceOffset sha1Digest shell shortFilePath sin specialFolderPath sqrt standardDeviation statRound stdDev sum sysError systemVersion tan tempName textDecode textEncode tick ticks time to tokenOffset toLower toUpper transpose truewordOffset trunc uniDecode uniEncode upper URLDecode URLEncode URLStatus uuid value variableNames variance version waitDepth weekdayNames wordOffset xsltApplyStylesheet xsltApplyStylesheetFromFile xsltLoadStylesheet xsltLoadStylesheetFromFile add breakpoint cancel clear local variable file word line folder directory URL close socket process combine constant convert create new alias folder directory decrypt delete variable word line folder directory URL dispatch divide do encrypt filter get include intersect kill libURLDownloadToFile libURLFollowHttpRedirects libURLftpUpload libURLftpUploadFile libURLresetAll libUrlSetAuthCallback libURLSetDriver libURLSetCustomHTTPHeaders libUrlSetExpect100 libURLSetFTPListCommand libURLSetFTPMode libURLSetFTPStopTime libURLSetStatusCallback load extension loadedExtensions multiply socket prepare process post seek rel relative read from process rename replace require resetAll resolve revAddXMLNode revAppendXML revCloseCursor revCloseDatabase revCommitDatabase revCopyFile revCopyFolder revCopyXMLNode revDeleteFolder revDeleteXMLNode revDeleteAllXMLTrees revDeleteXMLTree revExecuteSQL revGoURL revInsertXMLNode revMoveFolder revMoveToFirstRecord revMoveToLastRecord revMoveToNextRecord revMoveToPreviousRecord revMoveToRecord revMoveXMLNode revPutIntoXMLNode revRollBackDatabase revSetDatabaseDriverPath revSetXMLAttribute revXMLRPC_AddParam revXMLRPC_DeleteAllDocuments revXMLAddDTD revXMLRPC_Free revXMLRPC_FreeAll revXMLRPC_DeleteDocument revXMLRPC_DeleteParam revXMLRPC_SetHost revXMLRPC_SetMethod revXMLRPC_SetPort revXMLRPC_SetProtocol revXMLRPC_SetSocket revZipAddItemWithData revZipAddItemWithFile revZipAddUncompressedItemWithData revZipAddUncompressedItemWithFile revZipCancel revZipCloseArchive revZipDeleteItem revZipExtractItemToFile revZipExtractItemToVariable revZipSetProgressCallback revZipRenameItem revZipReplaceItemWithData revZipReplaceItemWithFile revZipOpenArchive send set sort split start stop subtract symmetric union unload vectorDotProduct wait write"
+},contains:[r,{className:"keyword",begin:"\\bend\\sif\\b"},{
+className:"function",beginKeywords:"function",end:"$",
+contains:[r,o,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE,a]
+},{className:"function",begin:"\\bend\\s+",end:"$",keywords:"end",
+contains:[o,a],relevance:0},{beginKeywords:"command on",end:"$",
+contains:[r,o,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE,a]
+},{className:"meta",variants:[{begin:"<\\?(rev|lc|livecode)",relevance:10},{
+begin:"<\\?"},{begin:"\\?>"}]
+},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE,a].concat(t),
+illegal:";$|^\\[|^=|&|\\{"}}})());
+hljs.registerLanguage("livescript",(()=>{"use strict"
+;const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"])
+;return t=>{const r={
+keyword:e.concat(["then","unless","until","loop","of","by","when","and","or","is","isnt","not","it","that","otherwise","from","to","til","fallthrough","case","enum","native","list","map","__hasProp","__extends","__slice","__bind","__indexOf"]),
+literal:n.concat(["yes","no","on","off","it","that","void"]),
+built_in:a.concat(["npm","print"])
+},i="[A-Za-z$_](?:-[0-9A-Za-z$_]|[0-9A-Za-z$_])*",s=t.inherit(t.TITLE_MODE,{
+begin:i}),o={className:"subst",begin:/#\{/,end:/\}/,keywords:r},c={
+className:"subst",begin:/#[A-Za-z$_]/,end:/(?:-[0-9A-Za-z$_]|[0-9A-Za-z$_])*/,
+keywords:r},l=[t.BINARY_NUMBER_MODE,{className:"number",
+begin:"(\\b0[xX][a-fA-F0-9_]+)|(\\b\\d(\\d|_\\d)*(\\.(\\d(\\d|_\\d)*)?)?(_*[eE]([-+]\\d(_\\d|\\d)*)?)?[_a-z]*)",
+relevance:0,starts:{end:"(\\s*/)?",relevance:0}},{className:"string",variants:[{
+begin:/'''/,end:/'''/,contains:[t.BACKSLASH_ESCAPE]},{begin:/'/,end:/'/,
+contains:[t.BACKSLASH_ESCAPE]},{begin:/"""/,end:/"""/,
+contains:[t.BACKSLASH_ESCAPE,o,c]},{begin:/"/,end:/"/,
+contains:[t.BACKSLASH_ESCAPE,o,c]},{begin:/\\/,end:/(\s|$)/,excludeEnd:!0}]},{
+className:"regexp",variants:[{begin:"//",end:"//[gim]*",
+contains:[o,t.HASH_COMMENT_MODE]},{
+begin:/\/(?![ *])(\\.|[^\\\n])*?\/[gim]*(?=\W)/}]},{begin:"@"+i},{begin:"``",
+end:"``",excludeBegin:!0,excludeEnd:!0,subLanguage:"javascript"}];o.contains=l
+;const d={className:"params",begin:"\\(",returnBegin:!0,contains:[{begin:/\(/,
+end:/\)/,keywords:r,contains:["self"].concat(l)}]};return{name:"LiveScript",
+aliases:["ls"],keywords:r,illegal:/\/\*/,
+contains:l.concat([t.COMMENT("\\/\\*","\\*\\/"),t.HASH_COMMENT_MODE,{
+begin:"(#=>|=>|\\|>>|-?->|!->)"},{className:"function",contains:[s,d],
+returnBegin:!0,variants:[{
+begin:"("+i+"\\s*(?:=|:=)\\s*)?(\\(.*\\)\\s*)?\\B->\\*?",end:"->\\*?"},{
+begin:"("+i+"\\s*(?:=|:=)\\s*)?!?(\\(.*\\)\\s*)?\\B[-~]{1,2}>\\*?",
+end:"[-~]{1,2}>\\*?"},{
+begin:"("+i+"\\s*(?:=|:=)\\s*)?(\\(.*\\)\\s*)?\\B!?[-~]{1,2}>\\*?",
+end:"!?[-~]{1,2}>\\*?"}]},{className:"class",beginKeywords:"class",end:"$",
+illegal:/[:="\[\]]/,contains:[{beginKeywords:"extends",endsWithParent:!0,
+illegal:/[:="\[\]]/,contains:[s]},s]},{begin:i+":",end:":",returnBegin:!0,
+returnEnd:!0,relevance:0}])}}})());
+hljs.registerLanguage("llvm",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>{const a=/([-a-zA-Z$._][\w$.-]*)/,t={
+className:"variable",variants:[{begin:e(/%/,a)},{begin:/%\d+/},{begin:/#\d+/}]
+},i={className:"title",variants:[{begin:e(/@/,a)},{begin:/@\d+/},{begin:e(/!/,a)
+},{begin:e(/!\d+/,a)},{begin:/!\d+/}]};return{name:"LLVM IR",
+keywords:"begin end true false declare define global constant private linker_private internal available_externally linkonce linkonce_odr weak weak_odr appending dllimport dllexport common default hidden protected extern_weak external thread_local zeroinitializer undef null to tail target triple datalayout volatile nuw nsw nnan ninf nsz arcp fast exact inbounds align addrspace section alias module asm sideeffect gc dbg linker_private_weak attributes blockaddress initialexec localdynamic localexec prefix unnamed_addr ccc fastcc coldcc x86_stdcallcc x86_fastcallcc arm_apcscc arm_aapcscc arm_aapcs_vfpcc ptx_device ptx_kernel intel_ocl_bicc msp430_intrcc spir_func spir_kernel x86_64_sysvcc x86_64_win64cc x86_thiscallcc cc c signext zeroext inreg sret nounwind noreturn noalias nocapture byval nest readnone readonly inlinehint noinline alwaysinline optsize ssp sspreq noredzone noimplicitfloat naked builtin cold nobuiltin noduplicate nonlazybind optnone returns_twice sanitize_address sanitize_memory sanitize_thread sspstrong uwtable returned type opaque eq ne slt sgt sle sge ult ugt ule uge oeq one olt ogt ole oge ord uno ueq une x acq_rel acquire alignstack atomic catch cleanup filter inteldialect max min monotonic nand personality release seq_cst singlethread umax umin unordered xchg add fadd sub fsub mul fmul udiv sdiv fdiv urem srem frem shl lshr ashr and or xor icmp fcmp phi call trunc zext sext fptrunc fpext uitofp sitofp fptoui fptosi inttoptr ptrtoint bitcast addrspacecast select va_arg ret br switch invoke unwind unreachable indirectbr landingpad resume malloc alloca free load store getelementptr extractelement insertelement shufflevector getresult extractvalue insertvalue atomicrmw cmpxchg fence argmemonly double",
+contains:[{className:"type",begin:/\bi\d+(?=\s|\b)/},n.COMMENT(/;\s*$/,null,{
+relevance:0}),n.COMMENT(/;/,/$/),n.QUOTE_STRING_MODE,{className:"string",
+variants:[{begin:/"/,end:/[^\\]"/}]},i,{className:"punctuation",relevance:0,
+begin:/,/},{className:"operator",relevance:0,begin:/=/},t,{className:"symbol",
+variants:[{begin:/^\s*[a-z]+:/}],relevance:0},{className:"number",variants:[{
+begin:/0[xX][a-fA-F0-9]+/},{begin:/-?\d+(?:[.]\d+)?(?:[eE][-+]?\d+(?:[.]\d+)?)?/
+}],relevance:0}]}}})());
+hljs.registerLanguage("lsl",(()=>{"use strict";return E=>{var T={
+className:"number",relevance:0,begin:E.C_NUMBER_RE};return{
+name:"LSL (Linden Scripting Language)",illegal:":",contains:[{
+className:"string",begin:'"',end:'"',contains:[{className:"subst",
+begin:/\\[tn"\\]/}]},{className:"comment",
+variants:[E.COMMENT("//","$"),E.COMMENT("/\\*","\\*/")],relevance:0},T,{
+className:"section",variants:[{begin:"\\b(state|default)\\b"},{
+begin:"\\b(state_(entry|exit)|touch(_(start|end))?|(land_)?collision(_(start|end))?|timer|listen|(no_)?sensor|control|(not_)?at_(rot_)?target|money|email|experience_permissions(_denied)?|run_time_permissions|changed|attach|dataserver|moving_(start|end)|link_message|(on|object)_rez|remote_data|http_re(sponse|quest)|path_update|transaction_result)\\b"
+}]},{className:"built_in",
+begin:"\\b(ll(AgentInExperience|(Create|DataSize|Delete|KeyCount|Keys|Read|Update)KeyValue|GetExperience(Details|ErrorMessage)|ReturnObjectsBy(ID|Owner)|Json(2List|[GS]etValue|ValueType)|Sin|Cos|Tan|Atan2|Sqrt|Pow|Abs|Fabs|Frand|Floor|Ceil|Round|Vec(Mag|Norm|Dist)|Rot(Between|2(Euler|Fwd|Left|Up))|(Euler|Axes)2Rot|Whisper|(Region|Owner)?Say|Shout|Listen(Control|Remove)?|Sensor(Repeat|Remove)?|Detected(Name|Key|Owner|Type|Pos|Vel|Grab|Rot|Group|LinkNumber)|Die|Ground|Wind|([GS]et)(AnimationOverride|MemoryLimit|PrimMediaParams|ParcelMusicURL|Object(Desc|Name)|PhysicsMaterial|Status|Scale|Color|Alpha|Texture|Pos|Rot|Force|Torque)|ResetAnimationOverride|(Scale|Offset|Rotate)Texture|(Rot)?Target(Remove)?|(Stop)?MoveToTarget|Apply(Rotational)?Impulse|Set(KeyframedMotion|ContentType|RegionPos|(Angular)?Velocity|Buoyancy|HoverHeight|ForceAndTorque|TimerEvent|ScriptState|Damage|TextureAnim|Sound(Queueing|Radius)|Vehicle(Type|(Float|Vector|Rotation)Param)|(Touch|Sit)?Text|Camera(Eye|At)Offset|PrimitiveParams|ClickAction|Link(Alpha|Color|PrimitiveParams(Fast)?|Texture(Anim)?|Camera|Media)|RemoteScriptAccessPin|PayPrice|LocalRot)|ScaleByFactor|Get((Max|Min)ScaleFactor|ClosestNavPoint|StaticPath|SimStats|Env|PrimitiveParams|Link(PrimitiveParams|Number(OfSides)?|Key|Name|Media)|HTTPHeader|FreeURLs|Object(Details|PermMask|PrimCount)|Parcel(MaxPrims|Details|Prim(Count|Owners))|Attached(List)?|(SPMax|Free|Used)Memory|Region(Name|TimeDilation|FPS|Corner|AgentCount)|Root(Position|Rotation)|UnixTime|(Parcel|Region)Flags|(Wall|GMT)clock|SimulatorHostname|BoundingBox|GeometricCenter|Creator|NumberOf(Prims|NotecardLines|Sides)|Animation(List)?|(Camera|Local)(Pos|Rot)|Vel|Accel|Omega|Time(stamp|OfDay)|(Object|CenterOf)?Mass|MassMKS|Energy|Owner|(Owner)?Key|SunDirection|Texture(Offset|Scale|Rot)|Inventory(Number|Name|Key|Type|Creator|PermMask)|Permissions(Key)?|StartParameter|List(Length|EntryType)|Date|Agent(Size|Info|Language|List)|LandOwnerAt|NotecardLine|Script(Name|State))|(Get|Reset|GetAndReset)Time|PlaySound(Slave)?|LoopSound(Master|Slave)?|(Trigger|Stop|Preload)Sound|((Get|Delete)Sub|Insert)String|To(Upper|Lower)|Give(InventoryList|Money)|RezObject|(Stop)?LookAt|Sleep|CollisionFilter|(Take|Release)Controls|DetachFromAvatar|AttachToAvatar(Temp)?|InstantMessage|(GetNext)?Email|StopHover|MinEventDelay|RotLookAt|String(Length|Trim)|(Start|Stop)Animation|TargetOmega|Request(Experience)?Permissions|(Create|Break)Link|BreakAllLinks|(Give|Remove)Inventory|Water|PassTouches|Request(Agent|Inventory)Data|TeleportAgent(Home|GlobalCoords)?|ModifyLand|CollisionSound|ResetScript|MessageLinked|PushObject|PassCollisions|AxisAngle2Rot|Rot2(Axis|Angle)|A(cos|sin)|AngleBetween|AllowInventoryDrop|SubStringIndex|List2(CSV|Integer|Json|Float|String|Key|Vector|Rot|List(Strided)?)|DeleteSubList|List(Statistics|Sort|Randomize|(Insert|Find|Replace)List)|EdgeOfWorld|AdjustSoundVolume|Key2Name|TriggerSoundLimited|EjectFromLand|(CSV|ParseString)2List|OverMyLand|SameGroup|UnSit|Ground(Slope|Normal|Contour)|GroundRepel|(Set|Remove)VehicleFlags|SitOnLink|(AvatarOn)?(Link)?SitTarget|Script(Danger|Profiler)|Dialog|VolumeDetect|ResetOtherScript|RemoteLoadScriptPin|(Open|Close)RemoteDataChannel|SendRemoteData|RemoteDataReply|(Integer|String)ToBase64|XorBase64|Log(10)?|Base64To(String|Integer)|ParseStringKeepNulls|RezAtRoot|RequestSimulatorData|ForceMouselook|(Load|Release|(E|Une)scape)URL|ParcelMedia(CommandList|Query)|ModPow|MapDestination|(RemoveFrom|AddTo|Reset)Land(Pass|Ban)List|(Set|Clear)CameraParams|HTTP(Request|Response)|TextBox|DetectedTouch(UV|Face|Pos|(N|Bin)ormal|ST)|(MD5|SHA1|DumpList2)String|Request(Secure)?URL|Clear(Prim|Link)Media|(Link)?ParticleSystem|(Get|Request)(Username|DisplayName)|RegionSayTo|CastRay|GenerateKey|TransferLindenDollars|ManageEstateAccess|(Create|Delete)Character|ExecCharacterCmd|Evade|FleeFrom|NavigateTo|PatrolPoints|Pursue|UpdateCharacter|WanderWithin))\\b"
+},{className:"literal",variants:[{
+begin:"\\b(PI|TWO_PI|PI_BY_TWO|DEG_TO_RAD|RAD_TO_DEG|SQRT2)\\b"},{
+begin:"\\b(XP_ERROR_(EXPERIENCES_DISABLED|EXPERIENCE_(DISABLED|SUSPENDED)|INVALID_(EXPERIENCE|PARAMETERS)|KEY_NOT_FOUND|MATURITY_EXCEEDED|NONE|NOT_(FOUND|PERMITTED(_LAND)?)|NO_EXPERIENCE|QUOTA_EXCEEDED|RETRY_UPDATE|STORAGE_EXCEPTION|STORE_DISABLED|THROTTLED|UNKNOWN_ERROR)|JSON_APPEND|STATUS_(PHYSICS|ROTATE_[XYZ]|PHANTOM|SANDBOX|BLOCK_GRAB(_OBJECT)?|(DIE|RETURN)_AT_EDGE|CAST_SHADOWS|OK|MALFORMED_PARAMS|TYPE_MISMATCH|BOUNDS_ERROR|NOT_(FOUND|SUPPORTED)|INTERNAL_ERROR|WHITELIST_FAILED)|AGENT(_(BY_(LEGACY_|USER)NAME|FLYING|ATTACHMENTS|SCRIPTED|MOUSELOOK|SITTING|ON_OBJECT|AWAY|WALKING|IN_AIR|TYPING|CROUCHING|BUSY|ALWAYS_RUN|AUTOPILOT|LIST_(PARCEL(_OWNER)?|REGION)))?|CAMERA_(PITCH|DISTANCE|BEHINDNESS_(ANGLE|LAG)|(FOCUS|POSITION)(_(THRESHOLD|LOCKED|LAG))?|FOCUS_OFFSET|ACTIVE)|ANIM_ON|LOOP|REVERSE|PING_PONG|SMOOTH|ROTATE|SCALE|ALL_SIDES|LINK_(ROOT|SET|ALL_(OTHERS|CHILDREN)|THIS)|ACTIVE|PASS(IVE|_(ALWAYS|IF_NOT_HANDLED|NEVER))|SCRIPTED|CONTROL_(FWD|BACK|(ROT_)?(LEFT|RIGHT)|UP|DOWN|(ML_)?LBUTTON)|PERMISSION_(RETURN_OBJECTS|DEBIT|OVERRIDE_ANIMATIONS|SILENT_ESTATE_MANAGEMENT|TAKE_CONTROLS|TRIGGER_ANIMATION|ATTACH|CHANGE_LINKS|(CONTROL|TRACK)_CAMERA|TELEPORT)|INVENTORY_(TEXTURE|SOUND|OBJECT|SCRIPT|LANDMARK|CLOTHING|NOTECARD|BODYPART|ANIMATION|GESTURE|ALL|NONE)|CHANGED_(INVENTORY|COLOR|SHAPE|SCALE|TEXTURE|LINK|ALLOWED_DROP|OWNER|REGION(_START)?|TELEPORT|MEDIA)|OBJECT_(CLICK_ACTION|HOVER_HEIGHT|LAST_OWNER_ID|(PHYSICS|SERVER|STREAMING)_COST|UNKNOWN_DETAIL|CHARACTER_TIME|PHANTOM|PHYSICS|TEMP_(ATTACHED|ON_REZ)|NAME|DESC|POS|PRIM_(COUNT|EQUIVALENCE)|RETURN_(PARCEL(_OWNER)?|REGION)|REZZER_KEY|ROO?T|VELOCITY|OMEGA|OWNER|GROUP(_TAG)?|CREATOR|ATTACHED_(POINT|SLOTS_AVAILABLE)|RENDER_WEIGHT|(BODY_SHAPE|PATHFINDING)_TYPE|(RUNNING|TOTAL)_SCRIPT_COUNT|TOTAL_INVENTORY_COUNT|SCRIPT_(MEMORY|TIME))|TYPE_(INTEGER|FLOAT|STRING|KEY|VECTOR|ROTATION|INVALID)|(DEBUG|PUBLIC)_CHANNEL|ATTACH_(AVATAR_CENTER|CHEST|HEAD|BACK|PELVIS|MOUTH|CHIN|NECK|NOSE|BELLY|[LR](SHOULDER|HAND|FOOT|EAR|EYE|[UL](ARM|LEG)|HIP)|(LEFT|RIGHT)_PEC|HUD_(CENTER_[12]|TOP_(RIGHT|CENTER|LEFT)|BOTTOM(_(RIGHT|LEFT))?)|[LR]HAND_RING1|TAIL_(BASE|TIP)|[LR]WING|FACE_(JAW|[LR]EAR|[LR]EYE|TOUNGE)|GROIN|HIND_[LR]FOOT)|LAND_(LEVEL|RAISE|LOWER|SMOOTH|NOISE|REVERT)|DATA_(ONLINE|NAME|BORN|SIM_(POS|STATUS|RATING)|PAYINFO)|PAYMENT_INFO_(ON_FILE|USED)|REMOTE_DATA_(CHANNEL|REQUEST|REPLY)|PSYS_(PART_(BF_(ZERO|ONE(_MINUS_(DEST_COLOR|SOURCE_(ALPHA|COLOR)))?|DEST_COLOR|SOURCE_(ALPHA|COLOR))|BLEND_FUNC_(DEST|SOURCE)|FLAGS|(START|END)_(COLOR|ALPHA|SCALE|GLOW)|MAX_AGE|(RIBBON|WIND|INTERP_(COLOR|SCALE)|BOUNCE|FOLLOW_(SRC|VELOCITY)|TARGET_(POS|LINEAR)|EMISSIVE)_MASK)|SRC_(MAX_AGE|PATTERN|ANGLE_(BEGIN|END)|BURST_(RATE|PART_COUNT|RADIUS|SPEED_(MIN|MAX))|ACCEL|TEXTURE|TARGET_KEY|OMEGA|PATTERN_(DROP|EXPLODE|ANGLE(_CONE(_EMPTY)?)?)))|VEHICLE_(REFERENCE_FRAME|TYPE_(NONE|SLED|CAR|BOAT|AIRPLANE|BALLOON)|(LINEAR|ANGULAR)_(FRICTION_TIMESCALE|MOTOR_DIRECTION)|LINEAR_MOTOR_OFFSET|HOVER_(HEIGHT|EFFICIENCY|TIMESCALE)|BUOYANCY|(LINEAR|ANGULAR)_(DEFLECTION_(EFFICIENCY|TIMESCALE)|MOTOR_(DECAY_)?TIMESCALE)|VERTICAL_ATTRACTION_(EFFICIENCY|TIMESCALE)|BANKING_(EFFICIENCY|MIX|TIMESCALE)|FLAG_(NO_DEFLECTION_UP|LIMIT_(ROLL_ONLY|MOTOR_UP)|HOVER_((WATER|TERRAIN|UP)_ONLY|GLOBAL_HEIGHT)|MOUSELOOK_(STEER|BANK)|CAMERA_DECOUPLED))|PRIM_(ALLOW_UNSIT|ALPHA_MODE(_(BLEND|EMISSIVE|MASK|NONE))?|NORMAL|SPECULAR|TYPE(_(BOX|CYLINDER|PRISM|SPHERE|TORUS|TUBE|RING|SCULPT))?|HOLE_(DEFAULT|CIRCLE|SQUARE|TRIANGLE)|MATERIAL(_(STONE|METAL|GLASS|WOOD|FLESH|PLASTIC|RUBBER))?|SHINY_(NONE|LOW|MEDIUM|HIGH)|BUMP_(NONE|BRIGHT|DARK|WOOD|BARK|BRICKS|CHECKER|CONCRETE|TILE|STONE|DISKS|GRAVEL|BLOBS|SIDING|LARGETILE|STUCCO|SUCTION|WEAVE)|TEXGEN_(DEFAULT|PLANAR)|SCRIPTED_SIT_ONLY|SCULPT_(TYPE_(SPHERE|TORUS|PLANE|CYLINDER|MASK)|FLAG_(MIRROR|INVERT))|PHYSICS(_(SHAPE_(CONVEX|NONE|PRIM|TYPE)))?|(POS|ROT)_LOCAL|SLICE|TEXT|FLEXIBLE|POINT_LIGHT|TEMP_ON_REZ|PHANTOM|POSITION|SIT_TARGET|SIZE|ROTATION|TEXTURE|NAME|OMEGA|DESC|LINK_TARGET|COLOR|BUMP_SHINY|FULLBRIGHT|TEXGEN|GLOW|MEDIA_(ALT_IMAGE_ENABLE|CONTROLS|(CURRENT|HOME)_URL|AUTO_(LOOP|PLAY|SCALE|ZOOM)|FIRST_CLICK_INTERACT|(WIDTH|HEIGHT)_PIXELS|WHITELIST(_ENABLE)?|PERMS_(INTERACT|CONTROL)|PARAM_MAX|CONTROLS_(STANDARD|MINI)|PERM_(NONE|OWNER|GROUP|ANYONE)|MAX_(URL_LENGTH|WHITELIST_(SIZE|COUNT)|(WIDTH|HEIGHT)_PIXELS)))|MASK_(BASE|OWNER|GROUP|EVERYONE|NEXT)|PERM_(TRANSFER|MODIFY|COPY|MOVE|ALL)|PARCEL_(MEDIA_COMMAND_(STOP|PAUSE|PLAY|LOOP|TEXTURE|URL|TIME|AGENT|UNLOAD|AUTO_ALIGN|TYPE|SIZE|DESC|LOOP_SET)|FLAG_(ALLOW_(FLY|(GROUP_)?SCRIPTS|LANDMARK|TERRAFORM|DAMAGE|CREATE_(GROUP_)?OBJECTS)|USE_(ACCESS_(GROUP|LIST)|BAN_LIST|LAND_PASS_LIST)|LOCAL_SOUND_ONLY|RESTRICT_PUSHOBJECT|ALLOW_(GROUP|ALL)_OBJECT_ENTRY)|COUNT_(TOTAL|OWNER|GROUP|OTHER|SELECTED|TEMP)|DETAILS_(NAME|DESC|OWNER|GROUP|AREA|ID|SEE_AVATARS))|LIST_STAT_(MAX|MIN|MEAN|MEDIAN|STD_DEV|SUM(_SQUARES)?|NUM_COUNT|GEOMETRIC_MEAN|RANGE)|PAY_(HIDE|DEFAULT)|REGION_FLAG_(ALLOW_DAMAGE|FIXED_SUN|BLOCK_TERRAFORM|SANDBOX|DISABLE_(COLLISIONS|PHYSICS)|BLOCK_FLY|ALLOW_DIRECT_TELEPORT|RESTRICT_PUSHOBJECT)|HTTP_(METHOD|MIMETYPE|BODY_(MAXLENGTH|TRUNCATED)|CUSTOM_HEADER|PRAGMA_NO_CACHE|VERBOSE_THROTTLE|VERIFY_CERT)|SIT_(INVALID_(AGENT|LINK_OBJECT)|NO(T_EXPERIENCE|_(ACCESS|EXPERIENCE_PERMISSION|SIT_TARGET)))|STRING_(TRIM(_(HEAD|TAIL))?)|CLICK_ACTION_(NONE|TOUCH|SIT|BUY|PAY|OPEN(_MEDIA)?|PLAY|ZOOM)|TOUCH_INVALID_FACE|PROFILE_(NONE|SCRIPT_MEMORY)|RC_(DATA_FLAGS|DETECT_PHANTOM|GET_(LINK_NUM|NORMAL|ROOT_KEY)|MAX_HITS|REJECT_(TYPES|AGENTS|(NON)?PHYSICAL|LAND))|RCERR_(CAST_TIME_EXCEEDED|SIM_PERF_LOW|UNKNOWN)|ESTATE_ACCESS_(ALLOWED_(AGENT|GROUP)_(ADD|REMOVE)|BANNED_AGENT_(ADD|REMOVE))|DENSITY|FRICTION|RESTITUTION|GRAVITY_MULTIPLIER|KFM_(COMMAND|CMD_(PLAY|STOP|PAUSE)|MODE|FORWARD|LOOP|PING_PONG|REVERSE|DATA|ROTATION|TRANSLATION)|ERR_(GENERIC|PARCEL_PERMISSIONS|MALFORMED_PARAMS|RUNTIME_PERMISSIONS|THROTTLED)|CHARACTER_(CMD_((SMOOTH_)?STOP|JUMP)|DESIRED_(TURN_)?SPEED|RADIUS|STAY_WITHIN_PARCEL|LENGTH|ORIENTATION|ACCOUNT_FOR_SKIPPED_FRAMES|AVOIDANCE_MODE|TYPE(_([ABCD]|NONE))?|MAX_(DECEL|TURN_RADIUS|(ACCEL|SPEED)))|PURSUIT_(OFFSET|FUZZ_FACTOR|GOAL_TOLERANCE|INTERCEPT)|REQUIRE_LINE_OF_SIGHT|FORCE_DIRECT_PATH|VERTICAL|HORIZONTAL|AVOID_(CHARACTERS|DYNAMIC_OBSTACLES|NONE)|PU_(EVADE_(HIDDEN|SPOTTED)|FAILURE_(DYNAMIC_PATHFINDING_DISABLED|INVALID_(GOAL|START)|NO_(NAVMESH|VALID_DESTINATION)|OTHER|TARGET_GONE|(PARCEL_)?UNREACHABLE)|(GOAL|SLOWDOWN_DISTANCE)_REACHED)|TRAVERSAL_TYPE(_(FAST|NONE|SLOW))?|CONTENT_TYPE_(ATOM|FORM|HTML|JSON|LLSD|RSS|TEXT|XHTML|XML)|GCNP_(RADIUS|STATIC)|(PATROL|WANDER)_PAUSE_AT_WAYPOINTS|OPT_(AVATAR|CHARACTER|EXCLUSION_VOLUME|LEGACY_LINKSET|MATERIAL_VOLUME|OTHER|STATIC_OBSTACLE|WALKABLE)|SIM_STAT_PCT_CHARS_STEPPED)\\b"
+},{begin:"\\b(FALSE|TRUE)\\b"},{begin:"\\b(ZERO_ROTATION)\\b"},{
+begin:"\\b(EOF|JSON_(ARRAY|DELETE|FALSE|INVALID|NULL|NUMBER|OBJECT|STRING|TRUE)|NULL_KEY|TEXTURE_(BLANK|DEFAULT|MEDIA|PLYWOOD|TRANSPARENT)|URL_REQUEST_(GRANTED|DENIED))\\b"
+},{begin:"\\b(ZERO_VECTOR|TOUCH_INVALID_(TEXCOORD|VECTOR))\\b"}]},{
+className:"type",
+begin:"\\b(integer|float|string|key|vector|quaternion|rotation|list)\\b"}]}}
+})());
+hljs.registerLanguage("lua",(()=>{"use strict";return e=>{
+const t="\\[=*\\[",a="\\]=*\\]",n={begin:t,end:a,contains:["self"]
+},o=[e.COMMENT("--(?!\\[=*\\[)","$"),e.COMMENT("--\\[=*\\[",a,{contains:[n],
+relevance:10})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE,
+literal:"true false nil",
+keyword:"and break do else elseif end for goto if in local not or repeat return then until while",
+built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"
+},contains:o.concat([{className:"function",beginKeywords:"function",end:"\\)",
+contains:[e.inherit(e.TITLE_MODE,{
+begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",
+begin:"\\(",endsWithParent:!0,contains:o}].concat(o)
+},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",
+begin:t,end:a,contains:[n],relevance:5}])}}})());
+hljs.registerLanguage("makefile",(()=>{"use strict";return e=>{const i={
+className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)",
+contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%<?\^\+\*]/}]},a={className:"string",
+begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,i]},n={className:"variable",
+begin:/\$\([\w-]+\s/,end:/\)/,keywords:{
+built_in:"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value"
+},contains:[i]},s={begin:"^"+e.UNDERSCORE_IDENT_RE+"\\s*(?=[:+?]?=)"},r={
+className:"section",begin:/^[^\s]+:/,end:/$/,contains:[i]};return{
+name:"Makefile",aliases:["mk","mak","make"],keywords:{$pattern:/[\w-]+/,
+keyword:"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath"
+},contains:[e.HASH_COMMENT_MODE,i,a,n,s,{className:"meta",begin:/^\.PHONY:/,
+end:/$/,keywords:{$pattern:/[\.\w]+/,"meta-keyword":".PHONY"}},r]}}})());
+hljs.registerLanguage("mathematica",(()=>{"use strict"
+;const e=["AASTriangle","AbelianGroup","Abort","AbortKernels","AbortProtect","AbortScheduledTask","Above","Abs","AbsArg","AbsArgPlot","Absolute","AbsoluteCorrelation","AbsoluteCorrelationFunction","AbsoluteCurrentValue","AbsoluteDashing","AbsoluteFileName","AbsoluteOptions","AbsolutePointSize","AbsoluteThickness","AbsoluteTime","AbsoluteTiming","AcceptanceThreshold","AccountingForm","Accumulate","Accuracy","AccuracyGoal","ActionDelay","ActionMenu","ActionMenuBox","ActionMenuBoxOptions","Activate","Active","ActiveClassification","ActiveClassificationObject","ActiveItem","ActivePrediction","ActivePredictionObject","ActiveStyle","AcyclicGraphQ","AddOnHelpPath","AddSides","AddTo","AddToSearchIndex","AddUsers","AdjacencyGraph","AdjacencyList","AdjacencyMatrix","AdjacentMeshCells","AdjustmentBox","AdjustmentBoxOptions","AdjustTimeSeriesForecast","AdministrativeDivisionData","AffineHalfSpace","AffineSpace","AffineStateSpaceModel","AffineTransform","After","AggregatedEntityClass","AggregationLayer","AircraftData","AirportData","AirPressureData","AirTemperatureData","AiryAi","AiryAiPrime","AiryAiZero","AiryBi","AiryBiPrime","AiryBiZero","AlgebraicIntegerQ","AlgebraicNumber","AlgebraicNumberDenominator","AlgebraicNumberNorm","AlgebraicNumberPolynomial","AlgebraicNumberTrace","AlgebraicRules","AlgebraicRulesData","Algebraics","AlgebraicUnitQ","Alignment","AlignmentMarker","AlignmentPoint","All","AllowAdultContent","AllowedCloudExtraParameters","AllowedCloudParameterExtensions","AllowedDimensions","AllowedFrequencyRange","AllowedHeads","AllowGroupClose","AllowIncomplete","AllowInlineCells","AllowKernelInitialization","AllowLooseGrammar","AllowReverseGroupClose","AllowScriptLevelChange","AllowVersionUpdate","AllTrue","Alphabet","AlphabeticOrder","AlphabeticSort","AlphaChannel","AlternateImage","AlternatingFactorial","AlternatingGroup","AlternativeHypothesis","Alternatives","AltitudeMethod","AmbientLight","AmbiguityFunction","AmbiguityList","Analytic","AnatomyData","AnatomyForm","AnatomyPlot3D","AnatomySkinStyle","AnatomyStyling","AnchoredSearch","And","AndersonDarlingTest","AngerJ","AngleBisector","AngleBracket","AnglePath","AnglePath3D","AngleVector","AngularGauge","Animate","AnimationCycleOffset","AnimationCycleRepetitions","AnimationDirection","AnimationDisplayTime","AnimationRate","AnimationRepetitions","AnimationRunning","AnimationRunTime","AnimationTimeIndex","Animator","AnimatorBox","AnimatorBoxOptions","AnimatorElements","Annotate","Annotation","AnnotationDelete","AnnotationKeys","AnnotationRules","AnnotationValue","Annuity","AnnuityDue","Annulus","AnomalyDetection","AnomalyDetector","AnomalyDetectorFunction","Anonymous","Antialiasing","AntihermitianMatrixQ","Antisymmetric","AntisymmetricMatrixQ","Antonyms","AnyOrder","AnySubset","AnyTrue","Apart","ApartSquareFree","APIFunction","Appearance","AppearanceElements","AppearanceRules","AppellF1","Append","AppendCheck","AppendLayer","AppendTo","Apply","ApplySides","ArcCos","ArcCosh","ArcCot","ArcCoth","ArcCsc","ArcCsch","ArcCurvature","ARCHProcess","ArcLength","ArcSec","ArcSech","ArcSin","ArcSinDistribution","ArcSinh","ArcTan","ArcTanh","Area","Arg","ArgMax","ArgMin","ArgumentCountQ","ARIMAProcess","ArithmeticGeometricMean","ARMAProcess","Around","AroundReplace","ARProcess","Array","ArrayComponents","ArrayDepth","ArrayFilter","ArrayFlatten","ArrayMesh","ArrayPad","ArrayPlot","ArrayQ","ArrayResample","ArrayReshape","ArrayRules","Arrays","Arrow","Arrow3DBox","ArrowBox","Arrowheads","ASATriangle","Ask","AskAppend","AskConfirm","AskDisplay","AskedQ","AskedValue","AskFunction","AskState","AskTemplateDisplay","AspectRatio","AspectRatioFixed","Assert","AssociateTo","Association","AssociationFormat","AssociationMap","AssociationQ","AssociationThread","AssumeDeterministic","Assuming","Assumptions","AstronomicalData","Asymptotic","AsymptoticDSolveValue","AsymptoticEqual","AsymptoticEquivalent","AsymptoticGreater","AsymptoticGreaterEqual","AsymptoticIntegrate","AsymptoticLess","AsymptoticLessEqual","AsymptoticOutputTracker","AsymptoticProduct","AsymptoticRSolveValue","AsymptoticSolve","AsymptoticSum","Asynchronous","AsynchronousTaskObject","AsynchronousTasks","Atom","AtomCoordinates","AtomCount","AtomDiagramCoordinates","AtomList","AtomQ","AttentionLayer","Attributes","Audio","AudioAmplify","AudioAnnotate","AudioAnnotationLookup","AudioBlockMap","AudioCapture","AudioChannelAssignment","AudioChannelCombine","AudioChannelMix","AudioChannels","AudioChannelSeparate","AudioData","AudioDelay","AudioDelete","AudioDevice","AudioDistance","AudioEncoding","AudioFade","AudioFrequencyShift","AudioGenerator","AudioIdentify","AudioInputDevice","AudioInsert","AudioInstanceQ","AudioIntervals","AudioJoin","AudioLabel","AudioLength","AudioLocalMeasurements","AudioLooping","AudioLoudness","AudioMeasurements","AudioNormalize","AudioOutputDevice","AudioOverlay","AudioPad","AudioPan","AudioPartition","AudioPause","AudioPitchShift","AudioPlay","AudioPlot","AudioQ","AudioRecord","AudioReplace","AudioResample","AudioReverb","AudioReverse","AudioSampleRate","AudioSpectralMap","AudioSpectralTransformation","AudioSplit","AudioStop","AudioStream","AudioStreams","AudioTimeStretch","AudioTracks","AudioTrim","AudioType","AugmentedPolyhedron","AugmentedSymmetricPolynomial","Authenticate","Authentication","AuthenticationDialog","AutoAction","Autocomplete","AutocompletionFunction","AutoCopy","AutocorrelationTest","AutoDelete","AutoEvaluateEvents","AutoGeneratedPackage","AutoIndent","AutoIndentSpacings","AutoItalicWords","AutoloadPath","AutoMatch","Automatic","AutomaticImageSize","AutoMultiplicationSymbol","AutoNumberFormatting","AutoOpenNotebooks","AutoOpenPalettes","AutoQuoteCharacters","AutoRefreshed","AutoRemove","AutorunSequencing","AutoScaling","AutoScroll","AutoSpacing","AutoStyleOptions","AutoStyleWords","AutoSubmitting","Axes","AxesEdge","AxesLabel","AxesOrigin","AxesStyle","AxiomaticTheory","Axis","BabyMonsterGroupB","Back","Background","BackgroundAppearance","BackgroundTasksSettings","Backslash","Backsubstitution","Backward","Ball","Band","BandpassFilter","BandstopFilter","BarabasiAlbertGraphDistribution","BarChart","BarChart3D","BarcodeImage","BarcodeRecognize","BaringhausHenzeTest","BarLegend","BarlowProschanImportance","BarnesG","BarOrigin","BarSpacing","BartlettHannWindow","BartlettWindow","BaseDecode","BaseEncode","BaseForm","Baseline","BaselinePosition","BaseStyle","BasicRecurrentLayer","BatchNormalizationLayer","BatchSize","BatesDistribution","BattleLemarieWavelet","BayesianMaximization","BayesianMaximizationObject","BayesianMinimization","BayesianMinimizationObject","Because","BeckmannDistribution","Beep","Before","Begin","BeginDialogPacket","BeginFrontEndInteractionPacket","BeginPackage","BellB","BellY","Below","BenfordDistribution","BeniniDistribution","BenktanderGibratDistribution","BenktanderWeibullDistribution","BernoulliB","BernoulliDistribution","BernoulliGraphDistribution","BernoulliProcess","BernsteinBasis","BesselFilterModel","BesselI","BesselJ","BesselJZero","BesselK","BesselY","BesselYZero","Beta","BetaBinomialDistribution","BetaDistribution","BetaNegativeBinomialDistribution","BetaPrimeDistribution","BetaRegularized","Between","BetweennessCentrality","BeveledPolyhedron","BezierCurve","BezierCurve3DBox","BezierCurve3DBoxOptions","BezierCurveBox","BezierCurveBoxOptions","BezierFunction","BilateralFilter","Binarize","BinaryDeserialize","BinaryDistance","BinaryFormat","BinaryImageQ","BinaryRead","BinaryReadList","BinarySerialize","BinaryWrite","BinCounts","BinLists","Binomial","BinomialDistribution","BinomialProcess","BinormalDistribution","BiorthogonalSplineWavelet","BipartiteGraphQ","BiquadraticFilterModel","BirnbaumImportance","BirnbaumSaundersDistribution","BitAnd","BitClear","BitGet","BitLength","BitNot","BitOr","BitSet","BitShiftLeft","BitShiftRight","BitXor","BiweightLocation","BiweightMidvariance","Black","BlackmanHarrisWindow","BlackmanNuttallWindow","BlackmanWindow","Blank","BlankForm","BlankNullSequence","BlankSequence","Blend","Block","BlockchainAddressData","BlockchainBase","BlockchainBlockData","BlockchainContractValue","BlockchainData","BlockchainGet","BlockchainKeyEncode","BlockchainPut","BlockchainTokenData","BlockchainTransaction","BlockchainTransactionData","BlockchainTransactionSign","BlockchainTransactionSubmit","BlockMap","BlockRandom","BlomqvistBeta","BlomqvistBetaTest","Blue","Blur","BodePlot","BohmanWindow","Bold","Bond","BondCount","BondList","BondQ","Bookmarks","Boole","BooleanConsecutiveFunction","BooleanConvert","BooleanCountingFunction","BooleanFunction","BooleanGraph","BooleanMaxterms","BooleanMinimize","BooleanMinterms","BooleanQ","BooleanRegion","Booleans","BooleanStrings","BooleanTable","BooleanVariables","BorderDimensions","BorelTannerDistribution","Bottom","BottomHatTransform","BoundaryDiscretizeGraphics","BoundaryDiscretizeRegion","BoundaryMesh","BoundaryMeshRegion","BoundaryMeshRegionQ","BoundaryStyle","BoundedRegionQ","BoundingRegion","Bounds","Box","BoxBaselineShift","BoxData","BoxDimensions","Boxed","Boxes","BoxForm","BoxFormFormatTypes","BoxFrame","BoxID","BoxMargins","BoxMatrix","BoxObject","BoxRatios","BoxRotation","BoxRotationPoint","BoxStyle","BoxWhiskerChart","Bra","BracketingBar","BraKet","BrayCurtisDistance","BreadthFirstScan","Break","BridgeData","BrightnessEqualize","BroadcastStationData","Brown","BrownForsytheTest","BrownianBridgeProcess","BrowserCategory","BSplineBasis","BSplineCurve","BSplineCurve3DBox","BSplineCurve3DBoxOptions","BSplineCurveBox","BSplineCurveBoxOptions","BSplineFunction","BSplineSurface","BSplineSurface3DBox","BSplineSurface3DBoxOptions","BubbleChart","BubbleChart3D","BubbleScale","BubbleSizes","BuildingData","BulletGauge","BusinessDayQ","ButterflyGraph","ButterworthFilterModel","Button","ButtonBar","ButtonBox","ButtonBoxOptions","ButtonCell","ButtonContents","ButtonData","ButtonEvaluator","ButtonExpandable","ButtonFrame","ButtonFunction","ButtonMargins","ButtonMinHeight","ButtonNote","ButtonNotebook","ButtonSource","ButtonStyle","ButtonStyleMenuListing","Byte","ByteArray","ByteArrayFormat","ByteArrayQ","ByteArrayToString","ByteCount","ByteOrdering","C","CachedValue","CacheGraphics","CachePersistence","CalendarConvert","CalendarData","CalendarType","Callout","CalloutMarker","CalloutStyle","CallPacket","CanberraDistance","Cancel","CancelButton","CandlestickChart","CanonicalGraph","CanonicalizePolygon","CanonicalizePolyhedron","CanonicalName","CanonicalWarpingCorrespondence","CanonicalWarpingDistance","CantorMesh","CantorStaircase","Cap","CapForm","CapitalDifferentialD","Capitalize","CapsuleShape","CaptureRunning","CardinalBSplineBasis","CarlemanLinearize","CarmichaelLambda","CaseOrdering","Cases","CaseSensitive","Cashflow","Casoratian","Catalan","CatalanNumber","Catch","CategoricalDistribution","Catenate","CatenateLayer","CauchyDistribution","CauchyWindow","CayleyGraph","CDF","CDFDeploy","CDFInformation","CDFWavelet","Ceiling","CelestialSystem","Cell","CellAutoOverwrite","CellBaseline","CellBoundingBox","CellBracketOptions","CellChangeTimes","CellContents","CellContext","CellDingbat","CellDynamicExpression","CellEditDuplicate","CellElementsBoundingBox","CellElementSpacings","CellEpilog","CellEvaluationDuplicate","CellEvaluationFunction","CellEvaluationLanguage","CellEventActions","CellFrame","CellFrameColor","CellFrameLabelMargins","CellFrameLabels","CellFrameMargins","CellGroup","CellGroupData","CellGrouping","CellGroupingRules","CellHorizontalScrolling","CellID","CellLabel","CellLabelAutoDelete","CellLabelMargins","CellLabelPositioning","CellLabelStyle","CellLabelTemplate","CellMargins","CellObject","CellOpen","CellPrint","CellProlog","Cells","CellSize","CellStyle","CellTags","CellularAutomaton","CensoredDistribution","Censoring","Center","CenterArray","CenterDot","CentralFeature","CentralMoment","CentralMomentGeneratingFunction","Cepstrogram","CepstrogramArray","CepstrumArray","CForm","ChampernowneNumber","ChangeOptions","ChannelBase","ChannelBrokerAction","ChannelDatabin","ChannelHistoryLength","ChannelListen","ChannelListener","ChannelListeners","ChannelListenerWait","ChannelObject","ChannelPreSendFunction","ChannelReceiverFunction","ChannelSend","ChannelSubscribers","ChanVeseBinarize","Character","CharacterCounts","CharacterEncoding","CharacterEncodingsPath","CharacteristicFunction","CharacteristicPolynomial","CharacterName","CharacterNormalize","CharacterRange","Characters","ChartBaseStyle","ChartElementData","ChartElementDataFunction","ChartElementFunction","ChartElements","ChartLabels","ChartLayout","ChartLegends","ChartStyle","Chebyshev1FilterModel","Chebyshev2FilterModel","ChebyshevDistance","ChebyshevT","ChebyshevU","Check","CheckAbort","CheckAll","Checkbox","CheckboxBar","CheckboxBox","CheckboxBoxOptions","ChemicalData","ChessboardDistance","ChiDistribution","ChineseRemainder","ChiSquareDistribution","ChoiceButtons","ChoiceDialog","CholeskyDecomposition","Chop","ChromaticityPlot","ChromaticityPlot3D","ChromaticPolynomial","Circle","CircleBox","CircleDot","CircleMinus","CirclePlus","CirclePoints","CircleThrough","CircleTimes","CirculantGraph","CircularOrthogonalMatrixDistribution","CircularQuaternionMatrixDistribution","CircularRealMatrixDistribution","CircularSymplecticMatrixDistribution","CircularUnitaryMatrixDistribution","Circumsphere","CityData","ClassifierFunction","ClassifierInformation","ClassifierMeasurements","ClassifierMeasurementsObject","Classify","ClassPriors","Clear","ClearAll","ClearAttributes","ClearCookies","ClearPermissions","ClearSystemCache","ClebschGordan","ClickPane","Clip","ClipboardNotebook","ClipFill","ClippingStyle","ClipPlanes","ClipPlanesStyle","ClipRange","Clock","ClockGauge","ClockwiseContourIntegral","Close","Closed","CloseKernels","ClosenessCentrality","Closing","ClosingAutoSave","ClosingEvent","ClosingSaveDialog","CloudAccountData","CloudBase","CloudConnect","CloudConnections","CloudDeploy","CloudDirectory","CloudDisconnect","CloudEvaluate","CloudExport","CloudExpression","CloudExpressions","CloudFunction","CloudGet","CloudImport","CloudLoggingData","CloudObject","CloudObjectInformation","CloudObjectInformationData","CloudObjectNameFormat","CloudObjects","CloudObjectURLType","CloudPublish","CloudPut","CloudRenderingMethod","CloudSave","CloudShare","CloudSubmit","CloudSymbol","CloudUnshare","CloudUserID","ClusterClassify","ClusterDissimilarityFunction","ClusteringComponents","ClusteringTree","CMYKColor","Coarse","CodeAssistOptions","Coefficient","CoefficientArrays","CoefficientDomain","CoefficientList","CoefficientRules","CoifletWavelet","Collect","Colon","ColonForm","ColorBalance","ColorCombine","ColorConvert","ColorCoverage","ColorData","ColorDataFunction","ColorDetect","ColorDistance","ColorFunction","ColorFunctionScaling","Colorize","ColorNegate","ColorOutput","ColorProfileData","ColorQ","ColorQuantize","ColorReplace","ColorRules","ColorSelectorSettings","ColorSeparate","ColorSetter","ColorSetterBox","ColorSetterBoxOptions","ColorSlider","ColorsNear","ColorSpace","ColorToneMapping","Column","ColumnAlignments","ColumnBackgrounds","ColumnForm","ColumnLines","ColumnsEqual","ColumnSpacings","ColumnWidths","CombinedEntityClass","CombinerFunction","CometData","CommonDefaultFormatTypes","Commonest","CommonestFilter","CommonName","CommonUnits","CommunityBoundaryStyle","CommunityGraphPlot","CommunityLabels","CommunityRegionStyle","CompanyData","CompatibleUnitQ","CompilationOptions","CompilationTarget","Compile","Compiled","CompiledCodeFunction","CompiledFunction","CompilerOptions","Complement","ComplementedEntityClass","CompleteGraph","CompleteGraphQ","CompleteKaryTree","CompletionsListPacket","Complex","ComplexContourPlot","Complexes","ComplexExpand","ComplexInfinity","ComplexityFunction","ComplexListPlot","ComplexPlot","ComplexPlot3D","ComplexRegionPlot","ComplexStreamPlot","ComplexVectorPlot","ComponentMeasurements","ComponentwiseContextMenu","Compose","ComposeList","ComposeSeries","CompositeQ","Composition","CompoundElement","CompoundExpression","CompoundPoissonDistribution","CompoundPoissonProcess","CompoundRenewalProcess","Compress","CompressedData","CompressionLevel","ComputeUncertainty","Condition","ConditionalExpression","Conditioned","Cone","ConeBox","ConfidenceLevel","ConfidenceRange","ConfidenceTransform","ConfigurationPath","ConformAudio","ConformImages","Congruent","ConicHullRegion","ConicHullRegion3DBox","ConicHullRegionBox","ConicOptimization","Conjugate","ConjugateTranspose","Conjunction","Connect","ConnectedComponents","ConnectedGraphComponents","ConnectedGraphQ","ConnectedMeshComponents","ConnectedMoleculeComponents","ConnectedMoleculeQ","ConnectionSettings","ConnectLibraryCallbackFunction","ConnectSystemModelComponents","ConnesWindow","ConoverTest","ConsoleMessage","ConsoleMessagePacket","Constant","ConstantArray","ConstantArrayLayer","ConstantImage","ConstantPlusLayer","ConstantRegionQ","Constants","ConstantTimesLayer","ConstellationData","ConstrainedMax","ConstrainedMin","Construct","Containing","ContainsAll","ContainsAny","ContainsExactly","ContainsNone","ContainsOnly","ContentFieldOptions","ContentLocationFunction","ContentObject","ContentPadding","ContentsBoundingBox","ContentSelectable","ContentSize","Context","ContextMenu","Contexts","ContextToFileName","Continuation","Continue","ContinuedFraction","ContinuedFractionK","ContinuousAction","ContinuousMarkovProcess","ContinuousTask","ContinuousTimeModelQ","ContinuousWaveletData","ContinuousWaveletTransform","ContourDetect","ContourGraphics","ContourIntegral","ContourLabels","ContourLines","ContourPlot","ContourPlot3D","Contours","ContourShading","ContourSmoothing","ContourStyle","ContraharmonicMean","ContrastiveLossLayer","Control","ControlActive","ControlAlignment","ControlGroupContentsBox","ControllabilityGramian","ControllabilityMatrix","ControllableDecomposition","ControllableModelQ","ControllerDuration","ControllerInformation","ControllerInformationData","ControllerLinking","ControllerManipulate","ControllerMethod","ControllerPath","ControllerState","ControlPlacement","ControlsRendering","ControlType","Convergents","ConversionOptions","ConversionRules","ConvertToBitmapPacket","ConvertToPostScript","ConvertToPostScriptPacket","ConvexHullMesh","ConvexPolygonQ","ConvexPolyhedronQ","ConvolutionLayer","Convolve","ConwayGroupCo1","ConwayGroupCo2","ConwayGroupCo3","CookieFunction","Cookies","CoordinateBoundingBox","CoordinateBoundingBoxArray","CoordinateBounds","CoordinateBoundsArray","CoordinateChartData","CoordinatesToolOptions","CoordinateTransform","CoordinateTransformData","CoprimeQ","Coproduct","CopulaDistribution","Copyable","CopyDatabin","CopyDirectory","CopyFile","CopyTag","CopyToClipboard","CornerFilter","CornerNeighbors","Correlation","CorrelationDistance","CorrelationFunction","CorrelationTest","Cos","Cosh","CoshIntegral","CosineDistance","CosineWindow","CosIntegral","Cot","Coth","Count","CountDistinct","CountDistinctBy","CounterAssignments","CounterBox","CounterBoxOptions","CounterClockwiseContourIntegral","CounterEvaluator","CounterFunction","CounterIncrements","CounterStyle","CounterStyleMenuListing","CountRoots","CountryData","Counts","CountsBy","Covariance","CovarianceEstimatorFunction","CovarianceFunction","CoxianDistribution","CoxIngersollRossProcess","CoxModel","CoxModelFit","CramerVonMisesTest","CreateArchive","CreateCellID","CreateChannel","CreateCloudExpression","CreateDatabin","CreateDataStructure","CreateDataSystemModel","CreateDialog","CreateDirectory","CreateDocument","CreateFile","CreateIntermediateDirectories","CreateManagedLibraryExpression","CreateNotebook","CreatePacletArchive","CreatePalette","CreatePalettePacket","CreatePermissionsGroup","CreateScheduledTask","CreateSearchIndex","CreateSystemModel","CreateTemporary","CreateUUID","CreateWindow","CriterionFunction","CriticalityFailureImportance","CriticalitySuccessImportance","CriticalSection","Cross","CrossEntropyLossLayer","CrossingCount","CrossingDetect","CrossingPolygon","CrossMatrix","Csc","Csch","CTCLossLayer","Cube","CubeRoot","Cubics","Cuboid","CuboidBox","Cumulant","CumulantGeneratingFunction","Cup","CupCap","Curl","CurlyDoubleQuote","CurlyQuote","CurrencyConvert","CurrentDate","CurrentImage","CurrentlySpeakingPacket","CurrentNotebookImage","CurrentScreenImage","CurrentValue","Curry","CurryApplied","CurvatureFlowFilter","CurveClosed","Cyan","CycleGraph","CycleIndexPolynomial","Cycles","CyclicGroup","Cyclotomic","Cylinder","CylinderBox","CylindricalDecomposition","D","DagumDistribution","DamData","DamerauLevenshteinDistance","DampingFactor","Darker","Dashed","Dashing","DatabaseConnect","DatabaseDisconnect","DatabaseReference","Databin","DatabinAdd","DatabinRemove","Databins","DatabinUpload","DataCompression","DataDistribution","DataRange","DataReversed","Dataset","DatasetDisplayPanel","DataStructure","DataStructureQ","Date","DateBounds","Dated","DateDelimiters","DateDifference","DatedUnit","DateFormat","DateFunction","DateHistogram","DateInterval","DateList","DateListLogPlot","DateListPlot","DateListStepPlot","DateObject","DateObjectQ","DateOverlapsQ","DatePattern","DatePlus","DateRange","DateReduction","DateString","DateTicksFormat","DateValue","DateWithinQ","DaubechiesWavelet","DavisDistribution","DawsonF","DayCount","DayCountConvention","DayHemisphere","DaylightQ","DayMatchQ","DayName","DayNightTerminator","DayPlus","DayRange","DayRound","DeBruijnGraph","DeBruijnSequence","Debug","DebugTag","Decapitalize","Decimal","DecimalForm","DeclareKnownSymbols","DeclarePackage","Decompose","DeconvolutionLayer","Decrement","Decrypt","DecryptFile","DedekindEta","DeepSpaceProbeData","Default","DefaultAxesStyle","DefaultBaseStyle","DefaultBoxStyle","DefaultButton","DefaultColor","DefaultControlPlacement","DefaultDuplicateCellStyle","DefaultDuration","DefaultElement","DefaultFaceGridsStyle","DefaultFieldHintStyle","DefaultFont","DefaultFontProperties","DefaultFormatType","DefaultFormatTypeForStyle","DefaultFrameStyle","DefaultFrameTicksStyle","DefaultGridLinesStyle","DefaultInlineFormatType","DefaultInputFormatType","DefaultLabelStyle","DefaultMenuStyle","DefaultNaturalLanguage","DefaultNewCellStyle","DefaultNewInlineCellStyle","DefaultNotebook","DefaultOptions","DefaultOutputFormatType","DefaultPrintPrecision","DefaultStyle","DefaultStyleDefinitions","DefaultTextFormatType","DefaultTextInlineFormatType","DefaultTicksStyle","DefaultTooltipStyle","DefaultValue","DefaultValues","Defer","DefineExternal","DefineInputStreamMethod","DefineOutputStreamMethod","DefineResourceFunction","Definition","Degree","DegreeCentrality","DegreeGraphDistribution","DegreeLexicographic","DegreeReverseLexicographic","DEigensystem","DEigenvalues","Deinitialization","Del","DelaunayMesh","Delayed","Deletable","Delete","DeleteAnomalies","DeleteBorderComponents","DeleteCases","DeleteChannel","DeleteCloudExpression","DeleteContents","DeleteDirectory","DeleteDuplicates","DeleteDuplicatesBy","DeleteFile","DeleteMissing","DeleteObject","DeletePermissionsKey","DeleteSearchIndex","DeleteSmallComponents","DeleteStopwords","DeleteWithContents","DeletionWarning","DelimitedArray","DelimitedSequence","Delimiter","DelimiterFlashTime","DelimiterMatching","Delimiters","DeliveryFunction","Dendrogram","Denominator","DensityGraphics","DensityHistogram","DensityPlot","DensityPlot3D","DependentVariables","Deploy","Deployed","Depth","DepthFirstScan","Derivative","DerivativeFilter","DerivedKey","DescriptorStateSpace","DesignMatrix","DestroyAfterEvaluation","Det","DeviceClose","DeviceConfigure","DeviceExecute","DeviceExecuteAsynchronous","DeviceObject","DeviceOpen","DeviceOpenQ","DeviceRead","DeviceReadBuffer","DeviceReadLatest","DeviceReadList","DeviceReadTimeSeries","Devices","DeviceStreams","DeviceWrite","DeviceWriteBuffer","DGaussianWavelet","DiacriticalPositioning","Diagonal","DiagonalizableMatrixQ","DiagonalMatrix","DiagonalMatrixQ","Dialog","DialogIndent","DialogInput","DialogLevel","DialogNotebook","DialogProlog","DialogReturn","DialogSymbols","Diamond","DiamondMatrix","DiceDissimilarity","DictionaryLookup","DictionaryWordQ","DifferenceDelta","DifferenceOrder","DifferenceQuotient","DifferenceRoot","DifferenceRootReduce","Differences","DifferentialD","DifferentialRoot","DifferentialRootReduce","DifferentiatorFilter","DigitalSignature","DigitBlock","DigitBlockMinimum","DigitCharacter","DigitCount","DigitQ","DihedralAngle","DihedralGroup","Dilation","DimensionalCombinations","DimensionalMeshComponents","DimensionReduce","DimensionReducerFunction","DimensionReduction","Dimensions","DiracComb","DiracDelta","DirectedEdge","DirectedEdges","DirectedGraph","DirectedGraphQ","DirectedInfinity","Direction","Directive","Directory","DirectoryName","DirectoryQ","DirectoryStack","DirichletBeta","DirichletCharacter","DirichletCondition","DirichletConvolve","DirichletDistribution","DirichletEta","DirichletL","DirichletLambda","DirichletTransform","DirichletWindow","DisableConsolePrintPacket","DisableFormatting","DiscreteAsymptotic","DiscreteChirpZTransform","DiscreteConvolve","DiscreteDelta","DiscreteHadamardTransform","DiscreteIndicator","DiscreteLimit","DiscreteLQEstimatorGains","DiscreteLQRegulatorGains","DiscreteLyapunovSolve","DiscreteMarkovProcess","DiscreteMaxLimit","DiscreteMinLimit","DiscretePlot","DiscretePlot3D","DiscreteRatio","DiscreteRiccatiSolve","DiscreteShift","DiscreteTimeModelQ","DiscreteUniformDistribution","DiscreteVariables","DiscreteWaveletData","DiscreteWaveletPacketTransform","DiscreteWaveletTransform","DiscretizeGraphics","DiscretizeRegion","Discriminant","DisjointQ","Disjunction","Disk","DiskBox","DiskMatrix","DiskSegment","Dispatch","DispatchQ","DispersionEstimatorFunction","Display","DisplayAllSteps","DisplayEndPacket","DisplayFlushImagePacket","DisplayForm","DisplayFunction","DisplayPacket","DisplayRules","DisplaySetSizePacket","DisplayString","DisplayTemporary","DisplayWith","DisplayWithRef","DisplayWithVariable","DistanceFunction","DistanceMatrix","DistanceTransform","Distribute","Distributed","DistributedContexts","DistributeDefinitions","DistributionChart","DistributionDomain","DistributionFitTest","DistributionParameterAssumptions","DistributionParameterQ","Dithering","Div","Divergence","Divide","DivideBy","Dividers","DivideSides","Divisible","Divisors","DivisorSigma","DivisorSum","DMSList","DMSString","Do","DockedCells","DocumentGenerator","DocumentGeneratorInformation","DocumentGeneratorInformationData","DocumentGenerators","DocumentNotebook","DocumentWeightingRules","Dodecahedron","DomainRegistrationInformation","DominantColors","DOSTextFormat","Dot","DotDashed","DotEqual","DotLayer","DotPlusLayer","Dotted","DoubleBracketingBar","DoubleContourIntegral","DoubleDownArrow","DoubleLeftArrow","DoubleLeftRightArrow","DoubleLeftTee","DoubleLongLeftArrow","DoubleLongLeftRightArrow","DoubleLongRightArrow","DoubleRightArrow","DoubleRightTee","DoubleUpArrow","DoubleUpDownArrow","DoubleVerticalBar","DoublyInfinite","Down","DownArrow","DownArrowBar","DownArrowUpArrow","DownLeftRightVector","DownLeftTeeVector","DownLeftVector","DownLeftVectorBar","DownRightTeeVector","DownRightVector","DownRightVectorBar","Downsample","DownTee","DownTeeArrow","DownValues","DragAndDrop","DrawEdges","DrawFrontFaces","DrawHighlighted","Drop","DropoutLayer","DSolve","DSolveValue","Dt","DualLinearProgramming","DualPolyhedron","DualSystemsModel","DumpGet","DumpSave","DuplicateFreeQ","Duration","Dynamic","DynamicBox","DynamicBoxOptions","DynamicEvaluationTimeout","DynamicGeoGraphics","DynamicImage","DynamicLocation","DynamicModule","DynamicModuleBox","DynamicModuleBoxOptions","DynamicModuleParent","DynamicModuleValues","DynamicName","DynamicNamespace","DynamicReference","DynamicSetting","DynamicUpdating","DynamicWrapper","DynamicWrapperBox","DynamicWrapperBoxOptions","E","EarthImpactData","EarthquakeData","EccentricityCentrality","Echo","EchoFunction","EclipseType","EdgeAdd","EdgeBetweennessCentrality","EdgeCapacity","EdgeCapForm","EdgeColor","EdgeConnectivity","EdgeContract","EdgeCost","EdgeCount","EdgeCoverQ","EdgeCycleMatrix","EdgeDashing","EdgeDelete","EdgeDetect","EdgeForm","EdgeIndex","EdgeJoinForm","EdgeLabeling","EdgeLabels","EdgeLabelStyle","EdgeList","EdgeOpacity","EdgeQ","EdgeRenderingFunction","EdgeRules","EdgeShapeFunction","EdgeStyle","EdgeTaggedGraph","EdgeTaggedGraphQ","EdgeTags","EdgeThickness","EdgeWeight","EdgeWeightedGraphQ","Editable","EditButtonSettings","EditCellTagsSettings","EditDistance","EffectiveInterest","Eigensystem","Eigenvalues","EigenvectorCentrality","Eigenvectors","Element","ElementData","ElementwiseLayer","ElidedForms","Eliminate","EliminationOrder","Ellipsoid","EllipticE","EllipticExp","EllipticExpPrime","EllipticF","EllipticFilterModel","EllipticK","EllipticLog","EllipticNomeQ","EllipticPi","EllipticReducedHalfPeriods","EllipticTheta","EllipticThetaPrime","EmbedCode","EmbeddedHTML","EmbeddedService","EmbeddingLayer","EmbeddingObject","EmitSound","EmphasizeSyntaxErrors","EmpiricalDistribution","Empty","EmptyGraphQ","EmptyRegion","EnableConsolePrintPacket","Enabled","Encode","Encrypt","EncryptedObject","EncryptFile","End","EndAdd","EndDialogPacket","EndFrontEndInteractionPacket","EndOfBuffer","EndOfFile","EndOfLine","EndOfString","EndPackage","EngineEnvironment","EngineeringForm","Enter","EnterExpressionPacket","EnterTextPacket","Entity","EntityClass","EntityClassList","EntityCopies","EntityFunction","EntityGroup","EntityInstance","EntityList","EntityPrefetch","EntityProperties","EntityProperty","EntityPropertyClass","EntityRegister","EntityStore","EntityStores","EntityTypeName","EntityUnregister","EntityValue","Entropy","EntropyFilter","Environment","Epilog","EpilogFunction","Equal","EqualColumns","EqualRows","EqualTilde","EqualTo","EquatedTo","Equilibrium","EquirippleFilterKernel","Equivalent","Erf","Erfc","Erfi","ErlangB","ErlangC","ErlangDistribution","Erosion","ErrorBox","ErrorBoxOptions","ErrorNorm","ErrorPacket","ErrorsDialogSettings","EscapeRadius","EstimatedBackground","EstimatedDistribution","EstimatedProcess","EstimatorGains","EstimatorRegulator","EuclideanDistance","EulerAngles","EulerCharacteristic","EulerE","EulerGamma","EulerianGraphQ","EulerMatrix","EulerPhi","Evaluatable","Evaluate","Evaluated","EvaluatePacket","EvaluateScheduledTask","EvaluationBox","EvaluationCell","EvaluationCompletionAction","EvaluationData","EvaluationElements","EvaluationEnvironment","EvaluationMode","EvaluationMonitor","EvaluationNotebook","EvaluationObject","EvaluationOrder","Evaluator","EvaluatorNames","EvenQ","EventData","EventEvaluator","EventHandler","EventHandlerTag","EventLabels","EventSeries","ExactBlackmanWindow","ExactNumberQ","ExactRootIsolation","ExampleData","Except","ExcludedForms","ExcludedLines","ExcludedPhysicalQuantities","ExcludePods","Exclusions","ExclusionsStyle","Exists","Exit","ExitDialog","ExoplanetData","Exp","Expand","ExpandAll","ExpandDenominator","ExpandFileName","ExpandNumerator","Expectation","ExpectationE","ExpectedValue","ExpGammaDistribution","ExpIntegralE","ExpIntegralEi","ExpirationDate","Exponent","ExponentFunction","ExponentialDistribution","ExponentialFamily","ExponentialGeneratingFunction","ExponentialMovingAverage","ExponentialPowerDistribution","ExponentPosition","ExponentStep","Export","ExportAutoReplacements","ExportByteArray","ExportForm","ExportPacket","ExportString","Expression","ExpressionCell","ExpressionGraph","ExpressionPacket","ExpressionUUID","ExpToTrig","ExtendedEntityClass","ExtendedGCD","Extension","ExtentElementFunction","ExtentMarkers","ExtentSize","ExternalBundle","ExternalCall","ExternalDataCharacterEncoding","ExternalEvaluate","ExternalFunction","ExternalFunctionName","ExternalIdentifier","ExternalObject","ExternalOptions","ExternalSessionObject","ExternalSessions","ExternalStorageBase","ExternalStorageDownload","ExternalStorageGet","ExternalStorageObject","ExternalStoragePut","ExternalStorageUpload","ExternalTypeSignature","ExternalValue","Extract","ExtractArchive","ExtractLayer","ExtractPacletArchive","ExtremeValueDistribution","FaceAlign","FaceForm","FaceGrids","FaceGridsStyle","FacialFeatures","Factor","FactorComplete","Factorial","Factorial2","FactorialMoment","FactorialMomentGeneratingFunction","FactorialPower","FactorInteger","FactorList","FactorSquareFree","FactorSquareFreeList","FactorTerms","FactorTermsList","Fail","Failure","FailureAction","FailureDistribution","FailureQ","False","FareySequence","FARIMAProcess","FeatureDistance","FeatureExtract","FeatureExtraction","FeatureExtractor","FeatureExtractorFunction","FeatureNames","FeatureNearest","FeatureSpacePlot","FeatureSpacePlot3D","FeatureTypes","FEDisableConsolePrintPacket","FeedbackLinearize","FeedbackSector","FeedbackSectorStyle","FeedbackType","FEEnableConsolePrintPacket","FetalGrowthData","Fibonacci","Fibonorial","FieldCompletionFunction","FieldHint","FieldHintStyle","FieldMasked","FieldSize","File","FileBaseName","FileByteCount","FileConvert","FileDate","FileExistsQ","FileExtension","FileFormat","FileHandler","FileHash","FileInformation","FileName","FileNameDepth","FileNameDialogSettings","FileNameDrop","FileNameForms","FileNameJoin","FileNames","FileNameSetter","FileNameSplit","FileNameTake","FilePrint","FileSize","FileSystemMap","FileSystemScan","FileTemplate","FileTemplateApply","FileType","FilledCurve","FilledCurveBox","FilledCurveBoxOptions","Filling","FillingStyle","FillingTransform","FilteredEntityClass","FilterRules","FinancialBond","FinancialData","FinancialDerivative","FinancialIndicator","Find","FindAnomalies","FindArgMax","FindArgMin","FindChannels","FindClique","FindClusters","FindCookies","FindCurvePath","FindCycle","FindDevices","FindDistribution","FindDistributionParameters","FindDivisions","FindEdgeCover","FindEdgeCut","FindEdgeIndependentPaths","FindEquationalProof","FindEulerianCycle","FindExternalEvaluators","FindFaces","FindFile","FindFit","FindFormula","FindFundamentalCycles","FindGeneratingFunction","FindGeoLocation","FindGeometricConjectures","FindGeometricTransform","FindGraphCommunities","FindGraphIsomorphism","FindGraphPartition","FindHamiltonianCycle","FindHamiltonianPath","FindHiddenMarkovStates","FindImageText","FindIndependentEdgeSet","FindIndependentVertexSet","FindInstance","FindIntegerNullVector","FindKClan","FindKClique","FindKClub","FindKPlex","FindLibrary","FindLinearRecurrence","FindList","FindMatchingColor","FindMaximum","FindMaximumCut","FindMaximumFlow","FindMaxValue","FindMeshDefects","FindMinimum","FindMinimumCostFlow","FindMinimumCut","FindMinValue","FindMoleculeSubstructure","FindPath","FindPeaks","FindPermutation","FindPostmanTour","FindProcessParameters","FindRepeat","FindRoot","FindSequenceFunction","FindSettings","FindShortestPath","FindShortestTour","FindSpanningTree","FindSystemModelEquilibrium","FindTextualAnswer","FindThreshold","FindTransientRepeat","FindVertexCover","FindVertexCut","FindVertexIndependentPaths","Fine","FinishDynamic","FiniteAbelianGroupCount","FiniteGroupCount","FiniteGroupData","First","FirstCase","FirstPassageTimeDistribution","FirstPosition","FischerGroupFi22","FischerGroupFi23","FischerGroupFi24Prime","FisherHypergeometricDistribution","FisherRatioTest","FisherZDistribution","Fit","FitAll","FitRegularization","FittedModel","FixedOrder","FixedPoint","FixedPointList","FlashSelection","Flat","Flatten","FlattenAt","FlattenLayer","FlatTopWindow","FlipView","Floor","FlowPolynomial","FlushPrintOutputPacket","Fold","FoldList","FoldPair","FoldPairList","FollowRedirects","Font","FontColor","FontFamily","FontForm","FontName","FontOpacity","FontPostScriptName","FontProperties","FontReencoding","FontSize","FontSlant","FontSubstitutions","FontTracking","FontVariations","FontWeight","For","ForAll","ForceVersionInstall","Format","FormatRules","FormatType","FormatTypeAutoConvert","FormatValues","FormBox","FormBoxOptions","FormControl","FormFunction","FormLayoutFunction","FormObject","FormPage","FormTheme","FormulaData","FormulaLookup","FortranForm","Forward","ForwardBackward","Fourier","FourierCoefficient","FourierCosCoefficient","FourierCosSeries","FourierCosTransform","FourierDCT","FourierDCTFilter","FourierDCTMatrix","FourierDST","FourierDSTMatrix","FourierMatrix","FourierParameters","FourierSequenceTransform","FourierSeries","FourierSinCoefficient","FourierSinSeries","FourierSinTransform","FourierTransform","FourierTrigSeries","FractionalBrownianMotionProcess","FractionalGaussianNoiseProcess","FractionalPart","FractionBox","FractionBoxOptions","FractionLine","Frame","FrameBox","FrameBoxOptions","Framed","FrameInset","FrameLabel","Frameless","FrameMargins","FrameRate","FrameStyle","FrameTicks","FrameTicksStyle","FRatioDistribution","FrechetDistribution","FreeQ","FrenetSerretSystem","FrequencySamplingFilterKernel","FresnelC","FresnelF","FresnelG","FresnelS","Friday","FrobeniusNumber","FrobeniusSolve","FromAbsoluteTime","FromCharacterCode","FromCoefficientRules","FromContinuedFraction","FromDate","FromDigits","FromDMS","FromEntity","FromJulianDate","FromLetterNumber","FromPolarCoordinates","FromRomanNumeral","FromSphericalCoordinates","FromUnixTime","Front","FrontEndDynamicExpression","FrontEndEventActions","FrontEndExecute","FrontEndObject","FrontEndResource","FrontEndResourceString","FrontEndStackSize","FrontEndToken","FrontEndTokenExecute","FrontEndValueCache","FrontEndVersion","FrontFaceColor","FrontFaceOpacity","Full","FullAxes","FullDefinition","FullForm","FullGraphics","FullInformationOutputRegulator","FullOptions","FullRegion","FullSimplify","Function","FunctionCompile","FunctionCompileExport","FunctionCompileExportByteArray","FunctionCompileExportLibrary","FunctionCompileExportString","FunctionDomain","FunctionExpand","FunctionInterpolation","FunctionPeriod","FunctionRange","FunctionSpace","FussellVeselyImportance","GaborFilter","GaborMatrix","GaborWavelet","GainMargins","GainPhaseMargins","GalaxyData","GalleryView","Gamma","GammaDistribution","GammaRegularized","GapPenalty","GARCHProcess","GatedRecurrentLayer","Gather","GatherBy","GaugeFaceElementFunction","GaugeFaceStyle","GaugeFrameElementFunction","GaugeFrameSize","GaugeFrameStyle","GaugeLabels","GaugeMarkers","GaugeStyle","GaussianFilter","GaussianIntegers","GaussianMatrix","GaussianOrthogonalMatrixDistribution","GaussianSymplecticMatrixDistribution","GaussianUnitaryMatrixDistribution","GaussianWindow","GCD","GegenbauerC","General","GeneralizedLinearModelFit","GenerateAsymmetricKeyPair","GenerateConditions","GeneratedCell","GeneratedDocumentBinding","GenerateDerivedKey","GenerateDigitalSignature","GenerateDocument","GeneratedParameters","GeneratedQuantityMagnitudes","GenerateFileSignature","GenerateHTTPResponse","GenerateSecuredAuthenticationKey","GenerateSymmetricKey","GeneratingFunction","GeneratorDescription","GeneratorHistoryLength","GeneratorOutputType","Generic","GenericCylindricalDecomposition","GenomeData","GenomeLookup","GeoAntipode","GeoArea","GeoArraySize","GeoBackground","GeoBoundingBox","GeoBounds","GeoBoundsRegion","GeoBubbleChart","GeoCenter","GeoCircle","GeoContourPlot","GeoDensityPlot","GeodesicClosing","GeodesicDilation","GeodesicErosion","GeodesicOpening","GeoDestination","GeodesyData","GeoDirection","GeoDisk","GeoDisplacement","GeoDistance","GeoDistanceList","GeoElevationData","GeoEntities","GeoGraphics","GeogravityModelData","GeoGridDirectionDifference","GeoGridLines","GeoGridLinesStyle","GeoGridPosition","GeoGridRange","GeoGridRangePadding","GeoGridUnitArea","GeoGridUnitDistance","GeoGridVector","GeoGroup","GeoHemisphere","GeoHemisphereBoundary","GeoHistogram","GeoIdentify","GeoImage","GeoLabels","GeoLength","GeoListPlot","GeoLocation","GeologicalPeriodData","GeomagneticModelData","GeoMarker","GeometricAssertion","GeometricBrownianMotionProcess","GeometricDistribution","GeometricMean","GeometricMeanFilter","GeometricOptimization","GeometricScene","GeometricTransformation","GeometricTransformation3DBox","GeometricTransformation3DBoxOptions","GeometricTransformationBox","GeometricTransformationBoxOptions","GeoModel","GeoNearest","GeoPath","GeoPosition","GeoPositionENU","GeoPositionXYZ","GeoProjection","GeoProjectionData","GeoRange","GeoRangePadding","GeoRegionValuePlot","GeoResolution","GeoScaleBar","GeoServer","GeoSmoothHistogram","GeoStreamPlot","GeoStyling","GeoStylingImageFunction","GeoVariant","GeoVector","GeoVectorENU","GeoVectorPlot","GeoVectorXYZ","GeoVisibleRegion","GeoVisibleRegionBoundary","GeoWithinQ","GeoZoomLevel","GestureHandler","GestureHandlerTag","Get","GetBoundingBoxSizePacket","GetContext","GetEnvironment","GetFileName","GetFrontEndOptionsDataPacket","GetLinebreakInformationPacket","GetMenusPacket","GetPageBreakInformationPacket","Glaisher","GlobalClusteringCoefficient","GlobalPreferences","GlobalSession","Glow","GoldenAngle","GoldenRatio","GompertzMakehamDistribution","GoochShading","GoodmanKruskalGamma","GoodmanKruskalGammaTest","Goto","Grad","Gradient","GradientFilter","GradientOrientationFilter","GrammarApply","GrammarRules","GrammarToken","Graph","Graph3D","GraphAssortativity","GraphAutomorphismGroup","GraphCenter","GraphComplement","GraphData","GraphDensity","GraphDiameter","GraphDifference","GraphDisjointUnion","GraphDistance","GraphDistanceMatrix","GraphElementData","GraphEmbedding","GraphHighlight","GraphHighlightStyle","GraphHub","Graphics","Graphics3D","Graphics3DBox","Graphics3DBoxOptions","GraphicsArray","GraphicsBaseline","GraphicsBox","GraphicsBoxOptions","GraphicsColor","GraphicsColumn","GraphicsComplex","GraphicsComplex3DBox","GraphicsComplex3DBoxOptions","GraphicsComplexBox","GraphicsComplexBoxOptions","GraphicsContents","GraphicsData","GraphicsGrid","GraphicsGridBox","GraphicsGroup","GraphicsGroup3DBox","GraphicsGroup3DBoxOptions","GraphicsGroupBox","GraphicsGroupBoxOptions","GraphicsGrouping","GraphicsHighlightColor","GraphicsRow","GraphicsSpacing","GraphicsStyle","GraphIntersection","GraphLayout","GraphLinkEfficiency","GraphPeriphery","GraphPlot","GraphPlot3D","GraphPower","GraphPropertyDistribution","GraphQ","GraphRadius","GraphReciprocity","GraphRoot","GraphStyle","GraphUnion","Gray","GrayLevel","Greater","GreaterEqual","GreaterEqualLess","GreaterEqualThan","GreaterFullEqual","GreaterGreater","GreaterLess","GreaterSlantEqual","GreaterThan","GreaterTilde","Green","GreenFunction","Grid","GridBaseline","GridBox","GridBoxAlignment","GridBoxBackground","GridBoxDividers","GridBoxFrame","GridBoxItemSize","GridBoxItemStyle","GridBoxOptions","GridBoxSpacings","GridCreationSettings","GridDefaultElement","GridElementStyleOptions","GridFrame","GridFrameMargins","GridGraph","GridLines","GridLinesStyle","GroebnerBasis","GroupActionBase","GroupBy","GroupCentralizer","GroupElementFromWord","GroupElementPosition","GroupElementQ","GroupElements","GroupElementToWord","GroupGenerators","Groupings","GroupMultiplicationTable","GroupOrbits","GroupOrder","GroupPageBreakWithin","GroupSetwiseStabilizer","GroupStabilizer","GroupStabilizerChain","GroupTogetherGrouping","GroupTogetherNestedGrouping","GrowCutComponents","Gudermannian","GuidedFilter","GumbelDistribution","HaarWavelet","HadamardMatrix","HalfLine","HalfNormalDistribution","HalfPlane","HalfSpace","HalftoneShading","HamiltonianGraphQ","HammingDistance","HammingWindow","HandlerFunctions","HandlerFunctionsKeys","HankelH1","HankelH2","HankelMatrix","HankelTransform","HannPoissonWindow","HannWindow","HaradaNortonGroupHN","HararyGraph","HarmonicMean","HarmonicMeanFilter","HarmonicNumber","Hash","HatchFilling","HatchShading","Haversine","HazardFunction","Head","HeadCompose","HeaderAlignment","HeaderBackground","HeaderDisplayFunction","HeaderLines","HeaderSize","HeaderStyle","Heads","HeavisideLambda","HeavisidePi","HeavisideTheta","HeldGroupHe","HeldPart","HelpBrowserLookup","HelpBrowserNotebook","HelpBrowserSettings","Here","HermiteDecomposition","HermiteH","HermitianMatrixQ","HessenbergDecomposition","Hessian","HeunB","HeunBPrime","HeunC","HeunCPrime","HeunD","HeunDPrime","HeunG","HeunGPrime","HeunT","HeunTPrime","HexadecimalCharacter","Hexahedron","HexahedronBox","HexahedronBoxOptions","HiddenItems","HiddenMarkovProcess","HiddenSurface","Highlighted","HighlightGraph","HighlightImage","HighlightMesh","HighpassFilter","HigmanSimsGroupHS","HilbertCurve","HilbertFilter","HilbertMatrix","Histogram","Histogram3D","HistogramDistribution","HistogramList","HistogramTransform","HistogramTransformInterpolation","HistoricalPeriodData","HitMissTransform","HITSCentrality","HjorthDistribution","HodgeDual","HoeffdingD","HoeffdingDTest","Hold","HoldAll","HoldAllComplete","HoldComplete","HoldFirst","HoldForm","HoldPattern","HoldRest","HolidayCalendar","HomeDirectory","HomePage","Horizontal","HorizontalForm","HorizontalGauge","HorizontalScrollPosition","HornerForm","HostLookup","HotellingTSquareDistribution","HoytDistribution","HTMLSave","HTTPErrorResponse","HTTPRedirect","HTTPRequest","HTTPRequestData","HTTPResponse","Hue","HumanGrowthData","HumpDownHump","HumpEqual","HurwitzLerchPhi","HurwitzZeta","HyperbolicDistribution","HypercubeGraph","HyperexponentialDistribution","Hyperfactorial","Hypergeometric0F1","Hypergeometric0F1Regularized","Hypergeometric1F1","Hypergeometric1F1Regularized","Hypergeometric2F1","Hypergeometric2F1Regularized","HypergeometricDistribution","HypergeometricPFQ","HypergeometricPFQRegularized","HypergeometricU","Hyperlink","HyperlinkAction","HyperlinkCreationSettings","Hyperplane","Hyphenation","HyphenationOptions","HypoexponentialDistribution","HypothesisTestData","I","IconData","Iconize","IconizedObject","IconRules","Icosahedron","Identity","IdentityMatrix","If","IgnoreCase","IgnoreDiacritics","IgnorePunctuation","IgnoreSpellCheck","IgnoringInactive","Im","Image","Image3D","Image3DProjection","Image3DSlices","ImageAccumulate","ImageAdd","ImageAdjust","ImageAlign","ImageApply","ImageApplyIndexed","ImageAspectRatio","ImageAssemble","ImageAugmentationLayer","ImageBoundingBoxes","ImageCache","ImageCacheValid","ImageCapture","ImageCaptureFunction","ImageCases","ImageChannels","ImageClip","ImageCollage","ImageColorSpace","ImageCompose","ImageContainsQ","ImageContents","ImageConvolve","ImageCooccurrence","ImageCorners","ImageCorrelate","ImageCorrespondingPoints","ImageCrop","ImageData","ImageDeconvolve","ImageDemosaic","ImageDifference","ImageDimensions","ImageDisplacements","ImageDistance","ImageEffect","ImageExposureCombine","ImageFeatureTrack","ImageFileApply","ImageFileFilter","ImageFileScan","ImageFilter","ImageFocusCombine","ImageForestingComponents","ImageFormattingWidth","ImageForwardTransformation","ImageGraphics","ImageHistogram","ImageIdentify","ImageInstanceQ","ImageKeypoints","ImageLabels","ImageLegends","ImageLevels","ImageLines","ImageMargins","ImageMarker","ImageMarkers","ImageMeasurements","ImageMesh","ImageMultiply","ImageOffset","ImagePad","ImagePadding","ImagePartition","ImagePeriodogram","ImagePerspectiveTransformation","ImagePosition","ImagePreviewFunction","ImagePyramid","ImagePyramidApply","ImageQ","ImageRangeCache","ImageRecolor","ImageReflect","ImageRegion","ImageResize","ImageResolution","ImageRestyle","ImageRotate","ImageRotated","ImageSaliencyFilter","ImageScaled","ImageScan","ImageSize","ImageSizeAction","ImageSizeCache","ImageSizeMultipliers","ImageSizeRaw","ImageSubtract","ImageTake","ImageTransformation","ImageTrim","ImageType","ImageValue","ImageValuePositions","ImagingDevice","ImplicitRegion","Implies","Import","ImportAutoReplacements","ImportByteArray","ImportOptions","ImportString","ImprovementImportance","In","Inactivate","Inactive","IncidenceGraph","IncidenceList","IncidenceMatrix","IncludeAromaticBonds","IncludeConstantBasis","IncludeDefinitions","IncludeDirectories","IncludeFileExtension","IncludeGeneratorTasks","IncludeHydrogens","IncludeInflections","IncludeMetaInformation","IncludePods","IncludeQuantities","IncludeRelatedTables","IncludeSingularTerm","IncludeWindowTimes","Increment","IndefiniteMatrixQ","Indent","IndentingNewlineSpacings","IndentMaxFraction","IndependenceTest","IndependentEdgeSetQ","IndependentPhysicalQuantity","IndependentUnit","IndependentUnitDimension","IndependentVertexSetQ","Indeterminate","IndeterminateThreshold","IndexCreationOptions","Indexed","IndexEdgeTaggedGraph","IndexGraph","IndexTag","Inequality","InexactNumberQ","InexactNumbers","InfiniteFuture","InfiniteLine","InfinitePast","InfinitePlane","Infinity","Infix","InflationAdjust","InflationMethod","Information","InformationData","InformationDataGrid","Inherited","InheritScope","InhomogeneousPoissonProcess","InitialEvaluationHistory","Initialization","InitializationCell","InitializationCellEvaluation","InitializationCellWarning","InitializationObjects","InitializationValue","Initialize","InitialSeeding","InlineCounterAssignments","InlineCounterIncrements","InlineRules","Inner","InnerPolygon","InnerPolyhedron","Inpaint","Input","InputAliases","InputAssumptions","InputAutoReplacements","InputField","InputFieldBox","InputFieldBoxOptions","InputForm","InputGrouping","InputNamePacket","InputNotebook","InputPacket","InputSettings","InputStream","InputString","InputStringPacket","InputToBoxFormPacket","Insert","InsertionFunction","InsertionPointObject","InsertLinebreaks","InsertResults","Inset","Inset3DBox","Inset3DBoxOptions","InsetBox","InsetBoxOptions","Insphere","Install","InstallService","InstanceNormalizationLayer","InString","Integer","IntegerDigits","IntegerExponent","IntegerLength","IntegerName","IntegerPart","IntegerPartitions","IntegerQ","IntegerReverse","Integers","IntegerString","Integral","Integrate","Interactive","InteractiveTradingChart","Interlaced","Interleaving","InternallyBalancedDecomposition","InterpolatingFunction","InterpolatingPolynomial","Interpolation","InterpolationOrder","InterpolationPoints","InterpolationPrecision","Interpretation","InterpretationBox","InterpretationBoxOptions","InterpretationFunction","Interpreter","InterpretTemplate","InterquartileRange","Interrupt","InterruptSettings","IntersectedEntityClass","IntersectingQ","Intersection","Interval","IntervalIntersection","IntervalMarkers","IntervalMarkersStyle","IntervalMemberQ","IntervalSlider","IntervalUnion","Into","Inverse","InverseBetaRegularized","InverseCDF","InverseChiSquareDistribution","InverseContinuousWaveletTransform","InverseDistanceTransform","InverseEllipticNomeQ","InverseErf","InverseErfc","InverseFourier","InverseFourierCosTransform","InverseFourierSequenceTransform","InverseFourierSinTransform","InverseFourierTransform","InverseFunction","InverseFunctions","InverseGammaDistribution","InverseGammaRegularized","InverseGaussianDistribution","InverseGudermannian","InverseHankelTransform","InverseHaversine","InverseImagePyramid","InverseJacobiCD","InverseJacobiCN","InverseJacobiCS","InverseJacobiDC","InverseJacobiDN","InverseJacobiDS","InverseJacobiNC","InverseJacobiND","InverseJacobiNS","InverseJacobiSC","InverseJacobiSD","InverseJacobiSN","InverseLaplaceTransform","InverseMellinTransform","InversePermutation","InverseRadon","InverseRadonTransform","InverseSeries","InverseShortTimeFourier","InverseSpectrogram","InverseSurvivalFunction","InverseTransformedRegion","InverseWaveletTransform","InverseWeierstrassP","InverseWishartMatrixDistribution","InverseZTransform","Invisible","InvisibleApplication","InvisibleTimes","IPAddress","IrreduciblePolynomialQ","IslandData","IsolatingInterval","IsomorphicGraphQ","IsotopeData","Italic","Item","ItemAspectRatio","ItemBox","ItemBoxOptions","ItemDisplayFunction","ItemSize","ItemStyle","ItoProcess","JaccardDissimilarity","JacobiAmplitude","Jacobian","JacobiCD","JacobiCN","JacobiCS","JacobiDC","JacobiDN","JacobiDS","JacobiNC","JacobiND","JacobiNS","JacobiP","JacobiSC","JacobiSD","JacobiSN","JacobiSymbol","JacobiZeta","JankoGroupJ1","JankoGroupJ2","JankoGroupJ3","JankoGroupJ4","JarqueBeraALMTest","JohnsonDistribution","Join","JoinAcross","Joined","JoinedCurve","JoinedCurveBox","JoinedCurveBoxOptions","JoinForm","JordanDecomposition","JordanModelDecomposition","JulianDate","JuliaSetBoettcher","JuliaSetIterationCount","JuliaSetPlot","JuliaSetPoints","K","KagiChart","KaiserBesselWindow","KaiserWindow","KalmanEstimator","KalmanFilter","KarhunenLoeveDecomposition","KaryTree","KatzCentrality","KCoreComponents","KDistribution","KEdgeConnectedComponents","KEdgeConnectedGraphQ","KeepExistingVersion","KelvinBei","KelvinBer","KelvinKei","KelvinKer","KendallTau","KendallTauTest","KernelExecute","KernelFunction","KernelMixtureDistribution","KernelObject","Kernels","Ket","Key","KeyCollisionFunction","KeyComplement","KeyDrop","KeyDropFrom","KeyExistsQ","KeyFreeQ","KeyIntersection","KeyMap","KeyMemberQ","KeypointStrength","Keys","KeySelect","KeySort","KeySortBy","KeyTake","KeyUnion","KeyValueMap","KeyValuePattern","Khinchin","KillProcess","KirchhoffGraph","KirchhoffMatrix","KleinInvariantJ","KnapsackSolve","KnightTourGraph","KnotData","KnownUnitQ","KochCurve","KolmogorovSmirnovTest","KroneckerDelta","KroneckerModelDecomposition","KroneckerProduct","KroneckerSymbol","KuiperTest","KumaraswamyDistribution","Kurtosis","KuwaharaFilter","KVertexConnectedComponents","KVertexConnectedGraphQ","LABColor","Label","Labeled","LabeledSlider","LabelingFunction","LabelingSize","LabelStyle","LabelVisibility","LaguerreL","LakeData","LambdaComponents","LambertW","LaminaData","LanczosWindow","LandauDistribution","Language","LanguageCategory","LanguageData","LanguageIdentify","LanguageOptions","LaplaceDistribution","LaplaceTransform","Laplacian","LaplacianFilter","LaplacianGaussianFilter","Large","Larger","Last","Latitude","LatitudeLongitude","LatticeData","LatticeReduce","Launch","LaunchKernels","LayeredGraphPlot","LayerSizeFunction","LayoutInformation","LCHColor","LCM","LeaderSize","LeafCount","LeapYearQ","LearnDistribution","LearnedDistribution","LearningRate","LearningRateMultipliers","LeastSquares","LeastSquaresFilterKernel","Left","LeftArrow","LeftArrowBar","LeftArrowRightArrow","LeftDownTeeVector","LeftDownVector","LeftDownVectorBar","LeftRightArrow","LeftRightVector","LeftTee","LeftTeeArrow","LeftTeeVector","LeftTriangle","LeftTriangleBar","LeftTriangleEqual","LeftUpDownVector","LeftUpTeeVector","LeftUpVector","LeftUpVectorBar","LeftVector","LeftVectorBar","LegendAppearance","Legended","LegendFunction","LegendLabel","LegendLayout","LegendMargins","LegendMarkers","LegendMarkerSize","LegendreP","LegendreQ","LegendreType","Length","LengthWhile","LerchPhi","Less","LessEqual","LessEqualGreater","LessEqualThan","LessFullEqual","LessGreater","LessLess","LessSlantEqual","LessThan","LessTilde","LetterCharacter","LetterCounts","LetterNumber","LetterQ","Level","LeveneTest","LeviCivitaTensor","LevyDistribution","Lexicographic","LibraryDataType","LibraryFunction","LibraryFunctionError","LibraryFunctionInformation","LibraryFunctionLoad","LibraryFunctionUnload","LibraryLoad","LibraryUnload","LicenseID","LiftingFilterData","LiftingWaveletTransform","LightBlue","LightBrown","LightCyan","Lighter","LightGray","LightGreen","Lighting","LightingAngle","LightMagenta","LightOrange","LightPink","LightPurple","LightRed","LightSources","LightYellow","Likelihood","Limit","LimitsPositioning","LimitsPositioningTokens","LindleyDistribution","Line","Line3DBox","Line3DBoxOptions","LinearFilter","LinearFractionalOptimization","LinearFractionalTransform","LinearGradientImage","LinearizingTransformationData","LinearLayer","LinearModelFit","LinearOffsetFunction","LinearOptimization","LinearProgramming","LinearRecurrence","LinearSolve","LinearSolveFunction","LineBox","LineBoxOptions","LineBreak","LinebreakAdjustments","LineBreakChart","LinebreakSemicolonWeighting","LineBreakWithin","LineColor","LineGraph","LineIndent","LineIndentMaxFraction","LineIntegralConvolutionPlot","LineIntegralConvolutionScale","LineLegend","LineOpacity","LineSpacing","LineWrapParts","LinkActivate","LinkClose","LinkConnect","LinkConnectedQ","LinkCreate","LinkError","LinkFlush","LinkFunction","LinkHost","LinkInterrupt","LinkLaunch","LinkMode","LinkObject","LinkOpen","LinkOptions","LinkPatterns","LinkProtocol","LinkRankCentrality","LinkRead","LinkReadHeld","LinkReadyQ","Links","LinkService","LinkWrite","LinkWriteHeld","LiouvilleLambda","List","Listable","ListAnimate","ListContourPlot","ListContourPlot3D","ListConvolve","ListCorrelate","ListCurvePathPlot","ListDeconvolve","ListDensityPlot","ListDensityPlot3D","Listen","ListFormat","ListFourierSequenceTransform","ListInterpolation","ListLineIntegralConvolutionPlot","ListLinePlot","ListLogLinearPlot","ListLogLogPlot","ListLogPlot","ListPicker","ListPickerBox","ListPickerBoxBackground","ListPickerBoxOptions","ListPlay","ListPlot","ListPlot3D","ListPointPlot3D","ListPolarPlot","ListQ","ListSliceContourPlot3D","ListSliceDensityPlot3D","ListSliceVectorPlot3D","ListStepPlot","ListStreamDensityPlot","ListStreamPlot","ListSurfacePlot3D","ListVectorDensityPlot","ListVectorPlot","ListVectorPlot3D","ListZTransform","Literal","LiteralSearch","LocalAdaptiveBinarize","LocalCache","LocalClusteringCoefficient","LocalizeDefinitions","LocalizeVariables","LocalObject","LocalObjects","LocalResponseNormalizationLayer","LocalSubmit","LocalSymbol","LocalTime","LocalTimeZone","LocationEquivalenceTest","LocationTest","Locator","LocatorAutoCreate","LocatorBox","LocatorBoxOptions","LocatorCentering","LocatorPane","LocatorPaneBox","LocatorPaneBoxOptions","LocatorRegion","Locked","Log","Log10","Log2","LogBarnesG","LogGamma","LogGammaDistribution","LogicalExpand","LogIntegral","LogisticDistribution","LogisticSigmoid","LogitModelFit","LogLikelihood","LogLinearPlot","LogLogisticDistribution","LogLogPlot","LogMultinormalDistribution","LogNormalDistribution","LogPlot","LogRankTest","LogSeriesDistribution","LongEqual","Longest","LongestCommonSequence","LongestCommonSequencePositions","LongestCommonSubsequence","LongestCommonSubsequencePositions","LongestMatch","LongestOrderedSequence","LongForm","Longitude","LongLeftArrow","LongLeftRightArrow","LongRightArrow","LongShortTermMemoryLayer","Lookup","Loopback","LoopFreeGraphQ","Looping","LossFunction","LowerCaseQ","LowerLeftArrow","LowerRightArrow","LowerTriangularize","LowerTriangularMatrixQ","LowpassFilter","LQEstimatorGains","LQGRegulator","LQOutputRegulatorGains","LQRegulatorGains","LUBackSubstitution","LucasL","LuccioSamiComponents","LUDecomposition","LunarEclipse","LUVColor","LyapunovSolve","LyonsGroupLy","MachineID","MachineName","MachineNumberQ","MachinePrecision","MacintoshSystemPageSetup","Magenta","Magnification","Magnify","MailAddressValidation","MailExecute","MailFolder","MailItem","MailReceiverFunction","MailResponseFunction","MailSearch","MailServerConnect","MailServerConnection","MailSettings","MainSolve","MaintainDynamicCaches","Majority","MakeBoxes","MakeExpression","MakeRules","ManagedLibraryExpressionID","ManagedLibraryExpressionQ","MandelbrotSetBoettcher","MandelbrotSetDistance","MandelbrotSetIterationCount","MandelbrotSetMemberQ","MandelbrotSetPlot","MangoldtLambda","ManhattanDistance","Manipulate","Manipulator","MannedSpaceMissionData","MannWhitneyTest","MantissaExponent","Manual","Map","MapAll","MapAt","MapIndexed","MAProcess","MapThread","MarchenkoPasturDistribution","MarcumQ","MardiaCombinedTest","MardiaKurtosisTest","MardiaSkewnessTest","MarginalDistribution","MarkovProcessProperties","Masking","MatchingDissimilarity","MatchLocalNameQ","MatchLocalNames","MatchQ","Material","MathematicalFunctionData","MathematicaNotation","MathieuC","MathieuCharacteristicA","MathieuCharacteristicB","MathieuCharacteristicExponent","MathieuCPrime","MathieuGroupM11","MathieuGroupM12","MathieuGroupM22","MathieuGroupM23","MathieuGroupM24","MathieuS","MathieuSPrime","MathMLForm","MathMLText","Matrices","MatrixExp","MatrixForm","MatrixFunction","MatrixLog","MatrixNormalDistribution","MatrixPlot","MatrixPower","MatrixPropertyDistribution","MatrixQ","MatrixRank","MatrixTDistribution","Max","MaxBend","MaxCellMeasure","MaxColorDistance","MaxDate","MaxDetect","MaxDuration","MaxExtraBandwidths","MaxExtraConditions","MaxFeatureDisplacement","MaxFeatures","MaxFilter","MaximalBy","Maximize","MaxItems","MaxIterations","MaxLimit","MaxMemoryUsed","MaxMixtureKernels","MaxOverlapFraction","MaxPlotPoints","MaxPoints","MaxRecursion","MaxStableDistribution","MaxStepFraction","MaxSteps","MaxStepSize","MaxTrainingRounds","MaxValue","MaxwellDistribution","MaxWordGap","McLaughlinGroupMcL","Mean","MeanAbsoluteLossLayer","MeanAround","MeanClusteringCoefficient","MeanDegreeConnectivity","MeanDeviation","MeanFilter","MeanGraphDistance","MeanNeighborDegree","MeanShift","MeanShiftFilter","MeanSquaredLossLayer","Median","MedianDeviation","MedianFilter","MedicalTestData","Medium","MeijerG","MeijerGReduce","MeixnerDistribution","MellinConvolve","MellinTransform","MemberQ","MemoryAvailable","MemoryConstrained","MemoryConstraint","MemoryInUse","MengerMesh","Menu","MenuAppearance","MenuCommandKey","MenuEvaluator","MenuItem","MenuList","MenuPacket","MenuSortingValue","MenuStyle","MenuView","Merge","MergeDifferences","MergingFunction","MersennePrimeExponent","MersennePrimeExponentQ","Mesh","MeshCellCentroid","MeshCellCount","MeshCellHighlight","MeshCellIndex","MeshCellLabel","MeshCellMarker","MeshCellMeasure","MeshCellQuality","MeshCells","MeshCellShapeFunction","MeshCellStyle","MeshConnectivityGraph","MeshCoordinates","MeshFunctions","MeshPrimitives","MeshQualityGoal","MeshRange","MeshRefinementFunction","MeshRegion","MeshRegionQ","MeshShading","MeshStyle","Message","MessageDialog","MessageList","MessageName","MessageObject","MessageOptions","MessagePacket","Messages","MessagesNotebook","MetaCharacters","MetaInformation","MeteorShowerData","Method","MethodOptions","MexicanHatWavelet","MeyerWavelet","Midpoint","Min","MinColorDistance","MinDate","MinDetect","MineralData","MinFilter","MinimalBy","MinimalPolynomial","MinimalStateSpaceModel","Minimize","MinimumTimeIncrement","MinIntervalSize","MinkowskiQuestionMark","MinLimit","MinMax","MinorPlanetData","Minors","MinRecursion","MinSize","MinStableDistribution","Minus","MinusPlus","MinValue","Missing","MissingBehavior","MissingDataMethod","MissingDataRules","MissingQ","MissingString","MissingStyle","MissingValuePattern","MittagLefflerE","MixedFractionParts","MixedGraphQ","MixedMagnitude","MixedRadix","MixedRadixQuantity","MixedUnit","MixtureDistribution","Mod","Modal","Mode","Modular","ModularInverse","ModularLambda","Module","Modulus","MoebiusMu","Molecule","MoleculeContainsQ","MoleculeEquivalentQ","MoleculeGraph","MoleculeModify","MoleculePattern","MoleculePlot","MoleculePlot3D","MoleculeProperty","MoleculeQ","MoleculeRecognize","MoleculeValue","Moment","Momentary","MomentConvert","MomentEvaluate","MomentGeneratingFunction","MomentOfInertia","Monday","Monitor","MonomialList","MonomialOrder","MonsterGroupM","MoonPhase","MoonPosition","MorletWavelet","MorphologicalBinarize","MorphologicalBranchPoints","MorphologicalComponents","MorphologicalEulerNumber","MorphologicalGraph","MorphologicalPerimeter","MorphologicalTransform","MortalityData","Most","MountainData","MouseAnnotation","MouseAppearance","MouseAppearanceTag","MouseButtons","Mouseover","MousePointerNote","MousePosition","MovieData","MovingAverage","MovingMap","MovingMedian","MoyalDistribution","Multicolumn","MultiedgeStyle","MultigraphQ","MultilaunchWarning","MultiLetterItalics","MultiLetterStyle","MultilineFunction","Multinomial","MultinomialDistribution","MultinormalDistribution","MultiplicativeOrder","Multiplicity","MultiplySides","Multiselection","MultivariateHypergeometricDistribution","MultivariatePoissonDistribution","MultivariateTDistribution","N","NakagamiDistribution","NameQ","Names","NamespaceBox","NamespaceBoxOptions","Nand","NArgMax","NArgMin","NBernoulliB","NBodySimulation","NBodySimulationData","NCache","NDEigensystem","NDEigenvalues","NDSolve","NDSolveValue","Nearest","NearestFunction","NearestMeshCells","NearestNeighborGraph","NearestTo","NebulaData","NeedCurrentFrontEndPackagePacket","NeedCurrentFrontEndSymbolsPacket","NeedlemanWunschSimilarity","Needs","Negative","NegativeBinomialDistribution","NegativeDefiniteMatrixQ","NegativeIntegers","NegativeMultinomialDistribution","NegativeRationals","NegativeReals","NegativeSemidefiniteMatrixQ","NeighborhoodData","NeighborhoodGraph","Nest","NestedGreaterGreater","NestedLessLess","NestedScriptRules","NestGraph","NestList","NestWhile","NestWhileList","NetAppend","NetBidirectionalOperator","NetChain","NetDecoder","NetDelete","NetDrop","NetEncoder","NetEvaluationMode","NetExtract","NetFlatten","NetFoldOperator","NetGANOperator","NetGraph","NetInformation","NetInitialize","NetInsert","NetInsertSharedArrays","NetJoin","NetMapOperator","NetMapThreadOperator","NetMeasurements","NetModel","NetNestOperator","NetPairEmbeddingOperator","NetPort","NetPortGradient","NetPrepend","NetRename","NetReplace","NetReplacePart","NetSharedArray","NetStateObject","NetTake","NetTrain","NetTrainResultsObject","NetworkPacketCapture","NetworkPacketRecording","NetworkPacketRecordingDuring","NetworkPacketTrace","NeumannValue","NevilleThetaC","NevilleThetaD","NevilleThetaN","NevilleThetaS","NewPrimitiveStyle","NExpectation","Next","NextCell","NextDate","NextPrime","NextScheduledTaskTime","NHoldAll","NHoldFirst","NHoldRest","NicholsGridLines","NicholsPlot","NightHemisphere","NIntegrate","NMaximize","NMaxValue","NMinimize","NMinValue","NominalVariables","NonAssociative","NoncentralBetaDistribution","NoncentralChiSquareDistribution","NoncentralFRatioDistribution","NoncentralStudentTDistribution","NonCommutativeMultiply","NonConstants","NondimensionalizationTransform","None","NoneTrue","NonlinearModelFit","NonlinearStateSpaceModel","NonlocalMeansFilter","NonNegative","NonNegativeIntegers","NonNegativeRationals","NonNegativeReals","NonPositive","NonPositiveIntegers","NonPositiveRationals","NonPositiveReals","Nor","NorlundB","Norm","Normal","NormalDistribution","NormalGrouping","NormalizationLayer","Normalize","Normalized","NormalizedSquaredEuclideanDistance","NormalMatrixQ","NormalsFunction","NormFunction","Not","NotCongruent","NotCupCap","NotDoubleVerticalBar","Notebook","NotebookApply","NotebookAutoSave","NotebookClose","NotebookConvertSettings","NotebookCreate","NotebookCreateReturnObject","NotebookDefault","NotebookDelete","NotebookDirectory","NotebookDynamicExpression","NotebookEvaluate","NotebookEventActions","NotebookFileName","NotebookFind","NotebookFindReturnObject","NotebookGet","NotebookGetLayoutInformationPacket","NotebookGetMisspellingsPacket","NotebookImport","NotebookInformation","NotebookInterfaceObject","NotebookLocate","NotebookObject","NotebookOpen","NotebookOpenReturnObject","NotebookPath","NotebookPrint","NotebookPut","NotebookPutReturnObject","NotebookRead","NotebookResetGeneratedCells","Notebooks","NotebookSave","NotebookSaveAs","NotebookSelection","NotebookSetupLayoutInformationPacket","NotebooksMenu","NotebookTemplate","NotebookWrite","NotElement","NotEqualTilde","NotExists","NotGreater","NotGreaterEqual","NotGreaterFullEqual","NotGreaterGreater","NotGreaterLess","NotGreaterSlantEqual","NotGreaterTilde","Nothing","NotHumpDownHump","NotHumpEqual","NotificationFunction","NotLeftTriangle","NotLeftTriangleBar","NotLeftTriangleEqual","NotLess","NotLessEqual","NotLessFullEqual","NotLessGreater","NotLessLess","NotLessSlantEqual","NotLessTilde","NotNestedGreaterGreater","NotNestedLessLess","NotPrecedes","NotPrecedesEqual","NotPrecedesSlantEqual","NotPrecedesTilde","NotReverseElement","NotRightTriangle","NotRightTriangleBar","NotRightTriangleEqual","NotSquareSubset","NotSquareSubsetEqual","NotSquareSuperset","NotSquareSupersetEqual","NotSubset","NotSubsetEqual","NotSucceeds","NotSucceedsEqual","NotSucceedsSlantEqual","NotSucceedsTilde","NotSuperset","NotSupersetEqual","NotTilde","NotTildeEqual","NotTildeFullEqual","NotTildeTilde","NotVerticalBar","Now","NoWhitespace","NProbability","NProduct","NProductFactors","NRoots","NSolve","NSum","NSumTerms","NuclearExplosionData","NuclearReactorData","Null","NullRecords","NullSpace","NullWords","Number","NumberCompose","NumberDecompose","NumberExpand","NumberFieldClassNumber","NumberFieldDiscriminant","NumberFieldFundamentalUnits","NumberFieldIntegralBasis","NumberFieldNormRepresentatives","NumberFieldRegulator","NumberFieldRootsOfUnity","NumberFieldSignature","NumberForm","NumberFormat","NumberLinePlot","NumberMarks","NumberMultiplier","NumberPadding","NumberPoint","NumberQ","NumberSeparator","NumberSigns","NumberString","Numerator","NumeratorDenominator","NumericalOrder","NumericalSort","NumericArray","NumericArrayQ","NumericArrayType","NumericFunction","NumericQ","NuttallWindow","NValues","NyquistGridLines","NyquistPlot","O","ObservabilityGramian","ObservabilityMatrix","ObservableDecomposition","ObservableModelQ","OceanData","Octahedron","OddQ","Off","Offset","OLEData","On","ONanGroupON","Once","OneIdentity","Opacity","OpacityFunction","OpacityFunctionScaling","Open","OpenAppend","Opener","OpenerBox","OpenerBoxOptions","OpenerView","OpenFunctionInspectorPacket","Opening","OpenRead","OpenSpecialOptions","OpenTemporary","OpenWrite","Operate","OperatingSystem","OperatorApplied","OptimumFlowData","Optional","OptionalElement","OptionInspectorSettings","OptionQ","Options","OptionsPacket","OptionsPattern","OptionValue","OptionValueBox","OptionValueBoxOptions","Or","Orange","Order","OrderDistribution","OrderedQ","Ordering","OrderingBy","OrderingLayer","Orderless","OrderlessPatternSequence","OrnsteinUhlenbeckProcess","Orthogonalize","OrthogonalMatrixQ","Out","Outer","OuterPolygon","OuterPolyhedron","OutputAutoOverwrite","OutputControllabilityMatrix","OutputControllableModelQ","OutputForm","OutputFormData","OutputGrouping","OutputMathEditExpression","OutputNamePacket","OutputResponse","OutputSizeLimit","OutputStream","Over","OverBar","OverDot","Overflow","OverHat","Overlaps","Overlay","OverlayBox","OverlayBoxOptions","Overscript","OverscriptBox","OverscriptBoxOptions","OverTilde","OverVector","OverwriteTarget","OwenT","OwnValues","Package","PackingMethod","PackPaclet","PacletDataRebuild","PacletDirectoryAdd","PacletDirectoryLoad","PacletDirectoryRemove","PacletDirectoryUnload","PacletDisable","PacletEnable","PacletFind","PacletFindRemote","PacletInformation","PacletInstall","PacletInstallSubmit","PacletNewerQ","PacletObject","PacletObjectQ","PacletSite","PacletSiteObject","PacletSiteRegister","PacletSites","PacletSiteUnregister","PacletSiteUpdate","PacletUninstall","PacletUpdate","PaddedForm","Padding","PaddingLayer","PaddingSize","PadeApproximant","PadLeft","PadRight","PageBreakAbove","PageBreakBelow","PageBreakWithin","PageFooterLines","PageFooters","PageHeaderLines","PageHeaders","PageHeight","PageRankCentrality","PageTheme","PageWidth","Pagination","PairedBarChart","PairedHistogram","PairedSmoothHistogram","PairedTTest","PairedZTest","PaletteNotebook","PalettePath","PalindromeQ","Pane","PaneBox","PaneBoxOptions","Panel","PanelBox","PanelBoxOptions","Paneled","PaneSelector","PaneSelectorBox","PaneSelectorBoxOptions","PaperWidth","ParabolicCylinderD","ParagraphIndent","ParagraphSpacing","ParallelArray","ParallelCombine","ParallelDo","Parallelepiped","ParallelEvaluate","Parallelization","Parallelize","ParallelMap","ParallelNeeds","Parallelogram","ParallelProduct","ParallelSubmit","ParallelSum","ParallelTable","ParallelTry","Parameter","ParameterEstimator","ParameterMixtureDistribution","ParameterVariables","ParametricFunction","ParametricNDSolve","ParametricNDSolveValue","ParametricPlot","ParametricPlot3D","ParametricRampLayer","ParametricRegion","ParentBox","ParentCell","ParentConnect","ParentDirectory","ParentForm","Parenthesize","ParentList","ParentNotebook","ParetoDistribution","ParetoPickandsDistribution","ParkData","Part","PartBehavior","PartialCorrelationFunction","PartialD","ParticleAcceleratorData","ParticleData","Partition","PartitionGranularity","PartitionsP","PartitionsQ","PartLayer","PartOfSpeech","PartProtection","ParzenWindow","PascalDistribution","PassEventsDown","PassEventsUp","Paste","PasteAutoQuoteCharacters","PasteBoxFormInlineCells","PasteButton","Path","PathGraph","PathGraphQ","Pattern","PatternFilling","PatternSequence","PatternTest","PauliMatrix","PaulWavelet","Pause","PausedTime","PDF","PeakDetect","PeanoCurve","PearsonChiSquareTest","PearsonCorrelationTest","PearsonDistribution","PercentForm","PerfectNumber","PerfectNumberQ","PerformanceGoal","Perimeter","PeriodicBoundaryCondition","PeriodicInterpolation","Periodogram","PeriodogramArray","Permanent","Permissions","PermissionsGroup","PermissionsGroupMemberQ","PermissionsGroups","PermissionsKey","PermissionsKeys","PermutationCycles","PermutationCyclesQ","PermutationGroup","PermutationLength","PermutationList","PermutationListQ","PermutationMax","PermutationMin","PermutationOrder","PermutationPower","PermutationProduct","PermutationReplace","Permutations","PermutationSupport","Permute","PeronaMalikFilter","Perpendicular","PerpendicularBisector","PersistenceLocation","PersistenceTime","PersistentObject","PersistentObjects","PersistentValue","PersonData","PERTDistribution","PetersenGraph","PhaseMargins","PhaseRange","PhysicalSystemData","Pi","Pick","PIDData","PIDDerivativeFilter","PIDFeedforward","PIDTune","Piecewise","PiecewiseExpand","PieChart","PieChart3D","PillaiTrace","PillaiTraceTest","PingTime","Pink","PitchRecognize","Pivoting","PixelConstrained","PixelValue","PixelValuePositions","Placed","Placeholder","PlaceholderReplace","Plain","PlanarAngle","PlanarGraph","PlanarGraphQ","PlanckRadiationLaw","PlaneCurveData","PlanetaryMoonData","PlanetData","PlantData","Play","PlayRange","Plot","Plot3D","Plot3Matrix","PlotDivision","PlotJoined","PlotLabel","PlotLabels","PlotLayout","PlotLegends","PlotMarkers","PlotPoints","PlotRange","PlotRangeClipping","PlotRangeClipPlanesStyle","PlotRangePadding","PlotRegion","PlotStyle","PlotTheme","Pluralize","Plus","PlusMinus","Pochhammer","PodStates","PodWidth","Point","Point3DBox","Point3DBoxOptions","PointBox","PointBoxOptions","PointFigureChart","PointLegend","PointSize","PoissonConsulDistribution","PoissonDistribution","PoissonProcess","PoissonWindow","PolarAxes","PolarAxesOrigin","PolarGridLines","PolarPlot","PolarTicks","PoleZeroMarkers","PolyaAeppliDistribution","PolyGamma","Polygon","Polygon3DBox","Polygon3DBoxOptions","PolygonalNumber","PolygonAngle","PolygonBox","PolygonBoxOptions","PolygonCoordinates","PolygonDecomposition","PolygonHoleScale","PolygonIntersections","PolygonScale","Polyhedron","PolyhedronAngle","PolyhedronCoordinates","PolyhedronData","PolyhedronDecomposition","PolyhedronGenus","PolyLog","PolynomialExtendedGCD","PolynomialForm","PolynomialGCD","PolynomialLCM","PolynomialMod","PolynomialQ","PolynomialQuotient","PolynomialQuotientRemainder","PolynomialReduce","PolynomialRemainder","Polynomials","PoolingLayer","PopupMenu","PopupMenuBox","PopupMenuBoxOptions","PopupView","PopupWindow","Position","PositionIndex","Positive","PositiveDefiniteMatrixQ","PositiveIntegers","PositiveRationals","PositiveReals","PositiveSemidefiniteMatrixQ","PossibleZeroQ","Postfix","PostScript","Power","PowerDistribution","PowerExpand","PowerMod","PowerModList","PowerRange","PowerSpectralDensity","PowersRepresentations","PowerSymmetricPolynomial","Precedence","PrecedenceForm","Precedes","PrecedesEqual","PrecedesSlantEqual","PrecedesTilde","Precision","PrecisionGoal","PreDecrement","Predict","PredictionRoot","PredictorFunction","PredictorInformation","PredictorMeasurements","PredictorMeasurementsObject","PreemptProtect","PreferencesPath","Prefix","PreIncrement","Prepend","PrependLayer","PrependTo","PreprocessingRules","PreserveColor","PreserveImageOptions","Previous","PreviousCell","PreviousDate","PriceGraphDistribution","PrimaryPlaceholder","Prime","PrimeNu","PrimeOmega","PrimePi","PrimePowerQ","PrimeQ","Primes","PrimeZetaP","PrimitivePolynomialQ","PrimitiveRoot","PrimitiveRootList","PrincipalComponents","PrincipalValue","Print","PrintableASCIIQ","PrintAction","PrintForm","PrintingCopies","PrintingOptions","PrintingPageRange","PrintingStartingPageNumber","PrintingStyleEnvironment","Printout3D","Printout3DPreviewer","PrintPrecision","PrintTemporary","Prism","PrismBox","PrismBoxOptions","PrivateCellOptions","PrivateEvaluationOptions","PrivateFontOptions","PrivateFrontEndOptions","PrivateKey","PrivateNotebookOptions","PrivatePaths","Probability","ProbabilityDistribution","ProbabilityPlot","ProbabilityPr","ProbabilityScalePlot","ProbitModelFit","ProcessConnection","ProcessDirectory","ProcessEnvironment","Processes","ProcessEstimator","ProcessInformation","ProcessObject","ProcessParameterAssumptions","ProcessParameterQ","ProcessStateDomain","ProcessStatus","ProcessTimeDomain","Product","ProductDistribution","ProductLog","ProgressIndicator","ProgressIndicatorBox","ProgressIndicatorBoxOptions","Projection","Prolog","PromptForm","ProofObject","Properties","Property","PropertyList","PropertyValue","Proportion","Proportional","Protect","Protected","ProteinData","Pruning","PseudoInverse","PsychrometricPropertyData","PublicKey","PublisherID","PulsarData","PunctuationCharacter","Purple","Put","PutAppend","Pyramid","PyramidBox","PyramidBoxOptions","QBinomial","QFactorial","QGamma","QHypergeometricPFQ","QnDispersion","QPochhammer","QPolyGamma","QRDecomposition","QuadraticIrrationalQ","QuadraticOptimization","Quantile","QuantilePlot","Quantity","QuantityArray","QuantityDistribution","QuantityForm","QuantityMagnitude","QuantityQ","QuantityUnit","QuantityVariable","QuantityVariableCanonicalUnit","QuantityVariableDimensions","QuantityVariableIdentifier","QuantityVariablePhysicalQuantity","Quartics","QuartileDeviation","Quartiles","QuartileSkewness","Query","QueueingNetworkProcess","QueueingProcess","QueueProperties","Quiet","Quit","Quotient","QuotientRemainder","RadialGradientImage","RadialityCentrality","RadicalBox","RadicalBoxOptions","RadioButton","RadioButtonBar","RadioButtonBox","RadioButtonBoxOptions","Radon","RadonTransform","RamanujanTau","RamanujanTauL","RamanujanTauTheta","RamanujanTauZ","Ramp","Random","RandomChoice","RandomColor","RandomComplex","RandomEntity","RandomFunction","RandomGeoPosition","RandomGraph","RandomImage","RandomInstance","RandomInteger","RandomPermutation","RandomPoint","RandomPolygon","RandomPolyhedron","RandomPrime","RandomReal","RandomSample","RandomSeed","RandomSeeding","RandomVariate","RandomWalkProcess","RandomWord","Range","RangeFilter","RangeSpecification","RankedMax","RankedMin","RarerProbability","Raster","Raster3D","Raster3DBox","Raster3DBoxOptions","RasterArray","RasterBox","RasterBoxOptions","Rasterize","RasterSize","Rational","RationalFunctions","Rationalize","Rationals","Ratios","RawArray","RawBoxes","RawData","RawMedium","RayleighDistribution","Re","Read","ReadByteArray","ReadLine","ReadList","ReadProtected","ReadString","Real","RealAbs","RealBlockDiagonalForm","RealDigits","RealExponent","Reals","RealSign","Reap","RebuildPacletData","RecognitionPrior","RecognitionThreshold","Record","RecordLists","RecordSeparators","Rectangle","RectangleBox","RectangleBoxOptions","RectangleChart","RectangleChart3D","RectangularRepeatingElement","RecurrenceFilter","RecurrenceTable","RecurringDigitsForm","Red","Reduce","RefBox","ReferenceLineStyle","ReferenceMarkers","ReferenceMarkerStyle","Refine","ReflectionMatrix","ReflectionTransform","Refresh","RefreshRate","Region","RegionBinarize","RegionBoundary","RegionBoundaryStyle","RegionBounds","RegionCentroid","RegionDifference","RegionDimension","RegionDisjoint","RegionDistance","RegionDistanceFunction","RegionEmbeddingDimension","RegionEqual","RegionFillingStyle","RegionFunction","RegionImage","RegionIntersection","RegionMeasure","RegionMember","RegionMemberFunction","RegionMoment","RegionNearest","RegionNearestFunction","RegionPlot","RegionPlot3D","RegionProduct","RegionQ","RegionResize","RegionSize","RegionSymmetricDifference","RegionUnion","RegionWithin","RegisterExternalEvaluator","RegularExpression","Regularization","RegularlySampledQ","RegularPolygon","ReIm","ReImLabels","ReImPlot","ReImStyle","Reinstall","RelationalDatabase","RelationGraph","Release","ReleaseHold","ReliabilityDistribution","ReliefImage","ReliefPlot","RemoteAuthorizationCaching","RemoteConnect","RemoteConnectionObject","RemoteFile","RemoteRun","RemoteRunProcess","Remove","RemoveAlphaChannel","RemoveAsynchronousTask","RemoveAudioStream","RemoveBackground","RemoveChannelListener","RemoveChannelSubscribers","Removed","RemoveDiacritics","RemoveInputStreamMethod","RemoveOutputStreamMethod","RemoveProperty","RemoveScheduledTask","RemoveUsers","RemoveVideoStream","RenameDirectory","RenameFile","RenderAll","RenderingOptions","RenewalProcess","RenkoChart","RepairMesh","Repeated","RepeatedNull","RepeatedString","RepeatedTiming","RepeatingElement","Replace","ReplaceAll","ReplaceHeldPart","ReplaceImageValue","ReplaceList","ReplacePart","ReplacePixelValue","ReplaceRepeated","ReplicateLayer","RequiredPhysicalQuantities","Resampling","ResamplingAlgorithmData","ResamplingMethod","Rescale","RescalingTransform","ResetDirectory","ResetMenusPacket","ResetScheduledTask","ReshapeLayer","Residue","ResizeLayer","Resolve","ResourceAcquire","ResourceData","ResourceFunction","ResourceObject","ResourceRegister","ResourceRemove","ResourceSearch","ResourceSubmissionObject","ResourceSubmit","ResourceSystemBase","ResourceSystemPath","ResourceUpdate","ResourceVersion","ResponseForm","Rest","RestartInterval","Restricted","Resultant","ResumePacket","Return","ReturnEntersInput","ReturnExpressionPacket","ReturnInputFormPacket","ReturnPacket","ReturnReceiptFunction","ReturnTextPacket","Reverse","ReverseApplied","ReverseBiorthogonalSplineWavelet","ReverseElement","ReverseEquilibrium","ReverseGraph","ReverseSort","ReverseSortBy","ReverseUpEquilibrium","RevolutionAxis","RevolutionPlot3D","RGBColor","RiccatiSolve","RiceDistribution","RidgeFilter","RiemannR","RiemannSiegelTheta","RiemannSiegelZ","RiemannXi","Riffle","Right","RightArrow","RightArrowBar","RightArrowLeftArrow","RightComposition","RightCosetRepresentative","RightDownTeeVector","RightDownVector","RightDownVectorBar","RightTee","RightTeeArrow","RightTeeVector","RightTriangle","RightTriangleBar","RightTriangleEqual","RightUpDownVector","RightUpTeeVector","RightUpVector","RightUpVectorBar","RightVector","RightVectorBar","RiskAchievementImportance","RiskReductionImportance","RogersTanimotoDissimilarity","RollPitchYawAngles","RollPitchYawMatrix","RomanNumeral","Root","RootApproximant","RootIntervals","RootLocusPlot","RootMeanSquare","RootOfUnityQ","RootReduce","Roots","RootSum","Rotate","RotateLabel","RotateLeft","RotateRight","RotationAction","RotationBox","RotationBoxOptions","RotationMatrix","RotationTransform","Round","RoundImplies","RoundingRadius","Row","RowAlignments","RowBackgrounds","RowBox","RowHeights","RowLines","RowMinHeight","RowReduce","RowsEqual","RowSpacings","RSolve","RSolveValue","RudinShapiro","RudvalisGroupRu","Rule","RuleCondition","RuleDelayed","RuleForm","RulePlot","RulerUnits","Run","RunProcess","RunScheduledTask","RunThrough","RuntimeAttributes","RuntimeOptions","RussellRaoDissimilarity","SameQ","SameTest","SameTestProperties","SampledEntityClass","SampleDepth","SampledSoundFunction","SampledSoundList","SampleRate","SamplingPeriod","SARIMAProcess","SARMAProcess","SASTriangle","SatelliteData","SatisfiabilityCount","SatisfiabilityInstances","SatisfiableQ","Saturday","Save","Saveable","SaveAutoDelete","SaveConnection","SaveDefinitions","SavitzkyGolayMatrix","SawtoothWave","Scale","Scaled","ScaleDivisions","ScaledMousePosition","ScaleOrigin","ScalePadding","ScaleRanges","ScaleRangeStyle","ScalingFunctions","ScalingMatrix","ScalingTransform","Scan","ScheduledTask","ScheduledTaskActiveQ","ScheduledTaskInformation","ScheduledTaskInformationData","ScheduledTaskObject","ScheduledTasks","SchurDecomposition","ScientificForm","ScientificNotationThreshold","ScorerGi","ScorerGiPrime","ScorerHi","ScorerHiPrime","ScreenRectangle","ScreenStyleEnvironment","ScriptBaselineShifts","ScriptForm","ScriptLevel","ScriptMinSize","ScriptRules","ScriptSizeMultipliers","Scrollbars","ScrollingOptions","ScrollPosition","SearchAdjustment","SearchIndexObject","SearchIndices","SearchQueryString","SearchResultObject","Sec","Sech","SechDistribution","SecondOrderConeOptimization","SectionGrouping","SectorChart","SectorChart3D","SectorOrigin","SectorSpacing","SecuredAuthenticationKey","SecuredAuthenticationKeys","SeedRandom","Select","Selectable","SelectComponents","SelectedCells","SelectedNotebook","SelectFirst","Selection","SelectionAnimate","SelectionCell","SelectionCellCreateCell","SelectionCellDefaultStyle","SelectionCellParentStyle","SelectionCreateCell","SelectionDebuggerTag","SelectionDuplicateCell","SelectionEvaluate","SelectionEvaluateCreateCell","SelectionMove","SelectionPlaceholder","SelectionSetStyle","SelectWithContents","SelfLoops","SelfLoopStyle","SemanticImport","SemanticImportString","SemanticInterpretation","SemialgebraicComponentInstances","SemidefiniteOptimization","SendMail","SendMessage","Sequence","SequenceAlignment","SequenceAttentionLayer","SequenceCases","SequenceCount","SequenceFold","SequenceFoldList","SequenceForm","SequenceHold","SequenceLastLayer","SequenceMostLayer","SequencePosition","SequencePredict","SequencePredictorFunction","SequenceReplace","SequenceRestLayer","SequenceReverseLayer","SequenceSplit","Series","SeriesCoefficient","SeriesData","SeriesTermGoal","ServiceConnect","ServiceDisconnect","ServiceExecute","ServiceObject","ServiceRequest","ServiceResponse","ServiceSubmit","SessionSubmit","SessionTime","Set","SetAccuracy","SetAlphaChannel","SetAttributes","Setbacks","SetBoxFormNamesPacket","SetCloudDirectory","SetCookies","SetDelayed","SetDirectory","SetEnvironment","SetEvaluationNotebook","SetFileDate","SetFileLoadingContext","SetNotebookStatusLine","SetOptions","SetOptionsPacket","SetPermissions","SetPrecision","SetProperty","SetSecuredAuthenticationKey","SetSelectedNotebook","SetSharedFunction","SetSharedVariable","SetSpeechParametersPacket","SetStreamPosition","SetSystemModel","SetSystemOptions","Setter","SetterBar","SetterBox","SetterBoxOptions","Setting","SetUsers","SetValue","Shading","Shallow","ShannonWavelet","ShapiroWilkTest","Share","SharingList","Sharpen","ShearingMatrix","ShearingTransform","ShellRegion","ShenCastanMatrix","ShiftedGompertzDistribution","ShiftRegisterSequence","Short","ShortDownArrow","Shortest","ShortestMatch","ShortestPathFunction","ShortLeftArrow","ShortRightArrow","ShortTimeFourier","ShortTimeFourierData","ShortUpArrow","Show","ShowAutoConvert","ShowAutoSpellCheck","ShowAutoStyles","ShowCellBracket","ShowCellLabel","ShowCellTags","ShowClosedCellArea","ShowCodeAssist","ShowContents","ShowControls","ShowCursorTracker","ShowGroupOpenCloseIcon","ShowGroupOpener","ShowInvisibleCharacters","ShowPageBreaks","ShowPredictiveInterface","ShowSelection","ShowShortBoxForm","ShowSpecialCharacters","ShowStringCharacters","ShowSyntaxStyles","ShrinkingDelay","ShrinkWrapBoundingBox","SiderealTime","SiegelTheta","SiegelTukeyTest","SierpinskiCurve","SierpinskiMesh","Sign","Signature","SignedRankTest","SignedRegionDistance","SignificanceLevel","SignPadding","SignTest","SimilarityRules","SimpleGraph","SimpleGraphQ","SimplePolygonQ","SimplePolyhedronQ","Simplex","Simplify","Sin","Sinc","SinghMaddalaDistribution","SingleEvaluation","SingleLetterItalics","SingleLetterStyle","SingularValueDecomposition","SingularValueList","SingularValuePlot","SingularValues","Sinh","SinhIntegral","SinIntegral","SixJSymbol","Skeleton","SkeletonTransform","SkellamDistribution","Skewness","SkewNormalDistribution","SkinStyle","Skip","SliceContourPlot3D","SliceDensityPlot3D","SliceDistribution","SliceVectorPlot3D","Slider","Slider2D","Slider2DBox","Slider2DBoxOptions","SliderBox","SliderBoxOptions","SlideView","Slot","SlotSequence","Small","SmallCircle","Smaller","SmithDecomposition","SmithDelayCompensator","SmithWatermanSimilarity","SmoothDensityHistogram","SmoothHistogram","SmoothHistogram3D","SmoothKernelDistribution","SnDispersion","Snippet","SnubPolyhedron","SocialMediaData","Socket","SocketConnect","SocketListen","SocketListener","SocketObject","SocketOpen","SocketReadMessage","SocketReadyQ","Sockets","SocketWaitAll","SocketWaitNext","SoftmaxLayer","SokalSneathDissimilarity","SolarEclipse","SolarSystemFeatureData","SolidAngle","SolidData","SolidRegionQ","Solve","SolveAlways","SolveDelayed","Sort","SortBy","SortedBy","SortedEntityClass","Sound","SoundAndGraphics","SoundNote","SoundVolume","SourceLink","Sow","Space","SpaceCurveData","SpaceForm","Spacer","Spacings","Span","SpanAdjustments","SpanCharacterRounding","SpanFromAbove","SpanFromBoth","SpanFromLeft","SpanLineThickness","SpanMaxSize","SpanMinSize","SpanningCharacters","SpanSymmetric","SparseArray","SpatialGraphDistribution","SpatialMedian","SpatialTransformationLayer","Speak","SpeakerMatchQ","SpeakTextPacket","SpearmanRankTest","SpearmanRho","SpeciesData","SpecificityGoal","SpectralLineData","Spectrogram","SpectrogramArray","Specularity","SpeechCases","SpeechInterpreter","SpeechRecognize","SpeechSynthesize","SpellingCorrection","SpellingCorrectionList","SpellingDictionaries","SpellingDictionariesPath","SpellingOptions","SpellingSuggestionsPacket","Sphere","SphereBox","SpherePoints","SphericalBesselJ","SphericalBesselY","SphericalHankelH1","SphericalHankelH2","SphericalHarmonicY","SphericalPlot3D","SphericalRegion","SphericalShell","SpheroidalEigenvalue","SpheroidalJoiningFactor","SpheroidalPS","SpheroidalPSPrime","SpheroidalQS","SpheroidalQSPrime","SpheroidalRadialFactor","SpheroidalS1","SpheroidalS1Prime","SpheroidalS2","SpheroidalS2Prime","Splice","SplicedDistribution","SplineClosed","SplineDegree","SplineKnots","SplineWeights","Split","SplitBy","SpokenString","Sqrt","SqrtBox","SqrtBoxOptions","Square","SquaredEuclideanDistance","SquareFreeQ","SquareIntersection","SquareMatrixQ","SquareRepeatingElement","SquaresR","SquareSubset","SquareSubsetEqual","SquareSuperset","SquareSupersetEqual","SquareUnion","SquareWave","SSSTriangle","StabilityMargins","StabilityMarginsStyle","StableDistribution","Stack","StackBegin","StackComplete","StackedDateListPlot","StackedListPlot","StackInhibit","StadiumShape","StandardAtmosphereData","StandardDeviation","StandardDeviationFilter","StandardForm","Standardize","Standardized","StandardOceanData","StandbyDistribution","Star","StarClusterData","StarData","StarGraph","StartAsynchronousTask","StartExternalSession","StartingStepSize","StartOfLine","StartOfString","StartProcess","StartScheduledTask","StartupSound","StartWebSession","StateDimensions","StateFeedbackGains","StateOutputEstimator","StateResponse","StateSpaceModel","StateSpaceRealization","StateSpaceTransform","StateTransformationLinearize","StationaryDistribution","StationaryWaveletPacketTransform","StationaryWaveletTransform","StatusArea","StatusCentrality","StepMonitor","StereochemistryElements","StieltjesGamma","StippleShading","StirlingS1","StirlingS2","StopAsynchronousTask","StoppingPowerData","StopScheduledTask","StrataVariables","StratonovichProcess","StreamColorFunction","StreamColorFunctionScaling","StreamDensityPlot","StreamMarkers","StreamPlot","StreamPoints","StreamPosition","Streams","StreamScale","StreamStyle","String","StringBreak","StringByteCount","StringCases","StringContainsQ","StringCount","StringDelete","StringDrop","StringEndsQ","StringExpression","StringExtract","StringForm","StringFormat","StringFreeQ","StringInsert","StringJoin","StringLength","StringMatchQ","StringPadLeft","StringPadRight","StringPart","StringPartition","StringPosition","StringQ","StringRepeat","StringReplace","StringReplaceList","StringReplacePart","StringReverse","StringRiffle","StringRotateLeft","StringRotateRight","StringSkeleton","StringSplit","StringStartsQ","StringTake","StringTemplate","StringToByteArray","StringToStream","StringTrim","StripBoxes","StripOnInput","StripWrapperBoxes","StrokeForm","StructuralImportance","StructuredArray","StructuredArrayHeadQ","StructuredSelection","StruveH","StruveL","Stub","StudentTDistribution","Style","StyleBox","StyleBoxAutoDelete","StyleData","StyleDefinitions","StyleForm","StyleHints","StyleKeyMapping","StyleMenuListing","StyleNameDialogSettings","StyleNames","StylePrint","StyleSheetPath","Subdivide","Subfactorial","Subgraph","SubMinus","SubPlus","SubresultantPolynomialRemainders","SubresultantPolynomials","Subresultants","Subscript","SubscriptBox","SubscriptBoxOptions","Subscripted","Subsequences","Subset","SubsetCases","SubsetCount","SubsetEqual","SubsetMap","SubsetPosition","SubsetQ","SubsetReplace","Subsets","SubStar","SubstitutionSystem","Subsuperscript","SubsuperscriptBox","SubsuperscriptBoxOptions","SubtitleEncoding","SubtitleTracks","Subtract","SubtractFrom","SubtractSides","SubValues","Succeeds","SucceedsEqual","SucceedsSlantEqual","SucceedsTilde","Success","SuchThat","Sum","SumConvergence","SummationLayer","Sunday","SunPosition","Sunrise","Sunset","SuperDagger","SuperMinus","SupernovaData","SuperPlus","Superscript","SuperscriptBox","SuperscriptBoxOptions","Superset","SupersetEqual","SuperStar","Surd","SurdForm","SurfaceAppearance","SurfaceArea","SurfaceColor","SurfaceData","SurfaceGraphics","SurvivalDistribution","SurvivalFunction","SurvivalModel","SurvivalModelFit","SuspendPacket","SuzukiDistribution","SuzukiGroupSuz","SwatchLegend","Switch","Symbol","SymbolName","SymletWavelet","Symmetric","SymmetricGroup","SymmetricKey","SymmetricMatrixQ","SymmetricPolynomial","SymmetricReduction","Symmetrize","SymmetrizedArray","SymmetrizedArrayRules","SymmetrizedDependentComponents","SymmetrizedIndependentComponents","SymmetrizedReplacePart","SynchronousInitialization","SynchronousUpdating","Synonyms","Syntax","SyntaxForm","SyntaxInformation","SyntaxLength","SyntaxPacket","SyntaxQ","SynthesizeMissingValues","SystemCredential","SystemCredentialData","SystemCredentialKey","SystemCredentialKeys","SystemCredentialStoreObject","SystemDialogInput","SystemException","SystemGet","SystemHelpPath","SystemInformation","SystemInformationData","SystemInstall","SystemModel","SystemModeler","SystemModelExamples","SystemModelLinearize","SystemModelParametricSimulate","SystemModelPlot","SystemModelProgressReporting","SystemModelReliability","SystemModels","SystemModelSimulate","SystemModelSimulateSensitivity","SystemModelSimulationData","SystemOpen","SystemOptions","SystemProcessData","SystemProcesses","SystemsConnectionsModel","SystemsModelDelay","SystemsModelDelayApproximate","SystemsModelDelete","SystemsModelDimensions","SystemsModelExtract","SystemsModelFeedbackConnect","SystemsModelLabels","SystemsModelLinearity","SystemsModelMerge","SystemsModelOrder","SystemsModelParallelConnect","SystemsModelSeriesConnect","SystemsModelStateFeedbackConnect","SystemsModelVectorRelativeOrders","SystemStub","SystemTest","Tab","TabFilling","Table","TableAlignments","TableDepth","TableDirections","TableForm","TableHeadings","TableSpacing","TableView","TableViewBox","TableViewBoxBackground","TableViewBoxItemSize","TableViewBoxOptions","TabSpacings","TabView","TabViewBox","TabViewBoxOptions","TagBox","TagBoxNote","TagBoxOptions","TaggingRules","TagSet","TagSetDelayed","TagStyle","TagUnset","Take","TakeDrop","TakeLargest","TakeLargestBy","TakeList","TakeSmallest","TakeSmallestBy","TakeWhile","Tally","Tan","Tanh","TargetDevice","TargetFunctions","TargetSystem","TargetUnits","TaskAbort","TaskExecute","TaskObject","TaskRemove","TaskResume","Tasks","TaskSuspend","TaskWait","TautologyQ","TelegraphProcess","TemplateApply","TemplateArgBox","TemplateBox","TemplateBoxOptions","TemplateEvaluate","TemplateExpression","TemplateIf","TemplateObject","TemplateSequence","TemplateSlot","TemplateSlotSequence","TemplateUnevaluated","TemplateVerbatim","TemplateWith","TemporalData","TemporalRegularity","Temporary","TemporaryVariable","TensorContract","TensorDimensions","TensorExpand","TensorProduct","TensorQ","TensorRank","TensorReduce","TensorSymmetry","TensorTranspose","TensorWedge","TestID","TestReport","TestReportObject","TestResultObject","Tetrahedron","TetrahedronBox","TetrahedronBoxOptions","TeXForm","TeXSave","Text","Text3DBox","Text3DBoxOptions","TextAlignment","TextBand","TextBoundingBox","TextBox","TextCases","TextCell","TextClipboardType","TextContents","TextData","TextElement","TextForm","TextGrid","TextJustification","TextLine","TextPacket","TextParagraph","TextPosition","TextRecognize","TextSearch","TextSearchReport","TextSentences","TextString","TextStructure","TextStyle","TextTranslation","Texture","TextureCoordinateFunction","TextureCoordinateScaling","TextWords","Therefore","ThermodynamicData","ThermometerGauge","Thick","Thickness","Thin","Thinning","ThisLink","ThompsonGroupTh","Thread","ThreadingLayer","ThreeJSymbol","Threshold","Through","Throw","ThueMorse","Thumbnail","Thursday","Ticks","TicksStyle","TideData","Tilde","TildeEqual","TildeFullEqual","TildeTilde","TimeConstrained","TimeConstraint","TimeDirection","TimeFormat","TimeGoal","TimelinePlot","TimeObject","TimeObjectQ","TimeRemaining","Times","TimesBy","TimeSeries","TimeSeriesAggregate","TimeSeriesForecast","TimeSeriesInsert","TimeSeriesInvertibility","TimeSeriesMap","TimeSeriesMapThread","TimeSeriesModel","TimeSeriesModelFit","TimeSeriesResample","TimeSeriesRescale","TimeSeriesShift","TimeSeriesThread","TimeSeriesWindow","TimeUsed","TimeValue","TimeWarpingCorrespondence","TimeWarpingDistance","TimeZone","TimeZoneConvert","TimeZoneOffset","Timing","Tiny","TitleGrouping","TitsGroupT","ToBoxes","ToCharacterCode","ToColor","ToContinuousTimeModel","ToDate","Today","ToDiscreteTimeModel","ToEntity","ToeplitzMatrix","ToExpression","ToFileName","Together","Toggle","ToggleFalse","Toggler","TogglerBar","TogglerBox","TogglerBoxOptions","ToHeldExpression","ToInvertibleTimeSeries","TokenWords","Tolerance","ToLowerCase","Tomorrow","ToNumberField","TooBig","Tooltip","TooltipBox","TooltipBoxOptions","TooltipDelay","TooltipStyle","ToonShading","Top","TopHatTransform","ToPolarCoordinates","TopologicalSort","ToRadicals","ToRules","ToSphericalCoordinates","ToString","Total","TotalHeight","TotalLayer","TotalVariationFilter","TotalWidth","TouchPosition","TouchscreenAutoZoom","TouchscreenControlPlacement","ToUpperCase","Tr","Trace","TraceAbove","TraceAction","TraceBackward","TraceDepth","TraceDialog","TraceForward","TraceInternal","TraceLevel","TraceOff","TraceOn","TraceOriginal","TracePrint","TraceScan","TrackedSymbols","TrackingFunction","TracyWidomDistribution","TradingChart","TraditionalForm","TraditionalFunctionNotation","TraditionalNotation","TraditionalOrder","TrainingProgressCheckpointing","TrainingProgressFunction","TrainingProgressMeasurements","TrainingProgressReporting","TrainingStoppingCriterion","TrainingUpdateSchedule","TransferFunctionCancel","TransferFunctionExpand","TransferFunctionFactor","TransferFunctionModel","TransferFunctionPoles","TransferFunctionTransform","TransferFunctionZeros","TransformationClass","TransformationFunction","TransformationFunctions","TransformationMatrix","TransformedDistribution","TransformedField","TransformedProcess","TransformedRegion","TransitionDirection","TransitionDuration","TransitionEffect","TransitiveClosureGraph","TransitiveReductionGraph","Translate","TranslationOptions","TranslationTransform","Transliterate","Transparent","TransparentColor","Transpose","TransposeLayer","TrapSelection","TravelDirections","TravelDirectionsData","TravelDistance","TravelDistanceList","TravelMethod","TravelTime","TreeForm","TreeGraph","TreeGraphQ","TreePlot","TrendStyle","Triangle","TriangleCenter","TriangleConstruct","TriangleMeasurement","TriangleWave","TriangularDistribution","TriangulateMesh","Trig","TrigExpand","TrigFactor","TrigFactorList","Trigger","TrigReduce","TrigToExp","TrimmedMean","TrimmedVariance","TropicalStormData","True","TrueQ","TruncatedDistribution","TruncatedPolyhedron","TsallisQExponentialDistribution","TsallisQGaussianDistribution","TTest","Tube","TubeBezierCurveBox","TubeBezierCurveBoxOptions","TubeBox","TubeBoxOptions","TubeBSplineCurveBox","TubeBSplineCurveBoxOptions","Tuesday","TukeyLambdaDistribution","TukeyWindow","TunnelData","Tuples","TuranGraph","TuringMachine","TuttePolynomial","TwoWayRule","Typed","TypeSpecifier","UnateQ","Uncompress","UnconstrainedParameters","Undefined","UnderBar","Underflow","Underlined","Underoverscript","UnderoverscriptBox","UnderoverscriptBoxOptions","Underscript","UnderscriptBox","UnderscriptBoxOptions","UnderseaFeatureData","UndirectedEdge","UndirectedGraph","UndirectedGraphQ","UndoOptions","UndoTrackedVariables","Unequal","UnequalTo","Unevaluated","UniformDistribution","UniformGraphDistribution","UniformPolyhedron","UniformSumDistribution","Uninstall","Union","UnionedEntityClass","UnionPlus","Unique","UnitaryMatrixQ","UnitBox","UnitConvert","UnitDimensions","Unitize","UnitRootTest","UnitSimplify","UnitStep","UnitSystem","UnitTriangle","UnitVector","UnitVectorLayer","UnityDimensions","UniverseModelData","UniversityData","UnixTime","Unprotect","UnregisterExternalEvaluator","UnsameQ","UnsavedVariables","Unset","UnsetShared","UntrackedVariables","Up","UpArrow","UpArrowBar","UpArrowDownArrow","Update","UpdateDynamicObjects","UpdateDynamicObjectsSynchronous","UpdateInterval","UpdatePacletSites","UpdateSearchIndex","UpDownArrow","UpEquilibrium","UpperCaseQ","UpperLeftArrow","UpperRightArrow","UpperTriangularize","UpperTriangularMatrixQ","Upsample","UpSet","UpSetDelayed","UpTee","UpTeeArrow","UpTo","UpValues","URL","URLBuild","URLDecode","URLDispatcher","URLDownload","URLDownloadSubmit","URLEncode","URLExecute","URLExpand","URLFetch","URLFetchAsynchronous","URLParse","URLQueryDecode","URLQueryEncode","URLRead","URLResponseTime","URLSave","URLSaveAsynchronous","URLShorten","URLSubmit","UseGraphicsRange","UserDefinedWavelet","Using","UsingFrontEnd","UtilityFunction","V2Get","ValenceErrorHandling","ValidationLength","ValidationSet","Value","ValueBox","ValueBoxOptions","ValueDimensions","ValueForm","ValuePreprocessingFunction","ValueQ","Values","ValuesData","Variables","Variance","VarianceEquivalenceTest","VarianceEstimatorFunction","VarianceGammaDistribution","VarianceTest","VectorAngle","VectorAround","VectorAspectRatio","VectorColorFunction","VectorColorFunctionScaling","VectorDensityPlot","VectorGlyphData","VectorGreater","VectorGreaterEqual","VectorLess","VectorLessEqual","VectorMarkers","VectorPlot","VectorPlot3D","VectorPoints","VectorQ","VectorRange","Vectors","VectorScale","VectorScaling","VectorSizes","VectorStyle","Vee","Verbatim","Verbose","VerboseConvertToPostScriptPacket","VerificationTest","VerifyConvergence","VerifyDerivedKey","VerifyDigitalSignature","VerifyFileSignature","VerifyInterpretation","VerifySecurityCertificates","VerifySolutions","VerifyTestAssumptions","Version","VersionedPreferences","VersionNumber","VertexAdd","VertexCapacity","VertexColors","VertexComponent","VertexConnectivity","VertexContract","VertexCoordinateRules","VertexCoordinates","VertexCorrelationSimilarity","VertexCosineSimilarity","VertexCount","VertexCoverQ","VertexDataCoordinates","VertexDegree","VertexDelete","VertexDiceSimilarity","VertexEccentricity","VertexInComponent","VertexInDegree","VertexIndex","VertexJaccardSimilarity","VertexLabeling","VertexLabels","VertexLabelStyle","VertexList","VertexNormals","VertexOutComponent","VertexOutDegree","VertexQ","VertexRenderingFunction","VertexReplace","VertexShape","VertexShapeFunction","VertexSize","VertexStyle","VertexTextureCoordinates","VertexWeight","VertexWeightedGraphQ","Vertical","VerticalBar","VerticalForm","VerticalGauge","VerticalSeparator","VerticalSlider","VerticalTilde","Video","VideoEncoding","VideoExtractFrames","VideoFrameList","VideoFrameMap","VideoPause","VideoPlay","VideoQ","VideoStop","VideoStream","VideoStreams","VideoTimeSeries","VideoTracks","VideoTrim","ViewAngle","ViewCenter","ViewMatrix","ViewPoint","ViewPointSelectorSettings","ViewPort","ViewProjection","ViewRange","ViewVector","ViewVertical","VirtualGroupData","Visible","VisibleCell","VoiceStyleData","VoigtDistribution","VolcanoData","Volume","VonMisesDistribution","VoronoiMesh","WaitAll","WaitAsynchronousTask","WaitNext","WaitUntil","WakebyDistribution","WalleniusHypergeometricDistribution","WaringYuleDistribution","WarpingCorrespondence","WarpingDistance","WatershedComponents","WatsonUSquareTest","WattsStrogatzGraphDistribution","WaveletBestBasis","WaveletFilterCoefficients","WaveletImagePlot","WaveletListPlot","WaveletMapIndexed","WaveletMatrixPlot","WaveletPhi","WaveletPsi","WaveletScale","WaveletScalogram","WaveletThreshold","WeaklyConnectedComponents","WeaklyConnectedGraphComponents","WeaklyConnectedGraphQ","WeakStationarity","WeatherData","WeatherForecastData","WebAudioSearch","WebElementObject","WeberE","WebExecute","WebImage","WebImageSearch","WebSearch","WebSessionObject","WebSessions","WebWindowObject","Wedge","Wednesday","WeibullDistribution","WeierstrassE1","WeierstrassE2","WeierstrassE3","WeierstrassEta1","WeierstrassEta2","WeierstrassEta3","WeierstrassHalfPeriods","WeierstrassHalfPeriodW1","WeierstrassHalfPeriodW2","WeierstrassHalfPeriodW3","WeierstrassInvariantG2","WeierstrassInvariantG3","WeierstrassInvariants","WeierstrassP","WeierstrassPPrime","WeierstrassSigma","WeierstrassZeta","WeightedAdjacencyGraph","WeightedAdjacencyMatrix","WeightedData","WeightedGraphQ","Weights","WelchWindow","WheelGraph","WhenEvent","Which","While","White","WhiteNoiseProcess","WhitePoint","Whitespace","WhitespaceCharacter","WhittakerM","WhittakerW","WienerFilter","WienerProcess","WignerD","WignerSemicircleDistribution","WikidataData","WikidataSearch","WikipediaData","WikipediaSearch","WilksW","WilksWTest","WindDirectionData","WindingCount","WindingPolygon","WindowClickSelect","WindowElements","WindowFloating","WindowFrame","WindowFrameElements","WindowMargins","WindowMovable","WindowOpacity","WindowPersistentStyles","WindowSelected","WindowSize","WindowStatusArea","WindowTitle","WindowToolbars","WindowWidth","WindSpeedData","WindVectorData","WinsorizedMean","WinsorizedVariance","WishartMatrixDistribution","With","WolframAlpha","WolframAlphaDate","WolframAlphaQuantity","WolframAlphaResult","WolframLanguageData","Word","WordBoundary","WordCharacter","WordCloud","WordCount","WordCounts","WordData","WordDefinition","WordFrequency","WordFrequencyData","WordList","WordOrientation","WordSearch","WordSelectionFunction","WordSeparators","WordSpacings","WordStem","WordTranslation","WorkingPrecision","WrapAround","Write","WriteLine","WriteString","Wronskian","XMLElement","XMLObject","XMLTemplate","Xnor","Xor","XYZColor","Yellow","Yesterday","YuleDissimilarity","ZernikeR","ZeroSymmetric","ZeroTest","ZeroWidthTimes","Zeta","ZetaZero","ZIPCodeData","ZipfDistribution","ZoomCenter","ZoomFactor","ZTest","ZTransform","$Aborted","$ActivationGroupID","$ActivationKey","$ActivationUserRegistered","$AddOnsDirectory","$AllowDataUpdates","$AllowExternalChannelFunctions","$AllowInternet","$AssertFunction","$Assumptions","$AsynchronousTask","$AudioDecoders","$AudioEncoders","$AudioInputDevices","$AudioOutputDevices","$BaseDirectory","$BasePacletsDirectory","$BatchInput","$BatchOutput","$BlockchainBase","$BoxForms","$ByteOrdering","$CacheBaseDirectory","$Canceled","$ChannelBase","$CharacterEncoding","$CharacterEncodings","$CloudAccountName","$CloudBase","$CloudConnected","$CloudConnection","$CloudCreditsAvailable","$CloudEvaluation","$CloudExpressionBase","$CloudObjectNameFormat","$CloudObjectURLType","$CloudRootDirectory","$CloudSymbolBase","$CloudUserID","$CloudUserUUID","$CloudVersion","$CloudVersionNumber","$CloudWolframEngineVersionNumber","$CommandLine","$CompilationTarget","$ConditionHold","$ConfiguredKernels","$Context","$ContextPath","$ControlActiveSetting","$Cookies","$CookieStore","$CreationDate","$CurrentLink","$CurrentTask","$CurrentWebSession","$DataStructures","$DateStringFormat","$DefaultAudioInputDevice","$DefaultAudioOutputDevice","$DefaultFont","$DefaultFrontEnd","$DefaultImagingDevice","$DefaultLocalBase","$DefaultMailbox","$DefaultNetworkInterface","$DefaultPath","$DefaultProxyRules","$DefaultSystemCredentialStore","$Display","$DisplayFunction","$DistributedContexts","$DynamicEvaluation","$Echo","$EmbedCodeEnvironments","$EmbeddableServices","$EntityStores","$Epilog","$EvaluationCloudBase","$EvaluationCloudObject","$EvaluationEnvironment","$ExportFormats","$ExternalIdentifierTypes","$ExternalStorageBase","$Failed","$FinancialDataSource","$FontFamilies","$FormatType","$FrontEnd","$FrontEndSession","$GeoEntityTypes","$GeoLocation","$GeoLocationCity","$GeoLocationCountry","$GeoLocationPrecision","$GeoLocationSource","$HistoryLength","$HomeDirectory","$HTMLExportRules","$HTTPCookies","$HTTPRequest","$IgnoreEOF","$ImageFormattingWidth","$ImageResolution","$ImagingDevice","$ImagingDevices","$ImportFormats","$IncomingMailSettings","$InitialDirectory","$Initialization","$InitializationContexts","$Input","$InputFileName","$InputStreamMethods","$Inspector","$InstallationDate","$InstallationDirectory","$InterfaceEnvironment","$InterpreterTypes","$IterationLimit","$KernelCount","$KernelID","$Language","$LaunchDirectory","$LibraryPath","$LicenseExpirationDate","$LicenseID","$LicenseProcesses","$LicenseServer","$LicenseSubprocesses","$LicenseType","$Line","$Linked","$LinkSupported","$LoadedFiles","$LocalBase","$LocalSymbolBase","$MachineAddresses","$MachineDomain","$MachineDomains","$MachineEpsilon","$MachineID","$MachineName","$MachinePrecision","$MachineType","$MaxExtraPrecision","$MaxLicenseProcesses","$MaxLicenseSubprocesses","$MaxMachineNumber","$MaxNumber","$MaxPiecewiseCases","$MaxPrecision","$MaxRootDegree","$MessageGroups","$MessageList","$MessagePrePrint","$Messages","$MinMachineNumber","$MinNumber","$MinorReleaseNumber","$MinPrecision","$MobilePhone","$ModuleNumber","$NetworkConnected","$NetworkInterfaces","$NetworkLicense","$NewMessage","$NewSymbol","$NotebookInlineStorageLimit","$Notebooks","$NoValue","$NumberMarks","$Off","$OperatingSystem","$Output","$OutputForms","$OutputSizeLimit","$OutputStreamMethods","$Packages","$ParentLink","$ParentProcessID","$PasswordFile","$PatchLevelID","$Path","$PathnameSeparator","$PerformanceGoal","$Permissions","$PermissionsGroupBase","$PersistenceBase","$PersistencePath","$PipeSupported","$PlotTheme","$Post","$Pre","$PreferencesDirectory","$PreInitialization","$PrePrint","$PreRead","$PrintForms","$PrintLiteral","$Printout3DPreviewer","$ProcessID","$ProcessorCount","$ProcessorType","$ProductInformation","$ProgramName","$PublisherID","$RandomState","$RecursionLimit","$RegisteredDeviceClasses","$RegisteredUserName","$ReleaseNumber","$RequesterAddress","$RequesterWolframID","$RequesterWolframUUID","$RootDirectory","$ScheduledTask","$ScriptCommandLine","$ScriptInputString","$SecuredAuthenticationKeyTokens","$ServiceCreditsAvailable","$Services","$SessionID","$SetParentLink","$SharedFunctions","$SharedVariables","$SoundDisplay","$SoundDisplayFunction","$SourceLink","$SSHAuthentication","$SubtitleDecoders","$SubtitleEncoders","$SummaryBoxDataSizeLimit","$SuppressInputFormHeads","$SynchronousEvaluation","$SyntaxHandler","$System","$SystemCharacterEncoding","$SystemCredentialStore","$SystemID","$SystemMemory","$SystemShell","$SystemTimeZone","$SystemWordLength","$TemplatePath","$TemporaryDirectory","$TemporaryPrefix","$TestFileName","$TextStyle","$TimedOut","$TimeUnit","$TimeZone","$TimeZoneEntity","$TopDirectory","$TraceOff","$TraceOn","$TracePattern","$TracePostAction","$TracePreAction","$UnitSystem","$Urgent","$UserAddOnsDirectory","$UserAgentLanguages","$UserAgentMachine","$UserAgentName","$UserAgentOperatingSystem","$UserAgentString","$UserAgentVersion","$UserBaseDirectory","$UserBasePacletsDirectory","$UserDocumentsDirectory","$Username","$UserName","$UserURLBase","$Version","$VersionNumber","$VideoDecoders","$VideoEncoders","$VoiceStyles","$WolframDocumentsDirectory","$WolframID","$WolframUUID"]
+;function t(e){return e?"string"==typeof e?e:e.source:null}function i(e){
+return o("(",e,")?")}function o(...e){return e.map((e=>t(e))).join("")}
+function a(...e){return"("+e.map((e=>t(e))).join("|")+")"}return t=>{
+const n=a(o(/([2-9]|[1-2]\d|[3][0-5])\^\^/,/(\w*\.\w+|\w+\.\w*|\w+)/),/(\d*\.\d+|\d+\.\d*|\d+)/),r={
+className:"number",relevance:0,
+begin:o(n,i(a(/``[+-]?(\d*\.\d+|\d+\.\d*|\d+)/,/`([+-]?(\d*\.\d+|\d+\.\d*|\d+))?/)),i(/\*\^[+-]?\d+/))
+},l=/[a-zA-Z$][a-zA-Z0-9$]*/,s=new Set(e),c={variants:[{
+className:"builtin-symbol",begin:l,"on:begin":(e,t)=>{
+s.has(e[0])||t.ignoreMatch()}},{className:"symbol",relevance:0,begin:l}]},u={
+className:"message-name",relevance:0,begin:o("::",l)};return{name:"Mathematica",
+aliases:["mma","wl"],classNameAliases:{brace:"punctuation",pattern:"type",
+slot:"type",symbol:"variable","named-character":"variable",
+"builtin-symbol":"built_in","message-name":"string"},
+contains:[t.COMMENT(/\(\*/,/\*\)/,{contains:["self"]}),{className:"pattern",
+relevance:0,begin:/([a-zA-Z$][a-zA-Z0-9$]*)?_+([a-zA-Z$][a-zA-Z0-9$]*)?/},{
+className:"slot",relevance:0,begin:/#[a-zA-Z$][a-zA-Z0-9$]*|#+[0-9]?/},u,c,{
+className:"named-character",begin:/\\\[[$a-zA-Z][$a-zA-Z0-9]+\]/
+},t.QUOTE_STRING_MODE,r,{className:"operator",relevance:0,
+begin:/[+\-*/,;.:@~=><&|_`'^?!%]+/},{className:"brace",relevance:0,
+begin:/[[\](){}]/}]}}})());
+hljs.registerLanguage("matlab",(()=>{"use strict";return e=>{var a={relevance:0,
+contains:[{begin:"('|\\.')+"}]};return{name:"Matlab",keywords:{
+keyword:"arguments break case catch classdef continue else elseif end enumeration events for function global if methods otherwise parfor persistent properties return spmd switch try while",
+built_in:"sin sind sinh asin asind asinh cos cosd cosh acos acosd acosh tan tand tanh atan atand atan2 atanh sec secd sech asec asecd asech csc cscd csch acsc acscd acsch cot cotd coth acot acotd acoth hypot exp expm1 log log1p log10 log2 pow2 realpow reallog realsqrt sqrt nthroot nextpow2 abs angle complex conj imag real unwrap isreal cplxpair fix floor ceil round mod rem sign airy besselj bessely besselh besseli besselk beta betainc betaln ellipj ellipke erf erfc erfcx erfinv expint gamma gammainc gammaln psi legendre cross dot factor isprime primes gcd lcm rat rats perms nchoosek factorial cart2sph cart2pol pol2cart sph2cart hsv2rgb rgb2hsv zeros ones eye repmat rand randn linspace logspace freqspace meshgrid accumarray size length ndims numel disp isempty isequal isequalwithequalnans cat reshape diag blkdiag tril triu fliplr flipud flipdim rot90 find sub2ind ind2sub bsxfun ndgrid permute ipermute shiftdim circshift squeeze isscalar isvector ans eps realmax realmin pi i|0 inf nan isnan isinf isfinite j|0 why compan gallery hadamard hankel hilb invhilb magic pascal rosser toeplitz vander wilkinson max min nanmax nanmin mean nanmean type table readtable writetable sortrows sort figure plot plot3 scatter scatter3 cellfun legend intersect ismember procrustes hold num2cell "
+},illegal:'(//|"|#|/\\*|\\s+/\\w+)',contains:[{className:"function",
+beginKeywords:"function",end:"$",contains:[e.UNDERSCORE_TITLE_MODE,{
+className:"params",variants:[{begin:"\\(",end:"\\)"},{begin:"\\[",end:"\\]"}]}]
+},{className:"built_in",begin:/true|false/,relevance:0,starts:a},{
+begin:"[a-zA-Z][a-zA-Z_0-9]*('|\\.')+",relevance:0},{className:"number",
+begin:e.C_NUMBER_RE,relevance:0,starts:a},{className:"string",begin:"'",end:"'",
+contains:[e.BACKSLASH_ESCAPE,{begin:"''"}]},{begin:/\]|\}|\)/,relevance:0,
+starts:a},{className:"string",begin:'"',end:'"',contains:[e.BACKSLASH_ESCAPE,{
+begin:'""'}],starts:a
+},e.COMMENT("^\\s*%\\{\\s*$","^\\s*%\\}\\s*$"),e.COMMENT("%","$")]}}})());
+hljs.registerLanguage("maxima",(()=>{"use strict";return e=>({name:"Maxima",
+keywords:{$pattern:"[A-Za-z_%][0-9A-Za-z_%]*",
+keyword:"if then else elseif for thru do while unless step in and or not",
+literal:"true false unknown inf minf ind und %e %i %pi %phi %gamma",
+built_in:" abasep abs absint absolute_real_time acos acosh acot acoth acsc acsch activate addcol add_edge add_edges addmatrices addrow add_vertex add_vertices adjacency_matrix adjoin adjoint af agd airy airy_ai airy_bi airy_dai airy_dbi algsys alg_type alias allroots alphacharp alphanumericp amortization %and annuity_fv annuity_pv antid antidiff AntiDifference append appendfile apply apply1 apply2 applyb1 apropos args arit_amortization arithmetic arithsum array arrayapply arrayinfo arraymake arraysetapply ascii asec asech asin asinh askinteger asksign assoc assoc_legendre_p assoc_legendre_q assume assume_external_byte_order asympa at atan atan2 atanh atensimp atom atvalue augcoefmatrix augmented_lagrangian_method av average_degree backtrace bars barsplot barsplot_description base64 base64_decode bashindices batch batchload bc2 bdvac belln benefit_cost bern bernpoly bernstein_approx bernstein_expand bernstein_poly bessel bessel_i bessel_j bessel_k bessel_simplify bessel_y beta beta_incomplete beta_incomplete_generalized beta_incomplete_regularized bezout bfallroots bffac bf_find_root bf_fmin_cobyla bfhzeta bfloat bfloatp bfpsi bfpsi0 bfzeta biconnected_components bimetric binomial bipartition block blockmatrixp bode_gain bode_phase bothcoef box boxplot boxplot_description break bug_report build_info|10 buildq build_sample burn cabs canform canten cardinality carg cartan cartesian_product catch cauchy_matrix cbffac cdf_bernoulli cdf_beta cdf_binomial cdf_cauchy cdf_chi2 cdf_continuous_uniform cdf_discrete_uniform cdf_exp cdf_f cdf_gamma cdf_general_finite_discrete cdf_geometric cdf_gumbel cdf_hypergeometric cdf_laplace cdf_logistic cdf_lognormal cdf_negative_binomial cdf_noncentral_chi2 cdf_noncentral_student_t cdf_normal cdf_pareto cdf_poisson cdf_rank_sum cdf_rayleigh cdf_signed_rank cdf_student_t cdf_weibull cdisplay ceiling central_moment cequal cequalignore cf cfdisrep cfexpand cgeodesic cgreaterp cgreaterpignore changename changevar chaosgame charat charfun charfun2 charlist charp charpoly chdir chebyshev_t chebyshev_u checkdiv check_overlaps chinese cholesky christof chromatic_index chromatic_number cint circulant_graph clear_edge_weight clear_rules clear_vertex_label clebsch_gordan clebsch_graph clessp clesspignore close closefile cmetric coeff coefmatrix cograd col collapse collectterms columnop columnspace columnswap columnvector combination combine comp2pui compare compfile compile compile_file complement_graph complete_bipartite_graph complete_graph complex_number_p components compose_functions concan concat conjugate conmetderiv connected_components connect_vertices cons constant constantp constituent constvalue cont2part content continuous_freq contortion contour_plot contract contract_edge contragrad contrib_ode convert coord copy copy_file copy_graph copylist copymatrix cor cos cosh cot coth cov cov1 covdiff covect covers crc24sum create_graph create_list csc csch csetup cspline ctaylor ct_coordsys ctransform ctranspose cube_graph cuboctahedron_graph cunlisp cv cycle_digraph cycle_graph cylindrical days360 dblint deactivate declare declare_constvalue declare_dimensions declare_fundamental_dimensions declare_fundamental_units declare_qty declare_translated declare_unit_conversion declare_units declare_weights decsym defcon define define_alt_display define_variable defint defmatch defrule defstruct deftaylor degree_sequence del delete deleten delta demo demoivre denom depends derivdegree derivlist describe desolve determinant dfloat dgauss_a dgauss_b dgeev dgemm dgeqrf dgesv dgesvd diag diagmatrix diag_matrix diagmatrixp diameter diff digitcharp dimacs_export dimacs_import dimension dimensionless dimensions dimensions_as_list direct directory discrete_freq disjoin disjointp disolate disp dispcon dispform dispfun dispJordan display disprule dispterms distrib divide divisors divsum dkummer_m dkummer_u dlange dodecahedron_graph dotproduct dotsimp dpart draw draw2d draw3d drawdf draw_file draw_graph dscalar echelon edge_coloring edge_connectivity edges eigens_by_jacobi eigenvalues eigenvectors eighth einstein eivals eivects elapsed_real_time elapsed_run_time ele2comp ele2polynome ele2pui elem elementp elevation_grid elim elim_allbut eliminate eliminate_using ellipse elliptic_e elliptic_ec elliptic_eu elliptic_f elliptic_kc elliptic_pi ematrix empty_graph emptyp endcons entermatrix entertensor entier equal equalp equiv_classes erf erfc erf_generalized erfi errcatch error errormsg errors euler ev eval_string evenp every evolution evolution2d evundiff example exp expand expandwrt expandwrt_factored expint expintegral_chi expintegral_ci expintegral_e expintegral_e1 expintegral_ei expintegral_e_simplify expintegral_li expintegral_shi expintegral_si explicit explose exponentialize express expt exsec extdiff extract_linear_equations extremal_subset ezgcd %f f90 facsum factcomb factor factorfacsum factorial factorout factorsum facts fast_central_elements fast_linsolve fasttimes featurep fernfale fft fib fibtophi fifth filename_merge file_search file_type fillarray findde find_root find_root_abs find_root_error find_root_rel first fix flatten flength float floatnump floor flower_snark flush flush1deriv flushd flushnd flush_output fmin_cobyla forget fortran fourcos fourexpand fourier fourier_elim fourint fourintcos fourintsin foursimp foursin fourth fposition frame_bracket freeof freshline fresnel_c fresnel_s from_adjacency_matrix frucht_graph full_listify fullmap fullmapl fullratsimp fullratsubst fullsetify funcsolve fundamental_dimensions fundamental_units fundef funmake funp fv g0 g1 gamma gamma_greek gamma_incomplete gamma_incomplete_generalized gamma_incomplete_regularized gauss gauss_a gauss_b gaussprob gcd gcdex gcdivide gcfac gcfactor gd generalized_lambert_w genfact gen_laguerre genmatrix gensym geo_amortization geo_annuity_fv geo_annuity_pv geomap geometric geometric_mean geosum get getcurrentdirectory get_edge_weight getenv get_lu_factors get_output_stream_string get_pixel get_plot_option get_tex_environment get_tex_environment_default get_vertex_label gfactor gfactorsum ggf girth global_variances gn gnuplot_close gnuplot_replot gnuplot_reset gnuplot_restart gnuplot_start go Gosper GosperSum gr2d gr3d gradef gramschmidt graph6_decode graph6_encode graph6_export graph6_import graph_center graph_charpoly graph_eigenvalues graph_flow graph_order graph_periphery graph_product graph_size graph_union great_rhombicosidodecahedron_graph great_rhombicuboctahedron_graph grid_graph grind grobner_basis grotzch_graph hamilton_cycle hamilton_path hankel hankel_1 hankel_2 harmonic harmonic_mean hav heawood_graph hermite hessian hgfred hilbertmap hilbert_matrix hipow histogram histogram_description hodge horner hypergeometric i0 i1 %ibes ic1 ic2 ic_convert ichr1 ichr2 icosahedron_graph icosidodecahedron_graph icurvature ident identfor identity idiff idim idummy ieqn %if ifactors iframes ifs igcdex igeodesic_coords ilt image imagpart imetric implicit implicit_derivative implicit_plot indexed_tensor indices induced_subgraph inferencep inference_result infix info_display init_atensor init_ctensor in_neighbors innerproduct inpart inprod inrt integerp integer_partitions integrate intersect intersection intervalp intopois intosum invariant1 invariant2 inverse_fft inverse_jacobi_cd inverse_jacobi_cn inverse_jacobi_cs inverse_jacobi_dc inverse_jacobi_dn inverse_jacobi_ds inverse_jacobi_nc inverse_jacobi_nd inverse_jacobi_ns inverse_jacobi_sc inverse_jacobi_sd inverse_jacobi_sn invert invert_by_adjoint invert_by_lu inv_mod irr is is_biconnected is_bipartite is_connected is_digraph is_edge_in_graph is_graph is_graph_or_digraph ishow is_isomorphic isolate isomorphism is_planar isqrt isreal_p is_sconnected is_tree is_vertex_in_graph items_inference %j j0 j1 jacobi jacobian jacobi_cd jacobi_cn jacobi_cs jacobi_dc jacobi_dn jacobi_ds jacobi_nc jacobi_nd jacobi_ns jacobi_p jacobi_sc jacobi_sd jacobi_sn JF jn join jordan julia julia_set julia_sin %k kdels kdelta kill killcontext kostka kron_delta kronecker_product kummer_m kummer_u kurtosis kurtosis_bernoulli kurtosis_beta kurtosis_binomial kurtosis_chi2 kurtosis_continuous_uniform kurtosis_discrete_uniform kurtosis_exp kurtosis_f kurtosis_gamma kurtosis_general_finite_discrete kurtosis_geometric kurtosis_gumbel kurtosis_hypergeometric kurtosis_laplace kurtosis_logistic kurtosis_lognormal kurtosis_negative_binomial kurtosis_noncentral_chi2 kurtosis_noncentral_student_t kurtosis_normal kurtosis_pareto kurtosis_poisson kurtosis_rayleigh kurtosis_student_t kurtosis_weibull label labels lagrange laguerre lambda lambert_w laplace laplacian_matrix last lbfgs lc2kdt lcharp lc_l lcm lc_u ldefint ldisp ldisplay legendre_p legendre_q leinstein length let letrules letsimp levi_civita lfreeof lgtreillis lhs li liediff limit Lindstedt linear linearinterpol linear_program linear_regression line_graph linsolve listarray list_correlations listify list_matrix_entries list_nc_monomials listoftens listofvars listp lmax lmin load loadfile local locate_matrix_entry log logcontract log_gamma lopow lorentz_gauge lowercasep lpart lratsubst lreduce lriemann lsquares_estimates lsquares_estimates_approximate lsquares_estimates_exact lsquares_mse lsquares_residual_mse lsquares_residuals lsum ltreillis lu_backsub lucas lu_factor %m macroexpand macroexpand1 make_array makebox makefact makegamma make_graph make_level_picture makelist makeOrders make_poly_continent make_poly_country make_polygon make_random_state make_rgb_picture makeset make_string_input_stream make_string_output_stream make_transform mandelbrot mandelbrot_set map mapatom maplist matchdeclare matchfix mat_cond mat_fullunblocker mat_function mathml_display mat_norm matrix matrixmap matrixp matrix_size mattrace mat_trace mat_unblocker max max_clique max_degree max_flow maximize_lp max_independent_set max_matching maybe md5sum mean mean_bernoulli mean_beta mean_binomial mean_chi2 mean_continuous_uniform mean_deviation mean_discrete_uniform mean_exp mean_f mean_gamma mean_general_finite_discrete mean_geometric mean_gumbel mean_hypergeometric mean_laplace mean_logistic mean_lognormal mean_negative_binomial mean_noncentral_chi2 mean_noncentral_student_t mean_normal mean_pareto mean_poisson mean_rayleigh mean_student_t mean_weibull median median_deviation member mesh metricexpandall mgf1_sha1 min min_degree min_edge_cut minfactorial minimalPoly minimize_lp minimum_spanning_tree minor minpack_lsquares minpack_solve min_vertex_cover min_vertex_cut mkdir mnewton mod mode_declare mode_identity ModeMatrix moebius mon2schur mono monomial_dimensions multibernstein_poly multi_display_for_texinfo multi_elem multinomial multinomial_coeff multi_orbit multiplot_mode multi_pui multsym multthru mycielski_graph nary natural_unit nc_degree ncexpt ncharpoly negative_picture neighbors new newcontext newdet new_graph newline newton new_variable next_prime nicedummies niceindices ninth nofix nonarray noncentral_moment nonmetricity nonnegintegerp nonscalarp nonzeroandfreeof notequal nounify nptetrad npv nroots nterms ntermst nthroot nullity nullspace num numbered_boundaries numberp number_to_octets num_distinct_partitions numerval numfactor num_partitions nusum nzeta nzetai nzetar octets_to_number octets_to_oid odd_girth oddp ode2 ode_check odelin oid_to_octets op opena opena_binary openr openr_binary openw openw_binary operatorp opsubst optimize %or orbit orbits ordergreat ordergreatp orderless orderlessp orthogonal_complement orthopoly_recur orthopoly_weight outermap out_neighbors outofpois pade parabolic_cylinder_d parametric parametric_surface parg parGosper parse_string parse_timedate part part2cont partfrac partition partition_set partpol path_digraph path_graph pathname_directory pathname_name pathname_type pdf_bernoulli pdf_beta pdf_binomial pdf_cauchy pdf_chi2 pdf_continuous_uniform pdf_discrete_uniform pdf_exp pdf_f pdf_gamma pdf_general_finite_discrete pdf_geometric pdf_gumbel pdf_hypergeometric pdf_laplace pdf_logistic pdf_lognormal pdf_negative_binomial pdf_noncentral_chi2 pdf_noncentral_student_t pdf_normal pdf_pareto pdf_poisson pdf_rank_sum pdf_rayleigh pdf_signed_rank pdf_student_t pdf_weibull pearson_skewness permanent permut permutation permutations petersen_graph petrov pickapart picture_equalp picturep piechart piechart_description planar_embedding playback plog plot2d plot3d plotdf ploteq plsquares pochhammer points poisdiff poisexpt poisint poismap poisplus poissimp poissubst poistimes poistrim polar polarform polartorect polar_to_xy poly_add poly_buchberger poly_buchberger_criterion poly_colon_ideal poly_content polydecomp poly_depends_p poly_elimination_ideal poly_exact_divide poly_expand poly_expt poly_gcd polygon poly_grobner poly_grobner_equal poly_grobner_member poly_grobner_subsetp poly_ideal_intersection poly_ideal_polysaturation poly_ideal_polysaturation1 poly_ideal_saturation poly_ideal_saturation1 poly_lcm poly_minimization polymod poly_multiply polynome2ele polynomialp poly_normal_form poly_normalize poly_normalize_list poly_polysaturation_extension poly_primitive_part poly_pseudo_divide poly_reduced_grobner poly_reduction poly_saturation_extension poly_s_polynomial poly_subtract polytocompanion pop postfix potential power_mod powerseries powerset prefix prev_prime primep primes principal_components print printf printfile print_graph printpois printprops prodrac product properties propvars psi psubst ptriangularize pui pui2comp pui2ele pui2polynome pui_direct puireduc push put pv qput qrange qty quad_control quad_qag quad_qagi quad_qagp quad_qags quad_qawc quad_qawf quad_qawo quad_qaws quadrilateral quantile quantile_bernoulli quantile_beta quantile_binomial quantile_cauchy quantile_chi2 quantile_continuous_uniform quantile_discrete_uniform quantile_exp quantile_f quantile_gamma quantile_general_finite_discrete quantile_geometric quantile_gumbel quantile_hypergeometric quantile_laplace quantile_logistic quantile_lognormal quantile_negative_binomial quantile_noncentral_chi2 quantile_noncentral_student_t quantile_normal quantile_pareto quantile_poisson quantile_rayleigh quantile_student_t quantile_weibull quartile_skewness quit qunit quotient racah_v racah_w radcan radius random random_bernoulli random_beta random_binomial random_bipartite_graph random_cauchy random_chi2 random_continuous_uniform random_digraph random_discrete_uniform random_exp random_f random_gamma random_general_finite_discrete random_geometric random_graph random_graph1 random_gumbel random_hypergeometric random_laplace random_logistic random_lognormal random_negative_binomial random_network random_noncentral_chi2 random_noncentral_student_t random_normal random_pareto random_permutation random_poisson random_rayleigh random_regular_graph random_student_t random_tournament random_tree random_weibull range rank rat ratcoef ratdenom ratdiff ratdisrep ratexpand ratinterpol rational rationalize ratnumer ratnump ratp ratsimp ratsubst ratvars ratweight read read_array read_binary_array read_binary_list read_binary_matrix readbyte readchar read_hashed_array readline read_list read_matrix read_nested_list readonly read_xpm real_imagpart_to_conjugate realpart realroots rearray rectangle rectform rectform_log_if_constant recttopolar rediff reduce_consts reduce_order region region_boundaries region_boundaries_plus rem remainder remarray rembox remcomps remcon remcoord remfun remfunction remlet remove remove_constvalue remove_dimensions remove_edge remove_fundamental_dimensions remove_fundamental_units remove_plot_option remove_vertex rempart remrule remsym remvalue rename rename_file reset reset_displays residue resolvante resolvante_alternee1 resolvante_bipartite resolvante_diedrale resolvante_klein resolvante_klein3 resolvante_produit_sym resolvante_unitaire resolvante_vierer rest resultant return reveal reverse revert revert2 rgb2level rhs ricci riemann rinvariant risch rk rmdir rncombine romberg room rootscontract round row rowop rowswap rreduce run_testsuite %s save saving scalarp scaled_bessel_i scaled_bessel_i0 scaled_bessel_i1 scalefactors scanmap scatterplot scatterplot_description scene schur2comp sconcat scopy scsimp scurvature sdowncase sec sech second sequal sequalignore set_alt_display setdifference set_draw_defaults set_edge_weight setelmx setequalp setify setp set_partitions set_plot_option set_prompt set_random_state set_tex_environment set_tex_environment_default setunits setup_autoload set_up_dot_simplifications set_vertex_label seventh sexplode sf sha1sum sha256sum shortest_path shortest_weighted_path show showcomps showratvars sierpinskiale sierpinskimap sign signum similaritytransform simp_inequality simplify_sum simplode simpmetderiv simtran sin sinh sinsert sinvertcase sixth skewness skewness_bernoulli skewness_beta skewness_binomial skewness_chi2 skewness_continuous_uniform skewness_discrete_uniform skewness_exp skewness_f skewness_gamma skewness_general_finite_discrete skewness_geometric skewness_gumbel skewness_hypergeometric skewness_laplace skewness_logistic skewness_lognormal skewness_negative_binomial skewness_noncentral_chi2 skewness_noncentral_student_t skewness_normal skewness_pareto skewness_poisson skewness_rayleigh skewness_student_t skewness_weibull slength smake small_rhombicosidodecahedron_graph small_rhombicuboctahedron_graph smax smin smismatch snowmap snub_cube_graph snub_dodecahedron_graph solve solve_rec solve_rec_rat some somrac sort sparse6_decode sparse6_encode sparse6_export sparse6_import specint spherical spherical_bessel_j spherical_bessel_y spherical_hankel1 spherical_hankel2 spherical_harmonic spherical_to_xyz splice split sposition sprint sqfr sqrt sqrtdenest sremove sremovefirst sreverse ssearch ssort sstatus ssubst ssubstfirst staircase standardize standardize_inverse_trig starplot starplot_description status std std1 std_bernoulli std_beta std_binomial std_chi2 std_continuous_uniform std_discrete_uniform std_exp std_f std_gamma std_general_finite_discrete std_geometric std_gumbel std_hypergeometric std_laplace std_logistic std_lognormal std_negative_binomial std_noncentral_chi2 std_noncentral_student_t std_normal std_pareto std_poisson std_rayleigh std_student_t std_weibull stemplot stirling stirling1 stirling2 strim striml strimr string stringout stringp strong_components struve_h struve_l sublis sublist sublist_indices submatrix subsample subset subsetp subst substinpart subst_parallel substpart substring subvar subvarp sum sumcontract summand_to_rec supcase supcontext symbolp symmdifference symmetricp system take_channel take_inference tan tanh taylor taylorinfo taylorp taylor_simplifier taytorat tcl_output tcontract tellrat tellsimp tellsimpafter tentex tenth test_mean test_means_difference test_normality test_proportion test_proportions_difference test_rank_sum test_sign test_signed_rank test_variance test_variance_ratio tex tex1 tex_display texput %th third throw time timedate timer timer_info tldefint tlimit todd_coxeter toeplitz tokens to_lisp topological_sort to_poly to_poly_solve totaldisrep totalfourier totient tpartpol trace tracematrix trace_options transform_sample translate translate_file transpose treefale tree_reduce treillis treinat triangle triangularize trigexpand trigrat trigreduce trigsimp trunc truncate truncated_cube_graph truncated_dodecahedron_graph truncated_icosahedron_graph truncated_tetrahedron_graph tr_warnings_get tube tutte_graph ueivects uforget ultraspherical underlying_graph undiff union unique uniteigenvectors unitp units unit_step unitvector unorder unsum untellrat untimer untrace uppercasep uricci uriemann uvect vandermonde_matrix var var1 var_bernoulli var_beta var_binomial var_chi2 var_continuous_uniform var_discrete_uniform var_exp var_f var_gamma var_general_finite_discrete var_geometric var_gumbel var_hypergeometric var_laplace var_logistic var_lognormal var_negative_binomial var_noncentral_chi2 var_noncentral_student_t var_normal var_pareto var_poisson var_rayleigh var_student_t var_weibull vector vectorpotential vectorsimp verbify vers vertex_coloring vertex_connectivity vertex_degree vertex_distance vertex_eccentricity vertex_in_degree vertex_out_degree vertices vertices_to_cycle vertices_to_path %w weyl wheel_graph wiener_index wigner_3j wigner_6j wigner_9j with_stdout write_binary_data writebyte write_data writefile wronskian xreduce xthru %y Zeilberger zeroequiv zerofor zeromatrix zeromatrixp zeta zgeev zheev zlange zn_add_table zn_carmichael_lambda zn_characteristic_factors zn_determinant zn_factor_generators zn_invert_by_lu zn_log zn_mult_table absboxchar activecontexts adapt_depth additive adim aform algebraic algepsilon algexact aliases allbut all_dotsimp_denoms allocation allsym alphabetic animation antisymmetric arrays askexp assume_pos assume_pos_pred assumescalar asymbol atomgrad atrig1 axes axis_3d axis_bottom axis_left axis_right axis_top azimuth background background_color backsubst berlefact bernstein_explicit besselexpand beta_args_sum_to_integer beta_expand bftorat bftrunc bindtest border boundaries_array box boxchar breakup %c capping cauchysum cbrange cbtics center cflength cframe_flag cnonmet_flag color color_bar color_bar_tics colorbox columns commutative complex cone context contexts contour contour_levels cosnpiflag ctaypov ctaypt ctayswitch ctayvar ct_coords ctorsion_flag ctrgsimp cube current_let_rule_package cylinder data_file_name debugmode decreasing default_let_rule_package delay dependencies derivabbrev derivsubst detout diagmetric diff dim dimensions dispflag display2d|10 display_format_internal distribute_over doallmxops domain domxexpt domxmxops domxnctimes dontfactor doscmxops doscmxplus dot0nscsimp dot0simp dot1simp dotassoc dotconstrules dotdistrib dotexptsimp dotident dotscrules draw_graph_program draw_realpart edge_color edge_coloring edge_partition edge_type edge_width %edispflag elevation %emode endphi endtheta engineering_format_floats enhanced3d %enumer epsilon_lp erfflag erf_representation errormsg error_size error_syms error_type %e_to_numlog eval even evenfun evflag evfun ev_point expandwrt_denom expintexpand expintrep expon expop exptdispflag exptisolate exptsubst facexpand facsum_combine factlim factorflag factorial_expand factors_only fb feature features file_name file_output_append file_search_demo file_search_lisp file_search_maxima|10 file_search_tests file_search_usage file_type_lisp file_type_maxima|10 fill_color fill_density filled_func fixed_vertices flipflag float2bf font font_size fortindent fortspaces fpprec fpprintprec functions gamma_expand gammalim gdet genindex gensumnum GGFCFMAX GGFINFINITY globalsolve gnuplot_command gnuplot_curve_styles gnuplot_curve_titles gnuplot_default_term_command gnuplot_dumb_term_command gnuplot_file_args gnuplot_file_name gnuplot_out_file gnuplot_pdf_term_command gnuplot_pm3d gnuplot_png_term_command gnuplot_postamble gnuplot_preamble gnuplot_ps_term_command gnuplot_svg_term_command gnuplot_term gnuplot_view_args Gosper_in_Zeilberger gradefs grid grid2d grind halfangles head_angle head_both head_length head_type height hypergeometric_representation %iargs ibase icc1 icc2 icounter idummyx ieqnprint ifb ifc1 ifc2 ifg ifgi ifr iframe_bracket_form ifri igeowedge_flag ikt1 ikt2 imaginary inchar increasing infeval infinity inflag infolists inm inmc1 inmc2 intanalysis integer integervalued integrate_use_rootsof integration_constant integration_constant_counter interpolate_color intfaclim ip_grid ip_grid_in irrational isolate_wrt_times iterations itr julia_parameter %k1 %k2 keepfloat key key_pos kinvariant kt label label_alignment label_orientation labels lassociative lbfgs_ncorrections lbfgs_nfeval_max leftjust legend letrat let_rule_packages lfg lg lhospitallim limsubst linear linear_solver linechar linel|10 linenum line_type linewidth line_width linsolve_params linsolvewarn lispdisp listarith listconstvars listdummyvars lmxchar load_pathname loadprint logabs logarc logcb logconcoeffp logexpand lognegint logsimp logx logx_secondary logy logy_secondary logz lriem m1pbranch macroexpansion macros mainvar manual_demo maperror mapprint matrix_element_add matrix_element_mult matrix_element_transpose maxapplydepth maxapplyheight maxima_tempdir|10 maxima_userdir|10 maxnegex MAX_ORD maxposex maxpsifracdenom maxpsifracnum maxpsinegint maxpsiposint maxtayorder mesh_lines_color method mod_big_prime mode_check_errorp mode_checkp mode_check_warnp mod_test mod_threshold modular_linear_solver modulus multiplicative multiplicities myoptions nary negdistrib negsumdispflag newline newtonepsilon newtonmaxiter nextlayerfactor niceindicespref nm nmc noeval nolabels nonegative_lp noninteger nonscalar noun noundisp nouns np npi nticks ntrig numer numer_pbranch obase odd oddfun opacity opproperties opsubst optimprefix optionset orientation origin orthopoly_returns_intervals outative outchar packagefile palette partswitch pdf_file pfeformat phiresolution %piargs piece pivot_count_sx pivot_max_sx plot_format plot_options plot_realpart png_file pochhammer_max_index points pointsize point_size points_joined point_type poislim poisson poly_coefficient_ring poly_elimination_order polyfactor poly_grobner_algorithm poly_grobner_debug poly_monomial_order poly_primary_elimination_order poly_return_term_list poly_secondary_elimination_order poly_top_reduction_only posfun position powerdisp pred prederror primep_number_of_tests product_use_gamma program programmode promote_float_to_bigfloat prompt proportional_axes props psexpand ps_file radexpand radius radsubstflag rassociative ratalgdenom ratchristof ratdenomdivide rateinstein ratepsilon ratfac rational ratmx ratprint ratriemann ratsimpexpons ratvarswitch ratweights ratweyl ratwtlvl real realonly redraw refcheck resolution restart resultant ric riem rmxchar %rnum_list rombergabs rombergit rombergmin rombergtol rootsconmode rootsepsilon run_viewer same_xy same_xyz savedef savefactors scalar scalarmatrixp scale scale_lp setcheck setcheckbreak setval show_edge_color show_edges show_edge_type show_edge_width show_id show_label showtime show_vertex_color show_vertex_size show_vertex_type show_vertices show_weight simp simplified_output simplify_products simpproduct simpsum sinnpiflag solvedecomposes solveexplicit solvefactors solvenullwarn solveradcan solvetrigwarn space sparse sphere spring_embedding_depth sqrtdispflag stardisp startphi starttheta stats_numer stringdisp structures style sublis_apply_lambda subnumsimp sumexpand sumsplitfact surface surface_hide svg_file symmetric tab taylordepth taylor_logexpand taylor_order_coefficients taylor_truncate_polynomials tensorkill terminal testsuite_files thetaresolution timer_devalue title tlimswitch tr track transcompile transform transform_xy translate_fast_arrays transparent transrun tr_array_as_ref tr_bound_function_applyp tr_file_tty_messagesp tr_float_can_branch_complex tr_function_call_default trigexpandplus trigexpandtimes triginverses trigsign trivial_solutions tr_numer tr_optimize_max_loop tr_semicompile tr_state_vars tr_warn_bad_function_calls tr_warn_fexpr tr_warn_meval tr_warn_mode tr_warn_undeclared tr_warn_undefined_variable tstep ttyoff tube_extremes ufg ug %unitexpand unit_vectors uric uriem use_fast_arrays user_preamble usersetunits values vect_cross verbose vertex_color vertex_coloring vertex_partition vertex_size vertex_type view warnings weyl width windowname windowtitle wired_surface wireframe xaxis xaxis_color xaxis_secondary xaxis_type xaxis_width xlabel xlabel_secondary xlength xrange xrange_secondary xtics xtics_axis xtics_rotate xtics_rotate_secondary xtics_secondary xtics_secondary_axis xu_grid x_voxel xy_file xyplane xy_scale yaxis yaxis_color yaxis_secondary yaxis_type yaxis_width ylabel ylabel_secondary ylength yrange yrange_secondary ytics ytics_axis ytics_rotate ytics_rotate_secondary ytics_secondary ytics_secondary_axis yv_grid y_voxel yx_ratio zaxis zaxis_color zaxis_type zaxis_width zeroa zerob zerobern zeta%pi zlabel zlabel_rotate zlength zmin zn_primroot_limit zn_primroot_pretest",
+symbol:"_ __ %|0 %%|0"},contains:[{className:"comment",begin:"/\\*",end:"\\*/",
+contains:["self"]},e.QUOTE_STRING_MODE,{className:"number",relevance:0,
+variants:[{begin:"\\b(\\d+|\\d+\\.|\\.\\d+|\\d+\\.\\d+)[Ee][-+]?\\d+\\b"},{
+begin:"\\b(\\d+|\\d+\\.|\\.\\d+|\\d+\\.\\d+)[Bb][-+]?\\d+\\b",relevance:10},{
+begin:"\\b(\\.\\d+|\\d+\\.\\d+)\\b"},{begin:"\\b(\\d+|0[0-9A-Za-z]+)\\.?\\b"}]
+}],illegal:/@/})})());
+hljs.registerLanguage("mel",(()=>{"use strict";return e=>({name:"MEL",
+keywords:"int float string vector matrix if else switch case default while do for in break continue global proc return about abs addAttr addAttributeEditorNodeHelp addDynamic addNewShelfTab addPP addPanelCategory addPrefixToName advanceToNextDrivenKey affectedNet affects aimConstraint air alias aliasAttr align alignCtx alignCurve alignSurface allViewFit ambientLight angle angleBetween animCone animCurveEditor animDisplay animView annotate appendStringArray applicationName applyAttrPreset applyTake arcLenDimContext arcLengthDimension arclen arrayMapper art3dPaintCtx artAttrCtx artAttrPaintVertexCtx artAttrSkinPaintCtx artAttrTool artBuildPaintMenu artFluidAttrCtx artPuttyCtx artSelectCtx artSetPaintCtx artUserPaintCtx assignCommand assignInputDevice assignViewportFactories attachCurve attachDeviceAttr attachSurface attrColorSliderGrp attrCompatibility attrControlGrp attrEnumOptionMenu attrEnumOptionMenuGrp attrFieldGrp attrFieldSliderGrp attrNavigationControlGrp attrPresetEditWin attributeExists attributeInfo attributeMenu attributeQuery autoKeyframe autoPlace bakeClip bakeFluidShading bakePartialHistory bakeResults bakeSimulation basename basenameEx batchRender bessel bevel bevelPlus binMembership bindSkin blend2 blendShape blendShapeEditor blendShapePanel blendTwoAttr blindDataType boneLattice boundary boxDollyCtx boxZoomCtx bufferCurve buildBookmarkMenu buildKeyframeMenu button buttonManip CBG cacheFile cacheFileCombine cacheFileMerge cacheFileTrack camera cameraView canCreateManip canvas capitalizeString catch catchQuiet ceil changeSubdivComponentDisplayLevel changeSubdivRegion channelBox character characterMap characterOutlineEditor characterize chdir checkBox checkBoxGrp checkDefaultRenderGlobals choice circle circularFillet clamp clear clearCache clip clipEditor clipEditorCurrentTimeCtx clipSchedule clipSchedulerOutliner clipTrimBefore closeCurve closeSurface cluster cmdFileOutput cmdScrollFieldExecuter cmdScrollFieldReporter cmdShell coarsenSubdivSelectionList collision color colorAtPoint colorEditor colorIndex colorIndexSliderGrp colorSliderButtonGrp colorSliderGrp columnLayout commandEcho commandLine commandPort compactHairSystem componentEditor compositingInterop computePolysetVolume condition cone confirmDialog connectAttr connectControl connectDynamic connectJoint connectionInfo constrain constrainValue constructionHistory container containsMultibyte contextInfo control convertFromOldLayers convertIffToPsd convertLightmap convertSolidTx convertTessellation convertUnit copyArray copyFlexor copyKey copySkinWeights cos cpButton cpCache cpClothSet cpCollision cpConstraint cpConvClothToMesh cpForces cpGetSolverAttr cpPanel cpProperty cpRigidCollisionFilter cpSeam cpSetEdit cpSetSolverAttr cpSolver cpSolverTypes cpTool cpUpdateClothUVs createDisplayLayer createDrawCtx createEditor createLayeredPsdFile createMotionField createNewShelf createNode createRenderLayer createSubdivRegion cross crossProduct ctxAbort ctxCompletion ctxEditMode ctxTraverse currentCtx currentTime currentTimeCtx currentUnit curve curveAddPtCtx curveCVCtx curveEPCtx curveEditorCtx curveIntersect curveMoveEPCtx curveOnSurface curveSketchCtx cutKey cycleCheck cylinder dagPose date defaultLightListCheckBox defaultNavigation defineDataServer defineVirtualDevice deformer deg_to_rad delete deleteAttr deleteShadingGroupsAndMaterials deleteShelfTab deleteUI deleteUnusedBrushes delrandstr detachCurve detachDeviceAttr detachSurface deviceEditor devicePanel dgInfo dgdirty dgeval dgtimer dimWhen directKeyCtx directionalLight dirmap dirname disable disconnectAttr disconnectJoint diskCache displacementToPoly displayAffected displayColor displayCull displayLevelOfDetail displayPref displayRGBColor displaySmoothness displayStats displayString displaySurface distanceDimContext distanceDimension doBlur dolly dollyCtx dopeSheetEditor dot dotProduct doubleProfileBirailSurface drag dragAttrContext draggerContext dropoffLocator duplicate duplicateCurve duplicateSurface dynCache dynControl dynExport dynExpression dynGlobals dynPaintEditor dynParticleCtx dynPref dynRelEdPanel dynRelEditor dynamicLoad editAttrLimits editDisplayLayerGlobals editDisplayLayerMembers editRenderLayerAdjustment editRenderLayerGlobals editRenderLayerMembers editor editorTemplate effector emit emitter enableDevice encodeString endString endsWith env equivalent equivalentTol erf error eval evalDeferred evalEcho event exactWorldBoundingBox exclusiveLightCheckBox exec executeForEachObject exists exp expression expressionEditorListen extendCurve extendSurface extrude fcheck fclose feof fflush fgetline fgetword file fileBrowserDialog fileDialog fileExtension fileInfo filetest filletCurve filter filterCurve filterExpand filterStudioImport findAllIntersections findAnimCurves findKeyframe findMenuItem findRelatedSkinCluster finder firstParentOf fitBspline flexor floatEq floatField floatFieldGrp floatScrollBar floatSlider floatSlider2 floatSliderButtonGrp floatSliderGrp floor flow fluidCacheInfo fluidEmitter fluidVoxelInfo flushUndo fmod fontDialog fopen formLayout format fprint frameLayout fread freeFormFillet frewind fromNativePath fwrite gamma gauss geometryConstraint getApplicationVersionAsFloat getAttr getClassification getDefaultBrush getFileList getFluidAttr getInputDeviceRange getMayaPanelTypes getModifiers getPanel getParticleAttr getPluginResource getenv getpid glRender glRenderEditor globalStitch gmatch goal gotoBindPose grabColor gradientControl gradientControlNoAttr graphDollyCtx graphSelectContext graphTrackCtx gravity grid gridLayout group groupObjectsByName HfAddAttractorToAS HfAssignAS HfBuildEqualMap HfBuildFurFiles HfBuildFurImages HfCancelAFR HfConnectASToHF HfCreateAttractor HfDeleteAS HfEditAS HfPerformCreateAS HfRemoveAttractorFromAS HfSelectAttached HfSelectAttractors HfUnAssignAS hardenPointCurve hardware hardwareRenderPanel headsUpDisplay headsUpMessage help helpLine hermite hide hilite hitTest hotBox hotkey hotkeyCheck hsv_to_rgb hudButton hudSlider hudSliderButton hwReflectionMap hwRender hwRenderLoad hyperGraph hyperPanel hyperShade hypot iconTextButton iconTextCheckBox iconTextRadioButton iconTextRadioCollection iconTextScrollList iconTextStaticLabel ikHandle ikHandleCtx ikHandleDisplayScale ikSolver ikSplineHandleCtx ikSystem ikSystemInfo ikfkDisplayMethod illustratorCurves image imfPlugins inheritTransform insertJoint insertJointCtx insertKeyCtx insertKnotCurve insertKnotSurface instance instanceable instancer intField intFieldGrp intScrollBar intSlider intSliderGrp interToUI internalVar intersect iprEngine isAnimCurve isConnected isDirty isParentOf isSameObject isTrue isValidObjectName isValidString isValidUiName isolateSelect itemFilter itemFilterAttr itemFilterRender itemFilterType joint jointCluster jointCtx jointDisplayScale jointLattice keyTangent keyframe keyframeOutliner keyframeRegionCurrentTimeCtx keyframeRegionDirectKeyCtx keyframeRegionDollyCtx keyframeRegionInsertKeyCtx keyframeRegionMoveKeyCtx keyframeRegionScaleKeyCtx keyframeRegionSelectKeyCtx keyframeRegionSetKeyCtx keyframeRegionTrackCtx keyframeStats lassoContext lattice latticeDeformKeyCtx launch launchImageEditor layerButton layeredShaderPort layeredTexturePort layout layoutDialog lightList lightListEditor lightListPanel lightlink lineIntersection linearPrecision linstep listAnimatable listAttr listCameras listConnections listDeviceAttachments listHistory listInputDeviceAxes listInputDeviceButtons listInputDevices listMenuAnnotation listNodeTypes listPanelCategories listRelatives listSets listTransforms listUnselected listerEditor loadFluid loadNewShelf loadPlugin loadPluginLanguageResources loadPrefObjects localizedPanelLabel lockNode loft log longNameOf lookThru ls lsThroughFilter lsType lsUI Mayatomr mag makeIdentity makeLive makePaintable makeRoll makeSingleSurface makeTubeOn makebot manipMoveContext manipMoveLimitsCtx manipOptions manipRotateContext manipRotateLimitsCtx manipScaleContext manipScaleLimitsCtx marker match max memory menu menuBarLayout menuEditor menuItem menuItemToShelf menuSet menuSetPref messageLine min minimizeApp mirrorJoint modelCurrentTimeCtx modelEditor modelPanel mouse movIn movOut move moveIKtoFK moveKeyCtx moveVertexAlongDirection multiProfileBirailSurface mute nParticle nameCommand nameField namespace namespaceInfo newPanelItems newton nodeCast nodeIconButton nodeOutliner nodePreset nodeType noise nonLinear normalConstraint normalize nurbsBoolean nurbsCopyUVSet nurbsCube nurbsEditUV nurbsPlane nurbsSelect nurbsSquare nurbsToPoly nurbsToPolygonsPref nurbsToSubdiv nurbsToSubdivPref nurbsUVSet nurbsViewDirectionVector objExists objectCenter objectLayer objectType objectTypeUI obsoleteProc oceanNurbsPreviewPlane offsetCurve offsetCurveOnSurface offsetSurface openGLExtension openMayaPref optionMenu optionMenuGrp optionVar orbit orbitCtx orientConstraint outlinerEditor outlinerPanel overrideModifier paintEffectsDisplay pairBlend palettePort paneLayout panel panelConfiguration panelHistory paramDimContext paramDimension paramLocator parent parentConstraint particle particleExists particleInstancer particleRenderInfo partition pasteKey pathAnimation pause pclose percent performanceOptions pfxstrokes pickWalk picture pixelMove planarSrf plane play playbackOptions playblast plugAttr plugNode pluginInfo pluginResourceUtil pointConstraint pointCurveConstraint pointLight pointMatrixMult pointOnCurve pointOnSurface pointPosition poleVectorConstraint polyAppend polyAppendFacetCtx polyAppendVertex polyAutoProjection polyAverageNormal polyAverageVertex polyBevel polyBlendColor polyBlindData polyBoolOp polyBridgeEdge polyCacheMonitor polyCheck polyChipOff polyClipboard polyCloseBorder polyCollapseEdge polyCollapseFacet polyColorBlindData polyColorDel polyColorPerVertex polyColorSet polyCompare polyCone polyCopyUV polyCrease polyCreaseCtx polyCreateFacet polyCreateFacetCtx polyCube polyCut polyCutCtx polyCylinder polyCylindricalProjection polyDelEdge polyDelFacet polyDelVertex polyDuplicateAndConnect polyDuplicateEdge polyEditUV polyEditUVShell polyEvaluate polyExtrudeEdge polyExtrudeFacet polyExtrudeVertex polyFlipEdge polyFlipUV polyForceUV polyGeoSampler polyHelix polyInfo polyInstallAction polyLayoutUV polyListComponentConversion polyMapCut polyMapDel polyMapSew polyMapSewMove polyMergeEdge polyMergeEdgeCtx polyMergeFacet polyMergeFacetCtx polyMergeUV polyMergeVertex polyMirrorFace polyMoveEdge polyMoveFacet polyMoveFacetUV polyMoveUV polyMoveVertex polyNormal polyNormalPerVertex polyNormalizeUV polyOptUvs polyOptions polyOutput polyPipe polyPlanarProjection polyPlane polyPlatonicSolid polyPoke polyPrimitive polyPrism polyProjection polyPyramid polyQuad polyQueryBlindData polyReduce polySelect polySelectConstraint polySelectConstraintMonitor polySelectCtx polySelectEditCtx polySeparate polySetToFaceNormal polySewEdge polyShortestPathCtx polySmooth polySoftEdge polySphere polySphericalProjection polySplit polySplitCtx polySplitEdge polySplitRing polySplitVertex polyStraightenUVBorder polySubdivideEdge polySubdivideFacet polyToSubdiv polyTorus polyTransfer polyTriangulate polyUVSet polyUnite polyWedgeFace popen popupMenu pose pow preloadRefEd print progressBar progressWindow projFileViewer projectCurve projectTangent projectionContext projectionManip promptDialog propModCtx propMove psdChannelOutliner psdEditTextureFile psdExport psdTextureFile putenv pwd python querySubdiv quit rad_to_deg radial radioButton radioButtonGrp radioCollection radioMenuItemCollection rampColorPort rand randomizeFollicles randstate rangeControl readTake rebuildCurve rebuildSurface recordAttr recordDevice redo reference referenceEdit referenceQuery refineSubdivSelectionList refresh refreshAE registerPluginResource rehash reloadImage removeJoint removeMultiInstance removePanelCategory rename renameAttr renameSelectionList renameUI render renderGlobalsNode renderInfo renderLayerButton renderLayerParent renderLayerPostProcess renderLayerUnparent renderManip renderPartition renderQualityNode renderSettings renderThumbnailUpdate renderWindowEditor renderWindowSelectContext renderer reorder reorderDeformers requires reroot resampleFluid resetAE resetPfxToPolyCamera resetTool resolutionNode retarget reverseCurve reverseSurface revolve rgb_to_hsv rigidBody rigidSolver roll rollCtx rootOf rot rotate rotationInterpolation roundConstantRadius rowColumnLayout rowLayout runTimeCommand runup sampleImage saveAllShelves saveAttrPreset saveFluid saveImage saveInitialState saveMenu savePrefObjects savePrefs saveShelf saveToolSettings scale scaleBrushBrightness scaleComponents scaleConstraint scaleKey scaleKeyCtx sceneEditor sceneUIReplacement scmh scriptCtx scriptEditorInfo scriptJob scriptNode scriptTable scriptToShelf scriptedPanel scriptedPanelType scrollField scrollLayout sculpt searchPathArray seed selLoadSettings select selectContext selectCurveCV selectKey selectKeyCtx selectKeyframeRegionCtx selectMode selectPref selectPriority selectType selectedNodes selectionConnection separator setAttr setAttrEnumResource setAttrMapping setAttrNiceNameResource setConstraintRestPosition setDefaultShadingGroup setDrivenKeyframe setDynamic setEditCtx setEditor setFluidAttr setFocus setInfinity setInputDeviceMapping setKeyCtx setKeyPath setKeyframe setKeyframeBlendshapeTargetWts setMenuMode setNodeNiceNameResource setNodeTypeFlag setParent setParticleAttr setPfxToPolyCamera setPluginResource setProject setStampDensity setStartupMessage setState setToolTo setUITemplate setXformManip sets shadingConnection shadingGeometryRelCtx shadingLightRelCtx shadingNetworkCompare shadingNode shapeCompare shelfButton shelfLayout shelfTabLayout shellField shortNameOf showHelp showHidden showManipCtx showSelectionInTitle showShadingGroupAttrEditor showWindow sign simplify sin singleProfileBirailSurface size sizeBytes skinCluster skinPercent smoothCurve smoothTangentSurface smoothstep snap2to2 snapKey snapMode snapTogetherCtx snapshot soft softMod softModCtx sort sound soundControl source spaceLocator sphere sphrand spotLight spotLightPreviewPort spreadSheetEditor spring sqrt squareSurface srtContext stackTrace startString startsWith stitchAndExplodeShell stitchSurface stitchSurfacePoints strcmp stringArrayCatenate stringArrayContains stringArrayCount stringArrayInsertAtIndex stringArrayIntersector stringArrayRemove stringArrayRemoveAtIndex stringArrayRemoveDuplicates stringArrayRemoveExact stringArrayToString stringToStringArray strip stripPrefixFromName stroke subdAutoProjection subdCleanTopology subdCollapse subdDuplicateAndConnect subdEditUV subdListComponentConversion subdMapCut subdMapSewMove subdMatchTopology subdMirror subdToBlind subdToPoly subdTransferUVsToCache subdiv subdivCrease subdivDisplaySmoothness substitute substituteAllString substituteGeometry substring surface surfaceSampler surfaceShaderList swatchDisplayPort switchTable symbolButton symbolCheckBox sysFile system tabLayout tan tangentConstraint texLatticeDeformContext texManipContext texMoveContext texMoveUVShellContext texRotateContext texScaleContext texSelectContext texSelectShortestPathCtx texSmudgeUVContext texWinToolCtx text textCurves textField textFieldButtonGrp textFieldGrp textManip textScrollList textToShelf textureDisplacePlane textureHairColor texturePlacementContext textureWindow threadCount threePointArcCtx timeControl timePort timerX toNativePath toggle toggleAxis toggleWindowVisibility tokenize tokenizeList tolerance tolower toolButton toolCollection toolDropped toolHasOptions toolPropertyWindow torus toupper trace track trackCtx transferAttributes transformCompare transformLimits translator trim trunc truncateFluidCache truncateHairCache tumble tumbleCtx turbulence twoPointArcCtx uiRes uiTemplate unassignInputDevice undo undoInfo ungroup uniform unit unloadPlugin untangleUV untitledFileName untrim upAxis updateAE userCtx uvLink uvSnapshot validateShelfName vectorize view2dToolCtx viewCamera viewClipPlane viewFit viewHeadOn viewLookAt viewManip viewPlace viewSet visor volumeAxis vortex waitCursor warning webBrowser webBrowserPrefs whatIs window windowPref wire wireContext workspace wrinkle wrinkleContext writeTake xbmLangPathList xform",
+illegal:"</",contains:[e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{
+className:"string",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE]},{
+begin:/[$%@](\^\w\b|#\w+|[^\s\w{]|\{\w+\}|\w+)/
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]})})());
+hljs.registerLanguage("mercury",(()=>{"use strict";return e=>{
+const i=e.COMMENT("%","$"),n=e.inherit(e.APOS_STRING_MODE,{relevance:0
+}),r=e.inherit(e.QUOTE_STRING_MODE,{relevance:0})
+;return r.contains=r.contains.slice(),r.contains.push({className:"subst",
+begin:"\\\\[abfnrtv]\\|\\\\x[0-9a-fA-F]*\\\\\\|%[-+# *.0-9]*[dioxXucsfeEgGp]",
+relevance:0}),{name:"Mercury",aliases:["m","moo"],keywords:{
+keyword:"module use_module import_module include_module end_module initialise mutable initialize finalize finalise interface implementation pred mode func type inst solver any_pred any_func is semidet det nondet multi erroneous failure cc_nondet cc_multi typeclass instance where pragma promise external trace atomic or_else require_complete_switch require_det require_semidet require_multi require_nondet require_cc_multi require_cc_nondet require_erroneous require_failure",
 meta:"inline no_inline type_spec source_file fact_table obsolete memo loop_check minimal_model terminates does_not_terminate check_termination promise_equivalent_clauses foreign_proc foreign_decl foreign_code foreign_type foreign_import_module foreign_export_enum foreign_export foreign_enum may_call_mercury will_not_call_mercury thread_safe not_thread_safe maybe_thread_safe promise_pure promise_semipure tabled_for_io local untrailed trailed attach_to_io_state can_pass_as_mercury_type stable will_not_throw_exception may_modify_trail will_not_modify_trail may_duplicate may_not_duplicate affects_liveness does_not_affect_liveness doesnt_affect_liveness no_sharing unknown_sharing sharing",
-built_in:"some all not if then else true fail false try catch catch_any semidet_true semidet_false semidet_fail impure_true impure semipure"},contains:[{className:"built_in",variants:[{begin:"<=>"},{begin:"<=",relevance:0},{begin:"=>",relevance:0},{begin:"/\\\\"},{begin:"\\\\/"}]},{className:"built_in",variants:[{begin:":-\\|--\x3e"},{begin:"=",relevance:0}]},b,a.C_BLOCK_COMMENT_MODE,{className:"number",begin:"0'.\\|0[box][0-9a-fA-F]*"},a.NUMBER_MODE,c,d,{begin:/:-/},{begin:/\.$/}]}}}());
-hljs.registerLanguage("mipsasm",function(){return function(a){return{name:"MIPS Assembly",case_insensitive:!0,aliases:["mips"],keywords:{$pattern:"\\.?"+a.IDENT_RE,meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .ltorg ",built_in:"$0 $1 $2 $3 $4 $5 $6 $7 $8 $9 $10 $11 $12 $13 $14 $15 $16 $17 $18 $19 $20 $21 $22 $23 $24 $25 $26 $27 $28 $29 $30 $31 zero at v0 v1 a0 a1 a2 a3 a4 a5 a6 a7 t0 t1 t2 t3 t4 t5 t6 t7 t8 t9 s0 s1 s2 s3 s4 s5 s6 s7 s8 k0 k1 gp sp fp ra $f0 $f1 $f2 $f2 $f4 $f5 $f6 $f7 $f8 $f9 $f10 $f11 $f12 $f13 $f14 $f15 $f16 $f17 $f18 $f19 $f20 $f21 $f22 $f23 $f24 $f25 $f26 $f27 $f28 $f29 $f30 $f31 Context Random EntryLo0 EntryLo1 Context PageMask Wired EntryHi HWREna BadVAddr Count Compare SR IntCtl SRSCtl SRSMap Cause EPC PRId EBase Config Config1 Config2 Config3 LLAddr Debug DEPC DESAVE CacheErr ECC ErrorEPC TagLo DataLo TagHi DataHi WatchLo WatchHi PerfCtl PerfCnt "},
-contains:[{className:"keyword",begin:"\\b(addi?u?|andi?|b(al)?|beql?|bgez(al)?l?|bgtzl?|blezl?|bltz(al)?l?|bnel?|cl[oz]|divu?|ext|ins|j(al)?|jalr(.hb)?|jr(.hb)?|lbu?|lhu?|ll|lui|lw[lr]?|maddu?|mfhi|mflo|movn|movz|move|msubu?|mthi|mtlo|mul|multu?|nop|nor|ori?|rotrv?|sb|sc|se[bh]|sh|sllv?|slti?u?|srav?|srlv?|subu?|sw[lr]?|xori?|wsbh|abs.[sd]|add.[sd]|alnv.ps|bc1[ft]l?|c.(s?f|un|u?eq|[ou]lt|[ou]le|ngle?|seq|l[et]|ng[et]).[sd]|(ceil|floor|round|trunc).[lw].[sd]|cfc1|cvt.d.[lsw]|cvt.l.[dsw]|cvt.ps.s|cvt.s.[dlw]|cvt.s.p[lu]|cvt.w.[dls]|div.[ds]|ldx?c1|luxc1|lwx?c1|madd.[sd]|mfc1|mov[fntz]?.[ds]|msub.[sd]|mth?c1|mul.[ds]|neg.[ds]|nmadd.[ds]|nmsub.[ds]|p[lu][lu].ps|recip.fmt|r?sqrt.[ds]|sdx?c1|sub.[ds]|suxc1|swx?c1|break|cache|d?eret|[de]i|ehb|mfc0|mtc0|pause|prefx?|rdhwr|rdpgpr|sdbbp|ssnop|synci?|syscall|teqi?|tgei?u?|tlb(p|r|w[ir])|tlti?u?|tnei?|wait|wrpgpr)",
-end:"\\s"},a.COMMENT("[;#](?!s*$)","$"),a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"[^\\\\]'",relevance:0},{className:"title",begin:"\\|",end:"\\|",illegal:"\\n",relevance:0},{className:"number",variants:[{begin:"0x[0-9a-f]+"},{begin:"\\b-?\\d+"}],relevance:0},{className:"symbol",variants:[{begin:"^\\s*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{begin:"^\\s*[0-9]+:"},{begin:"[0-9]+[bf]"}],relevance:0}],illegal:"/"}}}());
-hljs.registerLanguage("mizar",function(){return function(a){return{name:"Mizar",keywords:"environ vocabularies notations constructors definitions registrations theorems schemes requirements begin end definition registration cluster existence pred func defpred deffunc theorem proof let take assume then thus hence ex for st holds consider reconsider such that and in provided of as from be being by means equals implies iff redefine define now not or attr is mode suppose per cases set thesis contradiction scheme reserve struct correctness compatibility coherence symmetry assymetry reflexivity irreflexivity connectedness uniqueness commutativity idempotence involutiveness projectivity",
-contains:[a.COMMENT("::","$")]}}}());
-hljs.registerLanguage("perl",function(){return function(a){var b={$pattern:/[\w.]+/,keyword:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qq fileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmget sub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedir ioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"},c=
-{className:"subst",begin:"[$@]\\{",end:"\\}",keywords:b},d={begin:"->{",end:"}"},e={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},f=[a.BACKSLASH_ESCAPE,c,e];a=[e,a.HASH_COMMENT_MODE,a.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),d,{className:"string",contains:f,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",
-end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",end:"\\>",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},{begin:"{\\w+}",contains:[],relevance:0},{begin:"-?\\w+\\s*\\=\\>",contains:[],relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+a.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",
-keywords:"split return print reverse grep",relevance:0,contains:[a.HASH_COMMENT_MODE,{className:"regexp",begin:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",relevance:10},{className:"regexp",begin:"(m|qr)?/",end:"/[a-z]*",contains:[a.BACKSLASH_ESCAPE],relevance:0}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[a.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",
-end:"$",className:"comment"}]}];c.contains=a;d.contains=a;return{name:"Perl",aliases:["pl","pm"],keywords:b,contains:a}}}());hljs.registerLanguage("mojolicious",function(){return function(a){return{name:"Mojolicious",subLanguage:"xml",contains:[{className:"meta",begin:"^__(END|DATA)__$"},{begin:"^\\s*%{1,2}={0,2}",end:"$",subLanguage:"perl"},{begin:"<%{1,2}={0,2}",end:"={0,1}%>",subLanguage:"perl",excludeBegin:!0,excludeEnd:!0}]}}}());
-hljs.registerLanguage("monkey",function(){return function(a){var b={className:"number",relevance:0,variants:[{begin:"[$][a-fA-F0-9]+"},a.NUMBER_MODE]};return{name:"Monkey",case_insensitive:!0,keywords:{keyword:"public private property continue exit extern new try catch eachin not abstract final select case default const local global field end if then else elseif endif while wend repeat until forever for to step next return module inline throw import",built_in:"DebugLog DebugStop Error Print ACos ACosr ASin ASinr ATan ATan2 ATan2r ATanr Abs Abs Ceil Clamp Clamp Cos Cosr Exp Floor Log Max Max Min Min Pow Sgn Sgn Sin Sinr Sqrt Tan Tanr Seed PI HALFPI TWOPI",
-literal:"true false null and or shl shr mod"},illegal:/\/\*/,contains:[a.COMMENT("#rem","#end"),a.COMMENT("'","$",{relevance:0}),{className:"function",beginKeywords:"function method",end:"[(=:]|$",illegal:/\n/,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"class",beginKeywords:"class interface",end:"$",contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{className:"built_in",begin:"\\b(self|super)\\b"},{className:"meta",begin:"\\s*#",end:"$",keywords:{"meta-keyword":"if else elseif endif end then"}},
-{className:"meta",begin:"^\\s*strict\\b"},{beginKeywords:"alias",end:"=",contains:[a.UNDERSCORE_TITLE_MODE]},a.QUOTE_STRING_MODE,b]}}}());
-hljs.registerLanguage("moonscript",function(){return function(a){var b={keyword:"if then not for in while do return else elseif break continue switch and or unless when class extends super local import export from using",literal:"true false nil",built_in:"_G _VERSION assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall coroutine debug io math os package string table"},c=
-{className:"subst",begin:/#\{/,end:/}/,keywords:b},d=[a.inherit(a.C_NUMBER_MODE,{starts:{end:"(\\s*/)?",relevance:0}}),{className:"string",variants:[{begin:/'/,end:/'/,contains:[a.BACKSLASH_ESCAPE]},{begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,c]}]},{className:"built_in",begin:"@__"+a.IDENT_RE},{begin:"@"+a.IDENT_RE},{begin:a.IDENT_RE+"\\\\"+a.IDENT_RE}];c.contains=d;c=a.inherit(a.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"});var e={className:"params",begin:"\\([^\\(]",returnBegin:!0,contains:[{begin:/\(/,
-end:/\)/,keywords:b,contains:["self"].concat(d)}]};return{name:"MoonScript",aliases:["moon"],keywords:b,illegal:/\/\*/,contains:d.concat([a.COMMENT("--","$"),{className:"function",begin:"^\\s*[A-Za-z$_][0-9A-Za-z$_]*\\s*=\\s*(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[c,e]},{begin:/[\(,:=]\s*/,relevance:0,contains:[{className:"function",begin:"(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[e]}]},{className:"class",beginKeywords:"class",end:"$",illegal:/[:="\[\]]/,
-contains:[{beginKeywords:"extends",endsWithParent:!0,illegal:/[:="\[\]]/,contains:[c]},c]},{className:"name",begin:"[A-Za-z$_][0-9A-Za-z$_]*:",end:":",returnBegin:!0,returnEnd:!0,relevance:0}])}}}());
-hljs.registerLanguage("n1ql",function(){return function(a){return{name:"N1QL",case_insensitive:!0,contains:[{beginKeywords:"build create index delete drop explain infer|10 insert merge prepare select update upsert|10",end:/;/,endsWithParent:!0,keywords:{keyword:"all alter analyze and any array as asc begin between binary boolean break bucket build by call case cast cluster collate collection commit connect continue correlate cover create database dataset datastore declare decrement delete derived desc describe distinct do drop each element else end every except exclude execute exists explain fetch first flatten for force from function grant group gsi having if ignore ilike in include increment index infer inline inner insert intersect into is join key keys keyspace known last left let letting like limit lsm map mapping matched materialized merge minus namespace nest not number object offset on option or order outer over parse partition password path pool prepare primary private privilege procedure public raw realm reduce rename return returning revoke right role rollback satisfies schema select self semi set show some start statistics string system then to transaction trigger truncate under union unique unknown unnest unset update upsert use user using validate value valued values via view when where while with within work xor",literal:"true false null missing|5",
-built_in:"array_agg array_append array_concat array_contains array_count array_distinct array_ifnull array_length array_max array_min array_position array_prepend array_put array_range array_remove array_repeat array_replace array_reverse array_sort array_sum avg count max min sum greatest least ifmissing ifmissingornull ifnull missingif nullif ifinf ifnan ifnanorinf naninf neginfif posinfif clock_millis clock_str date_add_millis date_add_str date_diff_millis date_diff_str date_part_millis date_part_str date_trunc_millis date_trunc_str duration_to_str millis str_to_millis millis_to_str millis_to_utc millis_to_zone_name now_millis now_str str_to_duration str_to_utc str_to_zone_name decode_json encode_json encoded_size poly_length base64 base64_encode base64_decode meta uuid abs acos asin atan atan2 ceil cos degrees e exp ln log floor pi power radians random round sign sin sqrt tan trunc object_length object_names object_pairs object_inner_pairs object_values object_inner_values object_add object_put object_remove object_unwrap regexp_contains regexp_like regexp_position regexp_replace contains initcap length lower ltrim position repeat replace rtrim split substr title trim upper isarray isatom isboolean isnumber isobject isstring type toarray toatom toboolean tonumber toobject tostring"},
-contains:[{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE],relevance:0},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE],relevance:0},{className:"symbol",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE],relevance:2},a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_BLOCK_COMMENT_MODE]}}}());
-hljs.registerLanguage("nginx",function(){return function(a){var b={className:"variable",variants:[{begin:/\$\d+/},{begin:/\$\{/,end:/}/},{begin:"[\\$\\@]"+a.UNDERSCORE_IDENT_RE}]};return{name:"Nginx config",aliases:["nginxconf"],contains:[a.HASH_COMMENT_MODE,{begin:a.UNDERSCORE_IDENT_RE+"\\s+{",returnBegin:!0,end:"{",contains:[{className:"section",begin:a.UNDERSCORE_IDENT_RE}],relevance:0},{begin:a.UNDERSCORE_IDENT_RE+"\\s",end:";|{",returnBegin:!0,contains:[{className:"attribute",begin:a.UNDERSCORE_IDENT_RE,
-starts:{endsWithParent:!0,keywords:{$pattern:"[a-z/_]+",literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},relevance:0,illegal:"=>",contains:[a.HASH_COMMENT_MODE,{className:"string",contains:[a.BACKSLASH_ESCAPE,b],variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/}]},{begin:"([a-z]+):/",end:"\\s",endsWithParent:!0,excludeEnd:!0,contains:[b]},{className:"regexp",contains:[a.BACKSLASH_ESCAPE,b],variants:[{begin:"\\s\\^",
-end:"\\s|{|;",returnEnd:!0},{begin:"~\\*?\\s+",end:"\\s|{|;",returnEnd:!0},{begin:"\\*(\\.[a-z\\-]+)+"},{begin:"([a-z\\-]+\\.)+\\*"}]},{className:"number",begin:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{className:"number",begin:"\\b\\d+[kKmMgGdshdwy]*\\b",relevance:0},b]}}],relevance:0}],illegal:"[^\\s\\}]"}}}());
-hljs.registerLanguage("nim",function(){return function(a){return{name:"Nim",aliases:["nim"],keywords:{keyword:"addr and as asm bind block break case cast const continue converter discard distinct div do elif else end enum except export finally for from func generic if import in include interface is isnot iterator let macro method mixin mod nil not notin object of or out proc ptr raise ref return shl shr static template try tuple type using var when while with without xor yield",literal:"shared guarded stdin stdout stderr result true false",
-built_in:"int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 float float32 float64 bool char string cstring pointer expr stmt void auto any range array openarray varargs seq set clong culong cchar cschar cshort cint csize clonglong cfloat cdouble clongdouble cuchar cushort cuint culonglong cstringarray semistatic"},contains:[{className:"meta",begin:/{\./,end:/\.}/,relevance:10},{className:"string",begin:/[a-zA-Z]\w*"/,end:/"/,contains:[{begin:/""/}]},{className:"string",begin:/([a-zA-Z]\w*)?"""/,
-end:/"""/},a.QUOTE_STRING_MODE,{className:"type",begin:/\b[A-Z]\w+\b/,relevance:0},{className:"number",relevance:0,variants:[{begin:/\b(0[xX][0-9a-fA-F][_0-9a-fA-F]*)('?[iIuU](8|16|32|64))?/},{begin:/\b(0o[0-7][_0-7]*)('?[iIuUfF](8|16|32|64))?/},{begin:/\b(0(b|B)[01][_01]*)('?[iIuUfF](8|16|32|64))?/},{begin:/\b(\d[_\d]*)('?[iIuUfF](8|16|32|64))?/}]},a.HASH_COMMENT_MODE]}}}());
-hljs.registerLanguage("nix",function(){return function(a){var b={keyword:"rec with let in inherit assert if else then",literal:"true false or and null",built_in:"import abort baseNameOf dirOf isNull builtins map removeAttrs throw toString derivation"},c={className:"subst",begin:/\$\{/,end:/}/,keywords:b};a=[a.NUMBER_MODE,a.HASH_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",contains:[c],variants:[{begin:"''",end:"''"},{begin:'"',end:'"'}]},{begin:/[a-zA-Z0-9-_]+(\s*=)/,returnBegin:!0,relevance:0,
-contains:[{className:"attr",begin:/\S+/}]}];c.contains=a;return{name:"Nix",aliases:["nixos"],keywords:b,contains:a}}}());
-hljs.registerLanguage("nsis",function(){return function(a){var b={className:"variable",begin:/\$+{[\w\.:-]+}/},c={className:"variable",begin:/\$+\w+/,illegal:/\(\){}/},d={className:"variable",begin:/\$+\([\w\^\.:-]+\)/},e={className:"string",variants:[{begin:'"',end:'"'},{begin:"'",end:"'"},{begin:"`",end:"`"}],illegal:/\n/,contains:[{className:"meta",begin:/\$(\\[nrt]|\$)/},{className:"variable",begin:/\$(ADMINTOOLS|APPDATA|CDBURN_AREA|CMDLINE|COMMONFILES32|COMMONFILES64|COMMONFILES|COOKIES|DESKTOP|DOCUMENTS|EXEDIR|EXEFILE|EXEPATH|FAVORITES|FONTS|HISTORY|HWNDPARENT|INSTDIR|INTERNET_CACHE|LANGUAGE|LOCALAPPDATA|MUSIC|NETHOOD|OUTDIR|PICTURES|PLUGINSDIR|PRINTHOOD|PROFILE|PROGRAMFILES32|PROGRAMFILES64|PROGRAMFILES|QUICKLAUNCH|RECENT|RESOURCES_LOCALIZED|RESOURCES|SENDTO|SMPROGRAMS|SMSTARTUP|STARTMENU|SYSDIR|TEMP|TEMPLATES|VIDEOS|WINDIR)/},
-b,c,d]};return{name:"NSIS",case_insensitive:!1,keywords:{keyword:"Abort AddBrandingImage AddSize AllowRootDirInstall AllowSkipFiles AutoCloseWindow BGFont BGGradient BrandingText BringToFront Call CallInstDLL Caption ChangeUI CheckBitmap ClearErrors CompletedText ComponentText CopyFiles CRCCheck CreateDirectory CreateFont CreateShortCut Delete DeleteINISec DeleteINIStr DeleteRegKey DeleteRegValue DetailPrint DetailsButtonText DirText DirVar DirVerify EnableWindow EnumRegKey EnumRegValue Exch Exec ExecShell ExecShellWait ExecWait ExpandEnvStrings File FileBufSize FileClose FileErrorText FileOpen FileRead FileReadByte FileReadUTF16LE FileReadWord FileSeek FileWrite FileWriteByte FileWriteUTF16LE FileWriteWord FindClose FindFirst FindNext FindWindow FlushINI FunctionEnd GetCurInstType GetCurrentAddress GetDlgItem GetDLLVersion GetDLLVersionLocal GetErrorLevel GetFileTime GetFileTimeLocal GetFullPathName GetFunctionAddress GetInstDirError GetLabelAddress GetTempFileName Goto HideWindow Icon IfAbort IfErrors IfFileExists IfRebootFlag IfSilent InitPluginsDir InstallButtonText InstallColors InstallDir InstallDirRegKey InstProgressFlags InstType InstTypeGetText InstTypeSetText Int64Cmp Int64CmpU Int64Fmt IntCmp IntCmpU IntFmt IntOp IntPtrCmp IntPtrCmpU IntPtrOp IsWindow LangString LicenseBkColor LicenseData LicenseForceSelection LicenseLangString LicenseText LoadLanguageFile LockWindow LogSet LogText ManifestDPIAware ManifestSupportedOS MessageBox MiscButtonText Name Nop OutFile Page PageCallbacks PageExEnd Pop Push Quit ReadEnvStr ReadINIStr ReadRegDWORD ReadRegStr Reboot RegDLL Rename RequestExecutionLevel ReserveFile Return RMDir SearchPath SectionEnd SectionGetFlags SectionGetInstTypes SectionGetSize SectionGetText SectionGroupEnd SectionIn SectionSetFlags SectionSetInstTypes SectionSetSize SectionSetText SendMessage SetAutoClose SetBrandingImage SetCompress SetCompressor SetCompressorDictSize SetCtlColors SetCurInstType SetDatablockOptimize SetDateSave SetDetailsPrint SetDetailsView SetErrorLevel SetErrors SetFileAttributes SetFont SetOutPath SetOverwrite SetRebootFlag SetRegView SetShellVarContext SetSilent ShowInstDetails ShowUninstDetails ShowWindow SilentInstall SilentUnInstall Sleep SpaceTexts StrCmp StrCmpS StrCpy StrLen SubCaption Unicode UninstallButtonText UninstallCaption UninstallIcon UninstallSubCaption UninstallText UninstPage UnRegDLL Var VIAddVersionKey VIFileVersion VIProductVersion WindowIcon WriteINIStr WriteRegBin WriteRegDWORD WriteRegExpandStr WriteRegMultiStr WriteRegNone WriteRegStr WriteUninstaller XPStyle",
-literal:"admin all auto both bottom bzip2 colored components current custom directory false force hide highest ifdiff ifnewer instfiles lastused leave left license listonly lzma nevershow none normal notset off on open print right show silent silentlog smooth textonly top true try un.components un.custom un.directory un.instfiles un.license uninstConfirm user Win10 Win7 Win8 WinVista zlib"},contains:[a.HASH_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.COMMENT(";","$",{relevance:0}),{className:"function",
-beginKeywords:"Function PageEx Section SectionGroup",end:"$"},e,{className:"keyword",begin:/!(addincludedir|addplugindir|appendfile|cd|define|delfile|echo|else|endif|error|execute|finalize|getdllversion|gettlbversion|if|ifdef|ifmacrodef|ifmacrondef|ifndef|include|insertmacro|macro|macroend|makensis|packhdr|searchparse|searchreplace|system|tempfile|undef|verbose|warning)/},b,c,d,{className:"params",begin:"(ARCHIVE|FILE_ATTRIBUTE_ARCHIVE|FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_OFFLINE|FILE_ATTRIBUTE_READONLY|FILE_ATTRIBUTE_SYSTEM|FILE_ATTRIBUTE_TEMPORARY|HKCR|HKCU|HKDD|HKEY_CLASSES_ROOT|HKEY_CURRENT_CONFIG|HKEY_CURRENT_USER|HKEY_DYN_DATA|HKEY_LOCAL_MACHINE|HKEY_PERFORMANCE_DATA|HKEY_USERS|HKLM|HKPD|HKU|IDABORT|IDCANCEL|IDIGNORE|IDNO|IDOK|IDRETRY|IDYES|MB_ABORTRETRYIGNORE|MB_DEFBUTTON1|MB_DEFBUTTON2|MB_DEFBUTTON3|MB_DEFBUTTON4|MB_ICONEXCLAMATION|MB_ICONINFORMATION|MB_ICONQUESTION|MB_ICONSTOP|MB_OK|MB_OKCANCEL|MB_RETRYCANCEL|MB_RIGHT|MB_RTLREADING|MB_SETFOREGROUND|MB_TOPMOST|MB_USERICON|MB_YESNO|NORMAL|OFFLINE|READONLY|SHCTX|SHELL_CONTEXT|SYSTEM|TEMPORARY)"},
-{className:"class",begin:/\w+::\w+/},a.NUMBER_MODE]}}}());
-hljs.registerLanguage("objectivec",function(){return function(a){var b=/[a-zA-Z@][a-zA-Z0-9_]*/,c={$pattern:b,keyword:"@interface @class @protocol @implementation"};return{name:"Objective-C",aliases:["mm","objc","obj-c"],keywords:{$pattern:b,keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",
-built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},illegal:"</",contains:[{className:"built_in",begin:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.C_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{className:"string",variants:[{begin:'@"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]}]},{className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},
-contains:[{begin:/\\\n/,relevance:0},a.inherit(a.QUOTE_STRING_MODE,{className:"meta-string"}),{className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"class",begin:"("+c.keyword.split(" ").join("|")+")\\b",end:"({|$)",excludeEnd:!0,keywords:c,contains:[a.UNDERSCORE_TITLE_MODE]},{begin:"\\."+a.UNDERSCORE_IDENT_RE,relevance:0}]}}}());
-hljs.registerLanguage("ocaml",function(){return function(a){return{name:"OCaml",aliases:["ml"],keywords:{$pattern:"[a-z_]\\w*!?",keyword:"and as assert asr begin class constraint do done downto else end exception external for fun function functor if in include inherit! inherit initializer land lazy let lor lsl lsr lxor match method!|10 method mod module mutable new object of open! open or private rec sig struct then to try type val! val virtual when while with parser value",built_in:"array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 string unit in_channel out_channel ref",
-literal:"true false"},illegal:/\/\/|>>/,contains:[{className:"literal",begin:"\\[(\\|\\|)?\\]|\\(\\)",relevance:0},a.COMMENT("\\(\\*","\\*\\)",{contains:["self"]}),{className:"symbol",begin:"'[A-Za-z_](?!')[\\w']*"},{className:"type",begin:"`[A-Z][\\w']*"},{className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},{begin:"[a-z_]\\w*'[\\w']*",relevance:0},a.inherit(a.APOS_STRING_MODE,{className:"string",relevance:0}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"number",begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",
-relevance:0},{begin:/[-=]>/}]}}}());
-hljs.registerLanguage("openscad",function(){return function(a){var b={className:"keyword",begin:"\\$(f[asn]|t|vp[rtd]|children)"},c={className:"number",begin:"\\b\\d+(\\.\\d+)?(e-?\\d+)?",relevance:0},d=a.inherit(a.QUOTE_STRING_MODE,{illegal:null});return{name:"OpenSCAD",aliases:["scad"],keywords:{keyword:"function module include use for intersection_for if else \\%",literal:"false true PI undef",built_in:"circle square polygon text sphere cube cylinder polyhedron translate rotate scale resize mirror multmatrix color offset hull minkowski union difference intersection abs sign sin cos tan acos asin atan atan2 floor round ceil ln log pow sqrt exp rands min max concat lookup str chr search version version_num norm cross parent_module echo import import_dxf dxf_linear_extrude linear_extrude rotate_extrude surface projection render children dxf_cross dxf_dim let assign"},contains:[a.C_LINE_COMMENT_MODE,
-a.C_BLOCK_COMMENT_MODE,c,{className:"meta",keywords:{"meta-keyword":"include use"},begin:"include|use <",end:">"},d,b,{begin:"[*!#%]",relevance:0},{className:"function",beginKeywords:"module function",end:"\\=|\\{",contains:[{className:"params",begin:"\\(",end:"\\)",contains:["self",c,d,b,{className:"literal",begin:"false|true|PI|undef"}]},a.UNDERSCORE_TITLE_MODE]}]}}}());
-hljs.registerLanguage("oxygene",function(){return function(a){var b={$pattern:/\.?\w+/,keyword:"abstract add and array as asc aspect assembly async begin break block by case class concat const copy constructor continue create default delegate desc distinct div do downto dynamic each else empty end ensure enum equals event except exit extension external false final finalize finalizer finally flags for forward from function future global group has if implementation implements implies in index inherited inline interface into invariants is iterator join locked locking loop matching method mod module namespace nested new nil not notify nullable of old on operator or order out override parallel params partial pinned private procedure property protected public queryable raise read readonly record reintroduce remove repeat require result reverse sealed select self sequence set shl shr skip static step soft take then to true try tuple type union unit unsafe until uses using var virtual raises volatile where while with write xor yield await mapped deprecated stdcall cdecl pascal register safecall overload library platform reference packed strict published autoreleasepool selector strong weak unretained"},c=
-a.COMMENT("{","}",{relevance:0}),d=a.COMMENT("\\(\\*","\\*\\)",{relevance:10}),e={className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},f={className:"string",begin:"(#\\d+)+"},h={className:"function",beginKeywords:"function constructor destructor procedure method",end:"[:;]",keywords:"function constructor|10 destructor|10 procedure|10 method|10",contains:[a.TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",keywords:b,contains:[e,f]},c,d]};return{name:"Oxygene",case_insensitive:!0,keywords:b,
-illegal:'("|\\$[G-Zg-z]|\\/\\*|</|=>|->)',contains:[c,d,a.C_LINE_COMMENT_MODE,e,f,a.NUMBER_MODE,h,{className:"class",begin:"=\\bclass\\b",end:"end;",keywords:b,contains:[e,f,c,d,a.C_LINE_COMMENT_MODE,h]}]}}}());
-hljs.registerLanguage("parser3",function(){return function(a){var b=a.COMMENT("{","}",{contains:["self"]});return{name:"Parser3",subLanguage:"xml",relevance:0,contains:[a.COMMENT("^#","$"),a.COMMENT("\\^rem{","}",{relevance:10,contains:[b]}),{className:"meta",begin:"^@(?:BASE|USE|CLASS|OPTIONS)$",relevance:10},{className:"title",begin:"@[\\w\\-]+\\[[\\w^;\\-]*\\](?:\\[[\\w^;\\-]*\\])?(?:.*)$"},{className:"variable",begin:"\\$\\{?[\\w\\-\\.\\:]+\\}?"},{className:"keyword",begin:"\\^[\\w\\-\\.\\:]+"},
-{className:"number",begin:"\\^#[0-9a-fA-F]+"},a.C_NUMBER_MODE]}}}());
-hljs.registerLanguage("pf",function(){return function(a){return{name:"Packet Filter config",aliases:["pf.conf"],keywords:{$pattern:/[a-z0-9_<>-]+/,built_in:"block match pass load anchor|5 antispoof|10 set table",keyword:"in out log quick on rdomain inet inet6 proto from port os to route allow-opts divert-packet divert-reply divert-to flags group icmp-type icmp6-type label once probability recieved-on rtable prio queue tos tag tagged user keep fragment for os drop af-to|10 binat-to|10 nat-to|10 rdr-to|10 bitmask least-stats random round-robin source-hash static-port dup-to reply-to route-to parent bandwidth default min max qlimit block-policy debug fingerprints hostid limit loginterface optimization reassemble ruleset-optimization basic none profile skip state-defaults state-policy timeout const counters persist no modulate synproxy state|5 floating if-bound no-sync pflow|10 sloppy source-track global rule max-src-nodes max-src-states max-src-conn max-src-conn-rate overload flush scrub|5 max-mss min-ttl no-df|10 random-id",literal:"all any no-route self urpf-failed egress|5 unknown"},
-contains:[a.HASH_COMMENT_MODE,a.NUMBER_MODE,a.QUOTE_STRING_MODE,{className:"variable",begin:/\$[\w\d#@][\w\d_]*/},{className:"variable",begin:/<(?!\/)/,end:/>/}]}}}());
-hljs.registerLanguage("pgsql",function(){return function(a){var b=a.COMMENT("--","$"),c="BIGINT INT8 BIGSERIAL SERIAL8 BIT VARYING VARBIT BOOLEAN BOOL BOX BYTEA CHARACTER CHAR VARCHAR CIDR CIRCLE DATE DOUBLE PRECISION FLOAT8 FLOAT INET INTEGER INT INT4 INTERVAL JSON JSONB LINE LSEG|10 MACADDR MACADDR8 MONEY NUMERIC DEC DECIMAL PATH POINT POLYGON REAL FLOAT4 SMALLINT INT2 SMALLSERIAL|10 SERIAL2|10 SERIAL|10 SERIAL4|10 TEXT TIME ZONE TIMETZ|10 TIMESTAMP TIMESTAMPTZ|10 TSQUERY|10 TSVECTOR|10 TXID_SNAPSHOT|10 UUID XML NATIONAL NCHAR INT4RANGE|10 INT8RANGE|10 NUMRANGE|10 TSRANGE|10 TSTZRANGE|10 DATERANGE|10 ANYELEMENT ANYARRAY ANYNONARRAY ANYENUM ANYRANGE CSTRING INTERNAL RECORD PG_DDL_COMMAND VOID UNKNOWN OPAQUE REFCURSOR NAME OID REGPROC|10 REGPROCEDURE|10 REGOPER|10 REGOPERATOR|10 REGCLASS|10 REGTYPE|10 REGROLE|10 REGNAMESPACE|10 REGCONFIG|10 REGDICTIONARY|10".split(" ").map(function(a){return a.split("|")[0]}).join("|"),d=
-"ARRAY_AGG AVG BIT_AND BIT_OR BOOL_AND BOOL_OR COUNT EVERY JSON_AGG JSONB_AGG JSON_OBJECT_AGG JSONB_OBJECT_AGG MAX MIN MODE STRING_AGG SUM XMLAGG CORR COVAR_POP COVAR_SAMP REGR_AVGX REGR_AVGY REGR_COUNT REGR_INTERCEPT REGR_R2 REGR_SLOPE REGR_SXX REGR_SXY REGR_SYY STDDEV STDDEV_POP STDDEV_SAMP VARIANCE VAR_POP VAR_SAMP PERCENTILE_CONT PERCENTILE_DISC ROW_NUMBER RANK DENSE_RANK PERCENT_RANK CUME_DIST NTILE LAG LEAD FIRST_VALUE LAST_VALUE NTH_VALUE NUM_NONNULLS NUM_NULLS ABS CBRT CEIL CEILING DEGREES DIV EXP FLOOR LN LOG MOD PI POWER RADIANS ROUND SCALE SIGN SQRT TRUNC WIDTH_BUCKET RANDOM SETSEED ACOS ACOSD ASIN ASIND ATAN ATAND ATAN2 ATAN2D COS COSD COT COTD SIN SIND TAN TAND BIT_LENGTH CHAR_LENGTH CHARACTER_LENGTH LOWER OCTET_LENGTH OVERLAY POSITION SUBSTRING TREAT TRIM UPPER ASCII BTRIM CHR CONCAT CONCAT_WS CONVERT CONVERT_FROM CONVERT_TO DECODE ENCODE INITCAP LEFT LENGTH LPAD LTRIM MD5 PARSE_IDENT PG_CLIENT_ENCODING QUOTE_IDENT|10 QUOTE_LITERAL|10 QUOTE_NULLABLE|10 REGEXP_MATCH REGEXP_MATCHES REGEXP_REPLACE REGEXP_SPLIT_TO_ARRAY REGEXP_SPLIT_TO_TABLE REPEAT REPLACE REVERSE RIGHT RPAD RTRIM SPLIT_PART STRPOS SUBSTR TO_ASCII TO_HEX TRANSLATE OCTET_LENGTH GET_BIT GET_BYTE SET_BIT SET_BYTE TO_CHAR TO_DATE TO_NUMBER TO_TIMESTAMP AGE CLOCK_TIMESTAMP|10 DATE_PART DATE_TRUNC ISFINITE JUSTIFY_DAYS JUSTIFY_HOURS JUSTIFY_INTERVAL MAKE_DATE MAKE_INTERVAL|10 MAKE_TIME MAKE_TIMESTAMP|10 MAKE_TIMESTAMPTZ|10 NOW STATEMENT_TIMESTAMP|10 TIMEOFDAY TRANSACTION_TIMESTAMP|10 ENUM_FIRST ENUM_LAST ENUM_RANGE AREA CENTER DIAMETER HEIGHT ISCLOSED ISOPEN NPOINTS PCLOSE POPEN RADIUS WIDTH BOX BOUND_BOX CIRCLE LINE LSEG PATH POLYGON ABBREV BROADCAST HOST HOSTMASK MASKLEN NETMASK NETWORK SET_MASKLEN TEXT INET_SAME_FAMILY INET_MERGE MACADDR8_SET7BIT ARRAY_TO_TSVECTOR GET_CURRENT_TS_CONFIG NUMNODE PLAINTO_TSQUERY PHRASETO_TSQUERY WEBSEARCH_TO_TSQUERY QUERYTREE SETWEIGHT STRIP TO_TSQUERY TO_TSVECTOR JSON_TO_TSVECTOR JSONB_TO_TSVECTOR TS_DELETE TS_FILTER TS_HEADLINE TS_RANK TS_RANK_CD TS_REWRITE TSQUERY_PHRASE TSVECTOR_TO_ARRAY TSVECTOR_UPDATE_TRIGGER TSVECTOR_UPDATE_TRIGGER_COLUMN XMLCOMMENT XMLCONCAT XMLELEMENT XMLFOREST XMLPI XMLROOT XMLEXISTS XML_IS_WELL_FORMED XML_IS_WELL_FORMED_DOCUMENT XML_IS_WELL_FORMED_CONTENT XPATH XPATH_EXISTS XMLTABLE XMLNAMESPACES TABLE_TO_XML TABLE_TO_XMLSCHEMA TABLE_TO_XML_AND_XMLSCHEMA QUERY_TO_XML QUERY_TO_XMLSCHEMA QUERY_TO_XML_AND_XMLSCHEMA CURSOR_TO_XML CURSOR_TO_XMLSCHEMA SCHEMA_TO_XML SCHEMA_TO_XMLSCHEMA SCHEMA_TO_XML_AND_XMLSCHEMA DATABASE_TO_XML DATABASE_TO_XMLSCHEMA DATABASE_TO_XML_AND_XMLSCHEMA XMLATTRIBUTES TO_JSON TO_JSONB ARRAY_TO_JSON ROW_TO_JSON JSON_BUILD_ARRAY JSONB_BUILD_ARRAY JSON_BUILD_OBJECT JSONB_BUILD_OBJECT JSON_OBJECT JSONB_OBJECT JSON_ARRAY_LENGTH JSONB_ARRAY_LENGTH JSON_EACH JSONB_EACH JSON_EACH_TEXT JSONB_EACH_TEXT JSON_EXTRACT_PATH JSONB_EXTRACT_PATH JSON_OBJECT_KEYS JSONB_OBJECT_KEYS JSON_POPULATE_RECORD JSONB_POPULATE_RECORD JSON_POPULATE_RECORDSET JSONB_POPULATE_RECORDSET JSON_ARRAY_ELEMENTS JSONB_ARRAY_ELEMENTS JSON_ARRAY_ELEMENTS_TEXT JSONB_ARRAY_ELEMENTS_TEXT JSON_TYPEOF JSONB_TYPEOF JSON_TO_RECORD JSONB_TO_RECORD JSON_TO_RECORDSET JSONB_TO_RECORDSET JSON_STRIP_NULLS JSONB_STRIP_NULLS JSONB_SET JSONB_INSERT JSONB_PRETTY CURRVAL LASTVAL NEXTVAL SETVAL COALESCE NULLIF GREATEST LEAST ARRAY_APPEND ARRAY_CAT ARRAY_NDIMS ARRAY_DIMS ARRAY_FILL ARRAY_LENGTH ARRAY_LOWER ARRAY_POSITION ARRAY_POSITIONS ARRAY_PREPEND ARRAY_REMOVE ARRAY_REPLACE ARRAY_TO_STRING ARRAY_UPPER CARDINALITY STRING_TO_ARRAY UNNEST ISEMPTY LOWER_INC UPPER_INC LOWER_INF UPPER_INF RANGE_MERGE GENERATE_SERIES GENERATE_SUBSCRIPTS CURRENT_DATABASE CURRENT_QUERY CURRENT_SCHEMA|10 CURRENT_SCHEMAS|10 INET_CLIENT_ADDR INET_CLIENT_PORT INET_SERVER_ADDR INET_SERVER_PORT ROW_SECURITY_ACTIVE FORMAT_TYPE TO_REGCLASS TO_REGPROC TO_REGPROCEDURE TO_REGOPER TO_REGOPERATOR TO_REGTYPE TO_REGNAMESPACE TO_REGROLE COL_DESCRIPTION OBJ_DESCRIPTION SHOBJ_DESCRIPTION TXID_CURRENT TXID_CURRENT_IF_ASSIGNED TXID_CURRENT_SNAPSHOT TXID_SNAPSHOT_XIP TXID_SNAPSHOT_XMAX TXID_SNAPSHOT_XMIN TXID_VISIBLE_IN_SNAPSHOT TXID_STATUS CURRENT_SETTING SET_CONFIG BRIN_SUMMARIZE_NEW_VALUES BRIN_SUMMARIZE_RANGE BRIN_DESUMMARIZE_RANGE GIN_CLEAN_PENDING_LIST SUPPRESS_REDUNDANT_UPDATES_TRIGGER LO_FROM_BYTEA LO_PUT LO_GET LO_CREAT LO_CREATE LO_UNLINK LO_IMPORT LO_EXPORT LOREAD LOWRITE GROUPING CAST".split(" ").map(function(a){return a.split("|")[0]}).join("|");
-return{name:"PostgreSQL",aliases:["postgres","postgresql"],case_insensitive:!0,keywords:{keyword:"ABORT ALTER ANALYZE BEGIN CALL CHECKPOINT|10 CLOSE CLUSTER COMMENT COMMIT COPY CREATE DEALLOCATE DECLARE DELETE DISCARD DO DROP END EXECUTE EXPLAIN FETCH GRANT IMPORT INSERT LISTEN LOAD LOCK MOVE NOTIFY PREPARE REASSIGN|10 REFRESH REINDEX RELEASE RESET REVOKE ROLLBACK SAVEPOINT SECURITY SELECT SET SHOW START TRUNCATE UNLISTEN|10 UPDATE VACUUM|10 VALUES AGGREGATE COLLATION CONVERSION|10 DATABASE DEFAULT PRIVILEGES DOMAIN TRIGGER EXTENSION FOREIGN WRAPPER|10 TABLE FUNCTION GROUP LANGUAGE LARGE OBJECT MATERIALIZED VIEW OPERATOR CLASS FAMILY POLICY PUBLICATION|10 ROLE RULE SCHEMA SEQUENCE SERVER STATISTICS SUBSCRIPTION SYSTEM TABLESPACE CONFIGURATION DICTIONARY PARSER TEMPLATE TYPE USER MAPPING PREPARED ACCESS METHOD CAST AS TRANSFORM TRANSACTION OWNED TO INTO SESSION AUTHORIZATION INDEX PROCEDURE ASSERTION ALL ANALYSE AND ANY ARRAY ASC ASYMMETRIC|10 BOTH CASE CHECK COLLATE COLUMN CONCURRENTLY|10 CONSTRAINT CROSS DEFERRABLE RANGE DESC DISTINCT ELSE EXCEPT FOR FREEZE|10 FROM FULL HAVING ILIKE IN INITIALLY INNER INTERSECT IS ISNULL JOIN LATERAL LEADING LIKE LIMIT NATURAL NOT NOTNULL NULL OFFSET ON ONLY OR ORDER OUTER OVERLAPS PLACING PRIMARY REFERENCES RETURNING SIMILAR SOME SYMMETRIC TABLESAMPLE THEN TRAILING UNION UNIQUE USING VARIADIC|10 VERBOSE WHEN WHERE WINDOW WITH BY RETURNS INOUT OUT SETOF|10 IF STRICT CURRENT CONTINUE OWNER LOCATION OVER PARTITION WITHIN BETWEEN ESCAPE EXTERNAL INVOKER DEFINER WORK RENAME VERSION CONNECTION CONNECT TABLES TEMP TEMPORARY FUNCTIONS SEQUENCES TYPES SCHEMAS OPTION CASCADE RESTRICT ADD ADMIN EXISTS VALID VALIDATE ENABLE DISABLE REPLICA|10 ALWAYS PASSING COLUMNS PATH REF VALUE OVERRIDING IMMUTABLE STABLE VOLATILE BEFORE AFTER EACH ROW PROCEDURAL ROUTINE NO HANDLER VALIDATOR OPTIONS STORAGE OIDS|10 WITHOUT INHERIT DEPENDS CALLED INPUT LEAKPROOF|10 COST ROWS NOWAIT SEARCH UNTIL ENCRYPTED|10 PASSWORD CONFLICT|10 INSTEAD INHERITS CHARACTERISTICS WRITE CURSOR ALSO STATEMENT SHARE EXCLUSIVE INLINE ISOLATION REPEATABLE READ COMMITTED SERIALIZABLE UNCOMMITTED LOCAL GLOBAL SQL PROCEDURES RECURSIVE SNAPSHOT ROLLUP CUBE TRUSTED|10 INCLUDE FOLLOWING PRECEDING UNBOUNDED RANGE GROUPS UNENCRYPTED|10 SYSID FORMAT DELIMITER HEADER QUOTE ENCODING FILTER OFF FORCE_QUOTE FORCE_NOT_NULL FORCE_NULL COSTS BUFFERS TIMING SUMMARY DISABLE_PAGE_SKIPPING RESTART CYCLE GENERATED IDENTITY DEFERRED IMMEDIATE LEVEL LOGGED UNLOGGED OF NOTHING NONE EXCLUDE ATTRIBUTE USAGE ROUTINES TRUE FALSE NAN INFINITY ALIAS BEGIN CONSTANT DECLARE END EXCEPTION RETURN PERFORM|10 RAISE GET DIAGNOSTICS STACKED|10 FOREACH LOOP ELSIF EXIT WHILE REVERSE SLICE DEBUG LOG INFO NOTICE WARNING ASSERT OPEN SUPERUSER NOSUPERUSER CREATEDB NOCREATEDB CREATEROLE NOCREATEROLE INHERIT NOINHERIT LOGIN NOLOGIN REPLICATION NOREPLICATION BYPASSRLS NOBYPASSRLS ",
-built_in:"CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURRENT_CATALOG|10 CURRENT_DATE LOCALTIME LOCALTIMESTAMP CURRENT_ROLE|10 CURRENT_SCHEMA|10 SESSION_USER PUBLIC FOUND NEW OLD TG_NAME|10 TG_WHEN|10 TG_LEVEL|10 TG_OP|10 TG_RELID|10 TG_RELNAME|10 TG_TABLE_NAME|10 TG_TABLE_SCHEMA|10 TG_NARGS|10 TG_ARGV|10 TG_EVENT|10 TG_TAG|10 ROW_COUNT RESULT_OID|10 PG_CONTEXT|10 RETURNED_SQLSTATE COLUMN_NAME CONSTRAINT_NAME PG_DATATYPE_NAME|10 MESSAGE_TEXT TABLE_NAME SCHEMA_NAME PG_EXCEPTION_DETAIL|10 PG_EXCEPTION_HINT|10 PG_EXCEPTION_CONTEXT|10 SQLSTATE SQLERRM|10 SUCCESSFUL_COMPLETION WARNING DYNAMIC_RESULT_SETS_RETURNED IMPLICIT_ZERO_BIT_PADDING NULL_VALUE_ELIMINATED_IN_SET_FUNCTION PRIVILEGE_NOT_GRANTED PRIVILEGE_NOT_REVOKED STRING_DATA_RIGHT_TRUNCATION DEPRECATED_FEATURE NO_DATA NO_ADDITIONAL_DYNAMIC_RESULT_SETS_RETURNED SQL_STATEMENT_NOT_YET_COMPLETE CONNECTION_EXCEPTION CONNECTION_DOES_NOT_EXIST CONNECTION_FAILURE SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION TRANSACTION_RESOLUTION_UNKNOWN PROTOCOL_VIOLATION TRIGGERED_ACTION_EXCEPTION FEATURE_NOT_SUPPORTED INVALID_TRANSACTION_INITIATION LOCATOR_EXCEPTION INVALID_LOCATOR_SPECIFICATION INVALID_GRANTOR INVALID_GRANT_OPERATION INVALID_ROLE_SPECIFICATION DIAGNOSTICS_EXCEPTION STACKED_DIAGNOSTICS_ACCESSED_WITHOUT_ACTIVE_HANDLER CASE_NOT_FOUND CARDINALITY_VIOLATION DATA_EXCEPTION ARRAY_SUBSCRIPT_ERROR CHARACTER_NOT_IN_REPERTOIRE DATETIME_FIELD_OVERFLOW DIVISION_BY_ZERO ERROR_IN_ASSIGNMENT ESCAPE_CHARACTER_CONFLICT INDICATOR_OVERFLOW INTERVAL_FIELD_OVERFLOW INVALID_ARGUMENT_FOR_LOGARITHM INVALID_ARGUMENT_FOR_NTILE_FUNCTION INVALID_ARGUMENT_FOR_NTH_VALUE_FUNCTION INVALID_ARGUMENT_FOR_POWER_FUNCTION INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION INVALID_CHARACTER_VALUE_FOR_CAST INVALID_DATETIME_FORMAT INVALID_ESCAPE_CHARACTER INVALID_ESCAPE_OCTET INVALID_ESCAPE_SEQUENCE NONSTANDARD_USE_OF_ESCAPE_CHARACTER INVALID_INDICATOR_PARAMETER_VALUE INVALID_PARAMETER_VALUE INVALID_REGULAR_EXPRESSION INVALID_ROW_COUNT_IN_LIMIT_CLAUSE INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE INVALID_TABLESAMPLE_ARGUMENT INVALID_TABLESAMPLE_REPEAT INVALID_TIME_ZONE_DISPLACEMENT_VALUE INVALID_USE_OF_ESCAPE_CHARACTER MOST_SPECIFIC_TYPE_MISMATCH NULL_VALUE_NOT_ALLOWED NULL_VALUE_NO_INDICATOR_PARAMETER NUMERIC_VALUE_OUT_OF_RANGE SEQUENCE_GENERATOR_LIMIT_EXCEEDED STRING_DATA_LENGTH_MISMATCH STRING_DATA_RIGHT_TRUNCATION SUBSTRING_ERROR TRIM_ERROR UNTERMINATED_C_STRING ZERO_LENGTH_CHARACTER_STRING FLOATING_POINT_EXCEPTION INVALID_TEXT_REPRESENTATION INVALID_BINARY_REPRESENTATION BAD_COPY_FILE_FORMAT UNTRANSLATABLE_CHARACTER NOT_AN_XML_DOCUMENT INVALID_XML_DOCUMENT INVALID_XML_CONTENT INVALID_XML_COMMENT INVALID_XML_PROCESSING_INSTRUCTION INTEGRITY_CONSTRAINT_VIOLATION RESTRICT_VIOLATION NOT_NULL_VIOLATION FOREIGN_KEY_VIOLATION UNIQUE_VIOLATION CHECK_VIOLATION EXCLUSION_VIOLATION INVALID_CURSOR_STATE INVALID_TRANSACTION_STATE ACTIVE_SQL_TRANSACTION BRANCH_TRANSACTION_ALREADY_ACTIVE HELD_CURSOR_REQUIRES_SAME_ISOLATION_LEVEL INAPPROPRIATE_ACCESS_MODE_FOR_BRANCH_TRANSACTION INAPPROPRIATE_ISOLATION_LEVEL_FOR_BRANCH_TRANSACTION NO_ACTIVE_SQL_TRANSACTION_FOR_BRANCH_TRANSACTION READ_ONLY_SQL_TRANSACTION SCHEMA_AND_DATA_STATEMENT_MIXING_NOT_SUPPORTED NO_ACTIVE_SQL_TRANSACTION IN_FAILED_SQL_TRANSACTION IDLE_IN_TRANSACTION_SESSION_TIMEOUT INVALID_SQL_STATEMENT_NAME TRIGGERED_DATA_CHANGE_VIOLATION INVALID_AUTHORIZATION_SPECIFICATION INVALID_PASSWORD DEPENDENT_PRIVILEGE_DESCRIPTORS_STILL_EXIST DEPENDENT_OBJECTS_STILL_EXIST INVALID_TRANSACTION_TERMINATION SQL_ROUTINE_EXCEPTION FUNCTION_EXECUTED_NO_RETURN_STATEMENT MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED INVALID_CURSOR_NAME EXTERNAL_ROUTINE_EXCEPTION CONTAINING_SQL_NOT_PERMITTED MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED EXTERNAL_ROUTINE_INVOCATION_EXCEPTION INVALID_SQLSTATE_RETURNED NULL_VALUE_NOT_ALLOWED TRIGGER_PROTOCOL_VIOLATED SRF_PROTOCOL_VIOLATED EVENT_TRIGGER_PROTOCOL_VIOLATED SAVEPOINT_EXCEPTION INVALID_SAVEPOINT_SPECIFICATION INVALID_CATALOG_NAME INVALID_SCHEMA_NAME TRANSACTION_ROLLBACK TRANSACTION_INTEGRITY_CONSTRAINT_VIOLATION SERIALIZATION_FAILURE STATEMENT_COMPLETION_UNKNOWN DEADLOCK_DETECTED SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION SYNTAX_ERROR INSUFFICIENT_PRIVILEGE CANNOT_COERCE GROUPING_ERROR WINDOWING_ERROR INVALID_RECURSION INVALID_FOREIGN_KEY INVALID_NAME NAME_TOO_LONG RESERVED_NAME DATATYPE_MISMATCH INDETERMINATE_DATATYPE COLLATION_MISMATCH INDETERMINATE_COLLATION WRONG_OBJECT_TYPE GENERATED_ALWAYS UNDEFINED_COLUMN UNDEFINED_FUNCTION UNDEFINED_TABLE UNDEFINED_PARAMETER UNDEFINED_OBJECT DUPLICATE_COLUMN DUPLICATE_CURSOR DUPLICATE_DATABASE DUPLICATE_FUNCTION DUPLICATE_PREPARED_STATEMENT DUPLICATE_SCHEMA DUPLICATE_TABLE DUPLICATE_ALIAS DUPLICATE_OBJECT AMBIGUOUS_COLUMN AMBIGUOUS_FUNCTION AMBIGUOUS_PARAMETER AMBIGUOUS_ALIAS INVALID_COLUMN_REFERENCE INVALID_COLUMN_DEFINITION INVALID_CURSOR_DEFINITION INVALID_DATABASE_DEFINITION INVALID_FUNCTION_DEFINITION INVALID_PREPARED_STATEMENT_DEFINITION INVALID_SCHEMA_DEFINITION INVALID_TABLE_DEFINITION INVALID_OBJECT_DEFINITION WITH_CHECK_OPTION_VIOLATION INSUFFICIENT_RESOURCES DISK_FULL OUT_OF_MEMORY TOO_MANY_CONNECTIONS CONFIGURATION_LIMIT_EXCEEDED PROGRAM_LIMIT_EXCEEDED STATEMENT_TOO_COMPLEX TOO_MANY_COLUMNS TOO_MANY_ARGUMENTS OBJECT_NOT_IN_PREREQUISITE_STATE OBJECT_IN_USE CANT_CHANGE_RUNTIME_PARAM LOCK_NOT_AVAILABLE OPERATOR_INTERVENTION QUERY_CANCELED ADMIN_SHUTDOWN CRASH_SHUTDOWN CANNOT_CONNECT_NOW DATABASE_DROPPED SYSTEM_ERROR IO_ERROR UNDEFINED_FILE DUPLICATE_FILE SNAPSHOT_TOO_OLD CONFIG_FILE_ERROR LOCK_FILE_EXISTS FDW_ERROR FDW_COLUMN_NAME_NOT_FOUND FDW_DYNAMIC_PARAMETER_VALUE_NEEDED FDW_FUNCTION_SEQUENCE_ERROR FDW_INCONSISTENT_DESCRIPTOR_INFORMATION FDW_INVALID_ATTRIBUTE_VALUE FDW_INVALID_COLUMN_NAME FDW_INVALID_COLUMN_NUMBER FDW_INVALID_DATA_TYPE FDW_INVALID_DATA_TYPE_DESCRIPTORS FDW_INVALID_DESCRIPTOR_FIELD_IDENTIFIER FDW_INVALID_HANDLE FDW_INVALID_OPTION_INDEX FDW_INVALID_OPTION_NAME FDW_INVALID_STRING_LENGTH_OR_BUFFER_LENGTH FDW_INVALID_STRING_FORMAT FDW_INVALID_USE_OF_NULL_POINTER FDW_TOO_MANY_HANDLES FDW_OUT_OF_MEMORY FDW_NO_SCHEMAS FDW_OPTION_NAME_NOT_FOUND FDW_REPLY_HANDLE FDW_SCHEMA_NOT_FOUND FDW_TABLE_NOT_FOUND FDW_UNABLE_TO_CREATE_EXECUTION FDW_UNABLE_TO_CREATE_REPLY FDW_UNABLE_TO_ESTABLISH_CONNECTION PLPGSQL_ERROR RAISE_EXCEPTION NO_DATA_FOUND TOO_MANY_ROWS ASSERT_FAILURE INTERNAL_ERROR DATA_CORRUPTED INDEX_CORRUPTED "},
-illegal:/:==|\W\s*\(\*|(^|\s)\$[a-z]|{{|[a-z]:\s*$|\.\.\.|TO:|DO:/,contains:[{className:"keyword",variants:[{begin:/\bTEXT\s*SEARCH\b/},{begin:/\b(PRIMARY|FOREIGN|FOR(\s+NO)?)\s+KEY\b/},{begin:/\bPARALLEL\s+(UNSAFE|RESTRICTED|SAFE)\b/},{begin:/\bSTORAGE\s+(PLAIN|EXTERNAL|EXTENDED|MAIN)\b/},{begin:/\bMATCH\s+(FULL|PARTIAL|SIMPLE)\b/},{begin:/\bNULLS\s+(FIRST|LAST)\b/},{begin:/\bEVENT\s+TRIGGER\b/},{begin:/\b(MAPPING|OR)\s+REPLACE\b/},{begin:/\b(FROM|TO)\s+(PROGRAM|STDIN|STDOUT)\b/},{begin:/\b(SHARE|EXCLUSIVE)\s+MODE\b/},
-{begin:/\b(LEFT|RIGHT)\s+(OUTER\s+)?JOIN\b/},{begin:/\b(FETCH|MOVE)\s+(NEXT|PRIOR|FIRST|LAST|ABSOLUTE|RELATIVE|FORWARD|BACKWARD)\b/},{begin:/\bPRESERVE\s+ROWS\b/},{begin:/\bDISCARD\s+PLANS\b/},{begin:/\bREFERENCING\s+(OLD|NEW)\b/},{begin:/\bSKIP\s+LOCKED\b/},{begin:/\bGROUPING\s+SETS\b/},{begin:/\b(BINARY|INSENSITIVE|SCROLL|NO\s+SCROLL)\s+(CURSOR|FOR)\b/},{begin:/\b(WITH|WITHOUT)\s+HOLD\b/},{begin:/\bWITH\s+(CASCADED|LOCAL)\s+CHECK\s+OPTION\b/},{begin:/\bEXCLUDE\s+(TIES|NO\s+OTHERS)\b/},{begin:/\bFORMAT\s+(TEXT|XML|JSON|YAML)\b/},
-{begin:/\bSET\s+((SESSION|LOCAL)\s+)?NAMES\b/},{begin:/\bIS\s+(NOT\s+)?UNKNOWN\b/},{begin:/\bSECURITY\s+LABEL\b/},{begin:/\bSTANDALONE\s+(YES|NO|NO\s+VALUE)\b/},{begin:/\bWITH\s+(NO\s+)?DATA\b/},{begin:/\b(FOREIGN|SET)\s+DATA\b/},{begin:/\bSET\s+(CATALOG|CONSTRAINTS)\b/},{begin:/\b(WITH|FOR)\s+ORDINALITY\b/},{begin:/\bIS\s+(NOT\s+)?DOCUMENT\b/},{begin:/\bXML\s+OPTION\s+(DOCUMENT|CONTENT)\b/},{begin:/\b(STRIP|PRESERVE)\s+WHITESPACE\b/},{begin:/\bNO\s+(ACTION|MAXVALUE|MINVALUE)\b/},{begin:/\bPARTITION\s+BY\s+(RANGE|LIST|HASH)\b/},
-{begin:/\bAT\s+TIME\s+ZONE\b/},{begin:/\bGRANTED\s+BY\b/},{begin:/\bRETURN\s+(QUERY|NEXT)\b/},{begin:/\b(ATTACH|DETACH)\s+PARTITION\b/},{begin:/\bFORCE\s+ROW\s+LEVEL\s+SECURITY\b/},{begin:/\b(INCLUDING|EXCLUDING)\s+(COMMENTS|CONSTRAINTS|DEFAULTS|IDENTITY|INDEXES|STATISTICS|STORAGE|ALL)\b/},{begin:/\bAS\s+(ASSIGNMENT|IMPLICIT|PERMISSIVE|RESTRICTIVE|ENUM|RANGE)\b/}]},{begin:/\b(FORMAT|FAMILY|VERSION)\s*\(/},{begin:/\bINCLUDE\s*\(/,keywords:"INCLUDE"},{begin:/\bRANGE(?!\s*(BETWEEN|UNBOUNDED|CURRENT|[-0-9]+))/},
-{begin:/\b(VERSION|OWNER|TEMPLATE|TABLESPACE|CONNECTION\s+LIMIT|PROCEDURE|RESTRICT|JOIN|PARSER|COPY|START|END|COLLATION|INPUT|ANALYZE|STORAGE|LIKE|DEFAULT|DELIMITER|ENCODING|COLUMN|CONSTRAINT|TABLE|SCHEMA)\s*=/},{begin:/\b(PG_\w+?|HAS_[A-Z_]+_PRIVILEGE)\b/,relevance:10},{begin:/\bEXTRACT\s*\(/,end:/\bFROM\b/,returnEnd:!0,keywords:{type:"CENTURY DAY DECADE DOW DOY EPOCH HOUR ISODOW ISOYEAR MICROSECONDS MILLENNIUM MILLISECONDS MINUTE MONTH QUARTER SECOND TIMEZONE TIMEZONE_HOUR TIMEZONE_MINUTE WEEK YEAR"}},
-{begin:/\b(XMLELEMENT|XMLPI)\s*\(\s*NAME/,keywords:{keyword:"NAME"}},{begin:/\b(XMLPARSE|XMLSERIALIZE)\s*\(\s*(DOCUMENT|CONTENT)/,keywords:{keyword:"DOCUMENT CONTENT"}},{beginKeywords:"CACHE INCREMENT MAXVALUE MINVALUE",end:a.C_NUMBER_RE,returnEnd:!0,keywords:"BY CACHE INCREMENT MAXVALUE MINVALUE"},{className:"type",begin:/\b(WITH|WITHOUT)\s+TIME\s+ZONE\b/},{className:"type",begin:/\bINTERVAL\s+(YEAR|MONTH|DAY|HOUR|MINUTE|SECOND)(\s+TO\s+(MONTH|HOUR|MINUTE|SECOND))?\b/},{begin:/\bRETURNS\s+(LANGUAGE_HANDLER|TRIGGER|EVENT_TRIGGER|FDW_HANDLER|INDEX_AM_HANDLER|TSM_HANDLER)\b/,
-keywords:{keyword:"RETURNS",type:"LANGUAGE_HANDLER TRIGGER EVENT_TRIGGER FDW_HANDLER INDEX_AM_HANDLER TSM_HANDLER"}},{begin:"\\b("+d+")\\s*\\("},{begin:"\\.("+c+")\\b"},{begin:"\\b("+c+")\\s+PATH\\b",keywords:{keyword:"PATH",type:"BIGINT INT8 BIGSERIAL SERIAL8 BIT VARYING VARBIT BOOLEAN BOOL BOX BYTEA CHARACTER CHAR VARCHAR CIDR CIRCLE DATE DOUBLE PRECISION FLOAT8 FLOAT INET INTEGER INT INT4 INTERVAL JSON JSONB LINE LSEG|10 MACADDR MACADDR8 MONEY NUMERIC DEC DECIMAL PATH POINT POLYGON REAL FLOAT4 SMALLINT INT2 SMALLSERIAL|10 SERIAL2|10 SERIAL|10 SERIAL4|10 TEXT TIME ZONE TIMETZ|10 TIMESTAMP TIMESTAMPTZ|10 TSQUERY|10 TSVECTOR|10 TXID_SNAPSHOT|10 UUID XML NATIONAL NCHAR INT4RANGE|10 INT8RANGE|10 NUMRANGE|10 TSRANGE|10 TSTZRANGE|10 DATERANGE|10 ANYELEMENT ANYARRAY ANYNONARRAY ANYENUM ANYRANGE CSTRING INTERNAL RECORD PG_DDL_COMMAND VOID UNKNOWN OPAQUE REFCURSOR NAME OID REGPROC|10 REGPROCEDURE|10 REGOPER|10 REGOPERATOR|10 REGCLASS|10 REGTYPE|10 REGROLE|10 REGNAMESPACE|10 REGCONFIG|10 REGDICTIONARY|10 ".replace("PATH ",
-"")}},{className:"type",begin:"\\b("+c+")\\b"},{className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{className:"string",begin:"(e|E|u&|U&)'",end:"'",contains:[{begin:"\\\\."}],relevance:10},a.END_SAME_AS_BEGIN({begin:"\\$([a-zA-Z_]?|[a-zA-Z_][a-zA-Z_0-9]*)\\$",end:"\\$([a-zA-Z_]?|[a-zA-Z_][a-zA-Z_0-9]*)\\$",contains:[{subLanguage:"pgsql perl python tcl r lua java php ruby bash scheme xml json".split(" "),endsWithParent:!0}]}),{begin:'"',end:'"',contains:[{begin:'""'}]},a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,
-b,{className:"meta",variants:[{begin:"%(ROW)?TYPE",relevance:10},{begin:"\\$\\d+"},{begin:"^#\\w",end:"$"}]},{className:"symbol",begin:"<<\\s*[a-zA-Z_][a-zA-Z_0-9$]*\\s*>>",relevance:10}]}}}());
-hljs.registerLanguage("php",function(){return function(a){var b={begin:"\\$+[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*"},c={className:"meta",variants:[{begin:/<\?php/,relevance:10},{begin:/<\?[=]?/},{begin:/\?>/}]},d={className:"string",contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},e={variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]},f={keyword:"__CLASS__ __DIR__ __FILE__ __FUNCTION__ __LINE__ __METHOD__ __NAMESPACE__ __TRAIT__ die echo exit include include_once print require require_once array abstract and as binary bool boolean break callable case catch class clone const continue declare default do double else elseif empty enddeclare endfor endforeach endif endswitch endwhile eval extends final finally float for foreach from global goto if implements instanceof insteadof int integer interface isset iterable list new object or private protected public real return string switch throw trait try unset use var void while xor yield",
-literal:"false null true",built_in:"Error|0 AppendIterator ArgumentCountError ArithmeticError ArrayIterator ArrayObject AssertionError BadFunctionCallException BadMethodCallException CachingIterator CallbackFilterIterator CompileError Countable DirectoryIterator DivisionByZeroError DomainException EmptyIterator ErrorException Exception FilesystemIterator FilterIterator GlobIterator InfiniteIterator InvalidArgumentException IteratorIterator LengthException LimitIterator LogicException MultipleIterator NoRewindIterator OutOfBoundsException OutOfRangeException OuterIterator OverflowException ParentIterator ParseError RangeException RecursiveArrayIterator RecursiveCachingIterator RecursiveCallbackFilterIterator RecursiveDirectoryIterator RecursiveFilterIterator RecursiveIterator RecursiveIteratorIterator RecursiveRegexIterator RecursiveTreeIterator RegexIterator RuntimeException SeekableIterator SplDoublyLinkedList SplFileInfo SplFileObject SplFixedArray SplHeap SplMaxHeap SplMinHeap SplObjectStorage SplObserver SplObserver SplPriorityQueue SplQueue SplStack SplSubject SplSubject SplTempFileObject TypeError UnderflowException UnexpectedValueException ArrayAccess Closure Generator Iterator IteratorAggregate Serializable Throwable Traversable WeakReference Directory __PHP_Incomplete_Class parent php_user_filter self static stdClass"};
-return{aliases:"php php3 php4 php5 php6 php7".split(" "),case_insensitive:!0,keywords:f,contains:[a.HASH_COMMENT_MODE,a.COMMENT("//","$",{contains:[c]}),a.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler"}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[a.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},c,{className:"keyword",begin:/\$this\b/},
-b,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,illegal:"[$%\\[]",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:f,contains:["self",b,a.C_BLOCK_COMMENT_MODE,d,e]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",
-end:";",illegal:/[\.']/,contains:[a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[a.UNDERSCORE_TITLE_MODE]},{begin:"=>"},d,e]}}}());
-hljs.registerLanguage("php-template",function(){return function(a){return{name:"PHP template",subLanguage:"xml",contains:[{begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0},a.inherit(a.APOS_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0})]}]}}}());
-hljs.registerLanguage("plaintext",function(){return function(a){return{name:"Plain text",aliases:["text","txt"],disableAutodetect:!0}}}());
-hljs.registerLanguage("pony",function(){return function(a){return{name:"Pony",keywords:{keyword:"actor addressof and as be break class compile_error compile_intrinsic consume continue delegate digestof do else elseif embed end error for fun if ifdef in interface is isnt lambda let match new not object or primitive recover repeat return struct then trait try type until use var where while with xor",meta:"iso val tag trn box ref",literal:"this false true"},contains:[{className:"type",begin:"\\b_?[A-Z][\\w]*",
-relevance:0},{className:"string",begin:'"""',end:'"""',relevance:10},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE],relevance:0},{begin:a.IDENT_RE+"'",relevance:0},{className:"number",begin:"(-?)(\\b0[xX][a-fA-F0-9]+|\\b0[bB][01]+|(\\b\\d+(_\\d+)?(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",relevance:0},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}}}());
-hljs.registerLanguage("powershell",function(){return function(a){var b={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},
-c={begin:"`[\\s\\S]",relevance:0},d={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},e={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[c,d,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},f={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},h=a.inherit(a.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},
-{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),g={className:"built_in",variants:[{begin:"(".concat("Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|New|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Complete|Confirm|Deny|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where",
-")+(-)[\\w\\d]+")}]},k={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[a.TITLE_MODE]},l={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[d]}]},m={begin:/using\s/,end:/$/,returnBegin:!0,contains:[e,f,{className:"keyword",
-begin:/(using|assembly|command|module|namespace|type)/}]},p={variants:[{className:"operator",begin:"(".concat("-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor",
-")\\b")},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},q={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(b.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},a.inherit(a.TITLE_MODE,{endsParent:!0})]};a=[q,h,c,a.NUMBER_MODE,e,f,g,d,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}];c={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,
-relevance:0,contains:[].concat("self",a,{begin:"(string|char|byte|int|long|bool|decimal|single|double|DateTime|xml|array|hashtable|void)",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};q.contains.unshift(c);return{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:b,contains:a.concat(k,l,m,p,c)}}}());
-hljs.registerLanguage("processing",function(){return function(a){return{name:"Processing",keywords:{keyword:"BufferedReader PVector PFont PImage PGraphics HashMap boolean byte char color double float int long String Array FloatDict FloatList IntDict IntList JSONArray JSONObject Object StringDict StringList Table TableRow XML false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private",literal:"P2D P3D HALF_PI PI QUARTER_PI TAU TWO_PI",
-title:"setup draw",built_in:"displayHeight displayWidth mouseY mouseX mousePressed pmouseX pmouseY key keyCode pixels focused frameCount frameRate height width size createGraphics beginDraw createShape loadShape PShape arc ellipse line point quad rect triangle bezier bezierDetail bezierPoint bezierTangent curve curveDetail curvePoint curveTangent curveTightness shape shapeMode beginContour beginShape bezierVertex curveVertex endContour endShape quadraticVertex vertex ellipseMode noSmooth rectMode smooth strokeCap strokeJoin strokeWeight mouseClicked mouseDragged mouseMoved mousePressed mouseReleased mouseWheel keyPressed keyPressedkeyReleased keyTyped print println save saveFrame day hour millis minute month second year background clear colorMode fill noFill noStroke stroke alpha blue brightness color green hue lerpColor red saturation modelX modelY modelZ screenX screenY screenZ ambient emissive shininess specular add createImage beginCamera camera endCamera frustum ortho perspective printCamera printProjection cursor frameRate noCursor exit loop noLoop popStyle pushStyle redraw binary boolean byte char float hex int str unbinary unhex join match matchAll nf nfc nfp nfs split splitTokens trim append arrayCopy concat expand reverse shorten sort splice subset box sphere sphereDetail createInput createReader loadBytes loadJSONArray loadJSONObject loadStrings loadTable loadXML open parseXML saveTable selectFolder selectInput beginRaw beginRecord createOutput createWriter endRaw endRecord PrintWritersaveBytes saveJSONArray saveJSONObject saveStream saveStrings saveXML selectOutput popMatrix printMatrix pushMatrix resetMatrix rotate rotateX rotateY rotateZ scale shearX shearY translate ambientLight directionalLight lightFalloff lights lightSpecular noLights normal pointLight spotLight image imageMode loadImage noTint requestImage tint texture textureMode textureWrap blend copy filter get loadPixels set updatePixels blendMode loadShader PShaderresetShader shader createFont loadFont text textFont textAlign textLeading textMode textSize textWidth textAscent textDescent abs ceil constrain dist exp floor lerp log mag map max min norm pow round sq sqrt acos asin atan atan2 cos degrees radians sin tan noise noiseDetail noiseSeed random randomGaussian randomSeed"},
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE]}}}());
-hljs.registerLanguage("profile",function(){return function(a){return{name:"Python profiler",contains:[a.C_NUMBER_MODE,{begin:"[a-zA-Z_][\\da-zA-Z_]+\\.[\\da-zA-Z_]{1,3}",end:":",excludeEnd:!0},{begin:"(ncalls|tottime|cumtime)",end:"$",keywords:"ncalls tottime|10 cumtime|10 filename",relevance:10},{begin:"function calls",end:"$",contains:[a.C_NUMBER_MODE],relevance:10},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"\\(",end:"\\)$",excludeBegin:!0,excludeEnd:!0,relevance:0}]}}}());
-hljs.registerLanguage("prolog",function(){return function(a){var b={begin:/\(/,end:/\)/,relevance:0},c={begin:/\[/,end:/\]/};a=[{begin:/[a-z][A-Za-z0-9_]*/,relevance:0},{className:"symbol",variants:[{begin:/[A-Z][a-zA-Z0-9_]*/},{begin:/_[A-Za-z0-9_]*/}],relevance:0},b,{begin:/:-/},c,{className:"comment",begin:/%/,end:/$/,contains:[a.PHRASAL_WORDS_MODE]},a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{className:"string",begin:/`/,end:/`/,contains:[a.BACKSLASH_ESCAPE]},{className:"string",
-begin:/0'(\\'|.)/},{className:"string",begin:/0'\\s/},a.C_NUMBER_MODE];b.contains=a;c.contains=a;return{name:"Prolog",contains:a.concat([{begin:/\.$/}])}}}());
-hljs.registerLanguage("properties",function(){return function(a){var b={end:"([ \\t\\f]*[:=][ \\t\\f]*|[ \\t\\f]+)",relevance:0,starts:{className:"string",end:/$/,relevance:0,contains:[{begin:"\\\\\\n"}]}};return{name:".properties",case_insensitive:!0,illegal:/\S/,contains:[a.COMMENT("^\\s*[!#]","$"),{begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+([ \\t\\f]*[:=][ \\t\\f]*|[ \\t\\f]+)",returnBegin:!0,contains:[{className:"attr",begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",endsParent:!0,relevance:0}],starts:b},
-{begin:"([^\\\\:= \\t\\f\\n]|\\\\.)+([ \\t\\f]*[:=][ \\t\\f]*|[ \\t\\f]+)",returnBegin:!0,relevance:0,contains:[{className:"meta",begin:"([^\\\\:= \\t\\f\\n]|\\\\.)+",endsParent:!0,relevance:0}],starts:b},{className:"attr",relevance:0,begin:"([^\\\\:= \\t\\f\\n]|\\\\.)+[ \\t\\f]*$"}]}}}());
-hljs.registerLanguage("protobuf",function(){return function(a){return{name:"Protocol Buffers",keywords:{keyword:"package import option optional required repeated group oneof",built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,{className:"class",beginKeywords:"message enum service",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,
-excludeEnd:!0}})]},{className:"function",beginKeywords:"rpc",end:/[{;]/,excludeEnd:!0,keywords:"rpc returns"},{begin:/^\s*[A-Z_]+/,end:/\s*=/,excludeEnd:!0}]}}}());
-hljs.registerLanguage("puppet",function(){return function(a){var b=a.COMMENT("#","$"),c=a.inherit(a.TITLE_MODE,{begin:"([A-Za-z_]|::)(\\w|::)*"}),d={className:"variable",begin:"\\$([A-Za-z_]|::)(\\w|::)*"},e={className:"string",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/}]};return{name:"Puppet",aliases:["pp"],contains:[b,d,e,{beginKeywords:"class",end:"\\{|;",illegal:/=/,contains:[c,b]},{beginKeywords:"define",end:/\{/,contains:[{className:"section",begin:a.IDENT_RE,
-endsParent:!0}]},{begin:a.IDENT_RE+"\\s+\\{",returnBegin:!0,end:/\S/,contains:[{className:"keyword",begin:a.IDENT_RE},{begin:/\{/,end:/\}/,keywords:{keyword:"and case default else elsif false if in import enherits node or true undef unless main settings $string ",literal:"alias audit before loglevel noop require subscribe tag owner ensure group mode name|0 changes context force incl lens load_path onlyif provider returns root show_diff type_check en_address ip_address realname command environment hour monute month monthday special target weekday creates cwd ogoutput refresh refreshonly tries try_sleep umask backup checksum content ctime force ignore links mtime purge recurse recurselimit replace selinux_ignore_defaults selrange selrole seltype seluser source souirce_permissions sourceselect validate_cmd validate_replacement allowdupe attribute_membership auth_membership forcelocal gid ia_load_module members system host_aliases ip allowed_trunk_vlans description device_url duplex encapsulation etherchannel native_vlan speed principals allow_root auth_class auth_type authenticate_user k_of_n mechanisms rule session_owner shared options device fstype enable hasrestart directory present absent link atboot blockdevice device dump pass remounts poller_tag use message withpath adminfile allow_virtual allowcdrom category configfiles flavor install_options instance package_settings platform responsefile status uninstall_options vendor unless_system_user unless_uid binary control flags hasstatus manifest pattern restart running start stop allowdupe auths expiry gid groups home iterations key_membership keys managehome membership password password_max_age password_min_age profile_membership profiles project purge_ssh_keys role_membership roles salt shell uid baseurl cost descr enabled enablegroups exclude failovermethod gpgcheck gpgkey http_caching include includepkgs keepalive metadata_expire metalink mirrorlist priority protect proxy proxy_password proxy_username repo_gpgcheck s3_enabled skip_if_unavailable sslcacert sslclientcert sslclientkey sslverify mounted",
-built_in:"architecture augeasversion blockdevices boardmanufacturer boardproductname boardserialnumber cfkey dhcp_servers domain ec2_ ec2_userdata facterversion filesystems ldom fqdn gid hardwareisa hardwaremodel hostname id|0 interfaces ipaddress ipaddress_ ipaddress6 ipaddress6_ iphostnumber is_virtual kernel kernelmajversion kernelrelease kernelversion kernelrelease kernelversion lsbdistcodename lsbdistdescription lsbdistid lsbdistrelease lsbmajdistrelease lsbminordistrelease lsbrelease macaddress macaddress_ macosx_buildversion macosx_productname macosx_productversion macosx_productverson_major macosx_productversion_minor manufacturer memoryfree memorysize netmask metmask_ network_ operatingsystem operatingsystemmajrelease operatingsystemrelease osfamily partitions path physicalprocessorcount processor processorcount productname ps puppetversion rubysitedir rubyversion selinux selinux_config_mode selinux_config_policy selinux_current_mode selinux_current_mode selinux_enforced selinux_policyversion serialnumber sp_ sshdsakey sshecdsakey sshrsakey swapencrypted swapfree swapsize timezone type uniqueid uptime uptime_days uptime_hours uptime_seconds uuid virtual vlans xendomains zfs_version zonenae zones zpool_version"},
-relevance:0,contains:[e,b,{begin:"[a-zA-Z_]+\\s*=>",returnBegin:!0,end:"=>",contains:[{className:"attr",begin:a.IDENT_RE}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},d]}],relevance:0}]}}}());
-hljs.registerLanguage("purebasic",function(){return function(a){return{name:"PureBASIC",aliases:["pb","pbi"],keywords:"Align And Array As Break CallDebugger Case CompilerCase CompilerDefault CompilerElse CompilerElseIf CompilerEndIf CompilerEndSelect CompilerError CompilerIf CompilerSelect CompilerWarning Continue Data DataSection Debug DebugLevel Declare DeclareC DeclareCDLL DeclareDLL DeclareModule Default Define Dim DisableASM DisableDebugger DisableExplicit Else ElseIf EnableASM EnableDebugger EnableExplicit End EndDataSection EndDeclareModule EndEnumeration EndIf EndImport EndInterface EndMacro EndModule EndProcedure EndSelect EndStructure EndStructureUnion EndWith Enumeration EnumerationBinary Extends FakeReturn For ForEach ForEver Global Gosub Goto If Import ImportC IncludeBinary IncludeFile IncludePath Interface List Macro MacroExpandedCount Map Module NewList NewMap Next Not Or Procedure ProcedureC ProcedureCDLL ProcedureDLL ProcedureReturn Protected Prototype PrototypeC ReDim Read Repeat Restore Return Runtime Select Shared Static Step Structure StructureUnion Swap Threaded To UndefineMacro Until Until  UnuseModule UseModule Wend While With XIncludeFile XOr",contains:[a.COMMENT(";",
-"$",{relevance:0}),{className:"function",begin:"\\b(Procedure|Declare)(C|CDLL|DLL)?\\b",end:"\\(",excludeEnd:!0,returnBegin:!0,contains:[{className:"keyword",begin:"(Procedure|Declare)(C|CDLL|DLL)?",excludeEnd:!0},{className:"type",begin:"\\.\\w*"},a.UNDERSCORE_TITLE_MODE]},{className:"string",begin:'(~)?"',end:'"',illegal:"\\n"},{className:"symbol",begin:"#[a-zA-Z_]\\w*\\$?"}]}}}());
-hljs.registerLanguage("python",function(){return function(a){var b={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10",built_in:"Ellipsis NotImplemented",literal:"False None True"},c={className:"meta",begin:/^(>>>|\.\.\.) /},d={className:"subst",begin:/\{/,end:/\}/,keywords:b,illegal:/#/},e={begin:/\{\{/,relevance:0};e={className:"string",contains:[a.BACKSLASH_ESCAPE],
-variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[a.BACKSLASH_ESCAPE,c],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[a.BACKSLASH_ESCAPE,c],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[a.BACKSLASH_ESCAPE,c,e,d]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[a.BACKSLASH_ESCAPE,c,e,d]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[a.BACKSLASH_ESCAPE,e,d]},{begin:/(fr|rf|f)"/,
-end:/"/,contains:[a.BACKSLASH_ESCAPE,e,d]},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]};var f={className:"number",relevance:0,variants:[{begin:a.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:a.C_NUMBER_RE+"[lLjJ]?"}]},h={className:"params",variants:[{begin:/\(\s*\)/,skip:!0,className:null},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:["self",c,f,e,a.HASH_COMMENT_MODE]}]};d.contains=[e,f,c];return{name:"Python",aliases:["py","gyp","ipython"],keywords:b,illegal:/(<\/|->|\?)|=>/,
-contains:[c,f,{beginKeywords:"if",relevance:0},e,a.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[a.UNDERSCORE_TITLE_MODE,h,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}}}());
-hljs.registerLanguage("python-repl",function(){return function(a){return{aliases:["pycon"],contains:[{className:"meta",starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}}}());
-hljs.registerLanguage("q",function(){return function(a){return{name:"Q",aliases:["k","kdb"],keywords:{$pattern:/(`?)[A-Za-z0-9_]+\b/,keyword:"do while select delete by update from",literal:"0b 1b",built_in:"neg not null string reciprocal floor ceiling signum mod xbar xlog and or each scan over prior mmu lsq inv md5 ltime gtime count first var dev med cov cor all any rand sums prds mins maxs fills deltas ratios avgs differ prev next rank reverse iasc idesc asc desc msum mcount mavg mdev xrank mmin mmax xprev rotate distinct group where flip type key til get value attr cut set upsert raze union inter except cross sv vs sublist enlist read0 read1 hopen hclose hdel hsym hcount peach system ltrim rtrim trim lower upper ssr view tables views cols xcols keys xkey xcol xasc xdesc fkeys meta lj aj aj0 ij pj asof uj ww wj wj1 fby xgroup ungroup ej save load rsave rload show csv parse eval min max avg wavg wsum sin cos tan sum",type:"`float `double int `timestamp `timespan `datetime `time `boolean `symbol `char `byte `short `long `real `month `date `minute `second `guid"},
-contains:[a.C_LINE_COMMENT_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE]}}}());
-hljs.registerLanguage("qml",function(){return function(a){var b={begin:"[a-zA-Z_][a-zA-Z0-9\\._]*\\s*{",end:"{",returnBegin:!0,relevance:0,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_][a-zA-Z0-9\\._]*"})]};return{name:"QML",aliases:["qt"],case_insensitive:!1,keywords:{keyword:"in of on if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await import",literal:"true false null undefined NaN Infinity",
-built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Behavior bool color coordinate date double enumeration font geocircle georectangle geoshape int list matrix4x4 parent point quaternion real rect size string url variant vector2d vector3d vector4d Promise"},
-contains:[{className:"meta",begin:/^\s*['"]use (strict|asm)['"]/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[a.C_LINE_COMMENT_MODE,
-a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{begin:/</,end:/>\s*[);\]]/,relevance:0,subLanguage:"xml"}],relevance:0},{className:"keyword",begin:"\\bsignal\\b",starts:{className:"string",end:"(\\(|:|=|;|,|//|/\\*|$)",returnEnd:!0}},{className:"keyword",begin:"\\bproperty\\b",starts:{className:"string",end:"(:|=|;|,|//|/\\*|$)",returnEnd:!0}},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{className:"params",begin:/\(/,
-end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}],illegal:/\[|%/},{begin:"\\."+a.IDENT_RE,relevance:0},{className:"attribute",begin:"\\bid\\s*:",starts:{className:"string",end:"[a-zA-Z_][a-zA-Z0-9\\._]*",returnEnd:!1}},{begin:"[a-zA-Z_][a-zA-Z0-9\\._]*\\s*:",returnBegin:!0,contains:[{className:"attribute",begin:"[a-zA-Z_][a-zA-Z0-9\\._]*",end:"\\s*:",excludeEnd:!0,relevance:0}],relevance:0},b],illegal:/#/}}}());
-hljs.registerLanguage("r",function(){return function(a){return{name:"R",contains:[a.HASH_COMMENT_MODE,{begin:"([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*",keywords:{$pattern:"([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*",keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},relevance:0},
-{className:"number",begin:"0[xX][0-9a-fA-F]+[Li]?\\b",relevance:0},{className:"number",begin:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",relevance:0},{className:"number",begin:"\\d+\\.(?!\\d)(?:i\\b)?",relevance:0},{className:"number",begin:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{className:"number",begin:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{begin:"`",end:"`",relevance:0},{className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]}]}}}());
-hljs.registerLanguage("reasonml",function(){return function(a){var b="("+function(a){return a.map(function(a){return a.split("").map(function(a){return"\\"+a}).join("")}).join("|")}("|| && ++ ** +. * / *. /. ... |>".split(" "))+"|==|===)",c="\\s+"+b+"\\s+",d={keyword:"and as asr assert begin class constraint do done downto else end exception external for fun function functor if in include inherit initializer land lazy let lor lsl lsr lxor match method mod module mutable new nonrec object of open or private rec sig struct then to try type val virtual when while with",
-built_in:"array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 ref string unit ",literal:"true false"},e={className:"number",relevance:0,variants:[{begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)"},{begin:"\\(\\-\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)\\)"}]},f={className:"operator",relevance:0,begin:b};b=[{className:"identifier",
-relevance:0,begin:"~?[a-z$_][0-9a-zA-Z$_]*"},f,e];var h=[a.QUOTE_STRING_MODE,f,{className:"module",begin:"\\b`?[A-Z$_][0-9a-zA-Z$_]*",returnBegin:!0,end:".",contains:[{className:"identifier",begin:"`?[A-Z$_][0-9a-zA-Z$_]*",relevance:0}]}],g=[{className:"module",begin:"\\b`?[A-Z$_][0-9a-zA-Z$_]*",returnBegin:!0,end:".",relevance:0,contains:[{className:"identifier",begin:"`?[A-Z$_][0-9a-zA-Z$_]*",relevance:0}]}],k={className:"function",relevance:0,keywords:d,variants:[{begin:"\\s(\\(\\.?.*?\\)|~?[a-z$_][0-9a-zA-Z$_]*)\\s*=>",
-end:"\\s*=>",returnBegin:!0,relevance:0,contains:[{className:"params",variants:[{begin:"~?[a-z$_][0-9a-zA-Z$_]*"},{begin:"~?[a-z$_][0-9a-zA-Z$_]*(s*:s*[a-z$_][0-9a-z$_]*((s*('?[a-z$_][0-9a-z$_]*s*(,'?[a-z$_][0-9a-z$_]*)*)?s*))?)?(s*:s*[a-z$_][0-9a-z$_]*((s*('?[a-z$_][0-9a-z$_]*s*(,'?[a-z$_][0-9a-z$_]*)*)?s*))?)?"},{begin:/\(\s*\)/}]}]},{begin:"\\s\\(\\.?[^;\\|]*\\)\\s*=>",end:"\\s=>",returnBegin:!0,relevance:0,contains:[{className:"params",relevance:0,variants:[{begin:"~?[a-z$_][0-9a-zA-Z$_]*",end:"(,|\\n|\\))",
-relevance:0,contains:[f,{className:"typing",begin:":",end:"(,|\\n)",returnBegin:!0,relevance:0,contains:g}]}]}]},{begin:"\\(\\.\\s~?[a-z$_][0-9a-zA-Z$_]*\\)\\s*=>"}]};h.push(k);var l={className:"constructor",begin:"`?[A-Z$_][0-9a-zA-Z$_]*\\(",end:"\\)",illegal:"\\n",keywords:d,contains:[a.QUOTE_STRING_MODE,f,{className:"params",begin:"\\b~?[a-z$_][0-9a-zA-Z$_]*"}]};f={className:"pattern-match",begin:"\\|",returnBegin:!0,keywords:d,end:"=>",relevance:0,contains:[l,f,{relevance:0,className:"constructor",
-begin:"`?[A-Z$_][0-9a-zA-Z$_]*"}]};var m={className:"module-access",keywords:d,returnBegin:!0,variants:[{begin:"\\b(`?[A-Z$_][0-9a-zA-Z$_]*\\.)+~?[a-z$_][0-9a-zA-Z$_]*"},{begin:"\\b(`?[A-Z$_][0-9a-zA-Z$_]*\\.)+\\(",end:"\\)",returnBegin:!0,contains:[k,{begin:"\\(",end:"\\)",skip:!0}].concat(h)},{begin:"\\b(`?[A-Z$_][0-9a-zA-Z$_]*\\.)+{",end:"}"}],contains:h};g.push(m);return{name:"ReasonML",aliases:["re"],keywords:d,illegal:"(:\\-|:=|\\${|\\+=)",contains:[a.COMMENT("/\\*","\\*/",{illegal:"^(\\#,\\/\\/)"}),
-{className:"character",begin:"'(\\\\[^']+|[^'])'",illegal:"\\n",relevance:0},a.QUOTE_STRING_MODE,{className:"literal",begin:"\\(\\)",relevance:0},{className:"literal",begin:"\\[\\|",end:"\\|\\]",relevance:0,contains:b},{className:"literal",begin:"\\[",end:"\\]",relevance:0,contains:b},l,{className:"operator",begin:c,illegal:"\\-\\->",relevance:0},e,a.C_LINE_COMMENT_MODE,f,k,{className:"module-def",begin:"\\bmodule\\s+~?[a-z$_][0-9a-zA-Z$_]*\\s+`?[A-Z$_][0-9a-zA-Z$_]*\\s+=\\s+{",end:"}",returnBegin:!0,
-keywords:d,relevance:0,contains:[{className:"module",relevance:0,begin:"`?[A-Z$_][0-9a-zA-Z$_]*"},{begin:"{",end:"}",skip:!0}].concat(h)},m]}}}());
-hljs.registerLanguage("rib",function(){return function(a){return{name:"RenderMan RIB",keywords:"ArchiveRecord AreaLightSource Atmosphere Attribute AttributeBegin AttributeEnd Basis Begin Blobby Bound Clipping ClippingPlane Color ColorSamples ConcatTransform Cone CoordinateSystem CoordSysTransform CropWindow Curves Cylinder DepthOfField Detail DetailRange Disk Displacement Display End ErrorHandler Exposure Exterior Format FrameAspectRatio FrameBegin FrameEnd GeneralPolygon GeometricApproximation Geometry Hider Hyperboloid Identity Illuminate Imager Interior LightSource MakeCubeFaceEnvironment MakeLatLongEnvironment MakeShadow MakeTexture Matte MotionBegin MotionEnd NuPatch ObjectBegin ObjectEnd ObjectInstance Opacity Option Orientation Paraboloid Patch PatchMesh Perspective PixelFilter PixelSamples PixelVariance Points PointsGeneralPolygons PointsPolygons Polygon Procedural Projection Quantize ReadArchive RelativeDetail ReverseOrientation Rotate Scale ScreenWindow ShadingInterpolation ShadingRate Shutter Sides Skew SolidBegin SolidEnd Sphere SubdivisionMesh Surface TextureCoordinates Torus Transform TransformBegin TransformEnd TransformPoints Translate TrimCurve WorldBegin WorldEnd",illegal:"</",
-contains:[a.HASH_COMMENT_MODE,a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}}}());
-hljs.registerLanguage("roboconf",function(){return function(a){var b={className:"attribute",begin:/[a-zA-Z-_]+/,end:/\s*:/,excludeEnd:!0,starts:{end:";",relevance:0,contains:[{className:"variable",begin:/\.[a-zA-Z-_]+/},{className:"keyword",begin:/\(optional\)/}]}};return{name:"Roboconf",aliases:["graph","instances"],case_insensitive:!0,keywords:"import",contains:[{begin:"^facet [a-zA-Z-_][^\\n{]+\\{",end:"}",keywords:"facet",contains:[b,a.HASH_COMMENT_MODE]},{begin:"^\\s*instance of [a-zA-Z-_][^\\n{]+\\{",
-end:"}",keywords:"name count channels instance-data instance-state instance of",illegal:/\S/,contains:["self",b,a.HASH_COMMENT_MODE]},{begin:"^[a-zA-Z-_][^\\n{]+\\{",end:"}",contains:[b,a.HASH_COMMENT_MODE]},a.HASH_COMMENT_MODE]}}}());
-hljs.registerLanguage("routeros",function(){return function(a){var b={className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},c={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,b,{className:"variable",begin:/\$\(/,end:/\)/,contains:[a.BACKSLASH_ESCAPE]}]},d={className:"string",begin:/'/,end:/'/};return{name:"Microtik RouterOS script",aliases:["routeros","mikrotik"],case_insensitive:!0,keywords:{$pattern:/:?[\w-]+/,literal:"true false yes no nothing nil null",
-keyword:"foreach do while for if from to step else on-error and or not in :foreach :do :while :for :if :from :to :step :else :on-error :and :or :not :in :global :local :beep :delay :put :len :typeof :pick :log :time :set :find :environment :terminal :error :execute :parse :resolve :toarray :tobool :toid :toip :toip6 :tonum :tostr :totime"},contains:[{variants:[{begin:/^@/,end:/$/},{begin:/\/\*/,end:/\*\//},{begin:/%%/,end:/$/},{begin:/^'/,end:/$/},{begin:/^\s*\/[\w-]+=/,end:/$/},{begin:/\/\//,end:/$/},
-{begin:/^\[</,end:/>\]$/},{begin:/<\//,end:/>/},{begin:/^facet /,end:/\}/},{begin:"^1\\.\\.(\\d+)$",end:/$/}],illegal:/./},a.COMMENT("^#","$"),c,d,b,{begin:/[\w-]+=([^\s\{\}\[\]\(\)]+)/,relevance:0,returnBegin:!0,contains:[{className:"attribute",begin:/[^=]+/},{begin:/=/,endsWithParent:!0,relevance:0,contains:[c,d,b,{className:"literal",begin:"\\b(true|false|yes|no|nothing|nil|null)\\b"},{begin:/("[^"]*"|[^\s\{\}\[\]]+)/}]}]},{className:"number",begin:/\*[0-9a-fA-F]+/},{begin:"\\b(add|remove|enable|disable|set|get|print|export|edit|find|run|debug|error|info|warning)([\\s[(]|])",
-returnBegin:!0,contains:[{className:"builtin-name",begin:/\w+/}]},{className:"built_in",variants:[{begin:"(\\.\\./|/|\\s)((traffic-flow|traffic-generator|firewall|scheduler|aaa|accounting|address-list|address|align|area|bandwidth-server|bfd|bgp|bridge|client|clock|community|config|connection|console|customer|default|dhcp-client|dhcp-server|discovery|dns|e-mail|ethernet|filter|firewall|firmware|gps|graphing|group|hardware|health|hotspot|identity|igmp-proxy|incoming|instance|interface|ip|ipsec|ipv6|irq|l2tp-server|lcd|ldp|logging|mac-server|mac-winbox|mangle|manual|mirror|mme|mpls|nat|nd|neighbor|network|note|ntp|ospf|ospf-v3|ovpn-server|page|peer|pim|ping|policy|pool|port|ppp|pppoe-client|pptp-server|prefix|profile|proposal|proxy|queue|radius|resource|rip|ripng|route|routing|screen|script|security-profiles|server|service|service-port|settings|shares|smb|sms|sniffer|snmp|snooper|socks|sstp-server|system|tool|tracking|type|upgrade|upnp|user-manager|users|user|vlan|secret|vrrp|watchdog|web-access|wireless|pptp|pppoe|lan|wan|layer7-protocol|lease|simple|raw);?\\s)+",
-relevance:10},{begin:/\.\./}]}]}}}());
-hljs.registerLanguage("rsl",function(){return function(a){return{name:"RenderMan RSL",keywords:{keyword:"float color point normal vector matrix while for if do return else break extern continue",built_in:"abs acos ambient area asin atan atmosphere attribute calculatenormal ceil cellnoise clamp comp concat cos degrees depth Deriv diffuse distance Du Dv environment exp faceforward filterstep floor format fresnel incident length lightsource log match max min mod noise normalize ntransform opposite option phong pnoise pow printf ptlined radians random reflect refract renderinfo round setcomp setxcomp setycomp setzcomp shadow sign sin smoothstep specular specularbrdf spline sqrt step tan texture textureinfo trace transform vtransform xcomp ycomp zcomp"},illegal:"</",
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_NUMBER_MODE,{className:"meta",begin:"#",end:"$"},{className:"class",beginKeywords:"surface displacement light volume imager",end:"\\("},{beginKeywords:"illuminate illuminance gather",end:"\\("}]}}}());
-hljs.registerLanguage("ruleslanguage",function(){return function(a){return{name:"Oracle Rules Language",keywords:{keyword:"BILL_PERIOD BILL_START BILL_STOP RS_EFFECTIVE_START RS_EFFECTIVE_STOP RS_JURIS_CODE RS_OPCO_CODE INTDADDATTRIBUTE|5 INTDADDVMSG|5 INTDBLOCKOP|5 INTDBLOCKOPNA|5 INTDCLOSE|5 INTDCOUNT|5 INTDCOUNTSTATUSCODE|5 INTDCREATEMASK|5 INTDCREATEDAYMASK|5 INTDCREATEFACTORMASK|5 INTDCREATEHANDLE|5 INTDCREATEOVERRIDEDAYMASK|5 INTDCREATEOVERRIDEMASK|5 INTDCREATESTATUSCODEMASK|5 INTDCREATETOUPERIOD|5 INTDDELETE|5 INTDDIPTEST|5 INTDEXPORT|5 INTDGETERRORCODE|5 INTDGETERRORMESSAGE|5 INTDISEQUAL|5 INTDJOIN|5 INTDLOAD|5 INTDLOADACTUALCUT|5 INTDLOADDATES|5 INTDLOADHIST|5 INTDLOADLIST|5 INTDLOADLISTDATES|5 INTDLOADLISTENERGY|5 INTDLOADLISTHIST|5 INTDLOADRELATEDCHANNEL|5 INTDLOADSP|5 INTDLOADSTAGING|5 INTDLOADUOM|5 INTDLOADUOMDATES|5 INTDLOADUOMHIST|5 INTDLOADVERSION|5 INTDOPEN|5 INTDREADFIRST|5 INTDREADNEXT|5 INTDRECCOUNT|5 INTDRELEASE|5 INTDREPLACE|5 INTDROLLAVG|5 INTDROLLPEAK|5 INTDSCALAROP|5 INTDSCALE|5 INTDSETATTRIBUTE|5 INTDSETDSTPARTICIPANT|5 INTDSETSTRING|5 INTDSETVALUE|5 INTDSETVALUESTATUS|5 INTDSHIFTSTARTTIME|5 INTDSMOOTH|5 INTDSORT|5 INTDSPIKETEST|5 INTDSUBSET|5 INTDTOU|5 INTDTOURELEASE|5 INTDTOUVALUE|5 INTDUPDATESTATS|5 INTDVALUE|5 STDEV INTDDELETEEX|5 INTDLOADEXACTUAL|5 INTDLOADEXCUT|5 INTDLOADEXDATES|5 INTDLOADEX|5 INTDLOADEXRELATEDCHANNEL|5 INTDSAVEEX|5 MVLOAD|5 MVLOADACCT|5 MVLOADACCTDATES|5 MVLOADACCTHIST|5 MVLOADDATES|5 MVLOADHIST|5 MVLOADLIST|5 MVLOADLISTDATES|5 MVLOADLISTHIST|5 IF FOR NEXT DONE SELECT END CALL ABORT CLEAR CHANNEL FACTOR LIST NUMBER OVERRIDE SET WEEK DISTRIBUTIONNODE ELSE WHEN THEN OTHERWISE IENUM CSV INCLUDE LEAVE RIDER SAVE DELETE NOVALUE SECTION WARN SAVE_UPDATE DETERMINANT LABEL REPORT REVENUE EACH IN FROM TOTAL CHARGE BLOCK AND OR CSV_FILE RATE_CODE AUXILIARY_DEMAND UIDACCOUNT RS BILL_PERIOD_SELECT HOURS_PER_MONTH INTD_ERROR_STOP SEASON_SCHEDULE_NAME ACCOUNTFACTOR ARRAYUPPERBOUND CALLSTOREDPROC GETADOCONNECTION GETCONNECT GETDATASOURCE GETQUALIFIER GETUSERID HASVALUE LISTCOUNT LISTOP LISTUPDATE LISTVALUE PRORATEFACTOR RSPRORATE SETBINPATH SETDBMONITOR WQ_OPEN BILLINGHOURS DATE DATEFROMFLOAT DATETIMEFROMSTRING DATETIMETOSTRING DATETOFLOAT DAY DAYDIFF DAYNAME DBDATETIME HOUR MINUTE MONTH MONTHDIFF MONTHHOURS MONTHNAME ROUNDDATE SAMEWEEKDAYLASTYEAR SECOND WEEKDAY WEEKDIFF YEAR YEARDAY YEARSTR COMPSUM HISTCOUNT HISTMAX HISTMIN HISTMINNZ HISTVALUE MAXNRANGE MAXRANGE MINRANGE COMPIKVA COMPKVA COMPKVARFROMKQKW COMPLF IDATTR FLAG LF2KW LF2KWH MAXKW POWERFACTOR READING2USAGE AVGSEASON MAXSEASON MONTHLYMERGE SEASONVALUE SUMSEASON ACCTREADDATES ACCTTABLELOAD CONFIGADD CONFIGGET CREATEOBJECT CREATEREPORT EMAILCLIENT EXPBLKMDMUSAGE EXPMDMUSAGE EXPORT_USAGE FACTORINEFFECT GETUSERSPECIFIEDSTOP INEFFECT ISHOLIDAY RUNRATE SAVE_PROFILE SETREPORTTITLE USEREXIT WATFORRUNRATE TO TABLE ACOS ASIN ATAN ATAN2 BITAND CEIL COS COSECANT COSH COTANGENT DIVQUOT DIVREM EXP FABS FLOOR FMOD FREPM FREXPN LOG LOG10 MAX MAXN MIN MINNZ MODF POW ROUND ROUND2VALUE ROUNDINT SECANT SIN SINH SQROOT TAN TANH FLOAT2STRING FLOAT2STRINGNC INSTR LEFT LEN LTRIM MID RIGHT RTRIM STRING STRINGNC TOLOWER TOUPPER TRIM NUMDAYS READ_DATE STAGING",built_in:"IDENTIFIER OPTIONS XML_ELEMENT XML_OP XML_ELEMENT_OF DOMDOCCREATE DOMDOCLOADFILE DOMDOCLOADXML DOMDOCSAVEFILE DOMDOCGETROOT DOMDOCADDPI DOMNODEGETNAME DOMNODEGETTYPE DOMNODEGETVALUE DOMNODEGETCHILDCT DOMNODEGETFIRSTCHILD DOMNODEGETSIBLING DOMNODECREATECHILDELEMENT DOMNODESETATTRIBUTE DOMNODEGETCHILDELEMENTCT DOMNODEGETFIRSTCHILDELEMENT DOMNODEGETSIBLINGELEMENT DOMNODEGETATTRIBUTECT DOMNODEGETATTRIBUTEI DOMNODEGETATTRIBUTEBYNAME DOMNODEGETBYNAME"},
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{className:"literal",variants:[{begin:"#\\s+[a-zA-Z\\ \\.]*",relevance:0},{begin:"#[a-zA-Z\\ \\.]+"}]}]}}}());
-hljs.registerLanguage("rust",function(){return function(a){return{name:"Rust",aliases:["rs"],keywords:{$pattern:a.IDENT_RE+"!?",keyword:"abstract as async await become box break const continue crate do dyn else enum extern false final fn for if impl in let loop macro match mod move mut override priv pub ref return self Self static struct super trait true try type typeof unsafe unsized use virtual where while yield",literal:"true false Some None Ok Err",built_in:"drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!"},
-illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.COMMENT("/\\*","\\*/",{contains:["self"]}),a.inherit(a.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{className:"string",variants:[{begin:/r(#*)"(.|\n)*?"\1(?!#)/},{begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{begin:"\\b0b([01_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},{begin:"\\b0o([0-7_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},{begin:"\\b0x([A-Fa-f0-9_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},
-{begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)([ui](8|16|32|64|128|size)|f(32|64))?"}],relevance:0},{className:"function",beginKeywords:"fn",end:"(\\(|<)",excludeEnd:!0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"meta",begin:"#\\!?\\[",end:"\\]",contains:[{className:"meta-string",begin:/"/,end:/"/}]},{className:"class",beginKeywords:"type",end:";",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"\\S"},{className:"class",beginKeywords:"trait enum struct union",end:"{",
-contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"[\\w\\d]"},{begin:a.IDENT_RE+"::",keywords:{built_in:"drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!"}},
-{begin:"->"}]}}}());
-hljs.registerLanguage("sas",function(){return function(a){return{name:"SAS",aliases:["sas","SAS"],case_insensitive:!0,keywords:{literal:"null missing _all_ _automatic_ _character_ _infile_ _n_ _name_ _null_ _numeric_ _user_ _webout_",meta:"do if then else end until while abort array attrib by call cards cards4 catname continue datalines datalines4 delete delim delimiter display dm drop endsas error file filename footnote format goto in infile informat input keep label leave length libname link list lostcard merge missing modify options output out page put redirect remove rename replace retain return select set skip startsas stop title update waitsas where window x systask add and alter as cascade check create delete describe distinct drop foreign from group having index insert into in key like message modify msgtype not null on or order primary references reset restrict select set table unique update validate view where"},contains:[{className:"keyword",
-begin:/^\s*(proc [\w\d_]+|data|run|quit)[\s;]/},{className:"variable",begin:/&[a-zA-Z_&][a-zA-Z0-9_]*\.?/},{className:"emphasis",begin:/^\s*datalines|cards.*;/,end:/^\s*;\s*$/},{className:"built_in",begin:"%(bquote|nrbquote|cmpres|qcmpres|compstor|datatyp|display|do|else|end|eval|global|goto|if|index|input|keydef|label|left|length|let|local|lowcase|macro|mend|nrbquote|nrquote|nrstr|put|qcmpres|qleft|qlowcase|qscan|qsubstr|qsysfunc|qtrim|quote|qupcase|scan|str|substr|superq|syscall|sysevalf|sysexec|sysfunc|sysget|syslput|sysprod|sysrc|sysrput|then|to|trim|unquote|until|upcase|verify|while|window)"},
-{className:"name",begin:/%[a-zA-Z_][a-zA-Z_0-9]*/},{className:"meta",begin:"[^%](abs|addr|airy|arcos|arsin|atan|attrc|attrn|band|betainv|blshift|bnot|bor|brshift|bxor|byte|cdf|ceil|cexist|cinv|close|cnonct|collate|compbl|compound|compress|cos|cosh|css|curobs|cv|daccdb|daccdbsl|daccsl|daccsyd|dacctab|dairy|date|datejul|datepart|datetime|day|dclose|depdb|depdbsl|depdbsl|depsl|depsl|depsyd|depsyd|deptab|deptab|dequote|dhms|dif|digamma|dim|dinfo|dnum|dopen|doptname|doptnum|dread|dropnote|dsname|erf|erfc|exist|exp|fappend|fclose|fcol|fdelete|fetch|fetchobs|fexist|fget|fileexist|filename|fileref|finfo|finv|fipname|fipnamel|fipstate|floor|fnonct|fnote|fopen|foptname|foptnum|fpoint|fpos|fput|fread|frewind|frlen|fsep|fuzz|fwrite|gaminv|gamma|getoption|getvarc|getvarn|hbound|hms|hosthelp|hour|ibessel|index|indexc|indexw|input|inputc|inputn|int|intck|intnx|intrr|irr|jbessel|juldate|kurtosis|lag|lbound|left|length|lgamma|libname|libref|log|log10|log2|logpdf|logpmf|logsdf|lowcase|max|mdy|mean|min|minute|mod|month|mopen|mort|n|netpv|nmiss|normal|note|npv|open|ordinal|pathname|pdf|peek|peekc|pmf|point|poisson|poke|probbeta|probbnml|probchi|probf|probgam|probhypr|probit|probnegb|probnorm|probt|put|putc|putn|qtr|quote|ranbin|rancau|ranexp|rangam|range|rank|rannor|ranpoi|rantbl|rantri|ranuni|repeat|resolve|reverse|rewind|right|round|saving|scan|sdf|second|sign|sin|sinh|skewness|soundex|spedis|sqrt|std|stderr|stfips|stname|stnamel|substr|sum|symget|sysget|sysmsg|sysprod|sysrc|system|tan|tanh|time|timepart|tinv|tnonct|today|translate|tranwrd|trigamma|trim|trimn|trunc|uniform|upcase|uss|var|varfmt|varinfmt|varlabel|varlen|varname|varnum|varray|varrayx|vartype|verify|vformat|vformatd|vformatdx|vformatn|vformatnx|vformatw|vformatwx|vformatx|vinarray|vinarrayx|vinformat|vinformatd|vinformatdx|vinformatn|vinformatnx|vinformatw|vinformatwx|vinformatx|vlabel|vlabelx|vlength|vlengthx|vname|vnamex|vtype|vtypex|weekday|year|yyq|zipfips|zipname|zipnamel|zipstate)[(]"},
-{className:"string",variants:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},a.COMMENT("\\*",";"),a.C_BLOCK_COMMENT_MODE]}}}());
-hljs.registerLanguage("scala",function(){return function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},c={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},d={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0};return{name:"Scala",keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[b],relevance:10}]},{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},c,{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[d]},{className:"class",beginKeywords:"class object trait type",
-end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[c]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[c]},d]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}}}());
-hljs.registerLanguage("scheme",function(){return function(a){var b={className:"literal",begin:"(#t|#f|#\\\\[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+|#\\\\.)"},c={className:"number",variants:[{begin:"(\\-|\\+)?\\d+([./]\\d+)?",relevance:0},{begin:"(\\-|\\+)?\\d+([./]\\d+)?[+\\-](\\-|\\+)?\\d+([./]\\d+)?i",relevance:0},{begin:"#b[0-1]+(/[0-1]+)?"},{begin:"#o[0-7]+(/[0-7]+)?"},{begin:"#x[0-9a-f]+(/[0-9a-f]+)?"}]},d=a.QUOTE_STRING_MODE,e=[a.COMMENT(";","$",{relevance:0}),a.COMMENT("#\\|","\\|#")],f={begin:"[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",
-relevance:0},h={className:"symbol",begin:"'[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+"},g={endsWithParent:!0,relevance:0},k={variants:[{begin:/'/},{begin:"`"}],contains:[{begin:"\\(",end:"\\)",contains:["self",b,d,c,f,h]}]},l={className:"name",begin:"[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",keywords:{$pattern:"[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+","builtin-name":"case-lambda call/cc class define-class exit-handler field import inherit init-field interface let*-values let-values let/ec mixin opt-lambda override protect provide public rename require require-for-syntax syntax syntax-case syntax-error unit/sig unless when with-syntax and begin call-with-current-continuation call-with-input-file call-with-output-file case cond define define-syntax delay do dynamic-wind else for-each if lambda let let* let-syntax letrec letrec-syntax map or syntax-rules ' * + , ,@ - ... / ; < <= = => > >= ` abs acos angle append apply asin assoc assq assv atan boolean? caar cadr call-with-input-file call-with-output-file call-with-values car cdddar cddddr cdr ceiling char->integer char-alphabetic? char-ci<=? char-ci<? char-ci=? char-ci>=? char-ci>? char-downcase char-lower-case? char-numeric? char-ready? char-upcase char-upper-case? char-whitespace? char<=? char<? char=? char>=? char>? char? close-input-port close-output-port complex? cons cos current-input-port current-output-port denominator display eof-object? eq? equal? eqv? eval even? exact->inexact exact? exp expt floor force gcd imag-part inexact->exact inexact? input-port? integer->char integer? interaction-environment lcm length list list->string list->vector list-ref list-tail list? load log magnitude make-polar make-rectangular make-string make-vector max member memq memv min modulo negative? newline not null-environment null? number->string number? numerator odd? open-input-file open-output-file output-port? pair? peek-char port? positive? procedure? quasiquote quote quotient rational? rationalize read read-char real-part real? remainder reverse round scheme-report-environment set! set-car! set-cdr! sin sqrt string string->list string->number string->symbol string-append string-ci<=? string-ci<? string-ci=? string-ci>=? string-ci>? string-copy string-fill! string-length string-ref string-set! string<=? string<? string=? string>=? string>? string? substring symbol->string symbol? tan transcript-off transcript-on truncate values vector vector->list vector-fill! vector-length vector-ref vector-set! with-input-from-file with-output-to-file write write-char zero?"}};
-l={variants:[{begin:"\\(",end:"\\)"},{begin:"\\[",end:"\\]"}],contains:[{begin:/lambda/,endsWithParent:!0,returnBegin:!0,contains:[l,{begin:/\(/,end:/\)/,endsParent:!0,contains:[f]}]},l,g]};g.contains=[b,c,d,f,h,k,l].concat(e);return{name:"Scheme",illegal:/\S/,contains:[a.SHEBANG(),c,d,h,k,l].concat(e)}}}());
-hljs.registerLanguage("scilab",function(){return function(a){var b=[a.C_NUMBER_MODE,{className:"string",begin:"'|\"",end:"'|\"",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]}];return{name:"Scilab",aliases:["sci"],keywords:{$pattern:/%?\w+/,keyword:"abort break case clear catch continue do elseif else endfunction end for function global if pause return resume select try then while",literal:"%f %F %t %T %pi %eps %inf %nan %e %i %z %s",built_in:"abs and acos asin atan ceil cd chdir clearglobal cosh cos cumprod deff disp error exec execstr exists exp eye gettext floor fprintf fread fsolve imag isdef isempty isinfisnan isvector lasterror length load linspace list listfiles log10 log2 log max min msprintf mclose mopen ones or pathconvert poly printf prod pwd rand real round sinh sin size gsort sprintf sqrt strcat strcmps tring sum system tanh tan type typename warning zeros matrix"},
-illegal:'("|#|/\\*|\\s+/\\w+)',contains:[{className:"function",beginKeywords:"function",end:"$",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)"}]},{begin:"[a-zA-Z_][a-zA-Z_0-9]*('+[\\.']*|[\\.']+)",end:"",relevance:0},{begin:"\\[",end:"\\]'*[\\.']*",relevance:0,contains:b},a.COMMENT("//","$")].concat(b)}}}());
-hljs.registerLanguage("scss",function(){return function(a){var b={className:"variable",begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b"},c={className:"number",begin:"#[0-9A-Fa-f]+"};return{name:"SCSS",case_insensitive:!0,illegal:"[=/|']",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:"\\#[A-Za-z0-9_-]+",relevance:0},{className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0},{className:"selector-attr",begin:"\\[",end:"\\]",illegal:"$"},{className:"selector-tag",
-begin:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",
-relevance:0},{className:"selector-pseudo",begin:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{className:"selector-pseudo",begin:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},
-b,{className:"attribute",begin:"\\b(src|z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",
-illegal:"[^\\s]"},{begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},
-{begin:":",end:";",contains:[b,c,a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{className:"meta",begin:"!important"}]},{begin:"@(page|font-face)",lexemes:"@[a-z-]+",keywords:"@page @font-face"},{begin:"@",end:"[{;]",returnBegin:!0,keywords:"and or not only",contains:[{begin:"@[a-z-]+",className:"keyword"},b,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,c,a.CSS_NUMBER_MODE]}]}}}());
-hljs.registerLanguage("shell",function(){return function(a){return{name:"Shell Session",aliases:["console"],contains:[{className:"meta",begin:"^\\s{0,3}[/\\w\\d\\[\\]()@-]*[>%$#]",starts:{end:"$",subLanguage:"bash"}}]}}}());
-hljs.registerLanguage("smali",function(){return function(a){var b="add and cmp cmpg cmpl const div double float goto if int long move mul neg new nop not or rem return shl shr sput sub throw ushr xor".split(" ");return{name:"Smali",aliases:["smali"],contains:[{className:"string",begin:'"',end:'"',relevance:0},a.COMMENT("#","$",{relevance:0}),{className:"keyword",variants:[{begin:"\\s*\\.end\\s[a-zA-Z0-9]*"},{begin:"^[ ]*\\.[a-zA-Z]*",relevance:0},{begin:"\\s:[a-zA-Z_0-9]*",relevance:0},{begin:"\\s(transient|constructor|abstract|final|synthetic|public|private|protected|static|bridge|system)"}]},
-{className:"built_in",variants:[{begin:"\\s("+b.join("|")+")\\s"},{begin:"\\s("+b.join("|")+")((\\-|/)[a-zA-Z0-9]+)+\\s",relevance:10},{begin:"\\s(aget|aput|array|check|execute|fill|filled|goto/16|goto/32|iget|instance|invoke|iput|monitor|packed|sget|sparse)((\\-|/)[a-zA-Z0-9]+)*\\s",relevance:10}]},{className:"class",begin:"L[^(;:\n]*;",relevance:0},{begin:"[vp][0-9]+"}]}}}());
-hljs.registerLanguage("smalltalk",function(){return function(a){var b={className:"string",begin:"\\$.{1}"},c={className:"symbol",begin:"#"+a.UNDERSCORE_IDENT_RE};return{name:"Smalltalk",aliases:["st"],keywords:"self super nil true false thisContext",contains:[a.COMMENT('"','"'),a.APOS_STRING_MODE,{className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},{begin:"[a-z][a-zA-Z0-9_]*:",relevance:0},a.C_NUMBER_MODE,c,b,{begin:"\\|[ ]*[a-z][a-zA-Z0-9_]*([ ]+[a-z][a-zA-Z0-9_]*)*[ ]*\\|",returnBegin:!0,
-end:/\|/,illegal:/\S/,contains:[{begin:"(\\|[ ]*)?[a-z][a-zA-Z0-9_]*"}]},{begin:"\\#\\(",end:"\\)",contains:[a.APOS_STRING_MODE,b,a.C_NUMBER_MODE,c]}]}}}());
-hljs.registerLanguage("sml",function(){return function(a){return{name:"SML (Standard ML)",aliases:["ml"],keywords:{$pattern:"[a-z_]\\w*!?",keyword:"abstype and andalso as case datatype do else end eqtype exception fn fun functor handle if in include infix infixr let local nonfix of op open orelse raise rec sharing sig signature struct structure then type val with withtype where while",built_in:"array bool char exn int list option order real ref string substring vector unit word",literal:"true false NONE SOME LESS EQUAL GREATER nil"},
-illegal:/\/\/|>>/,contains:[{className:"literal",begin:/\[(\|\|)?\]|\(\)/,relevance:0},a.COMMENT("\\(\\*","\\*\\)",{contains:["self"]}),{className:"symbol",begin:"'[A-Za-z_](?!')[\\w']*"},{className:"type",begin:"`[A-Z][\\w']*"},{className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},{begin:"[a-z_]\\w*'[\\w']*"},a.inherit(a.APOS_STRING_MODE,{className:"string",relevance:0}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"number",begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",
-relevance:0},{begin:/[-=]>/}]}}}());
-hljs.registerLanguage("soy",function(){return function(a){return{contains:[a.C_BLOCK_COMMENT_MODE,a.C_LINE_COMMENT_MODE,{begin:/\{delpackage/,ends:/\}/,relevance:10,keywords:"delpackage"},{begin:/\{namespace/,end:/\}/,relevance:10,keywords:"namespace autoescape",contains:[a.QUOTE_STRING_MODE]},{className:"template-tag",begin:/\{template/,end:/\{\/template\}/,relevance:10,keywords:"alias as autoescape call case default delcall else elseif fallbackmsg foreach if ifempty let msg namespace param print switch template",contains:[a.C_BLOCK_COMMENT_MODE,
-a.C_LINE_COMMENT_MODE,a.QUOTE_STRING_MODE,{className:"template-variable",relevance:0,begin:/\$[^}\s]+/},{className:"tag",begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},{endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/[^\s"'=<>`]+/}]}]}]}]}]}]}}}());
-hljs.registerLanguage("sqf",function(){return function(a){var b={className:"string",variants:[{begin:'"',end:'"',contains:[{begin:'""',relevance:0}]},{begin:"'",end:"'",contains:[{begin:"''",relevance:0}]}]},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"define undef ifdef ifndef else endif include"},contains:[{begin:/\\\n/,relevance:0},a.inherit(b,{className:"meta-string"}),{className:"meta-string",begin:/<[^\n>]*>/,end:/$/,illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]};
-return{name:"SQF",aliases:["sqf"],case_insensitive:!0,keywords:{keyword:"case catch default do else exit exitWith for forEach from if private switch then throw to try waitUntil while with",built_in:"abs accTime acos action actionIDs actionKeys actionKeysImages actionKeysNames actionKeysNamesArray actionName actionParams activateAddons activatedAddons activateKey add3DENConnection add3DENEventHandler add3DENLayer addAction addBackpack addBackpackCargo addBackpackCargoGlobal addBackpackGlobal addCamShake addCuratorAddons addCuratorCameraArea addCuratorEditableObjects addCuratorEditingArea addCuratorPoints addEditorObject addEventHandler addForce addGoggles addGroupIcon addHandgunItem addHeadgear addItem addItemCargo addItemCargoGlobal addItemPool addItemToBackpack addItemToUniform addItemToVest addLiveStats addMagazine addMagazineAmmoCargo addMagazineCargo addMagazineCargoGlobal addMagazineGlobal addMagazinePool addMagazines addMagazineTurret addMenu addMenuItem addMissionEventHandler addMPEventHandler addMusicEventHandler addOwnedMine addPlayerScores addPrimaryWeaponItem addPublicVariableEventHandler addRating addResources addScore addScoreSide addSecondaryWeaponItem addSwitchableUnit addTeamMember addToRemainsCollector addTorque addUniform addVehicle addVest addWaypoint addWeapon addWeaponCargo addWeaponCargoGlobal addWeaponGlobal addWeaponItem addWeaponPool addWeaponTurret admin agent agents AGLToASL aimedAtTarget aimPos airDensityRTD airplaneThrottle airportSide AISFinishHeal alive all3DENEntities allAirports allControls allCurators allCutLayers allDead allDeadMen allDisplays allGroups allMapMarkers allMines allMissionObjects allow3DMode allowCrewInImmobile allowCuratorLogicIgnoreAreas allowDamage allowDammage allowFileOperations allowFleeing allowGetIn allowSprint allPlayers allSimpleObjects allSites allTurrets allUnits allUnitsUAV allVariables ammo ammoOnPylon and animate animateBay animateDoor animatePylon animateSource animationNames animationPhase animationSourcePhase animationState append apply armoryPoints arrayIntersect asin ASLToAGL ASLToATL assert assignAsCargo assignAsCargoIndex assignAsCommander assignAsDriver assignAsGunner assignAsTurret assignCurator assignedCargo assignedCommander assignedDriver assignedGunner assignedItems assignedTarget assignedTeam assignedVehicle assignedVehicleRole assignItem assignTeam assignToAirport atan atan2 atg ATLToASL attachedObject attachedObjects attachedTo attachObject attachTo attackEnabled backpack backpackCargo backpackContainer backpackItems backpackMagazines backpackSpaceFor behaviour benchmark binocular boundingBox boundingBoxReal boundingCenter breakOut breakTo briefingName buildingExit buildingPos buttonAction buttonSetAction cadetMode call callExtension camCommand camCommit camCommitPrepared camCommitted camConstuctionSetParams camCreate camDestroy cameraEffect cameraEffectEnableHUD cameraInterest cameraOn cameraView campaignConfigFile camPreload camPreloaded camPrepareBank camPrepareDir camPrepareDive camPrepareFocus camPrepareFov camPrepareFovRange camPreparePos camPrepareRelPos camPrepareTarget camSetBank camSetDir camSetDive camSetFocus camSetFov camSetFovRange camSetPos camSetRelPos camSetTarget camTarget camUseNVG canAdd canAddItemToBackpack canAddItemToUniform canAddItemToVest cancelSimpleTaskDestination canFire canMove canSlingLoad canStand canSuspend canTriggerDynamicSimulation canUnloadInCombat canVehicleCargo captive captiveNum cbChecked cbSetChecked ceil channelEnabled cheatsEnabled checkAIFeature checkVisibility className clearAllItemsFromBackpack clearBackpackCargo clearBackpackCargoGlobal clearGroupIcons clearItemCargo clearItemCargoGlobal clearItemPool clearMagazineCargo clearMagazineCargoGlobal clearMagazinePool clearOverlay clearRadio clearWeaponCargo clearWeaponCargoGlobal clearWeaponPool clientOwner closeDialog closeDisplay closeOverlay collapseObjectTree collect3DENHistory collectiveRTD combatMode commandArtilleryFire commandChat commander commandFire commandFollow commandFSM commandGetOut commandingMenu commandMove commandRadio commandStop commandSuppressiveFire commandTarget commandWatch comment commitOverlay compile compileFinal completedFSM composeText configClasses configFile configHierarchy configName configProperties configSourceAddonList configSourceMod configSourceModList confirmSensorTarget connectTerminalToUAV controlsGroupCtrl copyFromClipboard copyToClipboard copyWaypoints cos count countEnemy countFriendly countSide countType countUnknown create3DENComposition create3DENEntity createAgent createCenter createDialog createDiaryLink createDiaryRecord createDiarySubject createDisplay createGearDialog createGroup createGuardedPoint createLocation createMarker createMarkerLocal createMenu createMine createMissionDisplay createMPCampaignDisplay createSimpleObject createSimpleTask createSite createSoundSource createTask createTeam createTrigger createUnit createVehicle createVehicleCrew createVehicleLocal crew ctAddHeader ctAddRow ctClear ctCurSel ctData ctFindHeaderRows ctFindRowHeader ctHeaderControls ctHeaderCount ctRemoveHeaders ctRemoveRows ctrlActivate ctrlAddEventHandler ctrlAngle ctrlAutoScrollDelay ctrlAutoScrollRewind ctrlAutoScrollSpeed ctrlChecked ctrlClassName ctrlCommit ctrlCommitted ctrlCreate ctrlDelete ctrlEnable ctrlEnabled ctrlFade ctrlHTMLLoaded ctrlIDC ctrlIDD ctrlMapAnimAdd ctrlMapAnimClear ctrlMapAnimCommit ctrlMapAnimDone ctrlMapCursor ctrlMapMouseOver ctrlMapScale ctrlMapScreenToWorld ctrlMapWorldToScreen ctrlModel ctrlModelDirAndUp ctrlModelScale ctrlParent ctrlParentControlsGroup ctrlPosition ctrlRemoveAllEventHandlers ctrlRemoveEventHandler ctrlScale ctrlSetActiveColor ctrlSetAngle ctrlSetAutoScrollDelay ctrlSetAutoScrollRewind ctrlSetAutoScrollSpeed ctrlSetBackgroundColor ctrlSetChecked ctrlSetEventHandler ctrlSetFade ctrlSetFocus ctrlSetFont ctrlSetFontH1 ctrlSetFontH1B ctrlSetFontH2 ctrlSetFontH2B ctrlSetFontH3 ctrlSetFontH3B ctrlSetFontH4 ctrlSetFontH4B ctrlSetFontH5 ctrlSetFontH5B ctrlSetFontH6 ctrlSetFontH6B ctrlSetFontHeight ctrlSetFontHeightH1 ctrlSetFontHeightH2 ctrlSetFontHeightH3 ctrlSetFontHeightH4 ctrlSetFontHeightH5 ctrlSetFontHeightH6 ctrlSetFontHeightSecondary ctrlSetFontP ctrlSetFontPB ctrlSetFontSecondary ctrlSetForegroundColor ctrlSetModel ctrlSetModelDirAndUp ctrlSetModelScale ctrlSetPixelPrecision ctrlSetPosition ctrlSetScale ctrlSetStructuredText ctrlSetText ctrlSetTextColor ctrlSetTooltip ctrlSetTooltipColorBox ctrlSetTooltipColorShade ctrlSetTooltipColorText ctrlShow ctrlShown ctrlText ctrlTextHeight ctrlTextWidth ctrlType ctrlVisible ctRowControls ctRowCount ctSetCurSel ctSetData ctSetHeaderTemplate ctSetRowTemplate ctSetValue ctValue curatorAddons curatorCamera curatorCameraArea curatorCameraAreaCeiling curatorCoef curatorEditableObjects curatorEditingArea curatorEditingAreaType curatorMouseOver curatorPoints curatorRegisteredObjects curatorSelected curatorWaypointCost current3DENOperation currentChannel currentCommand currentMagazine currentMagazineDetail currentMagazineDetailTurret currentMagazineTurret currentMuzzle currentNamespace currentTask currentTasks currentThrowable currentVisionMode currentWaypoint currentWeapon currentWeaponMode currentWeaponTurret currentZeroing cursorObject cursorTarget customChat customRadio cutFadeOut cutObj cutRsc cutText damage date dateToNumber daytime deActivateKey debriefingText debugFSM debugLog deg delete3DENEntities deleteAt deleteCenter deleteCollection deleteEditorObject deleteGroup deleteGroupWhenEmpty deleteIdentity deleteLocation deleteMarker deleteMarkerLocal deleteRange deleteResources deleteSite deleteStatus deleteTeam deleteVehicle deleteVehicleCrew deleteWaypoint detach detectedMines diag_activeMissionFSMs diag_activeScripts diag_activeSQFScripts diag_activeSQSScripts diag_captureFrame diag_captureFrameToFile diag_captureSlowFrame diag_codePerformance diag_drawMode diag_enable diag_enabled diag_fps diag_fpsMin diag_frameNo diag_lightNewLoad diag_list diag_log diag_logSlowFrame diag_mergeConfigFile diag_recordTurretLimits diag_setLightNew diag_tickTime diag_toggle dialog diarySubjectExists didJIP didJIPOwner difficulty difficultyEnabled difficultyEnabledRTD difficultyOption direction directSay disableAI disableCollisionWith disableConversation disableDebriefingStats disableMapIndicators disableNVGEquipment disableRemoteSensors disableSerialization disableTIEquipment disableUAVConnectability disableUserInput displayAddEventHandler displayCtrl displayParent displayRemoveAllEventHandlers displayRemoveEventHandler displaySetEventHandler dissolveTeam distance distance2D distanceSqr distributionRegion do3DENAction doArtilleryFire doFire doFollow doFSM doGetOut doMove doorPhase doStop doSuppressiveFire doTarget doWatch drawArrow drawEllipse drawIcon drawIcon3D drawLine drawLine3D drawLink drawLocation drawPolygon drawRectangle drawTriangle driver drop dynamicSimulationDistance dynamicSimulationDistanceCoef dynamicSimulationEnabled dynamicSimulationSystemEnabled echo edit3DENMissionAttributes editObject editorSetEventHandler effectiveCommander emptyPositions enableAI enableAIFeature enableAimPrecision enableAttack enableAudioFeature enableAutoStartUpRTD enableAutoTrimRTD enableCamShake enableCaustics enableChannel enableCollisionWith enableCopilot enableDebriefingStats enableDiagLegend enableDynamicSimulation enableDynamicSimulationSystem enableEndDialog enableEngineArtillery enableEnvironment enableFatigue enableGunLights enableInfoPanelComponent enableIRLasers enableMimics enablePersonTurret enableRadio enableReload enableRopeAttach enableSatNormalOnDetail enableSaving enableSentences enableSimulation enableSimulationGlobal enableStamina enableTeamSwitch enableTraffic enableUAVConnectability enableUAVWaypoints enableVehicleCargo enableVehicleSensor enableWeaponDisassembly endLoadingScreen endMission engineOn enginesIsOnRTD enginesRpmRTD enginesTorqueRTD entities environmentEnabled estimatedEndServerTime estimatedTimeLeft evalObjectArgument everyBackpack everyContainer exec execEditorScript execFSM execVM exp expectedDestination exportJIPMessages eyeDirection eyePos face faction fadeMusic fadeRadio fadeSound fadeSpeech failMission fillWeaponsFromPool find findCover findDisplay findEditorObject findEmptyPosition findEmptyPositionReady findIf findNearestEnemy finishMissionInit finite fire fireAtTarget firstBackpack flag flagAnimationPhase flagOwner flagSide flagTexture fleeing floor flyInHeight flyInHeightASL fog fogForecast fogParams forceAddUniform forcedMap forceEnd forceFlagTexture forceFollowRoad forceMap forceRespawn forceSpeed forceWalk forceWeaponFire forceWeatherChange forEachMember forEachMemberAgent forEachMemberTeam forgetTarget format formation formationDirection formationLeader formationMembers formationPosition formationTask formatText formLeader freeLook fromEditor fuel fullCrew gearIDCAmmoCount gearSlotAmmoCount gearSlotData get3DENActionState get3DENAttribute get3DENCamera get3DENConnections get3DENEntity get3DENEntityID get3DENGrid get3DENIconsVisible get3DENLayerEntities get3DENLinesVisible get3DENMissionAttribute get3DENMouseOver get3DENSelected getAimingCoef getAllEnvSoundControllers getAllHitPointsDamage getAllOwnedMines getAllSoundControllers getAmmoCargo getAnimAimPrecision getAnimSpeedCoef getArray getArtilleryAmmo getArtilleryComputerSettings getArtilleryETA getAssignedCuratorLogic getAssignedCuratorUnit getBackpackCargo getBleedingRemaining getBurningValue getCameraViewDirection getCargoIndex getCenterOfMass getClientState getClientStateNumber getCompatiblePylonMagazines getConnectedUAV getContainerMaxLoad getCursorObjectParams getCustomAimCoef getDammage getDescription getDir getDirVisual getDLCAssetsUsage getDLCAssetsUsageByName getDLCs getEditorCamera getEditorMode getEditorObjectScope getElevationOffset getEnvSoundController getFatigue getForcedFlagTexture getFriend getFSMVariable getFuelCargo getGroupIcon getGroupIconParams getGroupIcons getHideFrom getHit getHitIndex getHitPointDamage getItemCargo getMagazineCargo getMarkerColor getMarkerPos getMarkerSize getMarkerType getMass getMissionConfig getMissionConfigValue getMissionDLCs getMissionLayerEntities getModelInfo getMousePosition getMusicPlayedTime getNumber getObjectArgument getObjectChildren getObjectDLC getObjectMaterials getObjectProxy getObjectTextures getObjectType getObjectViewDistance getOxygenRemaining getPersonUsedDLCs getPilotCameraDirection getPilotCameraPosition getPilotCameraRotation getPilotCameraTarget getPlateNumber getPlayerChannel getPlayerScores getPlayerUID getPos getPosASL getPosASLVisual getPosASLW getPosATL getPosATLVisual getPosVisual getPosWorld getPylonMagazines getRelDir getRelPos getRemoteSensorsDisabled getRepairCargo getResolution getShadowDistance getShotParents getSlingLoad getSoundController getSoundControllerResult getSpeed getStamina getStatValue getSuppression getTerrainGrid getTerrainHeightASL getText getTotalDLCUsageTime getUnitLoadout getUnitTrait getUserMFDText getUserMFDvalue getVariable getVehicleCargo getWeaponCargo getWeaponSway getWingsOrientationRTD getWingsPositionRTD getWPPos glanceAt globalChat globalRadio goggles goto group groupChat groupFromNetId groupIconSelectable groupIconsVisible groupId groupOwner groupRadio groupSelectedUnits groupSelectUnit gunner gusts halt handgunItems handgunMagazine handgunWeapon handsHit hasInterface hasPilotCamera hasWeapon hcAllGroups hcGroupParams hcLeader hcRemoveAllGroups hcRemoveGroup hcSelected hcSelectGroup hcSetGroup hcShowBar hcShownBar headgear hideBody hideObject hideObjectGlobal hideSelection hint hintC hintCadet hintSilent hmd hostMission htmlLoad HUDMovementLevels humidity image importAllGroups importance in inArea inAreaArray incapacitatedState inflame inflamed infoPanel infoPanelComponentEnabled infoPanelComponents infoPanels inGameUISetEventHandler inheritsFrom initAmbientLife inPolygon inputAction inRangeOfArtillery insertEditorObject intersect is3DEN is3DENMultiplayer isAbleToBreathe isAgent isArray isAutoHoverOn isAutonomous isAutotest isBleeding isBurning isClass isCollisionLightOn isCopilotEnabled isDamageAllowed isDedicated isDLCAvailable isEngineOn isEqualTo isEqualType isEqualTypeAll isEqualTypeAny isEqualTypeArray isEqualTypeParams isFilePatchingEnabled isFlashlightOn isFlatEmpty isForcedWalk isFormationLeader isGroupDeletedWhenEmpty isHidden isInRemainsCollector isInstructorFigureEnabled isIRLaserOn isKeyActive isKindOf isLaserOn isLightOn isLocalized isManualFire isMarkedForCollection isMultiplayer isMultiplayerSolo isNil isNull isNumber isObjectHidden isObjectRTD isOnRoad isPipEnabled isPlayer isRealTime isRemoteExecuted isRemoteExecutedJIP isServer isShowing3DIcons isSimpleObject isSprintAllowed isStaminaEnabled isSteamMission isStreamFriendlyUIEnabled isText isTouchingGround isTurnedOut isTutHintsEnabled isUAVConnectable isUAVConnected isUIContext isUniformAllowed isVehicleCargo isVehicleRadarOn isVehicleSensorEnabled isWalking isWeaponDeployed isWeaponRested itemCargo items itemsWithMagazines join joinAs joinAsSilent joinSilent joinString kbAddDatabase kbAddDatabaseTargets kbAddTopic kbHasTopic kbReact kbRemoveTopic kbTell kbWasSaid keyImage keyName knowsAbout land landAt landResult language laserTarget lbAdd lbClear lbColor lbColorRight lbCurSel lbData lbDelete lbIsSelected lbPicture lbPictureRight lbSelection lbSetColor lbSetColorRight lbSetCurSel lbSetData lbSetPicture lbSetPictureColor lbSetPictureColorDisabled lbSetPictureColorSelected lbSetPictureRight lbSetPictureRightColor lbSetPictureRightColorDisabled lbSetPictureRightColorSelected lbSetSelectColor lbSetSelectColorRight lbSetSelected lbSetText lbSetTextRight lbSetTooltip lbSetValue lbSize lbSort lbSortByValue lbText lbTextRight lbValue leader leaderboardDeInit leaderboardGetRows leaderboardInit leaderboardRequestRowsFriends leaderboardsRequestUploadScore leaderboardsRequestUploadScoreKeepBest leaderboardState leaveVehicle libraryCredits libraryDisclaimers lifeState lightAttachObject lightDetachObject lightIsOn lightnings limitSpeed linearConversion lineIntersects lineIntersectsObjs lineIntersectsSurfaces lineIntersectsWith linkItem list listObjects listRemoteTargets listVehicleSensors ln lnbAddArray lnbAddColumn lnbAddRow lnbClear lnbColor lnbCurSelRow lnbData lnbDeleteColumn lnbDeleteRow lnbGetColumnsPosition lnbPicture lnbSetColor lnbSetColumnsPos lnbSetCurSelRow lnbSetData lnbSetPicture lnbSetText lnbSetValue lnbSize lnbSort lnbSortByValue lnbText lnbValue load loadAbs loadBackpack loadFile loadGame loadIdentity loadMagazine loadOverlay loadStatus loadUniform loadVest local localize locationPosition lock lockCameraTo lockCargo lockDriver locked lockedCargo lockedDriver lockedTurret lockIdentity lockTurret lockWP log logEntities logNetwork logNetworkTerminate lookAt lookAtPos magazineCargo magazines magazinesAllTurrets magazinesAmmo magazinesAmmoCargo magazinesAmmoFull magazinesDetail magazinesDetailBackpack magazinesDetailUniform magazinesDetailVest magazinesTurret magazineTurretAmmo mapAnimAdd mapAnimClear mapAnimCommit mapAnimDone mapCenterOnCamera mapGridPosition markAsFinishedOnSteam markerAlpha markerBrush markerColor markerDir markerPos markerShape markerSize markerText markerType max members menuAction menuAdd menuChecked menuClear menuCollapse menuData menuDelete menuEnable menuEnabled menuExpand menuHover menuPicture menuSetAction menuSetCheck menuSetData menuSetPicture menuSetValue menuShortcut menuShortcutText menuSize menuSort menuText menuURL menuValue min mineActive mineDetectedBy missionConfigFile missionDifficulty missionName missionNamespace missionStart missionVersion mod modelToWorld modelToWorldVisual modelToWorldVisualWorld modelToWorldWorld modParams moonIntensity moonPhase morale move move3DENCamera moveInAny moveInCargo moveInCommander moveInDriver moveInGunner moveInTurret moveObjectToEnd moveOut moveTime moveTo moveToCompleted moveToFailed musicVolume name nameSound nearEntities nearestBuilding nearestLocation nearestLocations nearestLocationWithDubbing nearestObject nearestObjects nearestTerrainObjects nearObjects nearObjectsReady nearRoads nearSupplies nearTargets needReload netId netObjNull newOverlay nextMenuItemIndex nextWeatherChange nMenuItems not numberOfEnginesRTD numberToDate objectCurators objectFromNetId objectParent objStatus onBriefingGroup onBriefingNotes onBriefingPlan onBriefingTeamSwitch onCommandModeChanged onDoubleClick onEachFrame onGroupIconClick onGroupIconOverEnter onGroupIconOverLeave onHCGroupSelectionChanged onMapSingleClick onPlayerConnected onPlayerDisconnected onPreloadFinished onPreloadStarted onShowNewObject onTeamSwitch openCuratorInterface openDLCPage openMap openSteamApp openYoutubeVideo or orderGetIn overcast overcastForecast owner param params parseNumber parseSimpleArray parseText parsingNamespace particlesQuality pickWeaponPool pitch pixelGrid pixelGridBase pixelGridNoUIScale pixelH pixelW playableSlotsNumber playableUnits playAction playActionNow player playerRespawnTime playerSide playersNumber playGesture playMission playMove playMoveNow playMusic playScriptedMission playSound playSound3D position positionCameraToWorld posScreenToWorld posWorldToScreen ppEffectAdjust ppEffectCommit ppEffectCommitted ppEffectCreate ppEffectDestroy ppEffectEnable ppEffectEnabled ppEffectForceInNVG precision preloadCamera preloadObject preloadSound preloadTitleObj preloadTitleRsc preprocessFile preprocessFileLineNumbers primaryWeapon primaryWeaponItems primaryWeaponMagazine priority processDiaryLink productVersion profileName profileNamespace profileNameSteam progressLoadingScreen progressPosition progressSetPosition publicVariable publicVariableClient publicVariableServer pushBack pushBackUnique putWeaponPool queryItemsPool queryMagazinePool queryWeaponPool rad radioChannelAdd radioChannelCreate radioChannelRemove radioChannelSetCallSign radioChannelSetLabel radioVolume rain rainbow random rank rankId rating rectangular registeredTasks registerTask reload reloadEnabled remoteControl remoteExec remoteExecCall remoteExecutedOwner remove3DENConnection remove3DENEventHandler remove3DENLayer removeAction removeAll3DENEventHandlers removeAllActions removeAllAssignedItems removeAllContainers removeAllCuratorAddons removeAllCuratorCameraAreas removeAllCuratorEditingAreas removeAllEventHandlers removeAllHandgunItems removeAllItems removeAllItemsWithMagazines removeAllMissionEventHandlers removeAllMPEventHandlers removeAllMusicEventHandlers removeAllOwnedMines removeAllPrimaryWeaponItems removeAllWeapons removeBackpack removeBackpackGlobal removeCuratorAddons removeCuratorCameraArea removeCuratorEditableObjects removeCuratorEditingArea removeDrawIcon removeDrawLinks removeEventHandler removeFromRemainsCollector removeGoggles removeGroupIcon removeHandgunItem removeHeadgear removeItem removeItemFromBackpack removeItemFromUniform removeItemFromVest removeItems removeMagazine removeMagazineGlobal removeMagazines removeMagazinesTurret removeMagazineTurret removeMenuItem removeMissionEventHandler removeMPEventHandler removeMusicEventHandler removeOwnedMine removePrimaryWeaponItem removeSecondaryWeaponItem removeSimpleTask removeSwitchableUnit removeTeamMember removeUniform removeVest removeWeapon removeWeaponAttachmentCargo removeWeaponCargo removeWeaponGlobal removeWeaponTurret reportRemoteTarget requiredVersion resetCamShake resetSubgroupDirection resize resources respawnVehicle restartEditorCamera reveal revealMine reverse reversedMouseY roadAt roadsConnectedTo roleDescription ropeAttachedObjects ropeAttachedTo ropeAttachEnabled ropeAttachTo ropeCreate ropeCut ropeDestroy ropeDetach ropeEndPosition ropeLength ropes ropeUnwind ropeUnwound rotorsForcesRTD rotorsRpmRTD round runInitScript safeZoneH safeZoneW safeZoneWAbs safeZoneX safeZoneXAbs safeZoneY save3DENInventory saveGame saveIdentity saveJoysticks saveOverlay saveProfileNamespace saveStatus saveVar savingEnabled say say2D say3D scopeName score scoreSide screenshot screenToWorld scriptDone scriptName scudState secondaryWeapon secondaryWeaponItems secondaryWeaponMagazine select selectBestPlaces selectDiarySubject selectedEditorObjects selectEditorObject selectionNames selectionPosition selectLeader selectMax selectMin selectNoPlayer selectPlayer selectRandom selectRandomWeighted selectWeapon selectWeaponTurret sendAUMessage sendSimpleCommand sendTask sendTaskResult sendUDPMessage serverCommand serverCommandAvailable serverCommandExecutable serverName serverTime set set3DENAttribute set3DENAttributes set3DENGrid set3DENIconsVisible set3DENLayer set3DENLinesVisible set3DENLogicType set3DENMissionAttribute set3DENMissionAttributes set3DENModelsVisible set3DENObjectType set3DENSelected setAccTime setActualCollectiveRTD setAirplaneThrottle setAirportSide setAmmo setAmmoCargo setAmmoOnPylon setAnimSpeedCoef setAperture setApertureNew setArmoryPoints setAttributes setAutonomous setBehaviour setBleedingRemaining setBrakesRTD setCameraInterest setCamShakeDefParams setCamShakeParams setCamUseTI setCaptive setCenterOfMass setCollisionLight setCombatMode setCompassOscillation setConvoySeparation setCuratorCameraAreaCeiling setCuratorCoef setCuratorEditingAreaType setCuratorWaypointCost setCurrentChannel setCurrentTask setCurrentWaypoint setCustomAimCoef setCustomWeightRTD setDamage setDammage setDate setDebriefingText setDefaultCamera setDestination setDetailMapBlendPars setDir setDirection setDrawIcon setDriveOnPath setDropInterval setDynamicSimulationDistance setDynamicSimulationDistanceCoef setEditorMode setEditorObjectScope setEffectCondition setEngineRPMRTD setFace setFaceAnimation setFatigue setFeatureType setFlagAnimationPhase setFlagOwner setFlagSide setFlagTexture setFog setFormation setFormationTask setFormDir setFriend setFromEditor setFSMVariable setFuel setFuelCargo setGroupIcon setGroupIconParams setGroupIconsSelectable setGroupIconsVisible setGroupId setGroupIdGlobal setGroupOwner setGusts setHideBehind setHit setHitIndex setHitPointDamage setHorizonParallaxCoef setHUDMovementLevels setIdentity setImportance setInfoPanel setLeader setLightAmbient setLightAttenuation setLightBrightness setLightColor setLightDayLight setLightFlareMaxDistance setLightFlareSize setLightIntensity setLightnings setLightUseFlare setLocalWindParams setMagazineTurretAmmo setMarkerAlpha setMarkerAlphaLocal setMarkerBrush setMarkerBrushLocal setMarkerColor setMarkerColorLocal setMarkerDir setMarkerDirLocal setMarkerPos setMarkerPosLocal setMarkerShape setMarkerShapeLocal setMarkerSize setMarkerSizeLocal setMarkerText setMarkerTextLocal setMarkerType setMarkerTypeLocal setMass setMimic setMousePosition setMusicEffect setMusicEventHandler setName setNameSound setObjectArguments setObjectMaterial setObjectMaterialGlobal setObjectProxy setObjectTexture setObjectTextureGlobal setObjectViewDistance setOvercast setOwner setOxygenRemaining setParticleCircle setParticleClass setParticleFire setParticleParams setParticleRandom setPilotCameraDirection setPilotCameraRotation setPilotCameraTarget setPilotLight setPiPEffect setPitch setPlateNumber setPlayable setPlayerRespawnTime setPos setPosASL setPosASL2 setPosASLW setPosATL setPosition setPosWorld setPylonLoadOut setPylonsPriority setRadioMsg setRain setRainbow setRandomLip setRank setRectangular setRepairCargo setRotorBrakeRTD setShadowDistance setShotParents setSide setSimpleTaskAlwaysVisible setSimpleTaskCustomData setSimpleTaskDescription setSimpleTaskDestination setSimpleTaskTarget setSimpleTaskType setSimulWeatherLayers setSize setSkill setSlingLoad setSoundEffect setSpeaker setSpeech setSpeedMode setStamina setStaminaScheme setStatValue setSuppression setSystemOfUnits setTargetAge setTaskMarkerOffset setTaskResult setTaskState setTerrainGrid setText setTimeMultiplier setTitleEffect setTrafficDensity setTrafficDistance setTrafficGap setTrafficSpeed setTriggerActivation setTriggerArea setTriggerStatements setTriggerText setTriggerTimeout setTriggerType setType setUnconscious setUnitAbility setUnitLoadout setUnitPos setUnitPosWeak setUnitRank setUnitRecoilCoefficient setUnitTrait setUnloadInCombat setUserActionText setUserMFDText setUserMFDvalue setVariable setVectorDir setVectorDirAndUp setVectorUp setVehicleAmmo setVehicleAmmoDef setVehicleArmor setVehicleCargo setVehicleId setVehicleLock setVehiclePosition setVehicleRadar setVehicleReceiveRemoteTargets setVehicleReportOwnPosition setVehicleReportRemoteTargets setVehicleTIPars setVehicleVarName setVelocity setVelocityModelSpace setVelocityTransformation setViewDistance setVisibleIfTreeCollapsed setWantedRPMRTD setWaves setWaypointBehaviour setWaypointCombatMode setWaypointCompletionRadius setWaypointDescription setWaypointForceBehaviour setWaypointFormation setWaypointHousePosition setWaypointLoiterRadius setWaypointLoiterType setWaypointName setWaypointPosition setWaypointScript setWaypointSpeed setWaypointStatements setWaypointTimeout setWaypointType setWaypointVisible setWeaponReloadingTime setWind setWindDir setWindForce setWindStr setWingForceScaleRTD setWPPos show3DIcons showChat showCinemaBorder showCommandingMenu showCompass showCuratorCompass showGPS showHUD showLegend showMap shownArtilleryComputer shownChat shownCompass shownCuratorCompass showNewEditorObject shownGPS shownHUD shownMap shownPad shownRadio shownScoretable shownUAVFeed shownWarrant shownWatch showPad showRadio showScoretable showSubtitles showUAVFeed showWarrant showWatch showWaypoint showWaypoints side sideChat sideEnemy sideFriendly sideRadio simpleTasks simulationEnabled simulCloudDensity simulCloudOcclusion simulInClouds simulWeatherSync sin size sizeOf skill skillFinal skipTime sleep sliderPosition sliderRange sliderSetPosition sliderSetRange sliderSetSpeed sliderSpeed slingLoadAssistantShown soldierMagazines someAmmo sort soundVolume spawn speaker speed speedMode splitString sqrt squadParams stance startLoadingScreen step stop stopEngineRTD stopped str sunOrMoon supportInfo suppressFor surfaceIsWater surfaceNormal surfaceType swimInDepth switchableUnits switchAction switchCamera switchGesture switchLight switchMove synchronizedObjects synchronizedTriggers synchronizedWaypoints synchronizeObjectsAdd synchronizeObjectsRemove synchronizeTrigger synchronizeWaypoint systemChat systemOfUnits tan targetKnowledge targets targetsAggregate targetsQuery taskAlwaysVisible taskChildren taskCompleted taskCustomData taskDescription taskDestination taskHint taskMarkerOffset taskParent taskResult taskState taskType teamMember teamName teams teamSwitch teamSwitchEnabled teamType terminate terrainIntersect terrainIntersectASL terrainIntersectAtASL text textLog textLogFormat tg time timeMultiplier titleCut titleFadeOut titleObj titleRsc titleText toArray toFixed toLower toString toUpper triggerActivated triggerActivation triggerArea triggerAttachedVehicle triggerAttachObject triggerAttachVehicle triggerDynamicSimulation triggerStatements triggerText triggerTimeout triggerTimeoutCurrent triggerType turretLocal turretOwner turretUnit tvAdd tvClear tvCollapse tvCollapseAll tvCount tvCurSel tvData tvDelete tvExpand tvExpandAll tvPicture tvSetColor tvSetCurSel tvSetData tvSetPicture tvSetPictureColor tvSetPictureColorDisabled tvSetPictureColorSelected tvSetPictureRight tvSetPictureRightColor tvSetPictureRightColorDisabled tvSetPictureRightColorSelected tvSetText tvSetTooltip tvSetValue tvSort tvSortByValue tvText tvTooltip tvValue type typeName typeOf UAVControl uiNamespace uiSleep unassignCurator unassignItem unassignTeam unassignVehicle underwater uniform uniformContainer uniformItems uniformMagazines unitAddons unitAimPosition unitAimPositionVisual unitBackpack unitIsUAV unitPos unitReady unitRecoilCoefficient units unitsBelowHeight unlinkItem unlockAchievement unregisterTask updateDrawIcon updateMenuItem updateObjectTree useAISteeringComponent useAudioTimeForMoves userInputDisabled vectorAdd vectorCos vectorCrossProduct vectorDiff vectorDir vectorDirVisual vectorDistance vectorDistanceSqr vectorDotProduct vectorFromTo vectorMagnitude vectorMagnitudeSqr vectorModelToWorld vectorModelToWorldVisual vectorMultiply vectorNormalized vectorUp vectorUpVisual vectorWorldToModel vectorWorldToModelVisual vehicle vehicleCargoEnabled vehicleChat vehicleRadio vehicleReceiveRemoteTargets vehicleReportOwnPosition vehicleReportRemoteTargets vehicles vehicleVarName velocity velocityModelSpace verifySignature vest vestContainer vestItems vestMagazines viewDistance visibleCompass visibleGPS visibleMap visiblePosition visiblePositionASL visibleScoretable visibleWatch waves waypointAttachedObject waypointAttachedVehicle waypointAttachObject waypointAttachVehicle waypointBehaviour waypointCombatMode waypointCompletionRadius waypointDescription waypointForceBehaviour waypointFormation waypointHousePosition waypointLoiterRadius waypointLoiterType waypointName waypointPosition waypoints waypointScript waypointsEnabledUAV waypointShow waypointSpeed waypointStatements waypointTimeout waypointTimeoutCurrent waypointType waypointVisible weaponAccessories weaponAccessoriesCargo weaponCargo weaponDirection weaponInertia weaponLowered weapons weaponsItems weaponsItemsCargo weaponState weaponsTurret weightRTD WFSideText wind ",
-literal:"blufor civilian configNull controlNull displayNull east endl false grpNull independent lineBreak locationNull nil objNull opfor pi resistance scriptNull sideAmbientLife sideEmpty sideLogic sideUnknown taskNull teamMemberNull true west"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.NUMBER_MODE,{className:"variable",begin:/\b_+[a-zA-Z_]\w*/},{className:"title",begin:/[a-zA-Z][a-zA-Z0-9]+_fnc_\w*/},b,c],illegal:/#|^\$ /}}}());
-hljs.registerLanguage("sql",function(){return function(a){var b=a.COMMENT("--","$");return{name:"SQL",case_insensitive:!0,illegal:/[<>{}*]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",
-end:/;/,endsWithParent:!0,keywords:{$pattern:/[\w\.]+/,keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",
-literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varchar2 varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[{begin:'""'}]},{className:"string",begin:"`",end:"`"},a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,b,a.HASH_COMMENT_MODE]},a.C_BLOCK_COMMENT_MODE,
-b,a.HASH_COMMENT_MODE]}}}());
-hljs.registerLanguage("stan",function(){return function(a){return{name:"Stan",aliases:["stanfuncs"],keywords:{$pattern:a.IDENT_RE,title:"functions model data parameters quantities transformed generated",keyword:"for in if else while break continue return".split(" ").concat("int real vector ordered positive_ordered simplex unit_vector row_vector matrix cholesky_factor_corr|10 cholesky_factor_cov|10 corr_matrix|10 cov_matrix|10 void".split(" ")).concat("print reject increment_log_prob|10 integrate_ode|10 integrate_ode_rk45|10 integrate_ode_bdf|10 algebra_solver".split(" ")).join(" "),built_in:"Phi Phi_approx abs acos acosh algebra_solver append_array append_col append_row asin asinh atan atan2 atanh bernoulli_cdf bernoulli_lccdf bernoulli_lcdf bernoulli_logit_lpmf bernoulli_logit_rng bernoulli_lpmf bernoulli_rng bessel_first_kind bessel_second_kind beta_binomial_cdf beta_binomial_lccdf beta_binomial_lcdf beta_binomial_lpmf beta_binomial_rng beta_cdf beta_lccdf beta_lcdf beta_lpdf beta_rng binary_log_loss binomial_cdf binomial_coefficient_log binomial_lccdf binomial_lcdf binomial_logit_lpmf binomial_lpmf binomial_rng block categorical_logit_lpmf categorical_logit_rng categorical_lpmf categorical_rng cauchy_cdf cauchy_lccdf cauchy_lcdf cauchy_lpdf cauchy_rng cbrt ceil chi_square_cdf chi_square_lccdf chi_square_lcdf chi_square_lpdf chi_square_rng cholesky_decompose choose col cols columns_dot_product columns_dot_self cos cosh cov_exp_quad crossprod csr_extract_u csr_extract_v csr_extract_w csr_matrix_times_vector csr_to_dense_matrix cumulative_sum determinant diag_matrix diag_post_multiply diag_pre_multiply diagonal digamma dims dirichlet_lpdf dirichlet_rng distance dot_product dot_self double_exponential_cdf double_exponential_lccdf double_exponential_lcdf double_exponential_lpdf double_exponential_rng e eigenvalues_sym eigenvectors_sym erf erfc exp exp2 exp_mod_normal_cdf exp_mod_normal_lccdf exp_mod_normal_lcdf exp_mod_normal_lpdf exp_mod_normal_rng expm1 exponential_cdf exponential_lccdf exponential_lcdf exponential_lpdf exponential_rng fabs falling_factorial fdim floor fma fmax fmin fmod frechet_cdf frechet_lccdf frechet_lcdf frechet_lpdf frechet_rng gamma_cdf gamma_lccdf gamma_lcdf gamma_lpdf gamma_p gamma_q gamma_rng gaussian_dlm_obs_lpdf get_lp gumbel_cdf gumbel_lccdf gumbel_lcdf gumbel_lpdf gumbel_rng head hypergeometric_lpmf hypergeometric_rng hypot inc_beta int_step integrate_ode integrate_ode_bdf integrate_ode_rk45 inv inv_Phi inv_chi_square_cdf inv_chi_square_lccdf inv_chi_square_lcdf inv_chi_square_lpdf inv_chi_square_rng inv_cloglog inv_gamma_cdf inv_gamma_lccdf inv_gamma_lcdf inv_gamma_lpdf inv_gamma_rng inv_logit inv_sqrt inv_square inv_wishart_lpdf inv_wishart_rng inverse inverse_spd is_inf is_nan lbeta lchoose lgamma lkj_corr_cholesky_lpdf lkj_corr_cholesky_rng lkj_corr_lpdf lkj_corr_rng lmgamma lmultiply log log10 log1m log1m_exp log1m_inv_logit log1p log1p_exp log2 log_determinant log_diff_exp log_falling_factorial log_inv_logit log_mix log_rising_factorial log_softmax log_sum_exp logistic_cdf logistic_lccdf logistic_lcdf logistic_lpdf logistic_rng logit lognormal_cdf lognormal_lccdf lognormal_lcdf lognormal_lpdf lognormal_rng machine_precision matrix_exp max mdivide_left_spd mdivide_left_tri_low mdivide_right_spd mdivide_right_tri_low mean min modified_bessel_first_kind modified_bessel_second_kind multi_gp_cholesky_lpdf multi_gp_lpdf multi_normal_cholesky_lpdf multi_normal_cholesky_rng multi_normal_lpdf multi_normal_prec_lpdf multi_normal_rng multi_student_t_lpdf multi_student_t_rng multinomial_lpmf multinomial_rng multiply_log multiply_lower_tri_self_transpose neg_binomial_2_cdf neg_binomial_2_lccdf neg_binomial_2_lcdf neg_binomial_2_log_lpmf neg_binomial_2_log_rng neg_binomial_2_lpmf neg_binomial_2_rng neg_binomial_cdf neg_binomial_lccdf neg_binomial_lcdf neg_binomial_lpmf neg_binomial_rng negative_infinity normal_cdf normal_lccdf normal_lcdf normal_lpdf normal_rng not_a_number num_elements ordered_logistic_lpmf ordered_logistic_rng owens_t pareto_cdf pareto_lccdf pareto_lcdf pareto_lpdf pareto_rng pareto_type_2_cdf pareto_type_2_lccdf pareto_type_2_lcdf pareto_type_2_lpdf pareto_type_2_rng pi poisson_cdf poisson_lccdf poisson_lcdf poisson_log_lpmf poisson_log_rng poisson_lpmf poisson_rng positive_infinity pow print prod qr_Q qr_R quad_form quad_form_diag quad_form_sym rank rayleigh_cdf rayleigh_lccdf rayleigh_lcdf rayleigh_lpdf rayleigh_rng reject rep_array rep_matrix rep_row_vector rep_vector rising_factorial round row rows rows_dot_product rows_dot_self scaled_inv_chi_square_cdf scaled_inv_chi_square_lccdf scaled_inv_chi_square_lcdf scaled_inv_chi_square_lpdf scaled_inv_chi_square_rng sd segment sin singular_values sinh size skew_normal_cdf skew_normal_lccdf skew_normal_lcdf skew_normal_lpdf skew_normal_rng softmax sort_asc sort_desc sort_indices_asc sort_indices_desc sqrt sqrt2 square squared_distance step student_t_cdf student_t_lccdf student_t_lcdf student_t_lpdf student_t_rng sub_col sub_row sum tail tan tanh target tcrossprod tgamma to_array_1d to_array_2d to_matrix to_row_vector to_vector trace trace_gen_quad_form trace_quad_form trigamma trunc uniform_cdf uniform_lccdf uniform_lcdf uniform_lpdf uniform_rng variance von_mises_lpdf von_mises_rng weibull_cdf weibull_lccdf weibull_lcdf weibull_lpdf weibull_rng wiener_lpdf wishart_lpdf wishart_rng"},
-contains:[a.C_LINE_COMMENT_MODE,a.COMMENT(/#/,/$/,{relevance:0,keywords:{"meta-keyword":"include"}}),a.COMMENT(/\/\*/,/\*\//,{relevance:0,contains:[{className:"doctag",begin:/@(return|param)/}]}),{begin:/<\s*lower\s*=/,keywords:"lower"},{begin:/[<,]\s*upper\s*=/,keywords:"upper"},{className:"keyword",begin:/\btarget\s*\+=/,relevance:10},{begin:"~\\s*("+a.IDENT_RE+")\\s*\\(",keywords:"bernoulli bernoulli_logit beta beta_binomial binomial binomial_logit categorical categorical_logit cauchy chi_square dirichlet double_exponential exp_mod_normal exponential frechet gamma gaussian_dlm_obs gumbel hypergeometric inv_chi_square inv_gamma inv_wishart lkj_corr lkj_corr_cholesky logistic lognormal multi_gp multi_gp_cholesky multi_normal multi_normal_cholesky multi_normal_prec multi_student_t multinomial neg_binomial neg_binomial_2 neg_binomial_2_log normal ordered_logistic pareto pareto_type_2 poisson poisson_log rayleigh scaled_inv_chi_square skew_normal student_t uniform von_mises weibull wiener wishart"},
-{className:"number",variants:[{begin:/\b\d+(?:\.\d*)?(?:[eE][+-]?\d+)?/},{begin:/\.\d+(?:[eE][+-]?\d+)?\b/}],relevance:0},{className:"string",begin:'"',end:'"',relevance:0}]}}}());
-hljs.registerLanguage("stata",function(){return function(a){return{name:"Stata",aliases:["do","ado"],case_insensitive:!0,keywords:"if else in foreach for forv forva forval forvalu forvalue forvalues by bys bysort xi quietly qui capture about ac ac_7 acprplot acprplot_7 adjust ado adopath adoupdate alpha ameans an ano anov anova anova_estat anova_terms anovadef aorder ap app appe appen append arch arch_dr arch_estat arch_p archlm areg areg_p args arima arima_dr arima_estat arima_p as asmprobit asmprobit_estat asmprobit_lf asmprobit_mfx__dlg asmprobit_p ass asse asser assert avplot avplot_7 avplots avplots_7 bcskew0 bgodfrey bias binreg bip0_lf biplot bipp_lf bipr_lf bipr_p biprobit bitest bitesti bitowt blogit bmemsize boot bootsamp bootstrap bootstrap_8 boxco_l boxco_p boxcox boxcox_6 boxcox_p bprobit br break brier bro brow brows browse brr brrstat bs bs_7 bsampl_w bsample bsample_7 bsqreg bstat bstat_7 bstat_8 bstrap bstrap_7 bubble bubbleplot ca ca_estat ca_p cabiplot camat canon canon_8 canon_8_p canon_estat canon_p cap caprojection capt captu captur capture cat cc cchart cchart_7 cci cd censobs_table centile cf char chdir checkdlgfiles checkestimationsample checkhlpfiles checksum chelp ci cii cl class classutil clear cli clis clist clo clog clog_lf clog_p clogi clogi_sw clogit clogit_lf clogit_p clogitp clogl_sw cloglog clonevar clslistarray cluster cluster_measures cluster_stop cluster_tree cluster_tree_8 clustermat cmdlog cnr cnre cnreg cnreg_p cnreg_sw cnsreg codebook collaps4 collapse colormult_nb colormult_nw compare compress conf confi confir confirm conren cons const constr constra constrai constrain constraint continue contract copy copyright copysource cor corc corr corr2data corr_anti corr_kmo corr_smc corre correl correla correlat correlate corrgram cou coun count cox cox_p cox_sw coxbase coxhaz coxvar cprplot cprplot_7 crc cret cretu cretur creturn cross cs cscript cscript_log csi ct ct_is ctset ctst_5 ctst_st cttost cumsp cumsp_7 cumul cusum cusum_7 cutil d|0 datasig datasign datasigna datasignat datasignatu datasignatur datasignature datetof db dbeta de dec deco decod decode deff des desc descr descri describ describe destring dfbeta dfgls dfuller di di_g dir dirstats dis discard disp disp_res disp_s displ displa display distinct do doe doed doedi doedit dotplot dotplot_7 dprobit drawnorm drop ds ds_util dstdize duplicates durbina dwstat dydx e|0 ed edi edit egen eivreg emdef en enc enco encod encode eq erase ereg ereg_lf ereg_p ereg_sw ereghet ereghet_glf ereghet_glf_sh ereghet_gp ereghet_ilf ereghet_ilf_sh ereghet_ip eret eretu eretur ereturn err erro error esize est est_cfexist est_cfname est_clickable est_expand est_hold est_table est_unhold est_unholdok estat estat_default estat_summ estat_vce_only esti estimates etodow etof etomdy ex exi exit expand expandcl fac fact facto factor factor_estat factor_p factor_pca_rotated factor_rotate factormat fcast fcast_compute fcast_graph fdades fdadesc fdadescr fdadescri fdadescrib fdadescribe fdasav fdasave fdause fh_st file open file read file close file filefilter fillin find_hlp_file findfile findit findit_7 fit fl fli flis flist for5_0 forest forestplot form forma format fpredict frac_154 frac_adj frac_chk frac_cox frac_ddp frac_dis frac_dv frac_in frac_mun frac_pp frac_pq frac_pv frac_wgt frac_xo fracgen fracplot fracplot_7 fracpoly fracpred fron_ex fron_hn fron_p fron_tn fron_tn2 frontier ftodate ftoe ftomdy ftowdate funnel funnelplot g|0 gamhet_glf gamhet_gp gamhet_ilf gamhet_ip gamma gamma_d2 gamma_p gamma_sw gammahet gdi_hexagon gdi_spokes ge gen gene gener genera generat generate genrank genstd genvmean gettoken gl gladder gladder_7 glim_l01 glim_l02 glim_l03 glim_l04 glim_l05 glim_l06 glim_l07 glim_l08 glim_l09 glim_l10 glim_l11 glim_l12 glim_lf glim_mu glim_nw1 glim_nw2 glim_nw3 glim_p glim_v1 glim_v2 glim_v3 glim_v4 glim_v5 glim_v6 glim_v7 glm glm_6 glm_p glm_sw glmpred glo glob globa global glogit glogit_8 glogit_p gmeans gnbre_lf gnbreg gnbreg_5 gnbreg_p gomp_lf gompe_sw gomper_p gompertz gompertzhet gomphet_glf gomphet_glf_sh gomphet_gp gomphet_ilf gomphet_ilf_sh gomphet_ip gphdot gphpen gphprint gprefs gprobi_p gprobit gprobit_8 gr gr7 gr_copy gr_current gr_db gr_describe gr_dir gr_draw gr_draw_replay gr_drop gr_edit gr_editviewopts gr_example gr_example2 gr_export gr_print gr_qscheme gr_query gr_read gr_rename gr_replay gr_save gr_set gr_setscheme gr_table gr_undo gr_use graph graph7 grebar greigen greigen_7 greigen_8 grmeanby grmeanby_7 gs_fileinfo gs_filetype gs_graphinfo gs_stat gsort gwood h|0 hadimvo hareg hausman haver he heck_d2 heckma_p heckman heckp_lf heckpr_p heckprob hel help hereg hetpr_lf hetpr_p hetprob hettest hexdump hilite hist hist_7 histogram hlogit hlu hmeans hotel hotelling hprobit hreg hsearch icd9 icd9_ff icd9p iis impute imtest inbase include inf infi infil infile infix inp inpu input ins insheet insp inspe inspec inspect integ inten intreg intreg_7 intreg_p intrg2_ll intrg_ll intrg_ll2 ipolate iqreg ir irf irf_create irfm iri is_svy is_svysum isid istdize ivprob_1_lf ivprob_lf ivprobit ivprobit_p ivreg ivreg_footnote ivtob_1_lf ivtob_lf ivtobit ivtobit_p jackknife jacknife jknife jknife_6 jknife_8 jkstat joinby kalarma1 kap kap_3 kapmeier kappa kapwgt kdensity kdensity_7 keep ksm ksmirnov ktau kwallis l|0 la lab labbe labbeplot labe label labelbook ladder levels levelsof leverage lfit lfit_p li lincom line linktest lis list lloghet_glf lloghet_glf_sh lloghet_gp lloghet_ilf lloghet_ilf_sh lloghet_ip llogi_sw llogis_p llogist llogistic llogistichet lnorm_lf lnorm_sw lnorma_p lnormal lnormalhet lnormhet_glf lnormhet_glf_sh lnormhet_gp lnormhet_ilf lnormhet_ilf_sh lnormhet_ip lnskew0 loadingplot loc loca local log logi logis_lf logistic logistic_p logit logit_estat logit_p loglogs logrank loneway lookfor lookup lowess lowess_7 lpredict lrecomp lroc lroc_7 lrtest ls lsens lsens_7 lsens_x lstat ltable ltable_7 ltriang lv lvr2plot lvr2plot_7 m|0 ma mac macr macro makecns man manova manova_estat manova_p manovatest mantel mark markin markout marksample mat mat_capp mat_order mat_put_rr mat_rapp mata mata_clear mata_describe mata_drop mata_matdescribe mata_matsave mata_matuse mata_memory mata_mlib mata_mosave mata_rename mata_which matalabel matcproc matlist matname matr matri matrix matrix_input__dlg matstrik mcc mcci md0_ md1_ md1debug_ md2_ md2debug_ mds mds_estat mds_p mdsconfig mdslong mdsmat mdsshepard mdytoe mdytof me_derd mean means median memory memsize menl meqparse mer merg merge meta mfp mfx mhelp mhodds minbound mixed_ll mixed_ll_reparm mkassert mkdir mkmat mkspline ml ml_5 ml_adjs ml_bhhhs ml_c_d ml_check ml_clear ml_cnt ml_debug ml_defd ml_e0 ml_e0_bfgs ml_e0_cycle ml_e0_dfp ml_e0i ml_e1 ml_e1_bfgs ml_e1_bhhh ml_e1_cycle ml_e1_dfp ml_e2 ml_e2_cycle ml_ebfg0 ml_ebfr0 ml_ebfr1 ml_ebh0q ml_ebhh0 ml_ebhr0 ml_ebr0i ml_ecr0i ml_edfp0 ml_edfr0 ml_edfr1 ml_edr0i ml_eds ml_eer0i ml_egr0i ml_elf ml_elf_bfgs ml_elf_bhhh ml_elf_cycle ml_elf_dfp ml_elfi ml_elfs ml_enr0i ml_enrr0 ml_erdu0 ml_erdu0_bfgs ml_erdu0_bhhh ml_erdu0_bhhhq ml_erdu0_cycle ml_erdu0_dfp ml_erdu0_nrbfgs ml_exde ml_footnote ml_geqnr ml_grad0 ml_graph ml_hbhhh ml_hd0 ml_hold ml_init ml_inv ml_log ml_max ml_mlout ml_mlout_8 ml_model ml_nb0 ml_opt ml_p ml_plot ml_query ml_rdgrd ml_repor ml_s_e ml_score ml_searc ml_technique ml_unhold mleval mlf_ mlmatbysum mlmatsum mlog mlogi mlogit mlogit_footnote mlogit_p mlopts mlsum mlvecsum mnl0_ mor more mov move mprobit mprobit_lf mprobit_p mrdu0_ mrdu1_ mvdecode mvencode mvreg mvreg_estat n|0 nbreg nbreg_al nbreg_lf nbreg_p nbreg_sw nestreg net newey newey_7 newey_p news nl nl_7 nl_9 nl_9_p nl_p nl_p_7 nlcom nlcom_p nlexp2 nlexp2_7 nlexp2a nlexp2a_7 nlexp3 nlexp3_7 nlgom3 nlgom3_7 nlgom4 nlgom4_7 nlinit nllog3 nllog3_7 nllog4 nllog4_7 nlog_rd nlogit nlogit_p nlogitgen nlogittree nlpred no nobreak noi nois noisi noisil noisily note notes notes_dlg nptrend numlabel numlist odbc old_ver olo olog ologi ologi_sw ologit ologit_p ologitp on one onew onewa oneway op_colnm op_comp op_diff op_inv op_str opr opro oprob oprob_sw oprobi oprobi_p oprobit oprobitp opts_exclusive order orthog orthpoly ou out outf outfi outfil outfile outs outsh outshe outshee outsheet ovtest pac pac_7 palette parse parse_dissim pause pca pca_8 pca_display pca_estat pca_p pca_rotate pcamat pchart pchart_7 pchi pchi_7 pcorr pctile pentium pergram pergram_7 permute permute_8 personal peto_st pkcollapse pkcross pkequiv pkexamine pkexamine_7 pkshape pksumm pksumm_7 pl plo plot plugin pnorm pnorm_7 poisgof poiss_lf poiss_sw poisso_p poisson poisson_estat post postclose postfile postutil pperron pr prais prais_e prais_e2 prais_p predict predictnl preserve print pro prob probi probit probit_estat probit_p proc_time procoverlay procrustes procrustes_estat procrustes_p profiler prog progr progra program prop proportion prtest prtesti pwcorr pwd q\\s qby qbys qchi qchi_7 qladder qladder_7 qnorm qnorm_7 qqplot qqplot_7 qreg qreg_c qreg_p qreg_sw qu quadchk quantile quantile_7 que quer query range ranksum ratio rchart rchart_7 rcof recast reclink recode reg reg3 reg3_p regdw regr regre regre_p2 regres regres_p regress regress_estat regriv_p remap ren rena renam rename renpfix repeat replace report reshape restore ret retu retur return rm rmdir robvar roccomp roccomp_7 roccomp_8 rocf_lf rocfit rocfit_8 rocgold rocplot rocplot_7 roctab roctab_7 rolling rologit rologit_p rot rota rotat rotate rotatemat rreg rreg_p ru run runtest rvfplot rvfplot_7 rvpplot rvpplot_7 sa safesum sample sampsi sav save savedresults saveold sc sca scal scala scalar scatter scm_mine sco scob_lf scob_p scobi_sw scobit scor score scoreplot scoreplot_help scree screeplot screeplot_help sdtest sdtesti se search separate seperate serrbar serrbar_7 serset set set_defaults sfrancia sh she shel shell shewhart shewhart_7 signestimationsample signrank signtest simul simul_7 simulate simulate_8 sktest sleep slogit slogit_d2 slogit_p smooth snapspan so sor sort spearman spikeplot spikeplot_7 spikeplt spline_x split sqreg sqreg_p sret sretu sretur sreturn ssc st st_ct st_hc st_hcd st_hcd_sh st_is st_issys st_note st_promo st_set st_show st_smpl st_subid stack statsby statsby_8 stbase stci stci_7 stcox stcox_estat stcox_fr stcox_fr_ll stcox_p stcox_sw stcoxkm stcoxkm_7 stcstat stcurv stcurve stcurve_7 stdes stem stepwise stereg stfill stgen stir stjoin stmc stmh stphplot stphplot_7 stphtest stphtest_7 stptime strate strate_7 streg streg_sw streset sts sts_7 stset stsplit stsum sttocc sttoct stvary stweib su suest suest_8 sum summ summa summar summari summariz summarize sunflower sureg survcurv survsum svar svar_p svmat svy svy_disp svy_dreg svy_est svy_est_7 svy_estat svy_get svy_gnbreg_p svy_head svy_header svy_heckman_p svy_heckprob_p svy_intreg_p svy_ivreg_p svy_logistic_p svy_logit_p svy_mlogit_p svy_nbreg_p svy_ologit_p svy_oprobit_p svy_poisson_p svy_probit_p svy_regress_p svy_sub svy_sub_7 svy_x svy_x_7 svy_x_p svydes svydes_8 svygen svygnbreg svyheckman svyheckprob svyintreg svyintreg_7 svyintrg svyivreg svylc svylog_p svylogit svymarkout svymarkout_8 svymean svymlog svymlogit svynbreg svyolog svyologit svyoprob svyoprobit svyopts svypois svypois_7 svypoisson svyprobit svyprobt svyprop svyprop_7 svyratio svyreg svyreg_p svyregress svyset svyset_7 svyset_8 svytab svytab_7 svytest svytotal sw sw_8 swcnreg swcox swereg swilk swlogis swlogit swologit swoprbt swpois swprobit swqreg swtobit swweib symmetry symmi symplot symplot_7 syntax sysdescribe sysdir sysuse szroeter ta tab tab1 tab2 tab_or tabd tabdi tabdis tabdisp tabi table tabodds tabodds_7 tabstat tabu tabul tabula tabulat tabulate te tempfile tempname tempvar tes test testnl testparm teststd tetrachoric time_it timer tis tob tobi tobit tobit_p tobit_sw token tokeni tokeniz tokenize tostring total translate translator transmap treat_ll treatr_p treatreg trim trimfill trnb_cons trnb_mean trpoiss_d2 trunc_ll truncr_p truncreg tsappend tset tsfill tsline tsline_ex tsreport tsrevar tsrline tsset tssmooth tsunab ttest ttesti tut_chk tut_wait tutorial tw tware_st two twoway twoway__fpfit_serset twoway__function_gen twoway__histogram_gen twoway__ipoint_serset twoway__ipoints_serset twoway__kdensity_gen twoway__lfit_serset twoway__normgen_gen twoway__pci_serset twoway__qfit_serset twoway__scatteri_serset twoway__sunflower_gen twoway_ksm_serset ty typ type typeof u|0 unab unabbrev unabcmd update us use uselabel var var_mkcompanion var_p varbasic varfcast vargranger varirf varirf_add varirf_cgraph varirf_create varirf_ctable varirf_describe varirf_dir varirf_drop varirf_erase varirf_graph varirf_ograph varirf_rename varirf_set varirf_table varlist varlmar varnorm varsoc varstable varstable_w varstable_w2 varwle vce vec vec_fevd vec_mkphi vec_p vec_p_w vecirf_create veclmar veclmar_w vecnorm vecnorm_w vecrank vecstable verinst vers versi versio version view viewsource vif vwls wdatetof webdescribe webseek webuse weib1_lf weib2_lf weib_lf weib_lf0 weibhet_glf weibhet_glf_sh weibhet_glfa weibhet_glfa_sh weibhet_gp weibhet_ilf weibhet_ilf_sh weibhet_ilfa weibhet_ilfa_sh weibhet_ip weibu_sw weibul_p weibull weibull_c weibull_s weibullhet wh whelp whi which whil while wilc_st wilcoxon win wind windo window winexec wntestb wntestb_7 wntestq xchart xchart_7 xcorr xcorr_7 xi xi_6 xmlsav xmlsave xmluse xpose xsh xshe xshel xshell xt_iis xt_tis xtab_p xtabond xtbin_p xtclog xtcloglog xtcloglog_8 xtcloglog_d2 xtcloglog_pa_p xtcloglog_re_p xtcnt_p xtcorr xtdata xtdes xtfront_p xtfrontier xtgee xtgee_elink xtgee_estat xtgee_makeivar xtgee_p xtgee_plink xtgls xtgls_p xthaus xthausman xtht_p xthtaylor xtile xtint_p xtintreg xtintreg_8 xtintreg_d2 xtintreg_p xtivp_1 xtivp_2 xtivreg xtline xtline_ex xtlogit xtlogit_8 xtlogit_d2 xtlogit_fe_p xtlogit_pa_p xtlogit_re_p xtmixed xtmixed_estat xtmixed_p xtnb_fe xtnb_lf xtnbreg xtnbreg_pa_p xtnbreg_refe_p xtpcse xtpcse_p xtpois xtpoisson xtpoisson_d2 xtpoisson_pa_p xtpoisson_refe_p xtpred xtprobit xtprobit_8 xtprobit_d2 xtprobit_re_p xtps_fe xtps_lf xtps_ren xtps_ren_8 xtrar_p xtrc xtrc_p xtrchh xtrefe_p xtreg xtreg_be xtreg_fe xtreg_ml xtreg_pa_p xtreg_re xtregar xtrere_p xtset xtsf_ll xtsf_llti xtsum xttab xttest0 xttobit xttobit_8 xttobit_p xttrans yx yxview__barlike_draw yxview_area_draw yxview_bar_draw yxview_dot_draw yxview_dropline_draw yxview_function_draw yxview_iarrow_draw yxview_ilabels_draw yxview_normal_draw yxview_pcarrow_draw yxview_pcbarrow_draw yxview_pccapsym_draw yxview_pcscatter_draw yxview_pcspike_draw yxview_rarea_draw yxview_rbar_draw yxview_rbarm_draw yxview_rcap_draw yxview_rcapsym_draw yxview_rconnected_draw yxview_rline_draw yxview_rscatter_draw yxview_rspike_draw yxview_spike_draw yxview_sunflower_draw zap_s zinb zinb_llf zinb_plf zip zip_llf zip_p zip_plf zt_ct_5 zt_hc_5 zt_hcd_5 zt_is_5 zt_iss_5 zt_sho_5 zt_smp_5 ztbase_5 ztcox_5 ztdes_5 ztereg_5 ztfill_5 ztgen_5 ztir_5 ztjoin_5 ztnb ztnb_p ztp ztp_p zts_5 ztset_5 ztspli_5 ztsum_5 zttoct_5 ztvary_5 ztweib_5",contains:[{className:"symbol",
-begin:/`[a-zA-Z0-9_]+'/},{className:"variable",begin:/\$\{?[a-zA-Z0-9_]+\}?/},{className:"string",variants:[{begin:'`"[^\r\n]*?"\''},{begin:'"[^\r\n"]*"'}]},{className:"built_in",variants:[{begin:"\\b(abs|acos|asin|atan|atan2|atanh|ceil|cloglog|comb|cos|digamma|exp|floor|invcloglog|invlogit|ln|lnfact|lnfactorial|lngamma|log|log10|max|min|mod|reldif|round|sign|sin|sqrt|sum|tan|tanh|trigamma|trunc|betaden|Binomial|binorm|binormal|chi2|chi2tail|dgammapda|dgammapdada|dgammapdadx|dgammapdx|dgammapdxdx|F|Fden|Ftail|gammaden|gammap|ibeta|invbinomial|invchi2|invchi2tail|invF|invFtail|invgammap|invibeta|invnchi2|invnFtail|invnibeta|invnorm|invnormal|invttail|nbetaden|nchi2|nFden|nFtail|nibeta|norm|normal|normalden|normd|npnchi2|tden|ttail|uniform|abbrev|char|index|indexnot|length|lower|ltrim|match|plural|proper|real|regexm|regexr|regexs|reverse|rtrim|string|strlen|strlower|strltrim|strmatch|strofreal|strpos|strproper|strreverse|strrtrim|strtrim|strupper|subinstr|subinword|substr|trim|upper|word|wordcount|_caller|autocode|byteorder|chop|clip|cond|e|epsdouble|epsfloat|group|inlist|inrange|irecode|matrix|maxbyte|maxdouble|maxfloat|maxint|maxlong|mi|minbyte|mindouble|minfloat|minint|minlong|missing|r|recode|replay|return|s|scalar|d|date|day|dow|doy|halfyear|mdy|month|quarter|week|year|d|daily|dofd|dofh|dofm|dofq|dofw|dofy|h|halfyearly|hofd|m|mofd|monthly|q|qofd|quarterly|tin|twithin|w|weekly|wofd|y|yearly|yh|ym|yofd|yq|yw|cholesky|colnumb|colsof|corr|det|diag|diag0cnt|el|get|hadamard|I|inv|invsym|issym|issymmetric|J|matmissing|matuniform|mreldif|nullmat|rownumb|rowsof|sweep|syminv|trace|vec|vecdiag)(?=\\()"}]},
-a.COMMENT("^[ \t]*\\*.*$",!1),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}}}());
-hljs.registerLanguage("step21",function(){return function(a){return{name:"STEP Part 21",aliases:["p21","step","stp"],case_insensitive:!0,keywords:{$pattern:"[A-Z_][A-Z0-9_.]*",keyword:"HEADER ENDSEC DATA"},contains:[{className:"meta",begin:"ISO-10303-21;",relevance:10},{className:"meta",begin:"END-ISO-10303-21;",relevance:10},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.COMMENT("/\\*\\*!","\\*/"),a.C_NUMBER_MODE,a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),
-{className:"string",begin:"'",end:"'"},{className:"symbol",variants:[{begin:"#",end:"\\d+",illegal:"\\W"}]}]}}}());
-hljs.registerLanguage("stylus",function(){return function(a){var b={className:"variable",begin:"\\$"+a.IDENT_RE},c={className:"number",begin:"#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})"};return{name:"Stylus",aliases:["styl"],case_insensitive:!1,keywords:"if else for in",illegal:"(\\?|(\\bReturn\\b)|(\\bEnd\\b)|(\\bend\\b)|(\\bdef\\b)|;|#\\s|\\*\\s|===\\s|\\||%)",contains:[a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,{begin:"\\.[a-zA-Z][a-zA-Z0-9_-]*(?=[\\.\\s\\n\\[\\:,])",
-className:"selector-class"},{begin:"\\#[a-zA-Z][a-zA-Z0-9_-]*(?=[\\.\\s\\n\\[\\:,])",className:"selector-id"},{begin:"\\b(a|abbr|address|article|aside|audio|b|blockquote|body|button|canvas|caption|cite|code|dd|del|details|dfn|div|dl|dt|em|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|html|i|iframe|img|input|ins|kbd|label|legend|li|mark|menu|nav|object|ol|p|q|quote|samp|section|span|strong|summary|sup|table|tbody|td|textarea|tfoot|th|thead|time|tr|ul|var|video)(?=[\\.\\s\\n\\[\\:,])",
-className:"selector-tag"},{begin:"&?:?:\\b(after|before|first-letter|first-line|active|first-child|focus|hover|lang|link|visited)(?=[\\.\\s\\n\\[\\:,])"},{begin:"@(charset|css|debug|extend|font-face|for|import|include|media|mixin|page|warn|while)\\b"},b,a.CSS_NUMBER_MODE,a.NUMBER_MODE,{className:"function",begin:"^[a-zA-Z][a-zA-Z0-9_-]*\\(.*\\)",illegal:"[\\n]",returnBegin:!0,contains:[{className:"title",begin:"\\b[a-zA-Z][a-zA-Z0-9_-]*"},{className:"params",begin:/\(/,end:/\)/,contains:[c,b,a.APOS_STRING_MODE,
-a.CSS_NUMBER_MODE,a.NUMBER_MODE,a.QUOTE_STRING_MODE]}]},{className:"attribute",begin:"\\b("+"align-content align-items align-self animation animation-delay animation-direction animation-duration animation-fill-mode animation-iteration-count animation-name animation-play-state animation-timing-function auto backface-visibility background background-attachment background-clip background-color background-image background-origin background-position background-repeat background-size border border-bottom border-bottom-color border-bottom-left-radius border-bottom-right-radius border-bottom-style border-bottom-width border-collapse border-color border-image border-image-outset border-image-repeat border-image-slice border-image-source border-image-width border-left border-left-color border-left-style border-left-width border-radius border-right border-right-color border-right-style border-right-width border-spacing border-style border-top border-top-color border-top-left-radius border-top-right-radius border-top-style border-top-width border-width bottom box-decoration-break box-shadow box-sizing break-after break-before break-inside caption-side clear clip clip-path color column-count column-fill column-gap column-rule column-rule-color column-rule-style column-rule-width column-span column-width columns content counter-increment counter-reset cursor direction display empty-cells filter flex flex-basis flex-direction flex-flow flex-grow flex-shrink flex-wrap float font font-family font-feature-settings font-kerning font-language-override font-size font-size-adjust font-stretch font-style font-variant font-variant-ligatures font-weight height hyphens icon image-orientation image-rendering image-resolution ime-mode inherit initial justify-content left letter-spacing line-height list-style list-style-image list-style-position list-style-type margin margin-bottom margin-left margin-right margin-top marks mask max-height max-width min-height min-width nav-down nav-index nav-left nav-right nav-up none normal object-fit object-position opacity order orphans outline outline-color outline-offset outline-style outline-width overflow overflow-wrap overflow-x overflow-y padding padding-bottom padding-left padding-right padding-top page-break-after page-break-before page-break-inside perspective perspective-origin pointer-events position quotes resize right tab-size table-layout text-align text-align-last text-decoration text-decoration-color text-decoration-line text-decoration-style text-indent text-overflow text-rendering text-shadow text-transform text-underline-position top transform transform-origin transform-style transition transition-delay transition-duration transition-property transition-timing-function unicode-bidi vertical-align visibility white-space widows width word-break word-spacing word-wrap z-index".split(" ").reverse().join("|")+
-")\\b",starts:{end:/;|$/,contains:[c,b,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE,a.NUMBER_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/\./,relevance:0}}]}}}());
-hljs.registerLanguage("subunit",function(){return function(a){return{name:"SubUnit",case_insensitive:!0,contains:[{className:"string",begin:"\\[\n(multipart)?",end:"\\]\n"},{className:"string",begin:"\\d{4}-\\d{2}-\\d{2}(\\s+)\\d{2}:\\d{2}:\\d{2}.\\d+Z"},{className:"string",begin:"(\\+|-)\\d+"},{className:"keyword",relevance:10,variants:[{begin:"^(test|testing|success|successful|failure|error|skip|xfail|uxsuccess)(:?)\\s+(test)?"},{begin:"^progress(:?)(\\s+)?(pop|push)?"},{begin:"^tags:"},{begin:"^time:"}]}]}}}());
-hljs.registerLanguage("swift",function(){return function(a){var b={keyword:"#available #colorLiteral #column #else #elseif #endif #file #fileLiteral #function #if #imageLiteral #line #selector #sourceLocation _ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",
-literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c compactMap contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},
-c=a.COMMENT("/\\*","\\*/",{contains:["self"]}),d={className:"subst",begin:/\\\(/,end:"\\)",keywords:b,contains:[]},e={className:"string",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:/"""/,end:/"""/},{begin:/"/,end:/"/}]},f={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0};d.contains=[f];return{name:"Swift",keywords:b,contains:[e,a.C_LINE_COMMENT_MODE,c,{className:"type",begin:"\\b[A-Z][\\w\u00c0-\u02b8']*[!?]"},{className:"type",
-begin:"\\b[A-Z][\\w\u00c0-\u02b8']*",relevance:0},f,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin:/</,end:/>/},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,contains:["self",f,e,a.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:b,end:"\\{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,
-{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain|@dynamicMemberLookup|@propertyWrapper)"},{beginKeywords:"import",end:/$/,contains:[a.C_LINE_COMMENT_MODE,c]}]}}}());
-hljs.registerLanguage("taggerscript",function(){return function(a){return{name:"Tagger Script",contains:[{className:"comment",begin:/\$noop\(/,end:/\)/,contains:[{begin:/\(/,end:/\)/,contains:["self",{begin:/\\./}]}],relevance:10},{className:"keyword",begin:/\$(?!noop)[a-zA-Z][_a-zA-Z0-9]*/,end:/\(/,excludeEnd:!0},{className:"variable",begin:/%[_a-zA-Z0-9:]*/,end:"%"},{className:"symbol",begin:/\\./}]}}}());
-hljs.registerLanguage("yaml",function(){return function(a){var b={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[a.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]},c=a.inherit(b,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),d={end:",",endsWithParent:!0,excludeEnd:!0,contains:[],keywords:"true false yes no null",relevance:0};a=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},
-{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>]([0-9]?[+-])?[ ]*\\n( *)[\\S ]+\\n(\\2[\\S ]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+![\\w#;/?:@&=+$,.~*\\'()[\\]]+"},{className:"type",begin:"!<[\\w#;/?:@&=+$,.~*\\'()[\\]]+>"},{className:"type",begin:"![\\w#;/?:@&=+$,.~*\\'()[\\]]+"},{className:"type",
-begin:"!![\\w#;/?:@&=+$,.~*\\'()[\\]]+"},{className:"meta",begin:"&"+a.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+a.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"\\-(?=[ ]|$)",relevance:0},a.HASH_COMMENT_MODE,{beginKeywords:"true false yes no null",keywords:{literal:"true false yes no null"}},{className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},{className:"number",begin:a.C_NUMBER_RE+
-"\\b"},{begin:"{",end:"}",contains:[d],illegal:"\\n",relevance:0},{begin:"\\[",end:"\\]",contains:[d],illegal:"\\n",relevance:0},b];b=[].concat($jscomp.arrayFromIterable(a));b.pop();b.push(c);d.contains=b;return{name:"YAML",case_insensitive:!0,aliases:["yml","YAML"],contains:a}}}());
-hljs.registerLanguage("tap",function(){return function(a){return{name:"Test Anything Protocol",case_insensitive:!0,contains:[a.HASH_COMMENT_MODE,{className:"meta",variants:[{begin:"^TAP version (\\d+)$"},{begin:"^1\\.\\.(\\d+)$"}]},{begin:"(s+)?---$",end:"\\.\\.\\.$",subLanguage:"yaml",relevance:0},{className:"number",begin:" (\\d+) "},{className:"symbol",variants:[{begin:"^ok"},{begin:"^not ok"}]}]}}}());
-hljs.registerLanguage("tcl",function(){return function(a){return{name:"Tcl",aliases:["tk"],keywords:"after append apply array auto_execok auto_import auto_load auto_mkindex auto_mkindex_old auto_qualify auto_reset bgerror binary break catch cd chan clock close concat continue dde dict encoding eof error eval exec exit expr fblocked fconfigure fcopy file fileevent filename flush for foreach format gets glob global history http if incr info interp join lappend|10 lassign|10 lindex|10 linsert|10 list llength|10 load lrange|10 lrepeat|10 lreplace|10 lreverse|10 lsearch|10 lset|10 lsort|10 mathfunc mathop memory msgcat namespace open package parray pid pkg::create pkg_mkIndex platform platform::shell proc puts pwd read refchan regexp registry regsub|10 rename return safe scan seek set socket source split string subst switch tcl_endOfWord tcl_findLibrary tcl_startOfNextWord tcl_startOfPreviousWord tcl_wordBreakAfter tcl_wordBreakBefore tcltest tclvars tell time tm trace unknown unload unset update uplevel upvar variable vwait while",contains:[a.COMMENT(";[ \\t]*#",
-"$"),a.COMMENT("^[ \\t]*#","$"),{beginKeywords:"proc",end:"[\\{]",excludeEnd:!0,contains:[{className:"title",begin:"[ \\t\\n\\r]+(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*",end:"[ \\t\\n\\r]",endsWithParent:!0,excludeEnd:!0}]},{excludeEnd:!0,variants:[{begin:"\\$(\\{)?(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*\\(([a-zA-Z0-9_])*\\)",end:"[^a-zA-Z0-9_\\}\\$]"},{begin:"\\$(\\{)?(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*",end:"(\\))?[^a-zA-Z0-9_\\}\\$]"}]},{className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[a.inherit(a.QUOTE_STRING_MODE,
-{illegal:null})]},{className:"number",variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]}]}}}());
-hljs.registerLanguage("thrift",function(){return function(a){return{name:"Thrift",keywords:{keyword:"namespace const typedef struct enum service exception void oneway set list map required optional",built_in:"bool byte i16 i32 i64 double string binary",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",beginKeywords:"struct enum service exception",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,
-excludeEnd:!0}})]},{begin:"\\b(set|list|map)\\s*<",end:">",keywords:"bool byte i16 i32 i64 double string binary",contains:["self"]}]}}}());
-hljs.registerLanguage("tp",function(){return function(a){var b={className:"number",begin:"[1-9][0-9]*",relevance:0},c={className:"symbol",begin:":[^\\]]+"};return{name:"TP",keywords:{keyword:"ABORT ACC ADJUST AND AP_LD BREAK CALL CNT COL CONDITION CONFIG DA DB DIV DETECT ELSE END ENDFOR ERR_NUM ERROR_PROG FINE FOR GP GUARD INC IF JMP LINEAR_MAX_SPEED LOCK MOD MONITOR OFFSET Offset OR OVERRIDE PAUSE PREG PTH RT_LD RUN SELECT SKIP Skip TA TB TO TOOL_OFFSET Tool_Offset UF UT UFRAME_NUM UTOOL_NUM UNLOCK WAIT X Y Z W P R STRLEN SUBSTR FINDSTR VOFFSET PROG ATTR MN POS",literal:"ON OFF max_speed LPOS JPOS ENABLE DISABLE START STOP RESET"},
-contains:[{className:"built_in",begin:"(AR|P|PAYLOAD|PR|R|SR|RSR|LBL|VR|UALM|MESSAGE|UTOOL|UFRAME|TIMER|TIMER_OVERFLOW|JOINT_MAX_SPEED|RESUME_PROG|DIAG_REC)\\[",end:"\\]",contains:["self",b,c]},{className:"built_in",begin:"(AI|AO|DI|DO|F|RI|RO|UI|UO|GI|GO|SI|SO)\\[",end:"\\]",contains:["self",b,a.QUOTE_STRING_MODE,c]},{className:"keyword",begin:"/(PROG|ATTR|MN|POS|END)\\b"},{className:"keyword",begin:"(CALL|RUN|POINT_LOGIC|LBL)\\b"},{className:"keyword",begin:"\\b(ACC|CNT|Skip|Offset|PSPD|RT_LD|AP_LD|Tool_Offset)"},
-{className:"number",begin:"\\d+(sec|msec|mm/sec|cm/min|inch/min|deg/sec|mm|in|cm)?\\b",relevance:0},a.COMMENT("//","[;$]"),a.COMMENT("!","[;$]"),a.COMMENT("--eg:","$"),a.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"'"},a.C_NUMBER_MODE,{className:"variable",begin:"\\$[A-Za-z0-9_]+"}]}}}());
-hljs.registerLanguage("twig",function(){return function(a){var b={beginKeywords:"attribute block constant cycle date dump include max min parent random range source template_from_string",keywords:{name:"attribute block constant cycle date dump include max min parent random range source template_from_string"},relevance:0,contains:[{className:"params",begin:"\\(",end:"\\)"}]},c={begin:/\|[A-Za-z_]+:?/,keywords:"abs batch capitalize column convert_encoding date date_modify default escape filter first format inky_to_html inline_css join json_encode keys last length lower map markdown merge nl2br number_format raw reduce replace reverse round slice sort spaceless split striptags title trim upper url_encode",
-contains:[b]},d="apply autoescape block deprecated do embed extends filter flush for from if import include macro sandbox set use verbatim with";d=d+" "+d.split(" ").map(function(a){return"end"+a}).join(" ");return{name:"Twig",aliases:["craftcms"],case_insensitive:!0,subLanguage:"xml",contains:[a.COMMENT(/\{#/,/#}/),{className:"template-tag",begin:/\{%/,end:/%}/,contains:[{className:"name",begin:/\w+/,keywords:d,starts:{endsWithParent:!0,contains:[c,b],relevance:0}}]},{className:"template-variable",
-begin:/\{\{/,end:/}}/,contains:["self",c,b]}]}}}());
-hljs.registerLanguage("typescript",function(){var a="as in of if for while finally var new function do return void else break catch instanceof with throw case default try switch continue typeof delete let yield const class debugger async await static import from export extends".split(" "),b="true false null undefined NaN Infinity".split(" "),c=[].concat("setInterval setTimeout clearInterval clearTimeout require exports eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape".split(" "),"arguments this super console window document localStorage module global".split(" "),
-"Intl DataView Number Math Date String RegExp Object Function Boolean Error Symbol Set Map WeakSet WeakMap Proxy Reflect JSON Promise Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Float32Array Array Uint8Array Uint8ClampedArray ArrayBuffer".split(" "),"EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError".split(" "));return function(d){var e={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:a.concat("type namespace typedef interface public private protected implements declare abstract".split(" ")).join(" "),
-literal:b.join(" "),built_in:c.concat("any void number boolean string object never enum".split(" ")).join(" ")},f={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},h={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:d.C_NUMBER_RE+"n?"}],relevance:0},g={className:"subst",begin:"\\$\\{",end:"\\}",keywords:e,contains:[]},k={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[d.BACKSLASH_ESCAPE,g],subLanguage:"xml"}},l={begin:"css`",end:"",starts:{end:"`",
-returnEnd:!1,contains:[d.BACKSLASH_ESCAPE,g],subLanguage:"css"}},m={className:"string",begin:"`",end:"`",contains:[d.BACKSLASH_ESCAPE,g]};g.contains=[d.APOS_STRING_MODE,d.QUOTE_STRING_MODE,k,l,m,h,d.REGEXP_MODE];g={begin:"\\(",end:/\)/,keywords:e,contains:["self",d.QUOTE_STRING_MODE,d.APOS_STRING_MODE,d.NUMBER_MODE]};var p={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:e,contains:[d.C_LINE_COMMENT_MODE,d.C_BLOCK_COMMENT_MODE,f,g]};return{name:"TypeScript",aliases:["ts"],
-keywords:e,contains:[d.SHEBANG(),{className:"meta",begin:/^\s*['"]use strict['"]/},d.APOS_STRING_MODE,d.QUOTE_STRING_MODE,k,l,m,d.C_LINE_COMMENT_MODE,d.C_BLOCK_COMMENT_MODE,h,{begin:"("+d.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[d.C_LINE_COMMENT_MODE,d.C_BLOCK_COMMENT_MODE,d.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+d.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:d.UNDERSCORE_IDENT_RE},
-{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:e,contains:g.contains}]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[\{;]/,excludeEnd:!0,keywords:e,contains:["self",d.inherit(d.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),p],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/[\{;]/,excludeEnd:!0,contains:["self",p]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},
-{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+d.IDENT_RE,relevance:0},f,g]}}}());
-hljs.registerLanguage("vala",function(){return function(a){return{name:"Vala",keywords:{keyword:"char uchar unichar int uint long ulong short ushort int8 int16 int32 int64 uint8 uint16 uint32 uint64 float double bool struct enum string void weak unowned owned async signal static abstract interface override virtual delegate if while do for foreach else switch case break default return try catch public private protected internal using new this get set const stdout stdin stderr var",built_in:"DBus GLib CCode Gee Object Gtk Posix",
-literal:"false true null"},contains:[{className:"class",beginKeywords:"class interface namespace",end:"{",excludeEnd:!0,illegal:"[^,:\\n\\s\\.]",contains:[a.UNDERSCORE_TITLE_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",begin:'"""',end:'"""',relevance:5},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{className:"meta",begin:"^#",end:"$",relevance:2}]}}}());
-hljs.registerLanguage("vbnet",function(){return function(a){return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0,keywords:{keyword:"addhandler addressof alias and andalso aggregate ansi as async assembly auto await binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into is isfalse isnot istrue iterator join key let lib like loop me mid mod module mustinherit mustoverride mybase myclass nameof namespace narrowing new next not notinheritable notoverridable of off on operator option optional or order orelse overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim rem removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly xor yield",
-built_in:"boolean byte cbool cbyte cchar cdate cdec cdbl char cint clng cobj csbyte cshort csng cstr ctype date decimal directcast double gettype getxmlnamespace iif integer long object sbyte short single string trycast typeof uinteger ulong ushort",literal:"true false nothing"},illegal:"//|{|}|endif|gosub|variant|wend|^\\$ ",contains:[a.inherit(a.QUOTE_STRING_MODE,{contains:[{begin:'""'}]}),a.COMMENT("'","$",{returnBegin:!0,contains:[{className:"doctag",begin:"'''|\x3c!--|--\x3e",contains:[a.PHRASAL_WORDS_MODE]},
-{className:"doctag",begin:"</?",end:">",contains:[a.PHRASAL_WORDS_MODE]}]}),a.C_NUMBER_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elseif end region externalsource"}}]}}}());
-hljs.registerLanguage("vbscript",function(){return function(a){return{name:"VBScript",aliases:["vbs"],case_insensitive:!0,keywords:{keyword:"call class const dim do loop erase execute executeglobal exit for each next function if then else on error option explicit new private property let get public randomize redim rem select case set stop sub while wend with end to elseif is or xor and not class_initialize class_terminate default preserve in me byval byref step resume goto",built_in:"lcase month vartype instrrev ubound setlocale getobject rgb getref string weekdayname rnd dateadd monthname now day minute isarray cbool round formatcurrency conversions csng timevalue second year space abs clng timeserial fixs len asc isempty maths dateserial atn timer isobject filter weekday datevalue ccur isdate instr datediff formatdatetime replace isnull right sgn array snumeric log cdbl hex chr lbound msgbox ucase getlocale cos cdate cbyte rtrim join hour oct typename trim strcomp int createobject loadpicture tan formatnumber mid scriptenginebuildversion scriptengine split scriptengineminorversion cint sin datepart ltrim sqr scriptenginemajorversion time derived eval date formatpercent exp inputbox left ascw chrw regexp server response request cstr err",
-literal:"true false null nothing empty"},illegal:"//",contains:[a.inherit(a.QUOTE_STRING_MODE,{contains:[{begin:'""'}]}),a.COMMENT(/'/,/$/,{relevance:0}),a.C_NUMBER_MODE]}}}());hljs.registerLanguage("vbscript-html",function(){return function(a){return{name:"VBScript in HTML",subLanguage:"xml",contains:[{begin:"<%",end:"%>",subLanguage:"vbscript"}]}}}());
-hljs.registerLanguage("verilog",function(){return function(a){return{name:"Verilog",aliases:["v","sv","svh"],case_insensitive:!1,keywords:{$pattern:/[\w\$]+/,keyword:"accept_on alias always always_comb always_ff always_latch and assert assign assume automatic before begin bind bins binsof bit break buf|0 bufif0 bufif1 byte case casex casez cell chandle checker class clocking cmos config const constraint context continue cover covergroup coverpoint cross deassign default defparam design disable dist do edge else end endcase endchecker endclass endclocking endconfig endfunction endgenerate endgroup endinterface endmodule endpackage endprimitive endprogram endproperty endspecify endsequence endtable endtask enum event eventually expect export extends extern final first_match for force foreach forever fork forkjoin function generate|5 genvar global highz0 highz1 if iff ifnone ignore_bins illegal_bins implements implies import incdir include initial inout input inside instance int integer interconnect interface intersect join join_any join_none large let liblist library local localparam logic longint macromodule matches medium modport module nand negedge nettype new nexttime nmos nor noshowcancelled not notif0 notif1 or output package packed parameter pmos posedge primitive priority program property protected pull0 pull1 pulldown pullup pulsestyle_ondetect pulsestyle_onevent pure rand randc randcase randsequence rcmos real realtime ref reg reject_on release repeat restrict return rnmos rpmos rtran rtranif0 rtranif1 s_always s_eventually s_nexttime s_until s_until_with scalared sequence shortint shortreal showcancelled signed small soft solve specify specparam static string strong strong0 strong1 struct super supply0 supply1 sync_accept_on sync_reject_on table tagged task this throughout time timeprecision timeunit tran tranif0 tranif1 tri tri0 tri1 triand trior trireg type typedef union unique unique0 unsigned until until_with untyped use uwire var vectored virtual void wait wait_order wand weak weak0 weak1 while wildcard wire with within wor xnor xor",
-literal:"null",built_in:"$finish $stop $exit $fatal $error $warning $info $realtime $time $printtimescale $bitstoreal $bitstoshortreal $itor $signed $cast $bits $stime $timeformat $realtobits $shortrealtobits $rtoi $unsigned $asserton $assertkill $assertpasson $assertfailon $assertnonvacuouson $assertoff $assertcontrol $assertpassoff $assertfailoff $assertvacuousoff $isunbounded $sampled $fell $changed $past_gclk $fell_gclk $changed_gclk $rising_gclk $steady_gclk $coverage_control $coverage_get $coverage_save $set_coverage_db_name $rose $stable $past $rose_gclk $stable_gclk $future_gclk $falling_gclk $changing_gclk $display $coverage_get_max $coverage_merge $get_coverage $load_coverage_db $typename $unpacked_dimensions $left $low $increment $clog2 $ln $log10 $exp $sqrt $pow $floor $ceil $sin $cos $tan $countbits $onehot $isunknown $fatal $warning $dimensions $right $high $size $asin $acos $atan $atan2 $hypot $sinh $cosh $tanh $asinh $acosh $atanh $countones $onehot0 $error $info $random $dist_chi_square $dist_erlang $dist_exponential $dist_normal $dist_poisson $dist_t $dist_uniform $q_initialize $q_remove $q_exam $async$and$array $async$nand$array $async$or$array $async$nor$array $sync$and$array $sync$nand$array $sync$or$array $sync$nor$array $q_add $q_full $psprintf $async$and$plane $async$nand$plane $async$or$plane $async$nor$plane $sync$and$plane $sync$nand$plane $sync$or$plane $sync$nor$plane $system $display $displayb $displayh $displayo $strobe $strobeb $strobeh $strobeo $write $readmemb $readmemh $writememh $value$plusargs $dumpvars $dumpon $dumplimit $dumpports $dumpportson $dumpportslimit $writeb $writeh $writeo $monitor $monitorb $monitorh $monitoro $writememb $dumpfile $dumpoff $dumpall $dumpflush $dumpportsoff $dumpportsall $dumpportsflush $fclose $fdisplay $fdisplayb $fdisplayh $fdisplayo $fstrobe $fstrobeb $fstrobeh $fstrobeo $swrite $swriteb $swriteh $swriteo $fscanf $fread $fseek $fflush $feof $fopen $fwrite $fwriteb $fwriteh $fwriteo $fmonitor $fmonitorb $fmonitorh $fmonitoro $sformat $sformatf $fgetc $ungetc $fgets $sscanf $rewind $ftell $ferror"},
-contains:[a.C_BLOCK_COMMENT_MODE,a.C_LINE_COMMENT_MODE,a.QUOTE_STRING_MODE,{className:"number",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:"\\b((\\d+'(b|h|o|d|B|H|O|D))[0-9xzXZa-fA-F_]+)"},{begin:"\\B(('(b|h|o|d|B|H|O|D))[0-9xzXZa-fA-F_]+)"},{begin:"\\b([0-9_])+",relevance:0}]},{className:"variable",variants:[{begin:"#\\((?!parameter).+\\)"},{begin:"\\.\\w+",relevance:0}]},{className:"meta",begin:"`",end:"$",keywords:{"meta-keyword":"define __FILE__ __LINE__ begin_keywords celldefine default_nettype define else elsif end_keywords endcelldefine endif ifdef ifndef include line nounconnected_drive pragma resetall timescale unconnected_drive undef undefineall"},
-relevance:0}]}}}());
-hljs.registerLanguage("vhdl",function(){return function(a){return{name:"VHDL",case_insensitive:!0,keywords:{keyword:"abs access after alias all and architecture array assert assume assume_guarantee attribute begin block body buffer bus case component configuration constant context cover disconnect downto default else elsif end entity exit fairness file for force function generate generic group guarded if impure in inertial inout is label library linkage literal loop map mod nand new next nor not null of on open or others out package parameter port postponed procedure process property protected pure range record register reject release rem report restrict restrict_guarantee return rol ror select sequence severity shared signal sla sll sra srl strong subtype then to transport type unaffected units until use variable view vmode vprop vunit wait when while with xnor xor",built_in:"boolean bit character integer time delay_length natural positive string bit_vector file_open_kind file_open_status std_logic std_logic_vector unsigned signed boolean_vector integer_vector std_ulogic std_ulogic_vector unresolved_unsigned u_unsigned unresolved_signed u_signed real_vector time_vector",
-literal:"false true note warning error failure line text side width"},illegal:"{",contains:[a.C_BLOCK_COMMENT_MODE,a.COMMENT("--","$"),a.QUOTE_STRING_MODE,{className:"number",begin:"\\b(\\d(_|\\d)*#\\w+(\\.\\w+)?#([eE][-+]?\\d(_|\\d)*)?|\\d(_|\\d)*(\\.\\d(_|\\d)*)?([eE][-+]?\\d(_|\\d)*)?)",relevance:0},{className:"string",begin:"'(U|X|0|1|Z|W|L|H|-)'",contains:[a.BACKSLASH_ESCAPE]},{className:"symbol",begin:"'[A-Za-z](_?[A-Za-z0-9])*",contains:[a.BACKSLASH_ESCAPE]}]}}}());
-hljs.registerLanguage("vim",function(){return function(a){return{name:"Vim Script",keywords:{$pattern:/[!#@\w]+/,keyword:"N|0 P|0 X|0 a|0 ab abc abo al am an|0 ar arga argd arge argdo argg argl argu as au aug aun b|0 bN ba bad bd be bel bf bl bm bn bo bp br brea breaka breakd breakl bro bufdo buffers bun bw c|0 cN cNf ca cabc caddb cad caddf cal cat cb cc ccl cd ce cex cf cfir cgetb cgete cg changes chd che checkt cl cla clo cm cmapc cme cn cnew cnf cno cnorea cnoreme co col colo com comc comp con conf cope cp cpf cq cr cs cst cu cuna cunme cw delm deb debugg delc delf dif diffg diffo diffp diffpu diffs diffthis dig di dl dell dj dli do doautoa dp dr ds dsp e|0 ea ec echoe echoh echom echon el elsei em en endfo endf endt endw ene ex exe exi exu f|0 files filet fin fina fini fir fix fo foldc foldd folddoc foldo for fu go gr grepa gu gv ha helpf helpg helpt hi hid his ia iabc if ij il im imapc ime ino inorea inoreme int is isp iu iuna iunme j|0 ju k|0 keepa kee keepj lN lNf l|0 lad laddb laddf la lan lat lb lc lch lcl lcs le lefta let lex lf lfir lgetb lgete lg lgr lgrepa lh ll lla lli lmak lm lmapc lne lnew lnf ln loadk lo loc lockv lol lope lp lpf lr ls lt lu lua luad luaf lv lvimgrepa lw m|0 ma mak map mapc marks mat me menut mes mk mks mksp mkv mkvie mod mz mzf nbc nb nbs new nm nmapc nme nn nnoreme noa no noh norea noreme norm nu nun nunme ol o|0 om omapc ome on ono onoreme opt ou ounme ow p|0 profd prof pro promptr pc ped pe perld po popu pp pre prev ps pt ptN ptf ptj ptl ptn ptp ptr pts pu pw py3 python3 py3d py3f py pyd pyf quita qa rec red redi redr redraws reg res ret retu rew ri rightb rub rubyd rubyf rund ru rv sN san sa sal sav sb sbN sba sbf sbl sbm sbn sbp sbr scrip scripte scs se setf setg setl sf sfir sh sim sig sil sl sla sm smap smapc sme sn sni sno snor snoreme sor so spelld spe spelli spellr spellu spellw sp spr sre st sta startg startr star stopi stj sts sun sunm sunme sus sv sw sy synti sync tN tabN tabc tabdo tabe tabf tabfir tabl tabm tabnew tabn tabo tabp tabr tabs tab ta tags tc tcld tclf te tf th tj tl tm tn to tp tr try ts tu u|0 undoj undol una unh unl unlo unm unme uns up ve verb vert vim vimgrepa vi viu vie vm vmapc vme vne vn vnoreme vs vu vunme windo w|0 wN wa wh wi winc winp wn wp wq wqa ws wu wv x|0 xa xmapc xm xme xn xnoreme xu xunme y|0 z|0 ~ Next Print append abbreviate abclear aboveleft all amenu anoremenu args argadd argdelete argedit argglobal arglocal argument ascii autocmd augroup aunmenu buffer bNext ball badd bdelete behave belowright bfirst blast bmodified bnext botright bprevious brewind break breakadd breakdel breaklist browse bunload bwipeout change cNext cNfile cabbrev cabclear caddbuffer caddexpr caddfile call catch cbuffer cclose center cexpr cfile cfirst cgetbuffer cgetexpr cgetfile chdir checkpath checktime clist clast close cmap cmapclear cmenu cnext cnewer cnfile cnoremap cnoreabbrev cnoremenu copy colder colorscheme command comclear compiler continue confirm copen cprevious cpfile cquit crewind cscope cstag cunmap cunabbrev cunmenu cwindow delete delmarks debug debuggreedy delcommand delfunction diffupdate diffget diffoff diffpatch diffput diffsplit digraphs display deletel djump dlist doautocmd doautoall deletep drop dsearch dsplit edit earlier echo echoerr echohl echomsg else elseif emenu endif endfor endfunction endtry endwhile enew execute exit exusage file filetype find finally finish first fixdel fold foldclose folddoopen folddoclosed foldopen function global goto grep grepadd gui gvim hardcopy help helpfind helpgrep helptags highlight hide history insert iabbrev iabclear ijump ilist imap imapclear imenu inoremap inoreabbrev inoremenu intro isearch isplit iunmap iunabbrev iunmenu join jumps keepalt keepmarks keepjumps lNext lNfile list laddexpr laddbuffer laddfile last language later lbuffer lcd lchdir lclose lcscope left leftabove lexpr lfile lfirst lgetbuffer lgetexpr lgetfile lgrep lgrepadd lhelpgrep llast llist lmake lmap lmapclear lnext lnewer lnfile lnoremap loadkeymap loadview lockmarks lockvar lolder lopen lprevious lpfile lrewind ltag lunmap luado luafile lvimgrep lvimgrepadd lwindow move mark make mapclear match menu menutranslate messages mkexrc mksession mkspell mkvimrc mkview mode mzscheme mzfile nbclose nbkey nbsart next nmap nmapclear nmenu nnoremap nnoremenu noautocmd noremap nohlsearch noreabbrev noremenu normal number nunmap nunmenu oldfiles open omap omapclear omenu only onoremap onoremenu options ounmap ounmenu ownsyntax print profdel profile promptfind promptrepl pclose pedit perl perldo pop popup ppop preserve previous psearch ptag ptNext ptfirst ptjump ptlast ptnext ptprevious ptrewind ptselect put pwd py3do py3file python pydo pyfile quit quitall qall read recover redo redir redraw redrawstatus registers resize retab return rewind right rightbelow ruby rubydo rubyfile rundo runtime rviminfo substitute sNext sandbox sargument sall saveas sbuffer sbNext sball sbfirst sblast sbmodified sbnext sbprevious sbrewind scriptnames scriptencoding scscope set setfiletype setglobal setlocal sfind sfirst shell simalt sign silent sleep slast smagic smapclear smenu snext sniff snomagic snoremap snoremenu sort source spelldump spellgood spellinfo spellrepall spellundo spellwrong split sprevious srewind stop stag startgreplace startreplace startinsert stopinsert stjump stselect sunhide sunmap sunmenu suspend sview swapname syntax syntime syncbind tNext tabNext tabclose tabedit tabfind tabfirst tablast tabmove tabnext tabonly tabprevious tabrewind tag tcl tcldo tclfile tearoff tfirst throw tjump tlast tmenu tnext topleft tprevious trewind tselect tunmenu undo undojoin undolist unabbreviate unhide unlet unlockvar unmap unmenu unsilent update vglobal version verbose vertical vimgrep vimgrepadd visual viusage view vmap vmapclear vmenu vnew vnoremap vnoremenu vsplit vunmap vunmenu write wNext wall while winsize wincmd winpos wnext wprevious wqall wsverb wundo wviminfo xit xall xmapclear xmap xmenu xnoremap xnoremenu xunmap xunmenu yank",
-built_in:"synIDtrans atan2 range matcharg did_filetype asin feedkeys xor argv complete_check add getwinposx getqflist getwinposy screencol clearmatches empty extend getcmdpos mzeval garbagecollect setreg ceil sqrt diff_hlID inputsecret get getfperm getpid filewritable shiftwidth max sinh isdirectory synID system inputrestore winline atan visualmode inputlist tabpagewinnr round getregtype mapcheck hasmapto histdel argidx findfile sha256 exists toupper getcmdline taglist string getmatches bufnr strftime winwidth bufexists strtrans tabpagebuflist setcmdpos remote_read printf setloclist getpos getline bufwinnr float2nr len getcmdtype diff_filler luaeval resolve libcallnr foldclosedend reverse filter has_key bufname str2float strlen setline getcharmod setbufvar index searchpos shellescape undofile foldclosed setqflist buflisted strchars str2nr virtcol floor remove undotree remote_expr winheight gettabwinvar reltime cursor tabpagenr finddir localtime acos getloclist search tanh matchend rename gettabvar strdisplaywidth type abs py3eval setwinvar tolower wildmenumode log10 spellsuggest bufloaded synconcealed nextnonblank server2client complete settabwinvar executable input wincol setmatches getftype hlID inputsave searchpair or screenrow line settabvar histadd deepcopy strpart remote_peek and eval getftime submatch screenchar winsaveview matchadd mkdir screenattr getfontname libcall reltimestr getfsize winnr invert pow getbufline byte2line soundfold repeat fnameescape tagfiles sin strwidth spellbadword trunc maparg log lispindent hostname setpos globpath remote_foreground getchar synIDattr fnamemodify cscope_connection stridx winbufnr indent min complete_add nr2char searchpairpos inputdialog values matchlist items hlexists strridx browsedir expand fmod pathshorten line2byte argc count getwinvar glob foldtextresult getreg foreground cosh matchdelete has char2nr simplify histget searchdecl iconv winrestcmd pumvisible writefile foldlevel haslocaldir keys cos matchstr foldtext histnr tan tempname getcwd byteidx getbufvar islocked escape eventhandler remote_send serverlist winrestview synstack pyeval prevnonblank readfile cindent filereadable changenr exp"},
-illegal:/;/,contains:[a.NUMBER_MODE,{className:"string",begin:"'",end:"'",illegal:"\\n"},{className:"string",begin:/"(\\"|\n\\|[^"\n])*"/},a.COMMENT('"',"$"),{className:"variable",begin:/[bwtglsav]:[\w\d_]*/},{className:"function",beginKeywords:"function function!",end:"$",relevance:0,contains:[a.TITLE_MODE,{className:"params",begin:"\\(",end:"\\)"}]},{className:"symbol",begin:/<[\w-]+>/}]}}}());
-hljs.registerLanguage("x86asm",function(){return function(a){return{name:"Intel x86 Assembly",case_insensitive:!0,keywords:{$pattern:"[.%]?"+a.IDENT_RE,keyword:"lock rep repe repz repne repnz xaquire xrelease bnd nobnd aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63",
+built_in:"some all not if then else true fail false try catch catch_any semidet_true semidet_false semidet_fail impure_true impure semipure"
+},contains:[{className:"built_in",variants:[{begin:"<=>"},{begin:"<=",
+relevance:0},{begin:"=>",relevance:0},{begin:"/\\\\"},{begin:"\\\\/"}]},{
+className:"built_in",variants:[{begin:":-\\|--\x3e"},{begin:"=",relevance:0}]
+},i,e.C_BLOCK_COMMENT_MODE,{className:"number",begin:"0'.\\|0[box][0-9a-fA-F]*"
+},e.NUMBER_MODE,n,r,{begin:/:-/},{begin:/\.$/}]}}})());
+hljs.registerLanguage("mipsasm",(()=>{"use strict";return e=>({
+name:"MIPS Assembly",case_insensitive:!0,aliases:["mips"],keywords:{
+$pattern:"\\.?"+e.IDENT_RE,
+meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .ltorg ",
+built_in:"$0 $1 $2 $3 $4 $5 $6 $7 $8 $9 $10 $11 $12 $13 $14 $15 $16 $17 $18 $19 $20 $21 $22 $23 $24 $25 $26 $27 $28 $29 $30 $31 zero at v0 v1 a0 a1 a2 a3 a4 a5 a6 a7 t0 t1 t2 t3 t4 t5 t6 t7 t8 t9 s0 s1 s2 s3 s4 s5 s6 s7 s8 k0 k1 gp sp fp ra $f0 $f1 $f2 $f2 $f4 $f5 $f6 $f7 $f8 $f9 $f10 $f11 $f12 $f13 $f14 $f15 $f16 $f17 $f18 $f19 $f20 $f21 $f22 $f23 $f24 $f25 $f26 $f27 $f28 $f29 $f30 $f31 Context Random EntryLo0 EntryLo1 Context PageMask Wired EntryHi HWREna BadVAddr Count Compare SR IntCtl SRSCtl SRSMap Cause EPC PRId EBase Config Config1 Config2 Config3 LLAddr Debug DEPC DESAVE CacheErr ECC ErrorEPC TagLo DataLo TagHi DataHi WatchLo WatchHi PerfCtl PerfCnt "
+},contains:[{className:"keyword",
+begin:"\\b(addi?u?|andi?|b(al)?|beql?|bgez(al)?l?|bgtzl?|blezl?|bltz(al)?l?|bnel?|cl[oz]|divu?|ext|ins|j(al)?|jalr(\\.hb)?|jr(\\.hb)?|lbu?|lhu?|ll|lui|lw[lr]?|maddu?|mfhi|mflo|movn|movz|move|msubu?|mthi|mtlo|mul|multu?|nop|nor|ori?|rotrv?|sb|sc|se[bh]|sh|sllv?|slti?u?|srav?|srlv?|subu?|sw[lr]?|xori?|wsbh|abs\\.[sd]|add\\.[sd]|alnv.ps|bc1[ft]l?|c\\.(s?f|un|u?eq|[ou]lt|[ou]le|ngle?|seq|l[et]|ng[et])\\.[sd]|(ceil|floor|round|trunc)\\.[lw]\\.[sd]|cfc1|cvt\\.d\\.[lsw]|cvt\\.l\\.[dsw]|cvt\\.ps\\.s|cvt\\.s\\.[dlw]|cvt\\.s\\.p[lu]|cvt\\.w\\.[dls]|div\\.[ds]|ldx?c1|luxc1|lwx?c1|madd\\.[sd]|mfc1|mov[fntz]?\\.[ds]|msub\\.[sd]|mth?c1|mul\\.[ds]|neg\\.[ds]|nmadd\\.[ds]|nmsub\\.[ds]|p[lu][lu]\\.ps|recip\\.fmt|r?sqrt\\.[ds]|sdx?c1|sub\\.[ds]|suxc1|swx?c1|break|cache|d?eret|[de]i|ehb|mfc0|mtc0|pause|prefx?|rdhwr|rdpgpr|sdbbp|ssnop|synci?|syscall|teqi?|tgei?u?|tlb(p|r|w[ir])|tlti?u?|tnei?|wait|wrpgpr)",
+end:"\\s"
+},e.COMMENT("[;#](?!\\s*$)","$"),e.C_BLOCK_COMMENT_MODE,e.QUOTE_STRING_MODE,{
+className:"string",begin:"'",end:"[^\\\\]'",relevance:0},{className:"title",
+begin:"\\|",end:"\\|",illegal:"\\n",relevance:0},{className:"number",variants:[{
+begin:"0x[0-9a-f]+"},{begin:"\\b-?\\d+"}],relevance:0},{className:"symbol",
+variants:[{begin:"^\\s*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{begin:"^\\s*[0-9]+:"},{
+begin:"[0-9]+[bf]"}],relevance:0}],illegal:/\//})})());
+hljs.registerLanguage("mizar",(()=>{"use strict";return e=>({name:"Mizar",
+keywords:"environ vocabularies notations constructors definitions registrations theorems schemes requirements begin end definition registration cluster existence pred func defpred deffunc theorem proof let take assume then thus hence ex for st holds consider reconsider such that and in provided of as from be being by means equals implies iff redefine define now not or attr is mode suppose per cases set thesis contradiction scheme reserve struct correctness compatibility coherence symmetry assymetry reflexivity irreflexivity connectedness uniqueness commutativity idempotence involutiveness projectivity",
+contains:[e.COMMENT("::","$")]})})());
+hljs.registerLanguage("perl",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function n(...n){
+return n.map((n=>e(n))).join("")}function t(...n){
+return"("+n.map((n=>e(n))).join("|")+")"}return e=>{
+const r=/[dualxmsipngr]{0,12}/,s={$pattern:/[\w.]+/,
+keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0"
+},i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:s},a={begin:/->\{/,
+end:/\}/},o={variants:[{begin:/\$\d/},{
+begin:n(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])")
+},{begin:/[$%@][^\s\w{]/,relevance:0}]
+},c=[e.BACKSLASH_ESCAPE,i,o],g=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],l=(e,t,s="\\1")=>{
+const i="\\1"===s?s:n(s,t)
+;return n(n("(?:",e,")"),t,/(?:\\.|[^\\\/])*?/,i,/(?:\\.|[^\\\/])*?/,s,r)
+},d=(e,t,s)=>n(n("(?:",e,")"),t,/(?:\\.|[^\\\/])*?/,s,r),p=[o,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{
+endsWithParent:!0}),a,{className:"string",contains:c,variants:[{
+begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",
+end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{
+begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">",
+relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",
+contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",
+contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{
+begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number",
+begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",
+relevance:0},{
+begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",
+keywords:"split return print reverse grep",relevance:0,
+contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{
+begin:l("s|tr|y",t(...g))},{begin:l("s|tr|y","\\(","\\)")},{
+begin:l("s|tr|y","\\[","\\]")},{begin:l("s|tr|y","\\{","\\}")}],relevance:2},{
+className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{
+begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",t(...g),/\1/)},{
+begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{
+begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub",
+end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{
+begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",
+subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]
+}];return i.contains=p,a.contains=p,{name:"Perl",aliases:["pl","pm"],keywords:s,
+contains:p}}})());
+hljs.registerLanguage("mojolicious",(()=>{"use strict";return e=>({
+name:"Mojolicious",subLanguage:"xml",contains:[{className:"meta",
+begin:"^__(END|DATA)__$"},{begin:"^\\s*%{1,2}={0,2}",end:"$",subLanguage:"perl"
+},{begin:"<%{1,2}={0,2}",end:"={0,1}%>",subLanguage:"perl",excludeBegin:!0,
+excludeEnd:!0}]})})());
+hljs.registerLanguage("monkey",(()=>{"use strict";return e=>{const n={
+className:"number",relevance:0,variants:[{begin:"[$][a-fA-F0-9]+"
+},e.NUMBER_MODE]};return{name:"Monkey",case_insensitive:!0,keywords:{
+keyword:"public private property continue exit extern new try catch eachin not abstract final select case default const local global field end if then else elseif endif while wend repeat until forever for to step next return module inline throw import",
+built_in:"DebugLog DebugStop Error Print ACos ACosr ASin ASinr ATan ATan2 ATan2r ATanr Abs Abs Ceil Clamp Clamp Cos Cosr Exp Floor Log Max Max Min Min Pow Sgn Sgn Sin Sinr Sqrt Tan Tanr Seed PI HALFPI TWOPI",
+literal:"true false null and or shl shr mod"},illegal:/\/\*/,
+contains:[e.COMMENT("#rem","#end"),e.COMMENT("'","$",{relevance:0}),{
+className:"function",beginKeywords:"function method",end:"[(=:]|$",illegal:/\n/,
+contains:[e.UNDERSCORE_TITLE_MODE]},{className:"class",
+beginKeywords:"class interface",end:"$",contains:[{
+beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{
+className:"built_in",begin:"\\b(self|super)\\b"},{className:"meta",
+begin:"\\s*#",end:"$",keywords:{"meta-keyword":"if else elseif endif end then"}
+},{className:"meta",begin:"^\\s*strict\\b"},{beginKeywords:"alias",end:"=",
+contains:[e.UNDERSCORE_TITLE_MODE]},e.QUOTE_STRING_MODE,n]}}})());
+hljs.registerLanguage("moonscript",(()=>{"use strict";return e=>{const n={
+keyword:"if then not for in while do return else elseif break continue switch and or unless when class extends super local import export from using",
+literal:"true false nil",
+built_in:"_G _VERSION assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall coroutine debug io math os package string table"
+},s="[A-Za-z$_][0-9A-Za-z$_]*",a={className:"subst",begin:/#\{/,end:/\}/,
+keywords:n},t=[e.inherit(e.C_NUMBER_MODE,{starts:{end:"(\\s*/)?",relevance:0}
+}),{className:"string",variants:[{begin:/'/,end:/'/,
+contains:[e.BACKSLASH_ESCAPE]},{begin:/"/,end:/"/,
+contains:[e.BACKSLASH_ESCAPE,a]}]},{className:"built_in",begin:"@__"+e.IDENT_RE
+},{begin:"@"+e.IDENT_RE},{begin:e.IDENT_RE+"\\\\"+e.IDENT_RE}];a.contains=t
+;const i=e.inherit(e.TITLE_MODE,{begin:s}),r="(\\(.*\\)\\s*)?\\B[-=]>",l={
+className:"params",begin:"\\([^\\(]",returnBegin:!0,contains:[{begin:/\(/,
+end:/\)/,keywords:n,contains:["self"].concat(t)}]};return{name:"MoonScript",
+aliases:["moon"],keywords:n,illegal:/\/\*/,
+contains:t.concat([e.COMMENT("--","$"),{className:"function",
+begin:"^\\s*"+s+"\\s*=\\s*"+r,end:"[-=]>",returnBegin:!0,contains:[i,l]},{
+begin:/[\(,:=]\s*/,relevance:0,contains:[{className:"function",begin:r,
+end:"[-=]>",returnBegin:!0,contains:[l]}]},{className:"class",
+beginKeywords:"class",end:"$",illegal:/[:="\[\]]/,contains:[{
+beginKeywords:"extends",endsWithParent:!0,illegal:/[:="\[\]]/,contains:[i]},i]
+},{className:"name",begin:s+":",end:":",returnBegin:!0,returnEnd:!0,relevance:0
+}])}}})());
+hljs.registerLanguage("n1ql",(()=>{"use strict";return e=>({name:"N1QL",
+case_insensitive:!0,contains:[{
+beginKeywords:"build create index delete drop explain infer|10 insert merge prepare select update upsert|10",
+end:/;/,endsWithParent:!0,keywords:{
+keyword:"all alter analyze and any array as asc begin between binary boolean break bucket build by call case cast cluster collate collection commit connect continue correlate cover create database dataset datastore declare decrement delete derived desc describe distinct do drop each element else end every except exclude execute exists explain fetch first flatten for force from function grant group gsi having if ignore ilike in include increment index infer inline inner insert intersect into is join key keys keyspace known last left let letting like limit lsm map mapping matched materialized merge minus namespace nest not number object offset on option or order outer over parse partition password path pool prepare primary private privilege procedure public raw realm reduce rename return returning revoke right role rollback satisfies schema select self semi set show some start statistics string system then to transaction trigger truncate under union unique unknown unnest unset update upsert use user using validate value valued values via view when where while with within work xor",
+literal:"true false null missing|5",
+built_in:"array_agg array_append array_concat array_contains array_count array_distinct array_ifnull array_length array_max array_min array_position array_prepend array_put array_range array_remove array_repeat array_replace array_reverse array_sort array_sum avg count max min sum greatest least ifmissing ifmissingornull ifnull missingif nullif ifinf ifnan ifnanorinf naninf neginfif posinfif clock_millis clock_str date_add_millis date_add_str date_diff_millis date_diff_str date_part_millis date_part_str date_trunc_millis date_trunc_str duration_to_str millis str_to_millis millis_to_str millis_to_utc millis_to_zone_name now_millis now_str str_to_duration str_to_utc str_to_zone_name decode_json encode_json encoded_size poly_length base64 base64_encode base64_decode meta uuid abs acos asin atan atan2 ceil cos degrees e exp ln log floor pi power radians random round sign sin sqrt tan trunc object_length object_names object_pairs object_inner_pairs object_values object_inner_values object_add object_put object_remove object_unwrap regexp_contains regexp_like regexp_position regexp_replace contains initcap length lower ltrim position repeat replace rtrim split substr title trim upper isarray isatom isboolean isnumber isobject isstring type toarray toatom toboolean tonumber toobject tostring"
+},contains:[{className:"string",begin:"'",end:"'",contains:[e.BACKSLASH_ESCAPE]
+},{className:"string",begin:'"',end:'"',contains:[e.BACKSLASH_ESCAPE]},{
+className:"symbol",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE],relevance:2
+},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE]},e.C_BLOCK_COMMENT_MODE]})})());
+hljs.registerLanguage("nginx",(()=>{"use strict";return e=>{const n={
+className:"variable",variants:[{begin:/\$\d+/},{begin:/\$\{/,end:/\}/},{
+begin:/[$@]/+e.UNDERSCORE_IDENT_RE}]},a={endsWithParent:!0,keywords:{
+$pattern:"[a-z/_]+",
+literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"
+},relevance:0,illegal:"=>",contains:[e.HASH_COMMENT_MODE,{className:"string",
+contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/
+}]},{begin:"([a-z]+):/",end:"\\s",endsWithParent:!0,excludeEnd:!0,contains:[n]
+},{className:"regexp",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:"\\s\\^",
+end:"\\s|\\{|;",returnEnd:!0},{begin:"~\\*?\\s+",end:"\\s|\\{|;",returnEnd:!0},{
+begin:"\\*(\\.[a-z\\-]+)+"},{begin:"([a-z\\-]+\\.)+\\*"}]},{className:"number",
+begin:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{
+className:"number",begin:"\\b\\d+[kKmMgGdshdwy]*\\b",relevance:0},n]};return{
+name:"Nginx config",aliases:["nginxconf"],contains:[e.HASH_COMMENT_MODE,{
+begin:e.UNDERSCORE_IDENT_RE+"\\s+\\{",returnBegin:!0,end:/\{/,contains:[{
+className:"section",begin:e.UNDERSCORE_IDENT_RE}],relevance:0},{
+begin:e.UNDERSCORE_IDENT_RE+"\\s",end:";|\\{",returnBegin:!0,contains:[{
+className:"attribute",begin:e.UNDERSCORE_IDENT_RE,starts:a}],relevance:0}],
+illegal:"[^\\s\\}]"}}})());
+hljs.registerLanguage("nim",(()=>{"use strict";return e=>({name:"Nim",keywords:{
+keyword:"addr and as asm bind block break case cast const continue converter discard distinct div do elif else end enum except export finally for from func generic if import in include interface is isnot iterator let macro method mixin mod nil not notin object of or out proc ptr raise ref return shl shr static template try tuple type using var when while with without xor yield",
+literal:"shared guarded stdin stdout stderr result true false",
+built_in:"int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 float float32 float64 bool char string cstring pointer expr stmt void auto any range array openarray varargs seq set clong culong cchar cschar cshort cint csize clonglong cfloat cdouble clongdouble cuchar cushort cuint culonglong cstringarray semistatic"
+},contains:[{className:"meta",begin:/\{\./,end:/\.\}/,relevance:10},{
+className:"string",begin:/[a-zA-Z]\w*"/,end:/"/,contains:[{begin:/""/}]},{
+className:"string",begin:/([a-zA-Z]\w*)?"""/,end:/"""/},e.QUOTE_STRING_MODE,{
+className:"type",begin:/\b[A-Z]\w+\b/,relevance:0},{className:"number",
+relevance:0,variants:[{
+begin:/\b(0[xX][0-9a-fA-F][_0-9a-fA-F]*)('?[iIuU](8|16|32|64))?/},{
+begin:/\b(0o[0-7][_0-7]*)('?[iIuUfF](8|16|32|64))?/},{
+begin:/\b(0(b|B)[01][_01]*)('?[iIuUfF](8|16|32|64))?/},{
+begin:/\b(\d[_\d]*)('?[iIuUfF](8|16|32|64))?/}]},e.HASH_COMMENT_MODE]})})());
+hljs.registerLanguage("nix",(()=>{"use strict";return e=>{const n={
+keyword:"rec with let in inherit assert if else then",
+literal:"true false or and null",
+built_in:"import abort baseNameOf dirOf isNull builtins map removeAttrs throw toString derivation"
+},i={className:"subst",begin:/\$\{/,end:/\}/,keywords:n},s={className:"string",
+contains:[i],variants:[{begin:"''",end:"''"},{begin:'"',end:'"'}]
+},t=[e.NUMBER_MODE,e.HASH_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,{
+begin:/[a-zA-Z0-9-_]+(\s*=)/,returnBegin:!0,relevance:0,contains:[{
+className:"attr",begin:/\S+/}]}];return i.contains=t,{name:"Nix",
+aliases:["nixos"],keywords:n,contains:t}}})());
+hljs.registerLanguage("node-repl",(()=>{"use strict";return e=>({
+name:"Node REPL",contains:[{className:"meta",starts:{end:/ |$/,starts:{end:"$",
+subLanguage:"javascript"}},variants:[{begin:/^>(?=[ ]|$)/},{
+begin:/^\.\.\.(?=[ ]|$)/}]}]})})());
+hljs.registerLanguage("nsis",(()=>{"use strict";return e=>{const t={
+className:"variable",begin:/\$+\{[\w.:-]+\}/},n={className:"variable",
+begin:/\$+\w+/,illegal:/\(\)\{\}/},i={className:"variable",
+begin:/\$+\([\w^.:-]+\)/},r={className:"string",variants:[{begin:'"',end:'"'},{
+begin:"'",end:"'"},{begin:"`",end:"`"}],illegal:/\n/,contains:[{
+className:"meta",begin:/\$(\\[nrt]|\$)/},{className:"variable",
+begin:/\$(ADMINTOOLS|APPDATA|CDBURN_AREA|CMDLINE|COMMONFILES32|COMMONFILES64|COMMONFILES|COOKIES|DESKTOP|DOCUMENTS|EXEDIR|EXEFILE|EXEPATH|FAVORITES|FONTS|HISTORY|HWNDPARENT|INSTDIR|INTERNET_CACHE|LANGUAGE|LOCALAPPDATA|MUSIC|NETHOOD|OUTDIR|PICTURES|PLUGINSDIR|PRINTHOOD|PROFILE|PROGRAMFILES32|PROGRAMFILES64|PROGRAMFILES|QUICKLAUNCH|RECENT|RESOURCES_LOCALIZED|RESOURCES|SENDTO|SMPROGRAMS|SMSTARTUP|STARTMENU|SYSDIR|TEMP|TEMPLATES|VIDEOS|WINDIR)/
+},t,n,i]};return{name:"NSIS",case_insensitive:!1,keywords:{
+keyword:"Abort AddBrandingImage AddSize AllowRootDirInstall AllowSkipFiles AutoCloseWindow BGFont BGGradient BrandingText BringToFront Call CallInstDLL Caption ChangeUI CheckBitmap ClearErrors CompletedText ComponentText CopyFiles CRCCheck CreateDirectory CreateFont CreateShortCut Delete DeleteINISec DeleteINIStr DeleteRegKey DeleteRegValue DetailPrint DetailsButtonText DirText DirVar DirVerify EnableWindow EnumRegKey EnumRegValue Exch Exec ExecShell ExecShellWait ExecWait ExpandEnvStrings File FileBufSize FileClose FileErrorText FileOpen FileRead FileReadByte FileReadUTF16LE FileReadWord FileWriteUTF16LE FileSeek FileWrite FileWriteByte FileWriteWord FindClose FindFirst FindNext FindWindow FlushINI GetCurInstType GetCurrentAddress GetDlgItem GetDLLVersion GetDLLVersionLocal GetErrorLevel GetFileTime GetFileTimeLocal GetFullPathName GetFunctionAddress GetInstDirError GetKnownFolderPath GetLabelAddress GetTempFileName Goto HideWindow Icon IfAbort IfErrors IfFileExists IfRebootFlag IfRtlLanguage IfShellVarContextAll IfSilent InitPluginsDir InstallButtonText InstallColors InstallDir InstallDirRegKey InstProgressFlags InstType InstTypeGetText InstTypeSetText Int64Cmp Int64CmpU Int64Fmt IntCmp IntCmpU IntFmt IntOp IntPtrCmp IntPtrCmpU IntPtrOp IsWindow LangString LicenseBkColor LicenseData LicenseForceSelection LicenseLangString LicenseText LoadAndSetImage LoadLanguageFile LockWindow LogSet LogText ManifestDPIAware ManifestLongPathAware ManifestMaxVersionTested ManifestSupportedOS MessageBox MiscButtonText Name Nop OutFile Page PageCallbacks PEAddResource PEDllCharacteristics PERemoveResource PESubsysVer Pop Push Quit ReadEnvStr ReadINIStr ReadRegDWORD ReadRegStr Reboot RegDLL Rename RequestExecutionLevel ReserveFile Return RMDir SearchPath SectionGetFlags SectionGetInstTypes SectionGetSize SectionGetText SectionIn SectionSetFlags SectionSetInstTypes SectionSetSize SectionSetText SendMessage SetAutoClose SetBrandingImage SetCompress SetCompressor SetCompressorDictSize SetCtlColors SetCurInstType SetDatablockOptimize SetDateSave SetDetailsPrint SetDetailsView SetErrorLevel SetErrors SetFileAttributes SetFont SetOutPath SetOverwrite SetRebootFlag SetRegView SetShellVarContext SetSilent ShowInstDetails ShowUninstDetails ShowWindow SilentInstall SilentUnInstall Sleep SpaceTexts StrCmp StrCmpS StrCpy StrLen SubCaption Unicode UninstallButtonText UninstallCaption UninstallIcon UninstallSubCaption UninstallText UninstPage UnRegDLL Var VIAddVersionKey VIFileVersion VIProductVersion WindowIcon WriteINIStr WriteRegBin WriteRegDWORD WriteRegExpandStr WriteRegMultiStr WriteRegNone WriteRegStr WriteUninstaller XPStyle",
+literal:"admin all auto both bottom bzip2 colored components current custom directory false force hide highest ifdiff ifnewer instfiles lastused leave left license listonly lzma nevershow none normal notset off on open print right show silent silentlog smooth textonly top true try un.components un.custom un.directory un.instfiles un.license uninstConfirm user Win10 Win7 Win8 WinVista zlib"
+},contains:[e.HASH_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.COMMENT(";","$",{
+relevance:0}),{className:"function",
+beginKeywords:"Function PageEx Section SectionGroup",end:"$"},r,{
+className:"keyword",
+begin:/!(addincludedir|addplugindir|appendfile|cd|define|delfile|echo|else|endif|error|execute|finalize|getdllversion|gettlbversion|if|ifdef|ifmacrodef|ifmacrondef|ifndef|include|insertmacro|macro|macroend|makensis|packhdr|searchparse|searchreplace|system|tempfile|undef|verbose|warning)/
+},t,n,i,{className:"params",
+begin:"(ARCHIVE|FILE_ATTRIBUTE_ARCHIVE|FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_OFFLINE|FILE_ATTRIBUTE_READONLY|FILE_ATTRIBUTE_SYSTEM|FILE_ATTRIBUTE_TEMPORARY|HKCR|HKCU|HKDD|HKEY_CLASSES_ROOT|HKEY_CURRENT_CONFIG|HKEY_CURRENT_USER|HKEY_DYN_DATA|HKEY_LOCAL_MACHINE|HKEY_PERFORMANCE_DATA|HKEY_USERS|HKLM|HKPD|HKU|IDABORT|IDCANCEL|IDIGNORE|IDNO|IDOK|IDRETRY|IDYES|MB_ABORTRETRYIGNORE|MB_DEFBUTTON1|MB_DEFBUTTON2|MB_DEFBUTTON3|MB_DEFBUTTON4|MB_ICONEXCLAMATION|MB_ICONINFORMATION|MB_ICONQUESTION|MB_ICONSTOP|MB_OK|MB_OKCANCEL|MB_RETRYCANCEL|MB_RIGHT|MB_RTLREADING|MB_SETFOREGROUND|MB_TOPMOST|MB_USERICON|MB_YESNO|NORMAL|OFFLINE|READONLY|SHCTX|SHELL_CONTEXT|SYSTEM|TEMPORARY)"
+},{className:"class",begin:/\w+::\w+/},e.NUMBER_MODE]}}})());
+hljs.registerLanguage("objectivec",(()=>{"use strict";return e=>{
+const n=/[a-zA-Z@][a-zA-Z0-9_]*/,_={$pattern:n,
+keyword:"@interface @class @protocol @implementation"};return{
+name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"],
+keywords:{$pattern:n,
+keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",
+literal:"false true FALSE TRUE nil YES NO NULL",
+built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"
+},illegal:"</",contains:[{className:"built_in",
+begin:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.C_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{
+className:"string",variants:[{begin:'@"',end:'"',illegal:"\\n",
+contains:[e.BACKSLASH_ESCAPE]}]},{className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,
+keywords:{
+"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"
+},contains:[{begin:/\\\n/,relevance:0},e.inherit(e.QUOTE_STRING_MODE,{
+className:"meta-string"}),{className:"meta-string",begin:/<.*?>/,end:/$/,
+illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{
+className:"class",begin:"("+_.keyword.split(" ").join("|")+")\\b",end:/(\{|$)/,
+excludeEnd:!0,keywords:_,contains:[e.UNDERSCORE_TITLE_MODE]},{
+begin:"\\."+e.UNDERSCORE_IDENT_RE,relevance:0}]}}})());
+hljs.registerLanguage("ocaml",(()=>{"use strict";return e=>({name:"OCaml",
+aliases:["ml"],keywords:{$pattern:"[a-z_]\\w*!?",
+keyword:"and as assert asr begin class constraint do done downto else end exception external for fun function functor if in include inherit! inherit initializer land lazy let lor lsl lsr lxor match method!|10 method mod module mutable new object of open! open or private rec sig struct then to try type val! val virtual when while with parser value",
+built_in:"array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 string unit in_channel out_channel ref",
+literal:"true false"},illegal:/\/\/|>>/,contains:[{className:"literal",
+begin:"\\[(\\|\\|)?\\]|\\(\\)",relevance:0},e.COMMENT("\\(\\*","\\*\\)",{
+contains:["self"]}),{className:"symbol",begin:"'[A-Za-z_](?!')[\\w']*"},{
+className:"type",begin:"`[A-Z][\\w']*"},{className:"type",
+begin:"\\b[A-Z][\\w']*",relevance:0},{begin:"[a-z_]\\w*'[\\w']*",relevance:0
+},e.inherit(e.APOS_STRING_MODE,{className:"string",relevance:0
+}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null}),{className:"number",
+begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",
+relevance:0},{begin:/->/}]})})());
+hljs.registerLanguage("openscad",(()=>{"use strict";return e=>{const n={
+className:"keyword",begin:"\\$(f[asn]|t|vp[rtd]|children)"},r={
+className:"number",begin:"\\b\\d+(\\.\\d+)?(e-?\\d+)?",relevance:0
+},s=e.inherit(e.QUOTE_STRING_MODE,{illegal:null}),a={className:"function",
+beginKeywords:"module function",end:/=|\{/,contains:[{className:"params",
+begin:"\\(",end:"\\)",contains:["self",r,s,n,{className:"literal",
+begin:"false|true|PI|undef"}]},e.UNDERSCORE_TITLE_MODE]};return{name:"OpenSCAD",
+aliases:["scad"],keywords:{
+keyword:"function module include use for intersection_for if else \\%",
+literal:"false true PI undef",
+built_in:"circle square polygon text sphere cube cylinder polyhedron translate rotate scale resize mirror multmatrix color offset hull minkowski union difference intersection abs sign sin cos tan acos asin atan atan2 floor round ceil ln log pow sqrt exp rands min max concat lookup str chr search version version_num norm cross parent_module echo import import_dxf dxf_linear_extrude linear_extrude rotate_extrude surface projection render children dxf_cross dxf_dim let assign"
+},contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,r,{className:"meta",
+keywords:{"meta-keyword":"include use"},begin:"include|use <",end:">"},s,n,{
+begin:"[*!#%]",relevance:0},a]}}})());
+hljs.registerLanguage("oxygene",(()=>{"use strict";return e=>{const n={
+$pattern:/\.?\w+/,
+keyword:"abstract add and array as asc aspect assembly async begin break block by case class concat const copy constructor continue create default delegate desc distinct div do downto dynamic each else empty end ensure enum equals event except exit extension external false final finalize finalizer finally flags for forward from function future global group has if implementation implements implies in index inherited inline interface into invariants is iterator join locked locking loop matching method mod module namespace nested new nil not notify nullable of old on operator or order out override parallel params partial pinned private procedure property protected public queryable raise read readonly record reintroduce remove repeat require result reverse sealed select self sequence set shl shr skip static step soft take then to true try tuple type union unit unsafe until uses using var virtual raises volatile where while with write xor yield await mapped deprecated stdcall cdecl pascal register safecall overload library platform reference packed strict published autoreleasepool selector strong weak unretained"
+},r=e.COMMENT(/\{/,/\}/,{relevance:0}),a=e.COMMENT("\\(\\*","\\*\\)",{
+relevance:10}),t={className:"string",begin:"'",end:"'",contains:[{begin:"''"}]
+},s={className:"string",begin:"(#\\d+)+"},i={className:"function",
+beginKeywords:"function constructor destructor procedure method",end:"[:;]",
+keywords:"function constructor|10 destructor|10 procedure|10 method|10",
+contains:[e.TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",keywords:n,
+contains:[t,s]},r,a]};return{name:"Oxygene",case_insensitive:!0,keywords:n,
+illegal:'("|\\$[G-Zg-z]|\\/\\*|</|=>|->)',
+contains:[r,a,e.C_LINE_COMMENT_MODE,t,s,e.NUMBER_MODE,i,{className:"class",
+begin:"=\\bclass\\b",end:"end;",keywords:n,
+contains:[t,s,r,a,e.C_LINE_COMMENT_MODE,i]}]}}})());
+hljs.registerLanguage("parser3",(()=>{"use strict";return e=>{
+const a=e.COMMENT(/\{/,/\}/,{contains:["self"]});return{name:"Parser3",
+subLanguage:"xml",relevance:0,
+contains:[e.COMMENT("^#","$"),e.COMMENT(/\^rem\{/,/\}/,{relevance:10,
+contains:[a]}),{className:"meta",begin:"^@(?:BASE|USE|CLASS|OPTIONS)$",
+relevance:10},{className:"title",
+begin:"@[\\w\\-]+\\[[\\w^;\\-]*\\](?:\\[[\\w^;\\-]*\\])?(?:.*)$"},{
+className:"variable",begin:/\$\{?[\w\-.:]+\}?/},{className:"keyword",
+begin:/\^[\w\-.:]+/},{className:"number",begin:"\\^#[0-9a-fA-F]+"
+},e.C_NUMBER_MODE]}}})());
+hljs.registerLanguage("pf",(()=>{"use strict";return t=>({
+name:"Packet Filter config",aliases:["pf.conf"],keywords:{
+$pattern:/[a-z0-9_<>-]+/,
+built_in:"block match pass load anchor|5 antispoof|10 set table",
+keyword:"in out log quick on rdomain inet inet6 proto from port os to route allow-opts divert-packet divert-reply divert-to flags group icmp-type icmp6-type label once probability recieved-on rtable prio queue tos tag tagged user keep fragment for os drop af-to|10 binat-to|10 nat-to|10 rdr-to|10 bitmask least-stats random round-robin source-hash static-port dup-to reply-to route-to parent bandwidth default min max qlimit block-policy debug fingerprints hostid limit loginterface optimization reassemble ruleset-optimization basic none profile skip state-defaults state-policy timeout const counters persist no modulate synproxy state|5 floating if-bound no-sync pflow|10 sloppy source-track global rule max-src-nodes max-src-states max-src-conn max-src-conn-rate overload flush scrub|5 max-mss min-ttl no-df|10 random-id",
+literal:"all any no-route self urpf-failed egress|5 unknown"},
+contains:[t.HASH_COMMENT_MODE,t.NUMBER_MODE,t.QUOTE_STRING_MODE,{
+className:"variable",begin:/\$[\w\d#@][\w\d_]*/},{className:"variable",
+begin:/<(?!\/)/,end:/>/}]})})());
+hljs.registerLanguage("pgsql",(()=>{"use strict";return E=>{
+const T=E.COMMENT("--","$"),N="\\$([a-zA-Z_]?|[a-zA-Z_][a-zA-Z_0-9]*)\\$",A="BIGINT INT8 BIGSERIAL SERIAL8 BIT VARYING VARBIT BOOLEAN BOOL BOX BYTEA CHARACTER CHAR VARCHAR CIDR CIRCLE DATE DOUBLE PRECISION FLOAT8 FLOAT INET INTEGER INT INT4 INTERVAL JSON JSONB LINE LSEG|10 MACADDR MACADDR8 MONEY NUMERIC DEC DECIMAL PATH POINT POLYGON REAL FLOAT4 SMALLINT INT2 SMALLSERIAL|10 SERIAL2|10 SERIAL|10 SERIAL4|10 TEXT TIME ZONE TIMETZ|10 TIMESTAMP TIMESTAMPTZ|10 TSQUERY|10 TSVECTOR|10 TXID_SNAPSHOT|10 UUID XML NATIONAL NCHAR INT4RANGE|10 INT8RANGE|10 NUMRANGE|10 TSRANGE|10 TSTZRANGE|10 DATERANGE|10 ANYELEMENT ANYARRAY ANYNONARRAY ANYENUM ANYRANGE CSTRING INTERNAL RECORD PG_DDL_COMMAND VOID UNKNOWN OPAQUE REFCURSOR NAME OID REGPROC|10 REGPROCEDURE|10 REGOPER|10 REGOPERATOR|10 REGCLASS|10 REGTYPE|10 REGROLE|10 REGNAMESPACE|10 REGCONFIG|10 REGDICTIONARY|10 ",R=A.trim().split(" ").map((E=>E.split("|")[0])).join("|"),I="ARRAY_AGG AVG BIT_AND BIT_OR BOOL_AND BOOL_OR COUNT EVERY JSON_AGG JSONB_AGG JSON_OBJECT_AGG JSONB_OBJECT_AGG MAX MIN MODE STRING_AGG SUM XMLAGG CORR COVAR_POP COVAR_SAMP REGR_AVGX REGR_AVGY REGR_COUNT REGR_INTERCEPT REGR_R2 REGR_SLOPE REGR_SXX REGR_SXY REGR_SYY STDDEV STDDEV_POP STDDEV_SAMP VARIANCE VAR_POP VAR_SAMP PERCENTILE_CONT PERCENTILE_DISC ROW_NUMBER RANK DENSE_RANK PERCENT_RANK CUME_DIST NTILE LAG LEAD FIRST_VALUE LAST_VALUE NTH_VALUE NUM_NONNULLS NUM_NULLS ABS CBRT CEIL CEILING DEGREES DIV EXP FLOOR LN LOG MOD PI POWER RADIANS ROUND SCALE SIGN SQRT TRUNC WIDTH_BUCKET RANDOM SETSEED ACOS ACOSD ASIN ASIND ATAN ATAND ATAN2 ATAN2D COS COSD COT COTD SIN SIND TAN TAND BIT_LENGTH CHAR_LENGTH CHARACTER_LENGTH LOWER OCTET_LENGTH OVERLAY POSITION SUBSTRING TREAT TRIM UPPER ASCII BTRIM CHR CONCAT CONCAT_WS CONVERT CONVERT_FROM CONVERT_TO DECODE ENCODE INITCAP LEFT LENGTH LPAD LTRIM MD5 PARSE_IDENT PG_CLIENT_ENCODING QUOTE_IDENT|10 QUOTE_LITERAL|10 QUOTE_NULLABLE|10 REGEXP_MATCH REGEXP_MATCHES REGEXP_REPLACE REGEXP_SPLIT_TO_ARRAY REGEXP_SPLIT_TO_TABLE REPEAT REPLACE REVERSE RIGHT RPAD RTRIM SPLIT_PART STRPOS SUBSTR TO_ASCII TO_HEX TRANSLATE OCTET_LENGTH GET_BIT GET_BYTE SET_BIT SET_BYTE TO_CHAR TO_DATE TO_NUMBER TO_TIMESTAMP AGE CLOCK_TIMESTAMP|10 DATE_PART DATE_TRUNC ISFINITE JUSTIFY_DAYS JUSTIFY_HOURS JUSTIFY_INTERVAL MAKE_DATE MAKE_INTERVAL|10 MAKE_TIME MAKE_TIMESTAMP|10 MAKE_TIMESTAMPTZ|10 NOW STATEMENT_TIMESTAMP|10 TIMEOFDAY TRANSACTION_TIMESTAMP|10 ENUM_FIRST ENUM_LAST ENUM_RANGE AREA CENTER DIAMETER HEIGHT ISCLOSED ISOPEN NPOINTS PCLOSE POPEN RADIUS WIDTH BOX BOUND_BOX CIRCLE LINE LSEG PATH POLYGON ABBREV BROADCAST HOST HOSTMASK MASKLEN NETMASK NETWORK SET_MASKLEN TEXT INET_SAME_FAMILY INET_MERGE MACADDR8_SET7BIT ARRAY_TO_TSVECTOR GET_CURRENT_TS_CONFIG NUMNODE PLAINTO_TSQUERY PHRASETO_TSQUERY WEBSEARCH_TO_TSQUERY QUERYTREE SETWEIGHT STRIP TO_TSQUERY TO_TSVECTOR JSON_TO_TSVECTOR JSONB_TO_TSVECTOR TS_DELETE TS_FILTER TS_HEADLINE TS_RANK TS_RANK_CD TS_REWRITE TSQUERY_PHRASE TSVECTOR_TO_ARRAY TSVECTOR_UPDATE_TRIGGER TSVECTOR_UPDATE_TRIGGER_COLUMN XMLCOMMENT XMLCONCAT XMLELEMENT XMLFOREST XMLPI XMLROOT XMLEXISTS XML_IS_WELL_FORMED XML_IS_WELL_FORMED_DOCUMENT XML_IS_WELL_FORMED_CONTENT XPATH XPATH_EXISTS XMLTABLE XMLNAMESPACES TABLE_TO_XML TABLE_TO_XMLSCHEMA TABLE_TO_XML_AND_XMLSCHEMA QUERY_TO_XML QUERY_TO_XMLSCHEMA QUERY_TO_XML_AND_XMLSCHEMA CURSOR_TO_XML CURSOR_TO_XMLSCHEMA SCHEMA_TO_XML SCHEMA_TO_XMLSCHEMA SCHEMA_TO_XML_AND_XMLSCHEMA DATABASE_TO_XML DATABASE_TO_XMLSCHEMA DATABASE_TO_XML_AND_XMLSCHEMA XMLATTRIBUTES TO_JSON TO_JSONB ARRAY_TO_JSON ROW_TO_JSON JSON_BUILD_ARRAY JSONB_BUILD_ARRAY JSON_BUILD_OBJECT JSONB_BUILD_OBJECT JSON_OBJECT JSONB_OBJECT JSON_ARRAY_LENGTH JSONB_ARRAY_LENGTH JSON_EACH JSONB_EACH JSON_EACH_TEXT JSONB_EACH_TEXT JSON_EXTRACT_PATH JSONB_EXTRACT_PATH JSON_OBJECT_KEYS JSONB_OBJECT_KEYS JSON_POPULATE_RECORD JSONB_POPULATE_RECORD JSON_POPULATE_RECORDSET JSONB_POPULATE_RECORDSET JSON_ARRAY_ELEMENTS JSONB_ARRAY_ELEMENTS JSON_ARRAY_ELEMENTS_TEXT JSONB_ARRAY_ELEMENTS_TEXT JSON_TYPEOF JSONB_TYPEOF JSON_TO_RECORD JSONB_TO_RECORD JSON_TO_RECORDSET JSONB_TO_RECORDSET JSON_STRIP_NULLS JSONB_STRIP_NULLS JSONB_SET JSONB_INSERT JSONB_PRETTY CURRVAL LASTVAL NEXTVAL SETVAL COALESCE NULLIF GREATEST LEAST ARRAY_APPEND ARRAY_CAT ARRAY_NDIMS ARRAY_DIMS ARRAY_FILL ARRAY_LENGTH ARRAY_LOWER ARRAY_POSITION ARRAY_POSITIONS ARRAY_PREPEND ARRAY_REMOVE ARRAY_REPLACE ARRAY_TO_STRING ARRAY_UPPER CARDINALITY STRING_TO_ARRAY UNNEST ISEMPTY LOWER_INC UPPER_INC LOWER_INF UPPER_INF RANGE_MERGE GENERATE_SERIES GENERATE_SUBSCRIPTS CURRENT_DATABASE CURRENT_QUERY CURRENT_SCHEMA|10 CURRENT_SCHEMAS|10 INET_CLIENT_ADDR INET_CLIENT_PORT INET_SERVER_ADDR INET_SERVER_PORT ROW_SECURITY_ACTIVE FORMAT_TYPE TO_REGCLASS TO_REGPROC TO_REGPROCEDURE TO_REGOPER TO_REGOPERATOR TO_REGTYPE TO_REGNAMESPACE TO_REGROLE COL_DESCRIPTION OBJ_DESCRIPTION SHOBJ_DESCRIPTION TXID_CURRENT TXID_CURRENT_IF_ASSIGNED TXID_CURRENT_SNAPSHOT TXID_SNAPSHOT_XIP TXID_SNAPSHOT_XMAX TXID_SNAPSHOT_XMIN TXID_VISIBLE_IN_SNAPSHOT TXID_STATUS CURRENT_SETTING SET_CONFIG BRIN_SUMMARIZE_NEW_VALUES BRIN_SUMMARIZE_RANGE BRIN_DESUMMARIZE_RANGE GIN_CLEAN_PENDING_LIST SUPPRESS_REDUNDANT_UPDATES_TRIGGER LO_FROM_BYTEA LO_PUT LO_GET LO_CREAT LO_CREATE LO_UNLINK LO_IMPORT LO_EXPORT LOREAD LOWRITE GROUPING CAST".split(" ").map((E=>E.split("|")[0])).join("|")
+;return{name:"PostgreSQL",aliases:["postgres","postgresql"],case_insensitive:!0,
+keywords:{
+keyword:"ABORT ALTER ANALYZE BEGIN CALL CHECKPOINT|10 CLOSE CLUSTER COMMENT COMMIT COPY CREATE DEALLOCATE DECLARE DELETE DISCARD DO DROP END EXECUTE EXPLAIN FETCH GRANT IMPORT INSERT LISTEN LOAD LOCK MOVE NOTIFY PREPARE REASSIGN|10 REFRESH REINDEX RELEASE RESET REVOKE ROLLBACK SAVEPOINT SECURITY SELECT SET SHOW START TRUNCATE UNLISTEN|10 UPDATE VACUUM|10 VALUES AGGREGATE COLLATION CONVERSION|10 DATABASE DEFAULT PRIVILEGES DOMAIN TRIGGER EXTENSION FOREIGN WRAPPER|10 TABLE FUNCTION GROUP LANGUAGE LARGE OBJECT MATERIALIZED VIEW OPERATOR CLASS FAMILY POLICY PUBLICATION|10 ROLE RULE SCHEMA SEQUENCE SERVER STATISTICS SUBSCRIPTION SYSTEM TABLESPACE CONFIGURATION DICTIONARY PARSER TEMPLATE TYPE USER MAPPING PREPARED ACCESS METHOD CAST AS TRANSFORM TRANSACTION OWNED TO INTO SESSION AUTHORIZATION INDEX PROCEDURE ASSERTION ALL ANALYSE AND ANY ARRAY ASC ASYMMETRIC|10 BOTH CASE CHECK COLLATE COLUMN CONCURRENTLY|10 CONSTRAINT CROSS DEFERRABLE RANGE DESC DISTINCT ELSE EXCEPT FOR FREEZE|10 FROM FULL HAVING ILIKE IN INITIALLY INNER INTERSECT IS ISNULL JOIN LATERAL LEADING LIKE LIMIT NATURAL NOT NOTNULL NULL OFFSET ON ONLY OR ORDER OUTER OVERLAPS PLACING PRIMARY REFERENCES RETURNING SIMILAR SOME SYMMETRIC TABLESAMPLE THEN TRAILING UNION UNIQUE USING VARIADIC|10 VERBOSE WHEN WHERE WINDOW WITH BY RETURNS INOUT OUT SETOF|10 IF STRICT CURRENT CONTINUE OWNER LOCATION OVER PARTITION WITHIN BETWEEN ESCAPE EXTERNAL INVOKER DEFINER WORK RENAME VERSION CONNECTION CONNECT TABLES TEMP TEMPORARY FUNCTIONS SEQUENCES TYPES SCHEMAS OPTION CASCADE RESTRICT ADD ADMIN EXISTS VALID VALIDATE ENABLE DISABLE REPLICA|10 ALWAYS PASSING COLUMNS PATH REF VALUE OVERRIDING IMMUTABLE STABLE VOLATILE BEFORE AFTER EACH ROW PROCEDURAL ROUTINE NO HANDLER VALIDATOR OPTIONS STORAGE OIDS|10 WITHOUT INHERIT DEPENDS CALLED INPUT LEAKPROOF|10 COST ROWS NOWAIT SEARCH UNTIL ENCRYPTED|10 PASSWORD CONFLICT|10 INSTEAD INHERITS CHARACTERISTICS WRITE CURSOR ALSO STATEMENT SHARE EXCLUSIVE INLINE ISOLATION REPEATABLE READ COMMITTED SERIALIZABLE UNCOMMITTED LOCAL GLOBAL SQL PROCEDURES RECURSIVE SNAPSHOT ROLLUP CUBE TRUSTED|10 INCLUDE FOLLOWING PRECEDING UNBOUNDED RANGE GROUPS UNENCRYPTED|10 SYSID FORMAT DELIMITER HEADER QUOTE ENCODING FILTER OFF FORCE_QUOTE FORCE_NOT_NULL FORCE_NULL COSTS BUFFERS TIMING SUMMARY DISABLE_PAGE_SKIPPING RESTART CYCLE GENERATED IDENTITY DEFERRED IMMEDIATE LEVEL LOGGED UNLOGGED OF NOTHING NONE EXCLUDE ATTRIBUTE USAGE ROUTINES TRUE FALSE NAN INFINITY ALIAS BEGIN CONSTANT DECLARE END EXCEPTION RETURN PERFORM|10 RAISE GET DIAGNOSTICS STACKED|10 FOREACH LOOP ELSIF EXIT WHILE REVERSE SLICE DEBUG LOG INFO NOTICE WARNING ASSERT OPEN SUPERUSER NOSUPERUSER CREATEDB NOCREATEDB CREATEROLE NOCREATEROLE INHERIT NOINHERIT LOGIN NOLOGIN REPLICATION NOREPLICATION BYPASSRLS NOBYPASSRLS ",
+built_in:"CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURRENT_CATALOG|10 CURRENT_DATE LOCALTIME LOCALTIMESTAMP CURRENT_ROLE|10 CURRENT_SCHEMA|10 SESSION_USER PUBLIC FOUND NEW OLD TG_NAME|10 TG_WHEN|10 TG_LEVEL|10 TG_OP|10 TG_RELID|10 TG_RELNAME|10 TG_TABLE_NAME|10 TG_TABLE_SCHEMA|10 TG_NARGS|10 TG_ARGV|10 TG_EVENT|10 TG_TAG|10 ROW_COUNT RESULT_OID|10 PG_CONTEXT|10 RETURNED_SQLSTATE COLUMN_NAME CONSTRAINT_NAME PG_DATATYPE_NAME|10 MESSAGE_TEXT TABLE_NAME SCHEMA_NAME PG_EXCEPTION_DETAIL|10 PG_EXCEPTION_HINT|10 PG_EXCEPTION_CONTEXT|10 SQLSTATE SQLERRM|10 SUCCESSFUL_COMPLETION WARNING DYNAMIC_RESULT_SETS_RETURNED IMPLICIT_ZERO_BIT_PADDING NULL_VALUE_ELIMINATED_IN_SET_FUNCTION PRIVILEGE_NOT_GRANTED PRIVILEGE_NOT_REVOKED STRING_DATA_RIGHT_TRUNCATION DEPRECATED_FEATURE NO_DATA NO_ADDITIONAL_DYNAMIC_RESULT_SETS_RETURNED SQL_STATEMENT_NOT_YET_COMPLETE CONNECTION_EXCEPTION CONNECTION_DOES_NOT_EXIST CONNECTION_FAILURE SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION TRANSACTION_RESOLUTION_UNKNOWN PROTOCOL_VIOLATION TRIGGERED_ACTION_EXCEPTION FEATURE_NOT_SUPPORTED INVALID_TRANSACTION_INITIATION LOCATOR_EXCEPTION INVALID_LOCATOR_SPECIFICATION INVALID_GRANTOR INVALID_GRANT_OPERATION INVALID_ROLE_SPECIFICATION DIAGNOSTICS_EXCEPTION STACKED_DIAGNOSTICS_ACCESSED_WITHOUT_ACTIVE_HANDLER CASE_NOT_FOUND CARDINALITY_VIOLATION DATA_EXCEPTION ARRAY_SUBSCRIPT_ERROR CHARACTER_NOT_IN_REPERTOIRE DATETIME_FIELD_OVERFLOW DIVISION_BY_ZERO ERROR_IN_ASSIGNMENT ESCAPE_CHARACTER_CONFLICT INDICATOR_OVERFLOW INTERVAL_FIELD_OVERFLOW INVALID_ARGUMENT_FOR_LOGARITHM INVALID_ARGUMENT_FOR_NTILE_FUNCTION INVALID_ARGUMENT_FOR_NTH_VALUE_FUNCTION INVALID_ARGUMENT_FOR_POWER_FUNCTION INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION INVALID_CHARACTER_VALUE_FOR_CAST INVALID_DATETIME_FORMAT INVALID_ESCAPE_CHARACTER INVALID_ESCAPE_OCTET INVALID_ESCAPE_SEQUENCE NONSTANDARD_USE_OF_ESCAPE_CHARACTER INVALID_INDICATOR_PARAMETER_VALUE INVALID_PARAMETER_VALUE INVALID_REGULAR_EXPRESSION INVALID_ROW_COUNT_IN_LIMIT_CLAUSE INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE INVALID_TABLESAMPLE_ARGUMENT INVALID_TABLESAMPLE_REPEAT INVALID_TIME_ZONE_DISPLACEMENT_VALUE INVALID_USE_OF_ESCAPE_CHARACTER MOST_SPECIFIC_TYPE_MISMATCH NULL_VALUE_NOT_ALLOWED NULL_VALUE_NO_INDICATOR_PARAMETER NUMERIC_VALUE_OUT_OF_RANGE SEQUENCE_GENERATOR_LIMIT_EXCEEDED STRING_DATA_LENGTH_MISMATCH STRING_DATA_RIGHT_TRUNCATION SUBSTRING_ERROR TRIM_ERROR UNTERMINATED_C_STRING ZERO_LENGTH_CHARACTER_STRING FLOATING_POINT_EXCEPTION INVALID_TEXT_REPRESENTATION INVALID_BINARY_REPRESENTATION BAD_COPY_FILE_FORMAT UNTRANSLATABLE_CHARACTER NOT_AN_XML_DOCUMENT INVALID_XML_DOCUMENT INVALID_XML_CONTENT INVALID_XML_COMMENT INVALID_XML_PROCESSING_INSTRUCTION INTEGRITY_CONSTRAINT_VIOLATION RESTRICT_VIOLATION NOT_NULL_VIOLATION FOREIGN_KEY_VIOLATION UNIQUE_VIOLATION CHECK_VIOLATION EXCLUSION_VIOLATION INVALID_CURSOR_STATE INVALID_TRANSACTION_STATE ACTIVE_SQL_TRANSACTION BRANCH_TRANSACTION_ALREADY_ACTIVE HELD_CURSOR_REQUIRES_SAME_ISOLATION_LEVEL INAPPROPRIATE_ACCESS_MODE_FOR_BRANCH_TRANSACTION INAPPROPRIATE_ISOLATION_LEVEL_FOR_BRANCH_TRANSACTION NO_ACTIVE_SQL_TRANSACTION_FOR_BRANCH_TRANSACTION READ_ONLY_SQL_TRANSACTION SCHEMA_AND_DATA_STATEMENT_MIXING_NOT_SUPPORTED NO_ACTIVE_SQL_TRANSACTION IN_FAILED_SQL_TRANSACTION IDLE_IN_TRANSACTION_SESSION_TIMEOUT INVALID_SQL_STATEMENT_NAME TRIGGERED_DATA_CHANGE_VIOLATION INVALID_AUTHORIZATION_SPECIFICATION INVALID_PASSWORD DEPENDENT_PRIVILEGE_DESCRIPTORS_STILL_EXIST DEPENDENT_OBJECTS_STILL_EXIST INVALID_TRANSACTION_TERMINATION SQL_ROUTINE_EXCEPTION FUNCTION_EXECUTED_NO_RETURN_STATEMENT MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED INVALID_CURSOR_NAME EXTERNAL_ROUTINE_EXCEPTION CONTAINING_SQL_NOT_PERMITTED MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED EXTERNAL_ROUTINE_INVOCATION_EXCEPTION INVALID_SQLSTATE_RETURNED NULL_VALUE_NOT_ALLOWED TRIGGER_PROTOCOL_VIOLATED SRF_PROTOCOL_VIOLATED EVENT_TRIGGER_PROTOCOL_VIOLATED SAVEPOINT_EXCEPTION INVALID_SAVEPOINT_SPECIFICATION INVALID_CATALOG_NAME INVALID_SCHEMA_NAME TRANSACTION_ROLLBACK TRANSACTION_INTEGRITY_CONSTRAINT_VIOLATION SERIALIZATION_FAILURE STATEMENT_COMPLETION_UNKNOWN DEADLOCK_DETECTED SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION SYNTAX_ERROR INSUFFICIENT_PRIVILEGE CANNOT_COERCE GROUPING_ERROR WINDOWING_ERROR INVALID_RECURSION INVALID_FOREIGN_KEY INVALID_NAME NAME_TOO_LONG RESERVED_NAME DATATYPE_MISMATCH INDETERMINATE_DATATYPE COLLATION_MISMATCH INDETERMINATE_COLLATION WRONG_OBJECT_TYPE GENERATED_ALWAYS UNDEFINED_COLUMN UNDEFINED_FUNCTION UNDEFINED_TABLE UNDEFINED_PARAMETER UNDEFINED_OBJECT DUPLICATE_COLUMN DUPLICATE_CURSOR DUPLICATE_DATABASE DUPLICATE_FUNCTION DUPLICATE_PREPARED_STATEMENT DUPLICATE_SCHEMA DUPLICATE_TABLE DUPLICATE_ALIAS DUPLICATE_OBJECT AMBIGUOUS_COLUMN AMBIGUOUS_FUNCTION AMBIGUOUS_PARAMETER AMBIGUOUS_ALIAS INVALID_COLUMN_REFERENCE INVALID_COLUMN_DEFINITION INVALID_CURSOR_DEFINITION INVALID_DATABASE_DEFINITION INVALID_FUNCTION_DEFINITION INVALID_PREPARED_STATEMENT_DEFINITION INVALID_SCHEMA_DEFINITION INVALID_TABLE_DEFINITION INVALID_OBJECT_DEFINITION WITH_CHECK_OPTION_VIOLATION INSUFFICIENT_RESOURCES DISK_FULL OUT_OF_MEMORY TOO_MANY_CONNECTIONS CONFIGURATION_LIMIT_EXCEEDED PROGRAM_LIMIT_EXCEEDED STATEMENT_TOO_COMPLEX TOO_MANY_COLUMNS TOO_MANY_ARGUMENTS OBJECT_NOT_IN_PREREQUISITE_STATE OBJECT_IN_USE CANT_CHANGE_RUNTIME_PARAM LOCK_NOT_AVAILABLE OPERATOR_INTERVENTION QUERY_CANCELED ADMIN_SHUTDOWN CRASH_SHUTDOWN CANNOT_CONNECT_NOW DATABASE_DROPPED SYSTEM_ERROR IO_ERROR UNDEFINED_FILE DUPLICATE_FILE SNAPSHOT_TOO_OLD CONFIG_FILE_ERROR LOCK_FILE_EXISTS FDW_ERROR FDW_COLUMN_NAME_NOT_FOUND FDW_DYNAMIC_PARAMETER_VALUE_NEEDED FDW_FUNCTION_SEQUENCE_ERROR FDW_INCONSISTENT_DESCRIPTOR_INFORMATION FDW_INVALID_ATTRIBUTE_VALUE FDW_INVALID_COLUMN_NAME FDW_INVALID_COLUMN_NUMBER FDW_INVALID_DATA_TYPE FDW_INVALID_DATA_TYPE_DESCRIPTORS FDW_INVALID_DESCRIPTOR_FIELD_IDENTIFIER FDW_INVALID_HANDLE FDW_INVALID_OPTION_INDEX FDW_INVALID_OPTION_NAME FDW_INVALID_STRING_LENGTH_OR_BUFFER_LENGTH FDW_INVALID_STRING_FORMAT FDW_INVALID_USE_OF_NULL_POINTER FDW_TOO_MANY_HANDLES FDW_OUT_OF_MEMORY FDW_NO_SCHEMAS FDW_OPTION_NAME_NOT_FOUND FDW_REPLY_HANDLE FDW_SCHEMA_NOT_FOUND FDW_TABLE_NOT_FOUND FDW_UNABLE_TO_CREATE_EXECUTION FDW_UNABLE_TO_CREATE_REPLY FDW_UNABLE_TO_ESTABLISH_CONNECTION PLPGSQL_ERROR RAISE_EXCEPTION NO_DATA_FOUND TOO_MANY_ROWS ASSERT_FAILURE INTERNAL_ERROR DATA_CORRUPTED INDEX_CORRUPTED "
+},illegal:/:==|\W\s*\(\*|(^|\s)\$[a-z]|\{\{|[a-z]:\s*$|\.\.\.|TO:|DO:/,
+contains:[{className:"keyword",variants:[{begin:/\bTEXT\s*SEARCH\b/},{
+begin:/\b(PRIMARY|FOREIGN|FOR(\s+NO)?)\s+KEY\b/},{
+begin:/\bPARALLEL\s+(UNSAFE|RESTRICTED|SAFE)\b/},{
+begin:/\bSTORAGE\s+(PLAIN|EXTERNAL|EXTENDED|MAIN)\b/},{
+begin:/\bMATCH\s+(FULL|PARTIAL|SIMPLE)\b/},{begin:/\bNULLS\s+(FIRST|LAST)\b/},{
+begin:/\bEVENT\s+TRIGGER\b/},{begin:/\b(MAPPING|OR)\s+REPLACE\b/},{
+begin:/\b(FROM|TO)\s+(PROGRAM|STDIN|STDOUT)\b/},{
+begin:/\b(SHARE|EXCLUSIVE)\s+MODE\b/},{
+begin:/\b(LEFT|RIGHT)\s+(OUTER\s+)?JOIN\b/},{
+begin:/\b(FETCH|MOVE)\s+(NEXT|PRIOR|FIRST|LAST|ABSOLUTE|RELATIVE|FORWARD|BACKWARD)\b/
+},{begin:/\bPRESERVE\s+ROWS\b/},{begin:/\bDISCARD\s+PLANS\b/},{
+begin:/\bREFERENCING\s+(OLD|NEW)\b/},{begin:/\bSKIP\s+LOCKED\b/},{
+begin:/\bGROUPING\s+SETS\b/},{
+begin:/\b(BINARY|INSENSITIVE|SCROLL|NO\s+SCROLL)\s+(CURSOR|FOR)\b/},{
+begin:/\b(WITH|WITHOUT)\s+HOLD\b/},{
+begin:/\bWITH\s+(CASCADED|LOCAL)\s+CHECK\s+OPTION\b/},{
+begin:/\bEXCLUDE\s+(TIES|NO\s+OTHERS)\b/},{
+begin:/\bFORMAT\s+(TEXT|XML|JSON|YAML)\b/},{
+begin:/\bSET\s+((SESSION|LOCAL)\s+)?NAMES\b/},{begin:/\bIS\s+(NOT\s+)?UNKNOWN\b/
+},{begin:/\bSECURITY\s+LABEL\b/},{begin:/\bSTANDALONE\s+(YES|NO|NO\s+VALUE)\b/
+},{begin:/\bWITH\s+(NO\s+)?DATA\b/},{begin:/\b(FOREIGN|SET)\s+DATA\b/},{
+begin:/\bSET\s+(CATALOG|CONSTRAINTS)\b/},{begin:/\b(WITH|FOR)\s+ORDINALITY\b/},{
+begin:/\bIS\s+(NOT\s+)?DOCUMENT\b/},{
+begin:/\bXML\s+OPTION\s+(DOCUMENT|CONTENT)\b/},{
+begin:/\b(STRIP|PRESERVE)\s+WHITESPACE\b/},{
+begin:/\bNO\s+(ACTION|MAXVALUE|MINVALUE)\b/},{
+begin:/\bPARTITION\s+BY\s+(RANGE|LIST|HASH)\b/},{begin:/\bAT\s+TIME\s+ZONE\b/},{
+begin:/\bGRANTED\s+BY\b/},{begin:/\bRETURN\s+(QUERY|NEXT)\b/},{
+begin:/\b(ATTACH|DETACH)\s+PARTITION\b/},{
+begin:/\bFORCE\s+ROW\s+LEVEL\s+SECURITY\b/},{
+begin:/\b(INCLUDING|EXCLUDING)\s+(COMMENTS|CONSTRAINTS|DEFAULTS|IDENTITY|INDEXES|STATISTICS|STORAGE|ALL)\b/
+},{begin:/\bAS\s+(ASSIGNMENT|IMPLICIT|PERMISSIVE|RESTRICTIVE|ENUM|RANGE)\b/}]},{
+begin:/\b(FORMAT|FAMILY|VERSION)\s*\(/},{begin:/\bINCLUDE\s*\(/,
+keywords:"INCLUDE"},{begin:/\bRANGE(?!\s*(BETWEEN|UNBOUNDED|CURRENT|[-0-9]+))/
+},{
+begin:/\b(VERSION|OWNER|TEMPLATE|TABLESPACE|CONNECTION\s+LIMIT|PROCEDURE|RESTRICT|JOIN|PARSER|COPY|START|END|COLLATION|INPUT|ANALYZE|STORAGE|LIKE|DEFAULT|DELIMITER|ENCODING|COLUMN|CONSTRAINT|TABLE|SCHEMA)\s*=/
+},{begin:/\b(PG_\w+?|HAS_[A-Z_]+_PRIVILEGE)\b/,relevance:10},{
+begin:/\bEXTRACT\s*\(/,end:/\bFROM\b/,returnEnd:!0,keywords:{
+type:"CENTURY DAY DECADE DOW DOY EPOCH HOUR ISODOW ISOYEAR MICROSECONDS MILLENNIUM MILLISECONDS MINUTE MONTH QUARTER SECOND TIMEZONE TIMEZONE_HOUR TIMEZONE_MINUTE WEEK YEAR"
+}},{begin:/\b(XMLELEMENT|XMLPI)\s*\(\s*NAME/,keywords:{keyword:"NAME"}},{
+begin:/\b(XMLPARSE|XMLSERIALIZE)\s*\(\s*(DOCUMENT|CONTENT)/,keywords:{
+keyword:"DOCUMENT CONTENT"}},{beginKeywords:"CACHE INCREMENT MAXVALUE MINVALUE",
+end:E.C_NUMBER_RE,returnEnd:!0,keywords:"BY CACHE INCREMENT MAXVALUE MINVALUE"
+},{className:"type",begin:/\b(WITH|WITHOUT)\s+TIME\s+ZONE\b/},{className:"type",
+begin:/\bINTERVAL\s+(YEAR|MONTH|DAY|HOUR|MINUTE|SECOND)(\s+TO\s+(MONTH|HOUR|MINUTE|SECOND))?\b/
+},{
+begin:/\bRETURNS\s+(LANGUAGE_HANDLER|TRIGGER|EVENT_TRIGGER|FDW_HANDLER|INDEX_AM_HANDLER|TSM_HANDLER)\b/,
+keywords:{keyword:"RETURNS",
+type:"LANGUAGE_HANDLER TRIGGER EVENT_TRIGGER FDW_HANDLER INDEX_AM_HANDLER TSM_HANDLER"
+}},{begin:"\\b("+I+")\\s*\\("},{begin:"\\.("+R+")\\b"},{
+begin:"\\b("+R+")\\s+PATH\\b",keywords:{keyword:"PATH",
+type:A.replace("PATH ","")}},{className:"type",begin:"\\b("+R+")\\b"},{
+className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{
+className:"string",begin:"(e|E|u&|U&)'",end:"'",contains:[{begin:"\\\\."}],
+relevance:10},E.END_SAME_AS_BEGIN({begin:N,end:N,contains:[{
+subLanguage:["pgsql","perl","python","tcl","r","lua","java","php","ruby","bash","scheme","xml","json"],
+endsWithParent:!0}]}),{begin:'"',end:'"',contains:[{begin:'""'}]
+},E.C_NUMBER_MODE,E.C_BLOCK_COMMENT_MODE,T,{className:"meta",variants:[{
+begin:"%(ROW)?TYPE",relevance:10},{begin:"\\$\\d+"},{begin:"^#\\w",end:"$"}]},{
+className:"symbol",begin:"<<\\s*[a-zA-Z_][a-zA-Z_0-9$]*\\s*>>",relevance:10}]}}
+})());
+hljs.registerLanguage("php",(()=>{"use strict";return e=>{const r={
+className:"variable",
+begin:"\\$+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?![A-Za-z0-9])(?![$])"},t={
+className:"meta",variants:[{begin:/<\?php/,relevance:10},{begin:/<\?[=]?/},{
+begin:/\?>/}]},a={className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,
+end:/\}/}]},n=e.inherit(e.APOS_STRING_MODE,{illegal:null
+}),i=e.inherit(e.QUOTE_STRING_MODE,{illegal:null,
+contains:e.QUOTE_STRING_MODE.contains.concat(a)}),o=e.END_SAME_AS_BEGIN({
+begin:/<<<[ \t]*(\w+)\n/,end:/[ \t]*(\w+)\b/,
+contains:e.QUOTE_STRING_MODE.contains.concat(a)}),l={className:"string",
+contains:[e.BACKSLASH_ESCAPE,t],variants:[e.inherit(n,{begin:"b'",end:"'"
+}),e.inherit(i,{begin:'b"',end:'"'}),i,n,o]},s={className:"number",variants:[{
+begin:"\\b0b[01]+(?:_[01]+)*\\b"},{begin:"\\b0o[0-7]+(?:_[0-7]+)*\\b"},{
+begin:"\\b0x[\\da-f]+(?:_[\\da-f]+)*\\b"},{
+begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:e[+-]?\\d+)?"
+}],relevance:0},c={
+keyword:"__CLASS__ __DIR__ __FILE__ __FUNCTION__ __LINE__ __METHOD__ __NAMESPACE__ __TRAIT__ die echo exit include include_once print require require_once array abstract and as binary bool boolean break callable case catch class clone const continue declare default do double else elseif empty enddeclare endfor endforeach endif endswitch endwhile enum eval extends final finally float for foreach from global goto if implements instanceof insteadof int integer interface isset iterable list match|0 mixed new object or private protected public real return string switch throw trait try unset use var void while xor yield",
+literal:"false null true",
+built_in:"Error|0 AppendIterator ArgumentCountError ArithmeticError ArrayIterator ArrayObject AssertionError BadFunctionCallException BadMethodCallException CachingIterator CallbackFilterIterator CompileError Countable DirectoryIterator DivisionByZeroError DomainException EmptyIterator ErrorException Exception FilesystemIterator FilterIterator GlobIterator InfiniteIterator InvalidArgumentException IteratorIterator LengthException LimitIterator LogicException MultipleIterator NoRewindIterator OutOfBoundsException OutOfRangeException OuterIterator OverflowException ParentIterator ParseError RangeException RecursiveArrayIterator RecursiveCachingIterator RecursiveCallbackFilterIterator RecursiveDirectoryIterator RecursiveFilterIterator RecursiveIterator RecursiveIteratorIterator RecursiveRegexIterator RecursiveTreeIterator RegexIterator RuntimeException SeekableIterator SplDoublyLinkedList SplFileInfo SplFileObject SplFixedArray SplHeap SplMaxHeap SplMinHeap SplObjectStorage SplObserver SplObserver SplPriorityQueue SplQueue SplStack SplSubject SplSubject SplTempFileObject TypeError UnderflowException UnexpectedValueException UnhandledMatchError ArrayAccess Closure Generator Iterator IteratorAggregate Serializable Stringable Throwable Traversable WeakReference WeakMap Directory __PHP_Incomplete_Class parent php_user_filter self static stdClass"
+};return{aliases:["php3","php4","php5","php6","php7","php8"],
+case_insensitive:!0,keywords:c,
+contains:[e.HASH_COMMENT_MODE,e.COMMENT("//","$",{contains:[t]
+}),e.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]
+}),e.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,
+keywords:"__halt_compiler"}),t,{className:"keyword",begin:/\$this\b/},r,{
+begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",
+relevance:0,beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,
+illegal:"[$%\\[]",contains:[{beginKeywords:"use"},e.UNDERSCORE_TITLE_MODE,{
+begin:"=>",endsParent:!0},{className:"params",begin:"\\(",end:"\\)",
+excludeBegin:!0,excludeEnd:!0,keywords:c,
+contains:["self",r,e.C_BLOCK_COMMENT_MODE,l,s]}]},{className:"class",variants:[{
+beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait",
+illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{
+beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{
+beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/,
+contains:[e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",relevance:0,end:";",
+contains:[e.UNDERSCORE_TITLE_MODE]},l,s]}}})());
+hljs.registerLanguage("php-template",(()=>{"use strict";return n=>({
+name:"PHP template",subLanguage:"xml",contains:[{begin:/<\?(php|=)?/,end:/\?>/,
+subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',
+end:'"',skip:!0},{begin:"b'",end:"'",skip:!0},n.inherit(n.APOS_STRING_MODE,{
+illegal:null,className:null,contains:null,skip:!0
+}),n.inherit(n.QUOTE_STRING_MODE,{illegal:null,className:null,contains:null,
+skip:!0})]}]})})());
+hljs.registerLanguage("plaintext",(()=>{"use strict";return t=>({
+name:"Plain text",aliases:["text","txt"],disableAutodetect:!0})})());
+hljs.registerLanguage("pony",(()=>{"use strict";return e=>({name:"Pony",
+keywords:{
+keyword:"actor addressof and as be break class compile_error compile_intrinsic consume continue delegate digestof do else elseif embed end error for fun if ifdef in interface is isnt lambda let match new not object or primitive recover repeat return struct then trait try type until use var where while with xor",
+meta:"iso val tag trn box ref",literal:"this false true"},contains:[{
+className:"type",begin:"\\b_?[A-Z][\\w]*",relevance:0},{className:"string",
+begin:'"""',end:'"""',relevance:10},{className:"string",begin:'"',end:'"',
+contains:[e.BACKSLASH_ESCAPE]},{className:"string",begin:"'",end:"'",
+contains:[e.BACKSLASH_ESCAPE],relevance:0},{begin:e.IDENT_RE+"'",relevance:0},{
+className:"number",
+begin:"(-?)(\\b0[xX][a-fA-F0-9]+|\\b0[bB][01]+|(\\b\\d+(_\\d+)?(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",
+relevance:0},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]})})());
+hljs.registerLanguage("powershell",(()=>{"use strict";return e=>{const n={
+$pattern:/-?[A-z\.\-]+\b/,
+keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",
+built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"
+},s={begin:"`[\\s\\S]",relevance:0},i={className:"variable",variants:[{
+begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]
+},a={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],
+contains:[s,i,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},t={
+className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]
+},r=e.inherit(e.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,
+end:/#>/}],contains:[{className:"doctag",variants:[{
+begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/
+},{
+begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/
+}]}]}),c={className:"class",beginKeywords:"class enum",end:/\s*[{]/,
+excludeEnd:!0,relevance:0,contains:[e.TITLE_MODE]},l={className:"function",
+begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,
+contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",
+begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,
+className:"params",relevance:0,contains:[i]}]},o={begin:/using\s/,end:/$/,
+returnBegin:!0,contains:[a,t,{className:"keyword",
+begin:/(using|assembly|command|module|namespace|type)/}]},p={
+className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,
+relevance:0,contains:[{className:"keyword",
+begin:"(".concat(n.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,
+relevance:0},e.inherit(e.TITLE_MODE,{endsParent:!0})]
+},g=[p,r,s,e.NUMBER_MODE,a,t,{className:"built_in",variants:[{
+begin:"(Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where)+(-)[\\w\\d]+"
+}]},i,{className:"literal",begin:/\$(null|true|false)\b/},{
+className:"selector-tag",begin:/@\B/,relevance:0}],m={begin:/\[/,end:/\]/,
+excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",g,{
+begin:"(string|char|byte|int|long|bool|decimal|single|double|DateTime|xml|array|hashtable|void)",
+className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,
+relevance:0})};return p.contains.unshift(m),{name:"PowerShell",
+aliases:["ps","ps1"],case_insensitive:!0,keywords:n,contains:g.concat(c,l,o,{
+variants:[{className:"operator",
+begin:"(-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor)\\b"
+},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},m)}}})());
+hljs.registerLanguage("processing",(()=>{"use strict";return e=>({
+name:"Processing",keywords:{
+keyword:"BufferedReader PVector PFont PImage PGraphics HashMap boolean byte char color double float int long String Array FloatDict FloatList IntDict IntList JSONArray JSONObject Object StringDict StringList Table TableRow XML false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private",
+literal:"P2D P3D HALF_PI PI QUARTER_PI TAU TWO_PI",title:"setup draw",
+built_in:"displayHeight displayWidth mouseY mouseX mousePressed pmouseX pmouseY key keyCode pixels focused frameCount frameRate height width size createGraphics beginDraw createShape loadShape PShape arc ellipse line point quad rect triangle bezier bezierDetail bezierPoint bezierTangent curve curveDetail curvePoint curveTangent curveTightness shape shapeMode beginContour beginShape bezierVertex curveVertex endContour endShape quadraticVertex vertex ellipseMode noSmooth rectMode smooth strokeCap strokeJoin strokeWeight mouseClicked mouseDragged mouseMoved mousePressed mouseReleased mouseWheel keyPressed keyPressedkeyReleased keyTyped print println save saveFrame day hour millis minute month second year background clear colorMode fill noFill noStroke stroke alpha blue brightness color green hue lerpColor red saturation modelX modelY modelZ screenX screenY screenZ ambient emissive shininess specular add createImage beginCamera camera endCamera frustum ortho perspective printCamera printProjection cursor frameRate noCursor exit loop noLoop popStyle pushStyle redraw binary boolean byte char float hex int str unbinary unhex join match matchAll nf nfc nfp nfs split splitTokens trim append arrayCopy concat expand reverse shorten sort splice subset box sphere sphereDetail createInput createReader loadBytes loadJSONArray loadJSONObject loadStrings loadTable loadXML open parseXML saveTable selectFolder selectInput beginRaw beginRecord createOutput createWriter endRaw endRecord PrintWritersaveBytes saveJSONArray saveJSONObject saveStream saveStrings saveXML selectOutput popMatrix printMatrix pushMatrix resetMatrix rotate rotateX rotateY rotateZ scale shearX shearY translate ambientLight directionalLight lightFalloff lights lightSpecular noLights normal pointLight spotLight image imageMode loadImage noTint requestImage tint texture textureMode textureWrap blend copy filter get loadPixels set updatePixels blendMode loadShader PShaderresetShader shader createFont loadFont text textFont textAlign textLeading textMode textSize textWidth textAscent textDescent abs ceil constrain dist exp floor lerp log mag map max min norm pow round sq sqrt acos asin atan atan2 cos degrees radians sin tan noise noiseDetail noiseSeed random randomGaussian randomSeed"
+},
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE]
+})})());
+hljs.registerLanguage("profile",(()=>{"use strict";return e=>({
+name:"Python profiler",contains:[e.C_NUMBER_MODE,{
+begin:"[a-zA-Z_][\\da-zA-Z_]+\\.[\\da-zA-Z_]{1,3}",end:":",excludeEnd:!0},{
+begin:"(ncalls|tottime|cumtime)",end:"$",
+keywords:"ncalls tottime|10 cumtime|10 filename",relevance:10},{
+begin:"function calls",end:"$",contains:[e.C_NUMBER_MODE],relevance:10
+},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",begin:"\\(",
+end:"\\)$",excludeBegin:!0,excludeEnd:!0,relevance:0}]})})());
+hljs.registerLanguage("prolog",(()=>{"use strict";return n=>{const e={
+begin:/\(/,end:/\)/,relevance:0},a={begin:/\[/,end:/\]/},s={className:"comment",
+begin:/%/,end:/$/,contains:[n.PHRASAL_WORDS_MODE]},i={className:"string",
+begin:/`/,end:/`/,contains:[n.BACKSLASH_ESCAPE]},g=[{begin:/[a-z][A-Za-z0-9_]*/,
+relevance:0},{className:"symbol",variants:[{begin:/[A-Z][a-zA-Z0-9_]*/},{
+begin:/_[A-Za-z0-9_]*/}],relevance:0},e,{begin:/:-/
+},a,s,n.C_BLOCK_COMMENT_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,i,{
+className:"string",begin:/0'(\\'|.)/},{className:"string",begin:/0'\\s/
+},n.C_NUMBER_MODE];return e.contains=g,a.contains=g,{name:"Prolog",
+contains:g.concat([{begin:/\.$/}])}}})());
+hljs.registerLanguage("properties",(()=>{"use strict";return e=>{
+var n="[ \\t\\f]*",a=n+"[:=]"+n,t="("+a+"|[ \\t\\f]+)",r="([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",s="([^\\\\:= \\t\\f\\n]|\\\\.)+",i={
+end:t,relevance:0,starts:{className:"string",end:/$/,relevance:0,contains:[{
+begin:"\\\\\\\\"},{begin:"\\\\\\n"}]}};return{name:".properties",
+case_insensitive:!0,illegal:/\S/,contains:[e.COMMENT("^\\s*[!#]","$"),{
+returnBegin:!0,variants:[{begin:r+a,relevance:1},{begin:r+"[ \\t\\f]+",
+relevance:0}],contains:[{className:"attr",begin:r,endsParent:!0,relevance:0}],
+starts:i},{begin:s+t,returnBegin:!0,relevance:0,contains:[{className:"meta",
+begin:s,endsParent:!0,relevance:0}],starts:i},{className:"attr",relevance:0,
+begin:s+n+"$"}]}}})());
+hljs.registerLanguage("protobuf",(()=>{"use strict";return e=>({
+name:"Protocol Buffers",keywords:{
+keyword:"package import option optional required repeated group oneof",
+built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",
+literal:"true false"},
+contains:[e.QUOTE_STRING_MODE,e.NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{
+className:"class",beginKeywords:"message enum service",end:/\{/,illegal:/\n/,
+contains:[e.inherit(e.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]},{
+className:"function",beginKeywords:"rpc",end:/[{;]/,excludeEnd:!0,
+keywords:"rpc returns"},{begin:/^\s*[A-Z_]+(?=\s*=[^\n]+;$)/}]})})());
+hljs.registerLanguage("puppet",(()=>{"use strict";return e=>{
+const s=e.COMMENT("#","$"),r="([A-Za-z_]|::)(\\w|::)*",a=e.inherit(e.TITLE_MODE,{
+begin:r}),n={className:"variable",begin:"\\$"+r},i={className:"string",
+contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/
+}]};return{name:"Puppet",aliases:["pp"],contains:[s,n,i,{beginKeywords:"class",
+end:"\\{|;",illegal:/=/,contains:[a,s]},{beginKeywords:"define",end:/\{/,
+contains:[{className:"section",begin:e.IDENT_RE,endsParent:!0}]},{
+begin:e.IDENT_RE+"\\s+\\{",returnBegin:!0,end:/\S/,contains:[{
+className:"keyword",begin:e.IDENT_RE},{begin:/\{/,end:/\}/,keywords:{
+keyword:"and case default else elsif false if in import enherits node or true undef unless main settings $string ",
+literal:"alias audit before loglevel noop require subscribe tag owner ensure group mode name|0 changes context force incl lens load_path onlyif provider returns root show_diff type_check en_address ip_address realname command environment hour monute month monthday special target weekday creates cwd ogoutput refresh refreshonly tries try_sleep umask backup checksum content ctime force ignore links mtime purge recurse recurselimit replace selinux_ignore_defaults selrange selrole seltype seluser source souirce_permissions sourceselect validate_cmd validate_replacement allowdupe attribute_membership auth_membership forcelocal gid ia_load_module members system host_aliases ip allowed_trunk_vlans description device_url duplex encapsulation etherchannel native_vlan speed principals allow_root auth_class auth_type authenticate_user k_of_n mechanisms rule session_owner shared options device fstype enable hasrestart directory present absent link atboot blockdevice device dump pass remounts poller_tag use message withpath adminfile allow_virtual allowcdrom category configfiles flavor install_options instance package_settings platform responsefile status uninstall_options vendor unless_system_user unless_uid binary control flags hasstatus manifest pattern restart running start stop allowdupe auths expiry gid groups home iterations key_membership keys managehome membership password password_max_age password_min_age profile_membership profiles project purge_ssh_keys role_membership roles salt shell uid baseurl cost descr enabled enablegroups exclude failovermethod gpgcheck gpgkey http_caching include includepkgs keepalive metadata_expire metalink mirrorlist priority protect proxy proxy_password proxy_username repo_gpgcheck s3_enabled skip_if_unavailable sslcacert sslclientcert sslclientkey sslverify mounted",
+built_in:"architecture augeasversion blockdevices boardmanufacturer boardproductname boardserialnumber cfkey dhcp_servers domain ec2_ ec2_userdata facterversion filesystems ldom fqdn gid hardwareisa hardwaremodel hostname id|0 interfaces ipaddress ipaddress_ ipaddress6 ipaddress6_ iphostnumber is_virtual kernel kernelmajversion kernelrelease kernelversion kernelrelease kernelversion lsbdistcodename lsbdistdescription lsbdistid lsbdistrelease lsbmajdistrelease lsbminordistrelease lsbrelease macaddress macaddress_ macosx_buildversion macosx_productname macosx_productversion macosx_productverson_major macosx_productversion_minor manufacturer memoryfree memorysize netmask metmask_ network_ operatingsystem operatingsystemmajrelease operatingsystemrelease osfamily partitions path physicalprocessorcount processor processorcount productname ps puppetversion rubysitedir rubyversion selinux selinux_config_mode selinux_config_policy selinux_current_mode selinux_current_mode selinux_enforced selinux_policyversion serialnumber sp_ sshdsakey sshecdsakey sshrsakey swapencrypted swapfree swapsize timezone type uniqueid uptime uptime_days uptime_hours uptime_seconds uuid virtual vlans xendomains zfs_version zonenae zones zpool_version"
+},relevance:0,contains:[i,s,{begin:"[a-zA-Z_]+\\s*=>",returnBegin:!0,end:"=>",
+contains:[{className:"attr",begin:e.IDENT_RE}]},{className:"number",
+begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",
+relevance:0},n]}],relevance:0}]}}})());
+hljs.registerLanguage("purebasic",(()=>{"use strict";return e=>({
+name:"PureBASIC",aliases:["pb","pbi"],
+keywords:"Align And Array As Break CallDebugger Case CompilerCase CompilerDefault CompilerElse CompilerElseIf CompilerEndIf CompilerEndSelect CompilerError CompilerIf CompilerSelect CompilerWarning Continue Data DataSection Debug DebugLevel Declare DeclareC DeclareCDLL DeclareDLL DeclareModule Default Define Dim DisableASM DisableDebugger DisableExplicit Else ElseIf EnableASM EnableDebugger EnableExplicit End EndDataSection EndDeclareModule EndEnumeration EndIf EndImport EndInterface EndMacro EndModule EndProcedure EndSelect EndStructure EndStructureUnion EndWith Enumeration EnumerationBinary Extends FakeReturn For ForEach ForEver Global Gosub Goto If Import ImportC IncludeBinary IncludeFile IncludePath Interface List Macro MacroExpandedCount Map Module NewList NewMap Next Not Or Procedure ProcedureC ProcedureCDLL ProcedureDLL ProcedureReturn Protected Prototype PrototypeC ReDim Read Repeat Restore Return Runtime Select Shared Static Step Structure StructureUnion Swap Threaded To UndefineMacro Until Until  UnuseModule UseModule Wend While With XIncludeFile XOr",
+contains:[e.COMMENT(";","$",{relevance:0}),{className:"function",
+begin:"\\b(Procedure|Declare)(C|CDLL|DLL)?\\b",end:"\\(",excludeEnd:!0,
+returnBegin:!0,contains:[{className:"keyword",
+begin:"(Procedure|Declare)(C|CDLL|DLL)?",excludeEnd:!0},{className:"type",
+begin:"\\.\\w*"},e.UNDERSCORE_TITLE_MODE]},{className:"string",begin:'(~)?"',
+end:'"',illegal:"\\n"},{className:"symbol",begin:"#[a-zA-Z_]\\w*\\$?"}]})})());
+hljs.registerLanguage("python",(()=>{"use strict";return e=>{const n={
+keyword:["and","as","assert","async","await","break","class","continue","def","del","elif","else","except","finally","for","","from","global","if","import","in","is","lambda","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],
+built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"],
+literal:["__debug__","Ellipsis","False","None","NotImplemented","True"]},a={
+className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/,
+end:/\}/,keywords:n,illegal:/#/},i={begin:/\{\{/,relevance:0},r={
+className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{
+begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/,
+contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{
+begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/,
+contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{
+begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/,
+contains:[e.BACKSLASH_ESCAPE,a,i,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/,
+end:/"""/,contains:[e.BACKSLASH_ESCAPE,a,i,s]},{begin:/([uU]|[rR])'/,end:/'/,
+relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{
+begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/,
+end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/,
+contains:[e.BACKSLASH_ESCAPE,i,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/,
+contains:[e.BACKSLASH_ESCAPE,i,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
+},t="[0-9](_?[0-9])*",l=`(\\b(${t}))?\\.(${t})|\\b(${t})\\.`,b={
+className:"number",relevance:0,variants:[{
+begin:`(\\b(${t})|(${l}))[eE][+-]?(${t})[jJ]?\\b`},{begin:`(${l})[jJ]?`},{
+begin:"\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?\\b"},{
+begin:"\\b0[bB](_?[01])+[lL]?\\b"},{begin:"\\b0[oO](_?[0-7])+[lL]?\\b"},{
+begin:"\\b0[xX](_?[0-9a-fA-F])+[lL]?\\b"},{begin:`\\b(${t})[jJ]\\b`}]},o={
+className:"params",variants:[{begin:/\(\s*\)/,skip:!0,className:null},{
+begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,
+contains:["self",a,b,r,e.HASH_COMMENT_MODE]}]};return s.contains=[r,b,a],{
+name:"Python",aliases:["py","gyp","ipython"],keywords:n,
+illegal:/(<\/|->|\?)|=>/,contains:[a,b,{begin:/\bself\b/},{beginKeywords:"if",
+relevance:0},r,e.HASH_COMMENT_MODE,{variants:[{className:"function",
+beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,
+illegal:/[${=;\n,]/,contains:[e.UNDERSCORE_TITLE_MODE,o,{begin:/->/,
+endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,
+end:/(?=#)|$/,contains:[b,o,r]},{begin:/\b(print|exec)\(/}]}}})());
+hljs.registerLanguage("python-repl",(()=>{"use strict";return s=>({
+aliases:["pycon"],contains:[{className:"meta",starts:{end:/ |$/,starts:{end:"$",
+subLanguage:"python"}},variants:[{begin:/^>>>(?=[ ]|$)/},{
+begin:/^\.\.\.(?=[ ]|$)/}]}]})})());
+hljs.registerLanguage("q",(()=>{"use strict";return e=>({name:"Q",
+aliases:["k","kdb"],keywords:{$pattern:/(`?)[A-Za-z0-9_]+\b/,
+keyword:"do while select delete by update from",literal:"0b 1b",
+built_in:"neg not null string reciprocal floor ceiling signum mod xbar xlog and or each scan over prior mmu lsq inv md5 ltime gtime count first var dev med cov cor all any rand sums prds mins maxs fills deltas ratios avgs differ prev next rank reverse iasc idesc asc desc msum mcount mavg mdev xrank mmin mmax xprev rotate distinct group where flip type key til get value attr cut set upsert raze union inter except cross sv vs sublist enlist read0 read1 hopen hclose hdel hsym hcount peach system ltrim rtrim trim lower upper ssr view tables views cols xcols keys xkey xcol xasc xdesc fkeys meta lj aj aj0 ij pj asof uj ww wj wj1 fby xgroup ungroup ej save load rsave rload show csv parse eval min max avg wavg wsum sin cos tan sum",
+type:"`float `double int `timestamp `timespan `datetime `time `boolean `symbol `char `byte `short `long `real `month `date `minute `second `guid"
+},contains:[e.C_LINE_COMMENT_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE]})})());
+hljs.registerLanguage("qml",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null;var n
+})).join("")}return n=>{const r="[a-zA-Z_][a-zA-Z0-9\\._]*",a={
+className:"attribute",begin:"\\bid\\s*:",starts:{className:"string",end:r,
+returnEnd:!1}},t={begin:r+"\\s*:",returnBegin:!0,contains:[{
+className:"attribute",begin:r,end:"\\s*:",excludeEnd:!0,relevance:0}],
+relevance:0},i={begin:e(r,/\s*\{/),end:/\{/,returnBegin:!0,relevance:0,
+contains:[n.inherit(n.TITLE_MODE,{begin:r})]};return{name:"QML",aliases:["qt"],
+case_insensitive:!1,keywords:{
+keyword:"in of on if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await import",
+literal:"true false null undefined NaN Infinity",
+built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Behavior bool color coordinate date double enumeration font geocircle georectangle geoshape int list matrix4x4 parent point quaternion real rect size string url variant vector2d vector3d vector4d Promise"
+},contains:[{className:"meta",begin:/^\s*['"]use (strict|asm)['"]/
+},n.APOS_STRING_MODE,n.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",
+contains:[n.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]
+},n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{
+begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:n.C_NUMBER_RE}],
+relevance:0},{begin:"("+n.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",
+keywords:"return throw case",
+contains:[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,n.REGEXP_MODE,{begin:/</,
+end:/>\s*[);\]]/,relevance:0,subLanguage:"xml"}],relevance:0},{
+className:"keyword",begin:"\\bsignal\\b",starts:{className:"string",
+end:"(\\(|:|=|;|,|//|/\\*|$)",returnEnd:!0}},{className:"keyword",
+begin:"\\bproperty\\b",starts:{className:"string",end:"(:|=|;|,|//|/\\*|$)",
+returnEnd:!0}},{className:"function",beginKeywords:"function",end:/\{/,
+excludeEnd:!0,contains:[n.inherit(n.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/
+}),{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,
+contains:[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE]}],illegal:/\[|%/},{
+begin:"\\."+n.IDENT_RE,relevance:0},a,t,i],illegal:/#/}}})());
+hljs.registerLanguage("r",(()=>{"use strict";function e(...e){return e.map((e=>{
+return(a=e)?"string"==typeof a?a:a.source:null;var a})).join("")}return a=>{
+const n=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/;return{name:"R",
+illegal:/->/,keywords:{$pattern:n,
+keyword:"function if in break next repeat else for while",
+literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10",
+built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm"
+},compilerExtensions:[(a,n)=>{if(!a.beforeMatch)return
+;if(a.starts)throw Error("beforeMatch cannot be used with starts")
+;const i=Object.assign({},a);Object.keys(a).forEach((e=>{delete a[e]
+})),a.begin=e(i.beforeMatch,e("(?=",i.begin,")")),a.starts={relevance:0,
+contains:[Object.assign(i,{endsParent:!0})]},a.relevance=0,delete i.beforeMatch
+}],contains:[a.COMMENT(/#'/,/$/,{contains:[{className:"doctag",
+begin:"@examples",starts:{contains:[{begin:/\n/},{begin:/#'\s*(?=@[a-zA-Z]+)/,
+endsParent:!0},{begin:/#'/,end:/$/,excludeBegin:!0}]}},{className:"doctag",
+begin:"@param",end:/$/,contains:[{className:"variable",variants:[{begin:n},{
+begin:/`(?:\\.|[^`\\])+`/}],endsParent:!0}]},{className:"doctag",
+begin:/@[a-zA-Z]+/},{className:"meta-keyword",begin:/\\[a-zA-Z]+/}]
+}),a.HASH_COMMENT_MODE,{className:"string",contains:[a.BACKSLASH_ESCAPE],
+variants:[a.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/
+}),a.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/
+}),a.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/
+}),a.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/
+}),a.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/
+}),a.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"',
+relevance:0},{begin:"'",end:"'",relevance:0}]},{className:"number",relevance:0,
+beforeMatch:/([^a-zA-Z0-9._])/,variants:[{
+match:/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/},{
+match:/0[xX][0-9a-fA-F]+([pP][+-]?\d+)?[Li]?/},{
+match:/(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?[Li]?/}]},{begin:"%",end:"%"},{
+begin:e(/[a-zA-Z][a-zA-Z_0-9]*/,"\\s+<-\\s+")},{begin:"`",end:"`",contains:[{
+begin:/\\./}]}]}}})());
+hljs.registerLanguage("reasonml",(()=>{"use strict";return e=>{
+const n="~?[a-z$_][0-9a-zA-Z$_]*",a="`?[A-Z$_][0-9a-zA-Z$_]*",s="("+["||","++","**","+.","*","/","*.","/.","..."].map((e=>e.split("").map((e=>"\\"+e)).join(""))).join("|")+"|\\|>|&&|==|===)",i="\\s+"+s+"\\s+",r={
+keyword:"and as asr assert begin class constraint do done downto else end exception external for fun function functor if in include inherit initializer land lazy let lor lsl lsr lxor match method mod module mutable new nonrec object of open or private rec sig struct then to try type val virtual when while with",
+built_in:"array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 ref string unit ",
+literal:"true false"
+},l="\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",t={
+className:"number",relevance:0,variants:[{begin:l},{begin:"\\(-"+l+"\\)"}]},c={
+className:"operator",relevance:0,begin:s},o=[{className:"identifier",
+relevance:0,begin:n},c,t],g=[e.QUOTE_STRING_MODE,c,{className:"module",
+begin:"\\b"+a,returnBegin:!0,end:".",contains:[{className:"identifier",begin:a,
+relevance:0}]}],b=[{className:"module",begin:"\\b"+a,returnBegin:!0,end:".",
+relevance:0,contains:[{className:"identifier",begin:a,relevance:0}]}],m={
+className:"function",relevance:0,keywords:r,variants:[{
+begin:"\\s(\\(\\.?.*?\\)|"+n+")\\s*=>",end:"\\s*=>",returnBegin:!0,relevance:0,
+contains:[{className:"params",variants:[{begin:n},{
+begin:"~?[a-z$_][0-9a-zA-Z$_]*(\\s*:\\s*[a-z$_][0-9a-z$_]*(\\(\\s*('?[a-z$_][0-9a-z$_]*\\s*(,'?[a-z$_][0-9a-z$_]*\\s*)*)?\\))?){0,2}"
+},{begin:/\(\s*\)/}]}]},{begin:"\\s\\(\\.?[^;\\|]*\\)\\s*=>",end:"\\s=>",
+returnBegin:!0,relevance:0,contains:[{className:"params",relevance:0,variants:[{
+begin:n,end:"(,|\\n|\\))",relevance:0,contains:[c,{className:"typing",begin:":",
+end:"(,|\\n)",returnBegin:!0,relevance:0,contains:b}]}]}]},{
+begin:"\\(\\.\\s"+n+"\\)\\s*=>"}]};g.push(m);const d={className:"constructor",
+begin:a+"\\(",end:"\\)",illegal:"\\n",keywords:r,
+contains:[e.QUOTE_STRING_MODE,c,{className:"params",begin:"\\b"+n}]},u={
+className:"pattern-match",begin:"\\|",returnBegin:!0,keywords:r,end:"=>",
+relevance:0,contains:[d,c,{relevance:0,className:"constructor",begin:a}]},v={
+className:"module-access",keywords:r,returnBegin:!0,variants:[{
+begin:"\\b("+a+"\\.)+"+n},{begin:"\\b("+a+"\\.)+\\(",end:"\\)",returnBegin:!0,
+contains:[m,{begin:"\\(",end:"\\)",skip:!0}].concat(g)},{
+begin:"\\b("+a+"\\.)+\\{",end:/\}/}],contains:g};return b.push(v),{
+name:"ReasonML",aliases:["re"],keywords:r,illegal:"(:-|:=|\\$\\{|\\+=)",
+contains:[e.COMMENT("/\\*","\\*/",{illegal:"^(#,\\/\\/)"}),{
+className:"character",begin:"'(\\\\[^']+|[^'])'",illegal:"\\n",relevance:0
+},e.QUOTE_STRING_MODE,{className:"literal",begin:"\\(\\)",relevance:0},{
+className:"literal",begin:"\\[\\|",end:"\\|\\]",relevance:0,contains:o},{
+className:"literal",begin:"\\[",end:"\\]",relevance:0,contains:o},d,{
+className:"operator",begin:i,illegal:"--\x3e",relevance:0
+},t,e.C_LINE_COMMENT_MODE,u,m,{className:"module-def",
+begin:"\\bmodule\\s+"+n+"\\s+"+a+"\\s+=\\s+\\{",end:/\}/,returnBegin:!0,
+keywords:r,relevance:0,contains:[{className:"module",relevance:0,begin:a},{
+begin:/\{/,end:/\}/,skip:!0}].concat(g)},v]}}})());
+hljs.registerLanguage("rib",(()=>{"use strict";return e=>({name:"RenderMan RIB",
+keywords:"ArchiveRecord AreaLightSource Atmosphere Attribute AttributeBegin AttributeEnd Basis Begin Blobby Bound Clipping ClippingPlane Color ColorSamples ConcatTransform Cone CoordinateSystem CoordSysTransform CropWindow Curves Cylinder DepthOfField Detail DetailRange Disk Displacement Display End ErrorHandler Exposure Exterior Format FrameAspectRatio FrameBegin FrameEnd GeneralPolygon GeometricApproximation Geometry Hider Hyperboloid Identity Illuminate Imager Interior LightSource MakeCubeFaceEnvironment MakeLatLongEnvironment MakeShadow MakeTexture Matte MotionBegin MotionEnd NuPatch ObjectBegin ObjectEnd ObjectInstance Opacity Option Orientation Paraboloid Patch PatchMesh Perspective PixelFilter PixelSamples PixelVariance Points PointsGeneralPolygons PointsPolygons Polygon Procedural Projection Quantize ReadArchive RelativeDetail ReverseOrientation Rotate Scale ScreenWindow ShadingInterpolation ShadingRate Shutter Sides Skew SolidBegin SolidEnd Sphere SubdivisionMesh Surface TextureCoordinates Torus Transform TransformBegin TransformEnd TransformPoints Translate TrimCurve WorldBegin WorldEnd",
+illegal:"</",
+contains:[e.HASH_COMMENT_MODE,e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
+})})());
+hljs.registerLanguage("roboconf",(()=>{"use strict";return e=>{
+const n="[a-zA-Z-_][^\\n{]+\\{",a={className:"attribute",begin:/[a-zA-Z-_]+/,
+end:/\s*:/,excludeEnd:!0,starts:{end:";",relevance:0,contains:[{
+className:"variable",begin:/\.[a-zA-Z-_]+/},{className:"keyword",
+begin:/\(optional\)/}]}};return{name:"Roboconf",aliases:["graph","instances"],
+case_insensitive:!0,keywords:"import",contains:[{begin:"^facet "+n,end:/\}/,
+keywords:"facet",contains:[a,e.HASH_COMMENT_MODE]},{begin:"^\\s*instance of "+n,
+end:/\}/,
+keywords:"name count channels instance-data instance-state instance of",
+illegal:/\S/,contains:["self",a,e.HASH_COMMENT_MODE]},{begin:"^"+n,end:/\}/,
+contains:[a,e.HASH_COMMENT_MODE]},e.HASH_COMMENT_MODE]}}})());
+hljs.registerLanguage("routeros",(()=>{"use strict";return e=>{
+const r="foreach do while for if from to step else on-error and or not in",n="true false yes no nothing nil null",i={
+className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)\}/
+}]},s={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,i,{
+className:"variable",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]}]},t={
+className:"string",begin:/'/,end:/'/};return{name:"Microtik RouterOS script",
+aliases:["mikrotik"],case_insensitive:!0,keywords:{$pattern:/:?[\w-]+/,
+literal:n,
+keyword:r+" :"+r.split(" ").join(" :")+" :"+"global local beep delay put len typeof pick log time set find environment terminal error execute parse resolve toarray tobool toid toip toip6 tonum tostr totime".split(" ").join(" :")
+},contains:[{variants:[{begin:/\/\*/,end:/\*\//},{begin:/\/\//,end:/$/},{
+begin:/<\//,end:/>/}],illegal:/./},e.COMMENT("^#","$"),s,t,i,{
+begin:/[\w-]+=([^\s{}[\]()>]+)/,relevance:0,returnBegin:!0,contains:[{
+className:"attribute",begin:/[^=]+/},{begin:/=/,endsWithParent:!0,relevance:0,
+contains:[s,t,i,{className:"literal",begin:"\\b("+n.split(" ").join("|")+")\\b"
+},{begin:/("[^"]*"|[^\s{}[\]]+)/}]}]},{className:"number",begin:/\*[0-9a-fA-F]+/
+},{
+begin:"\\b(add|remove|enable|disable|set|get|print|export|edit|find|run|debug|error|info|warning)([\\s[(\\]|])",
+returnBegin:!0,contains:[{className:"builtin-name",begin:/\w+/}]},{
+className:"built_in",variants:[{
+begin:"(\\.\\./|/|\\s)((traffic-flow|traffic-generator|firewall|scheduler|aaa|accounting|address-list|address|align|area|bandwidth-server|bfd|bgp|bridge|client|clock|community|config|connection|console|customer|default|dhcp-client|dhcp-server|discovery|dns|e-mail|ethernet|filter|firmware|gps|graphing|group|hardware|health|hotspot|identity|igmp-proxy|incoming|instance|interface|ip|ipsec|ipv6|irq|l2tp-server|lcd|ldp|logging|mac-server|mac-winbox|mangle|manual|mirror|mme|mpls|nat|nd|neighbor|network|note|ntp|ospf|ospf-v3|ovpn-server|page|peer|pim|ping|policy|pool|port|ppp|pppoe-client|pptp-server|prefix|profile|proposal|proxy|queue|radius|resource|rip|ripng|route|routing|screen|script|security-profiles|server|service|service-port|settings|shares|smb|sms|sniffer|snmp|snooper|socks|sstp-server|system|tool|tracking|type|upgrade|upnp|user-manager|users|user|vlan|secret|vrrp|watchdog|web-access|wireless|pptp|pppoe|lan|wan|layer7-protocol|lease|simple|raw);?\\s)+"
+},{begin:/\.\./,relevance:0}]}]}}})());
+hljs.registerLanguage("rsl",(()=>{"use strict";return e=>({name:"RenderMan RSL",
+keywords:{
+keyword:"float color point normal vector matrix while for if do return else break extern continue",
+built_in:"abs acos ambient area asin atan atmosphere attribute calculatenormal ceil cellnoise clamp comp concat cos degrees depth Deriv diffuse distance Du Dv environment exp faceforward filterstep floor format fresnel incident length lightsource log match max min mod noise normalize ntransform opposite option phong pnoise pow printf ptlined radians random reflect refract renderinfo round setcomp setxcomp setycomp setzcomp shadow sign sin smoothstep specular specularbrdf spline sqrt step tan texture textureinfo trace transform vtransform xcomp ycomp zcomp"
+},illegal:"</",
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,e.C_NUMBER_MODE,{
+className:"meta",begin:"#",end:"$"},{className:"class",
+beginKeywords:"surface displacement light volume imager",end:"\\("},{
+beginKeywords:"illuminate illuminance gather",end:"\\("}]})})());
+hljs.registerLanguage("ruleslanguage",(()=>{"use strict";return T=>({
+name:"Oracle Rules Language",keywords:{
+keyword:"BILL_PERIOD BILL_START BILL_STOP RS_EFFECTIVE_START RS_EFFECTIVE_STOP RS_JURIS_CODE RS_OPCO_CODE INTDADDATTRIBUTE|5 INTDADDVMSG|5 INTDBLOCKOP|5 INTDBLOCKOPNA|5 INTDCLOSE|5 INTDCOUNT|5 INTDCOUNTSTATUSCODE|5 INTDCREATEMASK|5 INTDCREATEDAYMASK|5 INTDCREATEFACTORMASK|5 INTDCREATEHANDLE|5 INTDCREATEOVERRIDEDAYMASK|5 INTDCREATEOVERRIDEMASK|5 INTDCREATESTATUSCODEMASK|5 INTDCREATETOUPERIOD|5 INTDDELETE|5 INTDDIPTEST|5 INTDEXPORT|5 INTDGETERRORCODE|5 INTDGETERRORMESSAGE|5 INTDISEQUAL|5 INTDJOIN|5 INTDLOAD|5 INTDLOADACTUALCUT|5 INTDLOADDATES|5 INTDLOADHIST|5 INTDLOADLIST|5 INTDLOADLISTDATES|5 INTDLOADLISTENERGY|5 INTDLOADLISTHIST|5 INTDLOADRELATEDCHANNEL|5 INTDLOADSP|5 INTDLOADSTAGING|5 INTDLOADUOM|5 INTDLOADUOMDATES|5 INTDLOADUOMHIST|5 INTDLOADVERSION|5 INTDOPEN|5 INTDREADFIRST|5 INTDREADNEXT|5 INTDRECCOUNT|5 INTDRELEASE|5 INTDREPLACE|5 INTDROLLAVG|5 INTDROLLPEAK|5 INTDSCALAROP|5 INTDSCALE|5 INTDSETATTRIBUTE|5 INTDSETDSTPARTICIPANT|5 INTDSETSTRING|5 INTDSETVALUE|5 INTDSETVALUESTATUS|5 INTDSHIFTSTARTTIME|5 INTDSMOOTH|5 INTDSORT|5 INTDSPIKETEST|5 INTDSUBSET|5 INTDTOU|5 INTDTOURELEASE|5 INTDTOUVALUE|5 INTDUPDATESTATS|5 INTDVALUE|5 STDEV INTDDELETEEX|5 INTDLOADEXACTUAL|5 INTDLOADEXCUT|5 INTDLOADEXDATES|5 INTDLOADEX|5 INTDLOADEXRELATEDCHANNEL|5 INTDSAVEEX|5 MVLOAD|5 MVLOADACCT|5 MVLOADACCTDATES|5 MVLOADACCTHIST|5 MVLOADDATES|5 MVLOADHIST|5 MVLOADLIST|5 MVLOADLISTDATES|5 MVLOADLISTHIST|5 IF FOR NEXT DONE SELECT END CALL ABORT CLEAR CHANNEL FACTOR LIST NUMBER OVERRIDE SET WEEK DISTRIBUTIONNODE ELSE WHEN THEN OTHERWISE IENUM CSV INCLUDE LEAVE RIDER SAVE DELETE NOVALUE SECTION WARN SAVE_UPDATE DETERMINANT LABEL REPORT REVENUE EACH IN FROM TOTAL CHARGE BLOCK AND OR CSV_FILE RATE_CODE AUXILIARY_DEMAND UIDACCOUNT RS BILL_PERIOD_SELECT HOURS_PER_MONTH INTD_ERROR_STOP SEASON_SCHEDULE_NAME ACCOUNTFACTOR ARRAYUPPERBOUND CALLSTOREDPROC GETADOCONNECTION GETCONNECT GETDATASOURCE GETQUALIFIER GETUSERID HASVALUE LISTCOUNT LISTOP LISTUPDATE LISTVALUE PRORATEFACTOR RSPRORATE SETBINPATH SETDBMONITOR WQ_OPEN BILLINGHOURS DATE DATEFROMFLOAT DATETIMEFROMSTRING DATETIMETOSTRING DATETOFLOAT DAY DAYDIFF DAYNAME DBDATETIME HOUR MINUTE MONTH MONTHDIFF MONTHHOURS MONTHNAME ROUNDDATE SAMEWEEKDAYLASTYEAR SECOND WEEKDAY WEEKDIFF YEAR YEARDAY YEARSTR COMPSUM HISTCOUNT HISTMAX HISTMIN HISTMINNZ HISTVALUE MAXNRANGE MAXRANGE MINRANGE COMPIKVA COMPKVA COMPKVARFROMKQKW COMPLF IDATTR FLAG LF2KW LF2KWH MAXKW POWERFACTOR READING2USAGE AVGSEASON MAXSEASON MONTHLYMERGE SEASONVALUE SUMSEASON ACCTREADDATES ACCTTABLELOAD CONFIGADD CONFIGGET CREATEOBJECT CREATEREPORT EMAILCLIENT EXPBLKMDMUSAGE EXPMDMUSAGE EXPORT_USAGE FACTORINEFFECT GETUSERSPECIFIEDSTOP INEFFECT ISHOLIDAY RUNRATE SAVE_PROFILE SETREPORTTITLE USEREXIT WATFORRUNRATE TO TABLE ACOS ASIN ATAN ATAN2 BITAND CEIL COS COSECANT COSH COTANGENT DIVQUOT DIVREM EXP FABS FLOOR FMOD FREPM FREXPN LOG LOG10 MAX MAXN MIN MINNZ MODF POW ROUND ROUND2VALUE ROUNDINT SECANT SIN SINH SQROOT TAN TANH FLOAT2STRING FLOAT2STRINGNC INSTR LEFT LEN LTRIM MID RIGHT RTRIM STRING STRINGNC TOLOWER TOUPPER TRIM NUMDAYS READ_DATE STAGING",
+built_in:"IDENTIFIER OPTIONS XML_ELEMENT XML_OP XML_ELEMENT_OF DOMDOCCREATE DOMDOCLOADFILE DOMDOCLOADXML DOMDOCSAVEFILE DOMDOCGETROOT DOMDOCADDPI DOMNODEGETNAME DOMNODEGETTYPE DOMNODEGETVALUE DOMNODEGETCHILDCT DOMNODEGETFIRSTCHILD DOMNODEGETSIBLING DOMNODECREATECHILDELEMENT DOMNODESETATTRIBUTE DOMNODEGETCHILDELEMENTCT DOMNODEGETFIRSTCHILDELEMENT DOMNODEGETSIBLINGELEMENT DOMNODEGETATTRIBUTECT DOMNODEGETATTRIBUTEI DOMNODEGETATTRIBUTEBYNAME DOMNODEGETBYNAME"
+},
+contains:[T.C_LINE_COMMENT_MODE,T.C_BLOCK_COMMENT_MODE,T.APOS_STRING_MODE,T.QUOTE_STRING_MODE,T.C_NUMBER_MODE,{
+className:"literal",variants:[{begin:"#\\s+",relevance:0},{begin:"#[a-zA-Z .]+"
+}]}]})})());
+hljs.registerLanguage("rust",(()=>{"use strict";return e=>{
+const n="([ui](8|16|32|64|128|size)|f(32|64))?",t="drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!"
+;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",
+keyword:"abstract as async await become box break const continue crate do dyn else enum extern false final fn for if impl in let loop macro match mod move mut override priv pub ref return self Self static struct super trait true try type typeof unsafe unsized use virtual where while yield",
+literal:"true false Some None Ok Err",built_in:t},illegal:"</",
+contains:[e.C_LINE_COMMENT_MODE,e.COMMENT("/\\*","\\*/",{contains:["self"]
+}),e.inherit(e.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{
+className:"string",variants:[{begin:/r(#*)"(.|\n)*?"\1(?!#)/},{
+begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",
+begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{
+begin:"\\b0b([01_]+)"+n},{begin:"\\b0o([0-7_]+)"+n},{
+begin:"\\b0x([A-Fa-f0-9_]+)"+n},{
+begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)"+n}],relevance:0},{
+className:"function",beginKeywords:"fn",end:"(\\(|<)",excludeEnd:!0,
+contains:[e.UNDERSCORE_TITLE_MODE]},{className:"meta",begin:"#!?\\[",end:"\\]",
+contains:[{className:"meta-string",begin:/"/,end:/"/}]},{className:"class",
+beginKeywords:"type",end:";",contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{
+endsParent:!0})],illegal:"\\S"},{className:"class",
+beginKeywords:"trait enum struct union",end:/\{/,
+contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"[\\w\\d]"
+},{begin:e.IDENT_RE+"::",keywords:{built_in:t}},{begin:"->"}]}}})());
+hljs.registerLanguage("sas",(()=>{"use strict";return e=>({name:"SAS",
+aliases:["SAS"],case_insensitive:!0,keywords:{
+literal:"null missing _all_ _automatic_ _character_ _infile_ _n_ _name_ _null_ _numeric_ _user_ _webout_",
+meta:"do if then else end until while abort array attrib by call cards cards4 catname continue datalines datalines4 delete delim delimiter display dm drop endsas error file filename footnote format goto in infile informat input keep label leave length libname link list lostcard merge missing modify options output out page put redirect remove rename replace retain return select set skip startsas stop title update waitsas where window x systask add and alter as cascade check create delete describe distinct drop foreign from group having index insert into in key like message modify msgtype not null on or order primary references reset restrict select set table unique update validate view where"
+},contains:[{className:"keyword",begin:/^\s*(proc [\w\d_]+|data|run|quit)[\s;]/
+},{className:"variable",begin:/&[a-zA-Z_&][a-zA-Z0-9_]*\.?/},{
+className:"emphasis",begin:/^\s*datalines|cards.*;/,end:/^\s*;\s*$/},{
+className:"built_in",
+begin:"%(bquote|nrbquote|cmpres|qcmpres|compstor|datatyp|display|do|else|end|eval|global|goto|if|index|input|keydef|label|left|length|let|local|lowcase|macro|mend|nrbquote|nrquote|nrstr|put|qcmpres|qleft|qlowcase|qscan|qsubstr|qsysfunc|qtrim|quote|qupcase|scan|str|substr|superq|syscall|sysevalf|sysexec|sysfunc|sysget|syslput|sysprod|sysrc|sysrput|then|to|trim|unquote|until|upcase|verify|while|window)"
+},{className:"name",begin:/%[a-zA-Z_][a-zA-Z_0-9]*/},{className:"meta",
+begin:"[^%](abs|addr|airy|arcos|arsin|atan|attrc|attrn|band|betainv|blshift|bnot|bor|brshift|bxor|byte|cdf|ceil|cexist|cinv|close|cnonct|collate|compbl|compound|compress|cos|cosh|css|curobs|cv|daccdb|daccdbsl|daccsl|daccsyd|dacctab|dairy|date|datejul|datepart|datetime|day|dclose|depdb|depdbsl|depdbsl|depsl|depsl|depsyd|depsyd|deptab|deptab|dequote|dhms|dif|digamma|dim|dinfo|dnum|dopen|doptname|doptnum|dread|dropnote|dsname|erf|erfc|exist|exp|fappend|fclose|fcol|fdelete|fetch|fetchobs|fexist|fget|fileexist|filename|fileref|finfo|finv|fipname|fipnamel|fipstate|floor|fnonct|fnote|fopen|foptname|foptnum|fpoint|fpos|fput|fread|frewind|frlen|fsep|fuzz|fwrite|gaminv|gamma|getoption|getvarc|getvarn|hbound|hms|hosthelp|hour|ibessel|index|indexc|indexw|input|inputc|inputn|int|intck|intnx|intrr|irr|jbessel|juldate|kurtosis|lag|lbound|left|length|lgamma|libname|libref|log|log10|log2|logpdf|logpmf|logsdf|lowcase|max|mdy|mean|min|minute|mod|month|mopen|mort|n|netpv|nmiss|normal|note|npv|open|ordinal|pathname|pdf|peek|peekc|pmf|point|poisson|poke|probbeta|probbnml|probchi|probf|probgam|probhypr|probit|probnegb|probnorm|probt|put|putc|putn|qtr|quote|ranbin|rancau|ranexp|rangam|range|rank|rannor|ranpoi|rantbl|rantri|ranuni|repeat|resolve|reverse|rewind|right|round|saving|scan|sdf|second|sign|sin|sinh|skewness|soundex|spedis|sqrt|std|stderr|stfips|stname|stnamel|substr|sum|symget|sysget|sysmsg|sysprod|sysrc|system|tan|tanh|time|timepart|tinv|tnonct|today|translate|tranwrd|trigamma|trim|trimn|trunc|uniform|upcase|uss|var|varfmt|varinfmt|varlabel|varlen|varname|varnum|varray|varrayx|vartype|verify|vformat|vformatd|vformatdx|vformatn|vformatnx|vformatw|vformatwx|vformatx|vinarray|vinarrayx|vinformat|vinformatd|vinformatdx|vinformatn|vinformatnx|vinformatw|vinformatwx|vinformatx|vlabel|vlabelx|vlength|vlengthx|vname|vnamex|vtype|vtypex|weekday|year|yyq|zipfips|zipname|zipnamel|zipstate)[(]"
+},{className:"string",variants:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
+},e.COMMENT("\\*",";"),e.C_BLOCK_COMMENT_MODE]})})());
+hljs.registerLanguage("scala",(()=>{"use strict";return e=>{const n={
+className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:/\$\{/,end:/\}/}]
+},a={className:"string",variants:[{begin:'"""',end:'"""'},{begin:'"',end:'"',
+illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:'[a-z]+"',end:'"',
+illegal:"\\n",contains:[e.BACKSLASH_ESCAPE,n]},{className:"string",
+begin:'[a-z]+"""',end:'"""',contains:[n],relevance:10}]},s={className:"type",
+begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},t={className:"title",
+begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,
+relevance:0},i={className:"class",beginKeywords:"class object trait type",
+end:/[:={\[\n;]/,excludeEnd:!0,
+contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{
+beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,
+excludeEnd:!0,relevance:0,contains:[s]},{className:"params",begin:/\(/,end:/\)/,
+excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},t]},l={
+className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,
+contains:[t]};return{name:"Scala",keywords:{literal:"true false null",
+keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"
+},contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,{className:"symbol",
+begin:"'\\w[\\w\\d_]*(?!')"},s,l,i,e.C_NUMBER_MODE,{className:"meta",
+begin:"@[A-Za-z]+"}]}}})());
+hljs.registerLanguage("scheme",(()=>{"use strict";return e=>{
+const t="[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",n={$pattern:t,
+"builtin-name":"case-lambda call/cc class define-class exit-handler field import inherit init-field interface let*-values let-values let/ec mixin opt-lambda override protect provide public rename require require-for-syntax syntax syntax-case syntax-error unit/sig unless when with-syntax and begin call-with-current-continuation call-with-input-file call-with-output-file case cond define define-syntax delay do dynamic-wind else for-each if lambda let let* let-syntax letrec letrec-syntax map or syntax-rules ' * + , ,@ - ... / ; < <= = => > >= ` abs acos angle append apply asin assoc assq assv atan boolean? caar cadr call-with-input-file call-with-output-file call-with-values car cdddar cddddr cdr ceiling char->integer char-alphabetic? char-ci<=? char-ci<? char-ci=? char-ci>=? char-ci>? char-downcase char-lower-case? char-numeric? char-ready? char-upcase char-upper-case? char-whitespace? char<=? char<? char=? char>=? char>? char? close-input-port close-output-port complex? cons cos current-input-port current-output-port denominator display eof-object? eq? equal? eqv? eval even? exact->inexact exact? exp expt floor force gcd imag-part inexact->exact inexact? input-port? integer->char integer? interaction-environment lcm length list list->string list->vector list-ref list-tail list? load log magnitude make-polar make-rectangular make-string make-vector max member memq memv min modulo negative? newline not null-environment null? number->string number? numerator odd? open-input-file open-output-file output-port? pair? peek-char port? positive? procedure? quasiquote quote quotient rational? rationalize read read-char real-part real? remainder reverse round scheme-report-environment set! set-car! set-cdr! sin sqrt string string->list string->number string->symbol string-append string-ci<=? string-ci<? string-ci=? string-ci>=? string-ci>? string-copy string-fill! string-length string-ref string-set! string<=? string<? string=? string>=? string>? string? substring symbol->string symbol? tan transcript-off transcript-on truncate values vector vector->list vector-fill! vector-length vector-ref vector-set! with-input-from-file with-output-to-file write write-char zero?"
+},r={className:"literal",begin:"(#t|#f|#\\\\"+t+"|#\\\\.)"},a={
+className:"number",variants:[{begin:"(-|\\+)?\\d+([./]\\d+)?",relevance:0},{
+begin:"(-|\\+)?\\d+([./]\\d+)?[+\\-](-|\\+)?\\d+([./]\\d+)?i",relevance:0},{
+begin:"#b[0-1]+(/[0-1]+)?"},{begin:"#o[0-7]+(/[0-7]+)?"},{
+begin:"#x[0-9a-f]+(/[0-9a-f]+)?"}]},i=e.QUOTE_STRING_MODE,c=[e.COMMENT(";","$",{
+relevance:0}),e.COMMENT("#\\|","\\|#")],s={begin:t,relevance:0},l={
+className:"symbol",begin:"'"+t},o={endsWithParent:!0,relevance:0},g={variants:[{
+begin:/'/},{begin:"`"}],contains:[{begin:"\\(",end:"\\)",
+contains:["self",r,i,a,s,l]}]},u={className:"name",relevance:0,begin:t,
+keywords:n},d={variants:[{begin:"\\(",end:"\\)"},{begin:"\\[",end:"\\]"}],
+contains:[{begin:/lambda/,endsWithParent:!0,returnBegin:!0,contains:[u,{
+endsParent:!0,variants:[{begin:/\(/,end:/\)/},{begin:/\[/,end:/\]/}],
+contains:[s]}]},u,o]};return o.contains=[r,a,i,s,l,g,d].concat(c),{
+name:"Scheme",illegal:/\S/,contains:[e.SHEBANG(),a,i,l,g,d].concat(c)}}})());
+hljs.registerLanguage("scilab",(()=>{"use strict";return e=>{
+const n=[e.C_NUMBER_MODE,{className:"string",begin:"'|\"",end:"'|\"",
+contains:[e.BACKSLASH_ESCAPE,{begin:"''"}]}];return{name:"Scilab",
+aliases:["sci"],keywords:{$pattern:/%?\w+/,
+keyword:"abort break case clear catch continue do elseif else endfunction end for function global if pause return resume select try then while",
+literal:"%f %F %t %T %pi %eps %inf %nan %e %i %z %s",
+built_in:"abs and acos asin atan ceil cd chdir clearglobal cosh cos cumprod deff disp error exec execstr exists exp eye gettext floor fprintf fread fsolve imag isdef isempty isinfisnan isvector lasterror length load linspace list listfiles log10 log2 log max min msprintf mclose mopen ones or pathconvert poly printf prod pwd rand real round sinh sin size gsort sprintf sqrt strcat strcmps tring sum system tanh tan type typename warning zeros matrix"
+},illegal:'("|#|/\\*|\\s+/\\w+)',contains:[{className:"function",
+beginKeywords:"function",end:"$",contains:[e.UNDERSCORE_TITLE_MODE,{
+className:"params",begin:"\\(",end:"\\)"}]},{
+begin:"[a-zA-Z_][a-zA-Z_0-9]*[\\.']+",relevance:0},{begin:"\\[",
+end:"\\][\\.']*",relevance:0,contains:n},e.COMMENT("//","$")].concat(n)}}})());
+hljs.registerLanguage("scss",(()=>{"use strict"
+;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],t=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],i=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],r=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","auto","backface-visibility","background","background-attachment","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","clear","clip","clip-path","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","content","counter-increment","counter-reset","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-variant-ligatures","font-variation-settings","font-weight","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inherit","initial","justify-content","left","letter-spacing","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marks","mask","max-height","max-width","min-height","min-width","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","perspective","perspective-origin","pointer-events","position","quotes","resize","right","src","tab-size","table-layout","text-align","text-align-last","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-indent","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","white-space","widows","width","word-break","word-spacing","word-wrap","z-index"].reverse()
+;return a=>{const n=(e=>({IMPORTANT:{className:"meta",begin:"!important"},
+HEXCOLOR:{className:"number",begin:"#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})"},
+ATTRIBUTE_SELECTOR_MODE:{className:"selector-attr",begin:/\[/,end:/\]/,
+illegal:"$",contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]}
+}))(a),l=r,s=i,d="@[a-z-]+",c={className:"variable",
+begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b"};return{name:"SCSS",case_insensitive:!0,
+illegal:"[=/|']",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{
+className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{
+className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0
+},n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag",
+begin:"\\b("+e.join("|")+")\\b",relevance:0},{className:"selector-pseudo",
+begin:":("+s.join("|")+")"},{className:"selector-pseudo",
+begin:"::("+l.join("|")+")"},c,{begin:/\(/,end:/\)/,contains:[a.CSS_NUMBER_MODE]
+},{className:"attribute",begin:"\\b("+o.join("|")+")\\b"},{
+begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"
+},{begin:":",end:";",
+contains:[c,n.HEXCOLOR,a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,n.IMPORTANT]
+},{begin:"@(page|font-face)",lexemes:d,keywords:"@page @font-face"},{begin:"@",
+end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/,
+keyword:"and or not only",attribute:t.join(" ")},contains:[{begin:d,
+className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute"
+},c,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,n.HEXCOLOR,a.CSS_NUMBER_MODE]}]}}
+})());
+hljs.registerLanguage("shell",(()=>{"use strict";return s=>({
+name:"Shell Session",aliases:["console"],contains:[{className:"meta",
+begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#]/,starts:{end:/[^\\](?=\s*$)/,
+subLanguage:"bash"}}]})})());
+hljs.registerLanguage("smali",(()=>{"use strict";return e=>{
+const n=["add","and","cmp","cmpg","cmpl","const","div","double","float","goto","if","int","long","move","mul","neg","new","nop","not","or","rem","return","shl","shr","sput","sub","throw","ushr","xor"]
+;return{name:"Smali",contains:[{className:"string",begin:'"',end:'"',relevance:0
+},e.COMMENT("#","$",{relevance:0}),{className:"keyword",variants:[{
+begin:"\\s*\\.end\\s[a-zA-Z0-9]*"},{begin:"^[ ]*\\.[a-zA-Z]*",relevance:0},{
+begin:"\\s:[a-zA-Z_0-9]*",relevance:0},{
+begin:"\\s(transient|constructor|abstract|final|synthetic|public|private|protected|static|bridge|system)"
+}]},{className:"built_in",variants:[{begin:"\\s("+n.join("|")+")\\s"},{
+begin:"\\s("+n.join("|")+")((-|/)[a-zA-Z0-9]+)+\\s",relevance:10},{
+begin:"\\s(aget|aput|array|check|execute|fill|filled|goto/16|goto/32|iget|instance|invoke|iput|monitor|packed|sget|sparse)((-|/)[a-zA-Z0-9]+)*\\s",
+relevance:10}]},{className:"class",begin:"L[^(;:\n]*;",relevance:0},{
+begin:"[vp][0-9]+"}]}}})());
+hljs.registerLanguage("smalltalk",(()=>{"use strict";return e=>{
+const n="[a-z][a-zA-Z0-9_]*",a={className:"string",begin:"\\$.{1}"},s={
+className:"symbol",begin:"#"+e.UNDERSCORE_IDENT_RE};return{name:"Smalltalk",
+aliases:["st"],keywords:"self super nil true false thisContext",
+contains:[e.COMMENT('"','"'),e.APOS_STRING_MODE,{className:"type",
+begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},{begin:n+":",relevance:0
+},e.C_NUMBER_MODE,s,a,{begin:"\\|[ ]*"+n+"([ ]+"+n+")*[ ]*\\|",returnBegin:!0,
+end:/\|/,illegal:/\S/,contains:[{begin:"(\\|[ ]*)?"+n}]},{begin:"#\\(",
+end:"\\)",contains:[e.APOS_STRING_MODE,a,e.C_NUMBER_MODE,s]}]}}})());
+hljs.registerLanguage("sml",(()=>{"use strict";return e=>({
+name:"SML (Standard ML)",aliases:["ml"],keywords:{$pattern:"[a-z_]\\w*!?",
+keyword:"abstype and andalso as case datatype do else end eqtype exception fn fun functor handle if in include infix infixr let local nonfix of op open orelse raise rec sharing sig signature struct structure then type val with withtype where while",
+built_in:"array bool char exn int list option order real ref string substring vector unit word",
+literal:"true false NONE SOME LESS EQUAL GREATER nil"},illegal:/\/\/|>>/,
+contains:[{className:"literal",begin:/\[(\|\|)?\]|\(\)/,relevance:0
+},e.COMMENT("\\(\\*","\\*\\)",{contains:["self"]}),{className:"symbol",
+begin:"'[A-Za-z_](?!')[\\w']*"},{className:"type",begin:"`[A-Z][\\w']*"},{
+className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},{
+begin:"[a-z_]\\w*'[\\w']*"},e.inherit(e.APOS_STRING_MODE,{className:"string",
+relevance:0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null}),{className:"number",
+begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",
+relevance:0},{begin:/[-=]>/}]})})());
+hljs.registerLanguage("soy",(()=>{"use strict";return e=>({
+contains:[e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE,{begin:/\{delpackage/,
+ends:/\}/,relevance:10,keywords:"delpackage"},{begin:/\{namespace/,end:/\}/,
+relevance:10,keywords:"namespace autoescape",contains:[e.QUOTE_STRING_MODE]},{
+className:"template-tag",begin:/\{template/,end:/\{\/template\}/,relevance:10,
+keywords:"alias as autoescape call case default delcall else elseif fallbackmsg foreach if ifempty let msg namespace param print switch template",
+contains:[e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE,e.QUOTE_STRING_MODE,{
+className:"template-variable",relevance:0,begin:/\$[^}\s]+/},{className:"tag",
+begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0
+},{endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",
+begin:"[A-Za-z0-9\\._:-]+",relevance:0},{begin:/=\s*/,relevance:0,contains:[{
+className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/},{begin:/'/,
+end:/'/},{begin:/[^\s"'=<>`]+/}]}]}]}]}]}]})})());
+hljs.registerLanguage("sqf",(()=>{"use strict";return e=>{const t={
+className:"string",variants:[{begin:'"',end:'"',contains:[{begin:'""',
+relevance:0}]},{begin:"'",end:"'",contains:[{begin:"''",relevance:0}]}]},a={
+className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
+"meta-keyword":"define undef ifdef ifndef else endif include"},contains:[{
+begin:/\\\n/,relevance:0},e.inherit(t,{className:"meta-string"}),{
+className:"meta-string",begin:/<[^\n>]*>/,end:/$/,illegal:"\\n"
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]};return{name:"SQF",
+aliases:["sqf"],case_insensitive:!0,keywords:{
+keyword:"case catch default do else exit exitWith for forEach from if private switch then throw to try waitUntil while with",
+built_in:"abs accTime acos action actionIDs actionKeys actionKeysImages actionKeysNames actionKeysNamesArray actionName actionParams activateAddons activatedAddons activateKey add3DENConnection add3DENEventHandler add3DENLayer addAction addBackpack addBackpackCargo addBackpackCargoGlobal addBackpackGlobal addCamShake addCuratorAddons addCuratorCameraArea addCuratorEditableObjects addCuratorEditingArea addCuratorPoints addEditorObject addEventHandler addForce addGoggles addGroupIcon addHandgunItem addHeadgear addItem addItemCargo addItemCargoGlobal addItemPool addItemToBackpack addItemToUniform addItemToVest addLiveStats addMagazine addMagazineAmmoCargo addMagazineCargo addMagazineCargoGlobal addMagazineGlobal addMagazinePool addMagazines addMagazineTurret addMenu addMenuItem addMissionEventHandler addMPEventHandler addMusicEventHandler addOwnedMine addPlayerScores addPrimaryWeaponItem addPublicVariableEventHandler addRating addResources addScore addScoreSide addSecondaryWeaponItem addSwitchableUnit addTeamMember addToRemainsCollector addTorque addUniform addVehicle addVest addWaypoint addWeapon addWeaponCargo addWeaponCargoGlobal addWeaponGlobal addWeaponItem addWeaponPool addWeaponTurret admin agent agents AGLToASL aimedAtTarget aimPos airDensityRTD airplaneThrottle airportSide AISFinishHeal alive all3DENEntities allAirports allControls allCurators allCutLayers allDead allDeadMen allDisplays allGroups allMapMarkers allMines allMissionObjects allow3DMode allowCrewInImmobile allowCuratorLogicIgnoreAreas allowDamage allowDammage allowFileOperations allowFleeing allowGetIn allowSprint allPlayers allSimpleObjects allSites allTurrets allUnits allUnitsUAV allVariables ammo ammoOnPylon and animate animateBay animateDoor animatePylon animateSource animationNames animationPhase animationSourcePhase animationState append apply armoryPoints arrayIntersect asin ASLToAGL ASLToATL assert assignAsCargo assignAsCargoIndex assignAsCommander assignAsDriver assignAsGunner assignAsTurret assignCurator assignedCargo assignedCommander assignedDriver assignedGunner assignedItems assignedTarget assignedTeam assignedVehicle assignedVehicleRole assignItem assignTeam assignToAirport atan atan2 atg ATLToASL attachedObject attachedObjects attachedTo attachObject attachTo attackEnabled backpack backpackCargo backpackContainer backpackItems backpackMagazines backpackSpaceFor behaviour benchmark binocular boundingBox boundingBoxReal boundingCenter breakOut breakTo briefingName buildingExit buildingPos buttonAction buttonSetAction cadetMode call callExtension camCommand camCommit camCommitPrepared camCommitted camConstuctionSetParams camCreate camDestroy cameraEffect cameraEffectEnableHUD cameraInterest cameraOn cameraView campaignConfigFile camPreload camPreloaded camPrepareBank camPrepareDir camPrepareDive camPrepareFocus camPrepareFov camPrepareFovRange camPreparePos camPrepareRelPos camPrepareTarget camSetBank camSetDir camSetDive camSetFocus camSetFov camSetFovRange camSetPos camSetRelPos camSetTarget camTarget camUseNVG canAdd canAddItemToBackpack canAddItemToUniform canAddItemToVest cancelSimpleTaskDestination canFire canMove canSlingLoad canStand canSuspend canTriggerDynamicSimulation canUnloadInCombat canVehicleCargo captive captiveNum cbChecked cbSetChecked ceil channelEnabled cheatsEnabled checkAIFeature checkVisibility className clearAllItemsFromBackpack clearBackpackCargo clearBackpackCargoGlobal clearGroupIcons clearItemCargo clearItemCargoGlobal clearItemPool clearMagazineCargo clearMagazineCargoGlobal clearMagazinePool clearOverlay clearRadio clearWeaponCargo clearWeaponCargoGlobal clearWeaponPool clientOwner closeDialog closeDisplay closeOverlay collapseObjectTree collect3DENHistory collectiveRTD combatMode commandArtilleryFire commandChat commander commandFire commandFollow commandFSM commandGetOut commandingMenu commandMove commandRadio commandStop commandSuppressiveFire commandTarget commandWatch comment commitOverlay compile compileFinal completedFSM composeText configClasses configFile configHierarchy configName configProperties configSourceAddonList configSourceMod configSourceModList confirmSensorTarget connectTerminalToUAV controlsGroupCtrl copyFromClipboard copyToClipboard copyWaypoints cos count countEnemy countFriendly countSide countType countUnknown create3DENComposition create3DENEntity createAgent createCenter createDialog createDiaryLink createDiaryRecord createDiarySubject createDisplay createGearDialog createGroup createGuardedPoint createLocation createMarker createMarkerLocal createMenu createMine createMissionDisplay createMPCampaignDisplay createSimpleObject createSimpleTask createSite createSoundSource createTask createTeam createTrigger createUnit createVehicle createVehicleCrew createVehicleLocal crew ctAddHeader ctAddRow ctClear ctCurSel ctData ctFindHeaderRows ctFindRowHeader ctHeaderControls ctHeaderCount ctRemoveHeaders ctRemoveRows ctrlActivate ctrlAddEventHandler ctrlAngle ctrlAutoScrollDelay ctrlAutoScrollRewind ctrlAutoScrollSpeed ctrlChecked ctrlClassName ctrlCommit ctrlCommitted ctrlCreate ctrlDelete ctrlEnable ctrlEnabled ctrlFade ctrlHTMLLoaded ctrlIDC ctrlIDD ctrlMapAnimAdd ctrlMapAnimClear ctrlMapAnimCommit ctrlMapAnimDone ctrlMapCursor ctrlMapMouseOver ctrlMapScale ctrlMapScreenToWorld ctrlMapWorldToScreen ctrlModel ctrlModelDirAndUp ctrlModelScale ctrlParent ctrlParentControlsGroup ctrlPosition ctrlRemoveAllEventHandlers ctrlRemoveEventHandler ctrlScale ctrlSetActiveColor ctrlSetAngle ctrlSetAutoScrollDelay ctrlSetAutoScrollRewind ctrlSetAutoScrollSpeed ctrlSetBackgroundColor ctrlSetChecked ctrlSetEventHandler ctrlSetFade ctrlSetFocus ctrlSetFont ctrlSetFontH1 ctrlSetFontH1B ctrlSetFontH2 ctrlSetFontH2B ctrlSetFontH3 ctrlSetFontH3B ctrlSetFontH4 ctrlSetFontH4B ctrlSetFontH5 ctrlSetFontH5B ctrlSetFontH6 ctrlSetFontH6B ctrlSetFontHeight ctrlSetFontHeightH1 ctrlSetFontHeightH2 ctrlSetFontHeightH3 ctrlSetFontHeightH4 ctrlSetFontHeightH5 ctrlSetFontHeightH6 ctrlSetFontHeightSecondary ctrlSetFontP ctrlSetFontPB ctrlSetFontSecondary ctrlSetForegroundColor ctrlSetModel ctrlSetModelDirAndUp ctrlSetModelScale ctrlSetPixelPrecision ctrlSetPosition ctrlSetScale ctrlSetStructuredText ctrlSetText ctrlSetTextColor ctrlSetTooltip ctrlSetTooltipColorBox ctrlSetTooltipColorShade ctrlSetTooltipColorText ctrlShow ctrlShown ctrlText ctrlTextHeight ctrlTextWidth ctrlType ctrlVisible ctRowControls ctRowCount ctSetCurSel ctSetData ctSetHeaderTemplate ctSetRowTemplate ctSetValue ctValue curatorAddons curatorCamera curatorCameraArea curatorCameraAreaCeiling curatorCoef curatorEditableObjects curatorEditingArea curatorEditingAreaType curatorMouseOver curatorPoints curatorRegisteredObjects curatorSelected curatorWaypointCost current3DENOperation currentChannel currentCommand currentMagazine currentMagazineDetail currentMagazineDetailTurret currentMagazineTurret currentMuzzle currentNamespace currentTask currentTasks currentThrowable currentVisionMode currentWaypoint currentWeapon currentWeaponMode currentWeaponTurret currentZeroing cursorObject cursorTarget customChat customRadio cutFadeOut cutObj cutRsc cutText damage date dateToNumber daytime deActivateKey debriefingText debugFSM debugLog deg delete3DENEntities deleteAt deleteCenter deleteCollection deleteEditorObject deleteGroup deleteGroupWhenEmpty deleteIdentity deleteLocation deleteMarker deleteMarkerLocal deleteRange deleteResources deleteSite deleteStatus deleteTeam deleteVehicle deleteVehicleCrew deleteWaypoint detach detectedMines diag_activeMissionFSMs diag_activeScripts diag_activeSQFScripts diag_activeSQSScripts diag_captureFrame diag_captureFrameToFile diag_captureSlowFrame diag_codePerformance diag_drawMode diag_enable diag_enabled diag_fps diag_fpsMin diag_frameNo diag_lightNewLoad diag_list diag_log diag_logSlowFrame diag_mergeConfigFile diag_recordTurretLimits diag_setLightNew diag_tickTime diag_toggle dialog diarySubjectExists didJIP didJIPOwner difficulty difficultyEnabled difficultyEnabledRTD difficultyOption direction directSay disableAI disableCollisionWith disableConversation disableDebriefingStats disableMapIndicators disableNVGEquipment disableRemoteSensors disableSerialization disableTIEquipment disableUAVConnectability disableUserInput displayAddEventHandler displayCtrl displayParent displayRemoveAllEventHandlers displayRemoveEventHandler displaySetEventHandler dissolveTeam distance distance2D distanceSqr distributionRegion do3DENAction doArtilleryFire doFire doFollow doFSM doGetOut doMove doorPhase doStop doSuppressiveFire doTarget doWatch drawArrow drawEllipse drawIcon drawIcon3D drawLine drawLine3D drawLink drawLocation drawPolygon drawRectangle drawTriangle driver drop dynamicSimulationDistance dynamicSimulationDistanceCoef dynamicSimulationEnabled dynamicSimulationSystemEnabled echo edit3DENMissionAttributes editObject editorSetEventHandler effectiveCommander emptyPositions enableAI enableAIFeature enableAimPrecision enableAttack enableAudioFeature enableAutoStartUpRTD enableAutoTrimRTD enableCamShake enableCaustics enableChannel enableCollisionWith enableCopilot enableDebriefingStats enableDiagLegend enableDynamicSimulation enableDynamicSimulationSystem enableEndDialog enableEngineArtillery enableEnvironment enableFatigue enableGunLights enableInfoPanelComponent enableIRLasers enableMimics enablePersonTurret enableRadio enableReload enableRopeAttach enableSatNormalOnDetail enableSaving enableSentences enableSimulation enableSimulationGlobal enableStamina enableTeamSwitch enableTraffic enableUAVConnectability enableUAVWaypoints enableVehicleCargo enableVehicleSensor enableWeaponDisassembly endLoadingScreen endMission engineOn enginesIsOnRTD enginesRpmRTD enginesTorqueRTD entities environmentEnabled estimatedEndServerTime estimatedTimeLeft evalObjectArgument everyBackpack everyContainer exec execEditorScript execFSM execVM exp expectedDestination exportJIPMessages eyeDirection eyePos face faction fadeMusic fadeRadio fadeSound fadeSpeech failMission fillWeaponsFromPool find findCover findDisplay findEditorObject findEmptyPosition findEmptyPositionReady findIf findNearestEnemy finishMissionInit finite fire fireAtTarget firstBackpack flag flagAnimationPhase flagOwner flagSide flagTexture fleeing floor flyInHeight flyInHeightASL fog fogForecast fogParams forceAddUniform forcedMap forceEnd forceFlagTexture forceFollowRoad forceMap forceRespawn forceSpeed forceWalk forceWeaponFire forceWeatherChange forEachMember forEachMemberAgent forEachMemberTeam forgetTarget format formation formationDirection formationLeader formationMembers formationPosition formationTask formatText formLeader freeLook fromEditor fuel fullCrew gearIDCAmmoCount gearSlotAmmoCount gearSlotData get3DENActionState get3DENAttribute get3DENCamera get3DENConnections get3DENEntity get3DENEntityID get3DENGrid get3DENIconsVisible get3DENLayerEntities get3DENLinesVisible get3DENMissionAttribute get3DENMouseOver get3DENSelected getAimingCoef getAllEnvSoundControllers getAllHitPointsDamage getAllOwnedMines getAllSoundControllers getAmmoCargo getAnimAimPrecision getAnimSpeedCoef getArray getArtilleryAmmo getArtilleryComputerSettings getArtilleryETA getAssignedCuratorLogic getAssignedCuratorUnit getBackpackCargo getBleedingRemaining getBurningValue getCameraViewDirection getCargoIndex getCenterOfMass getClientState getClientStateNumber getCompatiblePylonMagazines getConnectedUAV getContainerMaxLoad getCursorObjectParams getCustomAimCoef getDammage getDescription getDir getDirVisual getDLCAssetsUsage getDLCAssetsUsageByName getDLCs getEditorCamera getEditorMode getEditorObjectScope getElevationOffset getEnvSoundController getFatigue getForcedFlagTexture getFriend getFSMVariable getFuelCargo getGroupIcon getGroupIconParams getGroupIcons getHideFrom getHit getHitIndex getHitPointDamage getItemCargo getMagazineCargo getMarkerColor getMarkerPos getMarkerSize getMarkerType getMass getMissionConfig getMissionConfigValue getMissionDLCs getMissionLayerEntities getModelInfo getMousePosition getMusicPlayedTime getNumber getObjectArgument getObjectChildren getObjectDLC getObjectMaterials getObjectProxy getObjectTextures getObjectType getObjectViewDistance getOxygenRemaining getPersonUsedDLCs getPilotCameraDirection getPilotCameraPosition getPilotCameraRotation getPilotCameraTarget getPlateNumber getPlayerChannel getPlayerScores getPlayerUID getPos getPosASL getPosASLVisual getPosASLW getPosATL getPosATLVisual getPosVisual getPosWorld getPylonMagazines getRelDir getRelPos getRemoteSensorsDisabled getRepairCargo getResolution getShadowDistance getShotParents getSlingLoad getSoundController getSoundControllerResult getSpeed getStamina getStatValue getSuppression getTerrainGrid getTerrainHeightASL getText getTotalDLCUsageTime getUnitLoadout getUnitTrait getUserMFDText getUserMFDvalue getVariable getVehicleCargo getWeaponCargo getWeaponSway getWingsOrientationRTD getWingsPositionRTD getWPPos glanceAt globalChat globalRadio goggles goto group groupChat groupFromNetId groupIconSelectable groupIconsVisible groupId groupOwner groupRadio groupSelectedUnits groupSelectUnit gunner gusts halt handgunItems handgunMagazine handgunWeapon handsHit hasInterface hasPilotCamera hasWeapon hcAllGroups hcGroupParams hcLeader hcRemoveAllGroups hcRemoveGroup hcSelected hcSelectGroup hcSetGroup hcShowBar hcShownBar headgear hideBody hideObject hideObjectGlobal hideSelection hint hintC hintCadet hintSilent hmd hostMission htmlLoad HUDMovementLevels humidity image importAllGroups importance in inArea inAreaArray incapacitatedState inflame inflamed infoPanel infoPanelComponentEnabled infoPanelComponents infoPanels inGameUISetEventHandler inheritsFrom initAmbientLife inPolygon inputAction inRangeOfArtillery insertEditorObject intersect is3DEN is3DENMultiplayer isAbleToBreathe isAgent isArray isAutoHoverOn isAutonomous isAutotest isBleeding isBurning isClass isCollisionLightOn isCopilotEnabled isDamageAllowed isDedicated isDLCAvailable isEngineOn isEqualTo isEqualType isEqualTypeAll isEqualTypeAny isEqualTypeArray isEqualTypeParams isFilePatchingEnabled isFlashlightOn isFlatEmpty isForcedWalk isFormationLeader isGroupDeletedWhenEmpty isHidden isInRemainsCollector isInstructorFigureEnabled isIRLaserOn isKeyActive isKindOf isLaserOn isLightOn isLocalized isManualFire isMarkedForCollection isMultiplayer isMultiplayerSolo isNil isNull isNumber isObjectHidden isObjectRTD isOnRoad isPipEnabled isPlayer isRealTime isRemoteExecuted isRemoteExecutedJIP isServer isShowing3DIcons isSimpleObject isSprintAllowed isStaminaEnabled isSteamMission isStreamFriendlyUIEnabled isText isTouchingGround isTurnedOut isTutHintsEnabled isUAVConnectable isUAVConnected isUIContext isUniformAllowed isVehicleCargo isVehicleRadarOn isVehicleSensorEnabled isWalking isWeaponDeployed isWeaponRested itemCargo items itemsWithMagazines join joinAs joinAsSilent joinSilent joinString kbAddDatabase kbAddDatabaseTargets kbAddTopic kbHasTopic kbReact kbRemoveTopic kbTell kbWasSaid keyImage keyName knowsAbout land landAt landResult language laserTarget lbAdd lbClear lbColor lbColorRight lbCurSel lbData lbDelete lbIsSelected lbPicture lbPictureRight lbSelection lbSetColor lbSetColorRight lbSetCurSel lbSetData lbSetPicture lbSetPictureColor lbSetPictureColorDisabled lbSetPictureColorSelected lbSetPictureRight lbSetPictureRightColor lbSetPictureRightColorDisabled lbSetPictureRightColorSelected lbSetSelectColor lbSetSelectColorRight lbSetSelected lbSetText lbSetTextRight lbSetTooltip lbSetValue lbSize lbSort lbSortByValue lbText lbTextRight lbValue leader leaderboardDeInit leaderboardGetRows leaderboardInit leaderboardRequestRowsFriends leaderboardsRequestUploadScore leaderboardsRequestUploadScoreKeepBest leaderboardState leaveVehicle libraryCredits libraryDisclaimers lifeState lightAttachObject lightDetachObject lightIsOn lightnings limitSpeed linearConversion lineIntersects lineIntersectsObjs lineIntersectsSurfaces lineIntersectsWith linkItem list listObjects listRemoteTargets listVehicleSensors ln lnbAddArray lnbAddColumn lnbAddRow lnbClear lnbColor lnbCurSelRow lnbData lnbDeleteColumn lnbDeleteRow lnbGetColumnsPosition lnbPicture lnbSetColor lnbSetColumnsPos lnbSetCurSelRow lnbSetData lnbSetPicture lnbSetText lnbSetValue lnbSize lnbSort lnbSortByValue lnbText lnbValue load loadAbs loadBackpack loadFile loadGame loadIdentity loadMagazine loadOverlay loadStatus loadUniform loadVest local localize locationPosition lock lockCameraTo lockCargo lockDriver locked lockedCargo lockedDriver lockedTurret lockIdentity lockTurret lockWP log logEntities logNetwork logNetworkTerminate lookAt lookAtPos magazineCargo magazines magazinesAllTurrets magazinesAmmo magazinesAmmoCargo magazinesAmmoFull magazinesDetail magazinesDetailBackpack magazinesDetailUniform magazinesDetailVest magazinesTurret magazineTurretAmmo mapAnimAdd mapAnimClear mapAnimCommit mapAnimDone mapCenterOnCamera mapGridPosition markAsFinishedOnSteam markerAlpha markerBrush markerColor markerDir markerPos markerShape markerSize markerText markerType max members menuAction menuAdd menuChecked menuClear menuCollapse menuData menuDelete menuEnable menuEnabled menuExpand menuHover menuPicture menuSetAction menuSetCheck menuSetData menuSetPicture menuSetValue menuShortcut menuShortcutText menuSize menuSort menuText menuURL menuValue min mineActive mineDetectedBy missionConfigFile missionDifficulty missionName missionNamespace missionStart missionVersion mod modelToWorld modelToWorldVisual modelToWorldVisualWorld modelToWorldWorld modParams moonIntensity moonPhase morale move move3DENCamera moveInAny moveInCargo moveInCommander moveInDriver moveInGunner moveInTurret moveObjectToEnd moveOut moveTime moveTo moveToCompleted moveToFailed musicVolume name nameSound nearEntities nearestBuilding nearestLocation nearestLocations nearestLocationWithDubbing nearestObject nearestObjects nearestTerrainObjects nearObjects nearObjectsReady nearRoads nearSupplies nearTargets needReload netId netObjNull newOverlay nextMenuItemIndex nextWeatherChange nMenuItems not numberOfEnginesRTD numberToDate objectCurators objectFromNetId objectParent objStatus onBriefingGroup onBriefingNotes onBriefingPlan onBriefingTeamSwitch onCommandModeChanged onDoubleClick onEachFrame onGroupIconClick onGroupIconOverEnter onGroupIconOverLeave onHCGroupSelectionChanged onMapSingleClick onPlayerConnected onPlayerDisconnected onPreloadFinished onPreloadStarted onShowNewObject onTeamSwitch openCuratorInterface openDLCPage openMap openSteamApp openYoutubeVideo or orderGetIn overcast overcastForecast owner param params parseNumber parseSimpleArray parseText parsingNamespace particlesQuality pickWeaponPool pitch pixelGrid pixelGridBase pixelGridNoUIScale pixelH pixelW playableSlotsNumber playableUnits playAction playActionNow player playerRespawnTime playerSide playersNumber playGesture playMission playMove playMoveNow playMusic playScriptedMission playSound playSound3D position positionCameraToWorld posScreenToWorld posWorldToScreen ppEffectAdjust ppEffectCommit ppEffectCommitted ppEffectCreate ppEffectDestroy ppEffectEnable ppEffectEnabled ppEffectForceInNVG precision preloadCamera preloadObject preloadSound preloadTitleObj preloadTitleRsc preprocessFile preprocessFileLineNumbers primaryWeapon primaryWeaponItems primaryWeaponMagazine priority processDiaryLink productVersion profileName profileNamespace profileNameSteam progressLoadingScreen progressPosition progressSetPosition publicVariable publicVariableClient publicVariableServer pushBack pushBackUnique putWeaponPool queryItemsPool queryMagazinePool queryWeaponPool rad radioChannelAdd radioChannelCreate radioChannelRemove radioChannelSetCallSign radioChannelSetLabel radioVolume rain rainbow random rank rankId rating rectangular registeredTasks registerTask reload reloadEnabled remoteControl remoteExec remoteExecCall remoteExecutedOwner remove3DENConnection remove3DENEventHandler remove3DENLayer removeAction removeAll3DENEventHandlers removeAllActions removeAllAssignedItems removeAllContainers removeAllCuratorAddons removeAllCuratorCameraAreas removeAllCuratorEditingAreas removeAllEventHandlers removeAllHandgunItems removeAllItems removeAllItemsWithMagazines removeAllMissionEventHandlers removeAllMPEventHandlers removeAllMusicEventHandlers removeAllOwnedMines removeAllPrimaryWeaponItems removeAllWeapons removeBackpack removeBackpackGlobal removeCuratorAddons removeCuratorCameraArea removeCuratorEditableObjects removeCuratorEditingArea removeDrawIcon removeDrawLinks removeEventHandler removeFromRemainsCollector removeGoggles removeGroupIcon removeHandgunItem removeHeadgear removeItem removeItemFromBackpack removeItemFromUniform removeItemFromVest removeItems removeMagazine removeMagazineGlobal removeMagazines removeMagazinesTurret removeMagazineTurret removeMenuItem removeMissionEventHandler removeMPEventHandler removeMusicEventHandler removeOwnedMine removePrimaryWeaponItem removeSecondaryWeaponItem removeSimpleTask removeSwitchableUnit removeTeamMember removeUniform removeVest removeWeapon removeWeaponAttachmentCargo removeWeaponCargo removeWeaponGlobal removeWeaponTurret reportRemoteTarget requiredVersion resetCamShake resetSubgroupDirection resize resources respawnVehicle restartEditorCamera reveal revealMine reverse reversedMouseY roadAt roadsConnectedTo roleDescription ropeAttachedObjects ropeAttachedTo ropeAttachEnabled ropeAttachTo ropeCreate ropeCut ropeDestroy ropeDetach ropeEndPosition ropeLength ropes ropeUnwind ropeUnwound rotorsForcesRTD rotorsRpmRTD round runInitScript safeZoneH safeZoneW safeZoneWAbs safeZoneX safeZoneXAbs safeZoneY save3DENInventory saveGame saveIdentity saveJoysticks saveOverlay saveProfileNamespace saveStatus saveVar savingEnabled say say2D say3D scopeName score scoreSide screenshot screenToWorld scriptDone scriptName scudState secondaryWeapon secondaryWeaponItems secondaryWeaponMagazine select selectBestPlaces selectDiarySubject selectedEditorObjects selectEditorObject selectionNames selectionPosition selectLeader selectMax selectMin selectNoPlayer selectPlayer selectRandom selectRandomWeighted selectWeapon selectWeaponTurret sendAUMessage sendSimpleCommand sendTask sendTaskResult sendUDPMessage serverCommand serverCommandAvailable serverCommandExecutable serverName serverTime set set3DENAttribute set3DENAttributes set3DENGrid set3DENIconsVisible set3DENLayer set3DENLinesVisible set3DENLogicType set3DENMissionAttribute set3DENMissionAttributes set3DENModelsVisible set3DENObjectType set3DENSelected setAccTime setActualCollectiveRTD setAirplaneThrottle setAirportSide setAmmo setAmmoCargo setAmmoOnPylon setAnimSpeedCoef setAperture setApertureNew setArmoryPoints setAttributes setAutonomous setBehaviour setBleedingRemaining setBrakesRTD setCameraInterest setCamShakeDefParams setCamShakeParams setCamUseTI setCaptive setCenterOfMass setCollisionLight setCombatMode setCompassOscillation setConvoySeparation setCuratorCameraAreaCeiling setCuratorCoef setCuratorEditingAreaType setCuratorWaypointCost setCurrentChannel setCurrentTask setCurrentWaypoint setCustomAimCoef setCustomWeightRTD setDamage setDammage setDate setDebriefingText setDefaultCamera setDestination setDetailMapBlendPars setDir setDirection setDrawIcon setDriveOnPath setDropInterval setDynamicSimulationDistance setDynamicSimulationDistanceCoef setEditorMode setEditorObjectScope setEffectCondition setEngineRPMRTD setFace setFaceAnimation setFatigue setFeatureType setFlagAnimationPhase setFlagOwner setFlagSide setFlagTexture setFog setFormation setFormationTask setFormDir setFriend setFromEditor setFSMVariable setFuel setFuelCargo setGroupIcon setGroupIconParams setGroupIconsSelectable setGroupIconsVisible setGroupId setGroupIdGlobal setGroupOwner setGusts setHideBehind setHit setHitIndex setHitPointDamage setHorizonParallaxCoef setHUDMovementLevels setIdentity setImportance setInfoPanel setLeader setLightAmbient setLightAttenuation setLightBrightness setLightColor setLightDayLight setLightFlareMaxDistance setLightFlareSize setLightIntensity setLightnings setLightUseFlare setLocalWindParams setMagazineTurretAmmo setMarkerAlpha setMarkerAlphaLocal setMarkerBrush setMarkerBrushLocal setMarkerColor setMarkerColorLocal setMarkerDir setMarkerDirLocal setMarkerPos setMarkerPosLocal setMarkerShape setMarkerShapeLocal setMarkerSize setMarkerSizeLocal setMarkerText setMarkerTextLocal setMarkerType setMarkerTypeLocal setMass setMimic setMousePosition setMusicEffect setMusicEventHandler setName setNameSound setObjectArguments setObjectMaterial setObjectMaterialGlobal setObjectProxy setObjectTexture setObjectTextureGlobal setObjectViewDistance setOvercast setOwner setOxygenRemaining setParticleCircle setParticleClass setParticleFire setParticleParams setParticleRandom setPilotCameraDirection setPilotCameraRotation setPilotCameraTarget setPilotLight setPiPEffect setPitch setPlateNumber setPlayable setPlayerRespawnTime setPos setPosASL setPosASL2 setPosASLW setPosATL setPosition setPosWorld setPylonLoadOut setPylonsPriority setRadioMsg setRain setRainbow setRandomLip setRank setRectangular setRepairCargo setRotorBrakeRTD setShadowDistance setShotParents setSide setSimpleTaskAlwaysVisible setSimpleTaskCustomData setSimpleTaskDescription setSimpleTaskDestination setSimpleTaskTarget setSimpleTaskType setSimulWeatherLayers setSize setSkill setSlingLoad setSoundEffect setSpeaker setSpeech setSpeedMode setStamina setStaminaScheme setStatValue setSuppression setSystemOfUnits setTargetAge setTaskMarkerOffset setTaskResult setTaskState setTerrainGrid setText setTimeMultiplier setTitleEffect setTrafficDensity setTrafficDistance setTrafficGap setTrafficSpeed setTriggerActivation setTriggerArea setTriggerStatements setTriggerText setTriggerTimeout setTriggerType setType setUnconscious setUnitAbility setUnitLoadout setUnitPos setUnitPosWeak setUnitRank setUnitRecoilCoefficient setUnitTrait setUnloadInCombat setUserActionText setUserMFDText setUserMFDvalue setVariable setVectorDir setVectorDirAndUp setVectorUp setVehicleAmmo setVehicleAmmoDef setVehicleArmor setVehicleCargo setVehicleId setVehicleLock setVehiclePosition setVehicleRadar setVehicleReceiveRemoteTargets setVehicleReportOwnPosition setVehicleReportRemoteTargets setVehicleTIPars setVehicleVarName setVelocity setVelocityModelSpace setVelocityTransformation setViewDistance setVisibleIfTreeCollapsed setWantedRPMRTD setWaves setWaypointBehaviour setWaypointCombatMode setWaypointCompletionRadius setWaypointDescription setWaypointForceBehaviour setWaypointFormation setWaypointHousePosition setWaypointLoiterRadius setWaypointLoiterType setWaypointName setWaypointPosition setWaypointScript setWaypointSpeed setWaypointStatements setWaypointTimeout setWaypointType setWaypointVisible setWeaponReloadingTime setWind setWindDir setWindForce setWindStr setWingForceScaleRTD setWPPos show3DIcons showChat showCinemaBorder showCommandingMenu showCompass showCuratorCompass showGPS showHUD showLegend showMap shownArtilleryComputer shownChat shownCompass shownCuratorCompass showNewEditorObject shownGPS shownHUD shownMap shownPad shownRadio shownScoretable shownUAVFeed shownWarrant shownWatch showPad showRadio showScoretable showSubtitles showUAVFeed showWarrant showWatch showWaypoint showWaypoints side sideChat sideEnemy sideFriendly sideRadio simpleTasks simulationEnabled simulCloudDensity simulCloudOcclusion simulInClouds simulWeatherSync sin size sizeOf skill skillFinal skipTime sleep sliderPosition sliderRange sliderSetPosition sliderSetRange sliderSetSpeed sliderSpeed slingLoadAssistantShown soldierMagazines someAmmo sort soundVolume spawn speaker speed speedMode splitString sqrt squadParams stance startLoadingScreen step stop stopEngineRTD stopped str sunOrMoon supportInfo suppressFor surfaceIsWater surfaceNormal surfaceType swimInDepth switchableUnits switchAction switchCamera switchGesture switchLight switchMove synchronizedObjects synchronizedTriggers synchronizedWaypoints synchronizeObjectsAdd synchronizeObjectsRemove synchronizeTrigger synchronizeWaypoint systemChat systemOfUnits tan targetKnowledge targets targetsAggregate targetsQuery taskAlwaysVisible taskChildren taskCompleted taskCustomData taskDescription taskDestination taskHint taskMarkerOffset taskParent taskResult taskState taskType teamMember teamName teams teamSwitch teamSwitchEnabled teamType terminate terrainIntersect terrainIntersectASL terrainIntersectAtASL text textLog textLogFormat tg time timeMultiplier titleCut titleFadeOut titleObj titleRsc titleText toArray toFixed toLower toString toUpper triggerActivated triggerActivation triggerArea triggerAttachedVehicle triggerAttachObject triggerAttachVehicle triggerDynamicSimulation triggerStatements triggerText triggerTimeout triggerTimeoutCurrent triggerType turretLocal turretOwner turretUnit tvAdd tvClear tvCollapse tvCollapseAll tvCount tvCurSel tvData tvDelete tvExpand tvExpandAll tvPicture tvSetColor tvSetCurSel tvSetData tvSetPicture tvSetPictureColor tvSetPictureColorDisabled tvSetPictureColorSelected tvSetPictureRight tvSetPictureRightColor tvSetPictureRightColorDisabled tvSetPictureRightColorSelected tvSetText tvSetTooltip tvSetValue tvSort tvSortByValue tvText tvTooltip tvValue type typeName typeOf UAVControl uiNamespace uiSleep unassignCurator unassignItem unassignTeam unassignVehicle underwater uniform uniformContainer uniformItems uniformMagazines unitAddons unitAimPosition unitAimPositionVisual unitBackpack unitIsUAV unitPos unitReady unitRecoilCoefficient units unitsBelowHeight unlinkItem unlockAchievement unregisterTask updateDrawIcon updateMenuItem updateObjectTree useAISteeringComponent useAudioTimeForMoves userInputDisabled vectorAdd vectorCos vectorCrossProduct vectorDiff vectorDir vectorDirVisual vectorDistance vectorDistanceSqr vectorDotProduct vectorFromTo vectorMagnitude vectorMagnitudeSqr vectorModelToWorld vectorModelToWorldVisual vectorMultiply vectorNormalized vectorUp vectorUpVisual vectorWorldToModel vectorWorldToModelVisual vehicle vehicleCargoEnabled vehicleChat vehicleRadio vehicleReceiveRemoteTargets vehicleReportOwnPosition vehicleReportRemoteTargets vehicles vehicleVarName velocity velocityModelSpace verifySignature vest vestContainer vestItems vestMagazines viewDistance visibleCompass visibleGPS visibleMap visiblePosition visiblePositionASL visibleScoretable visibleWatch waves waypointAttachedObject waypointAttachedVehicle waypointAttachObject waypointAttachVehicle waypointBehaviour waypointCombatMode waypointCompletionRadius waypointDescription waypointForceBehaviour waypointFormation waypointHousePosition waypointLoiterRadius waypointLoiterType waypointName waypointPosition waypoints waypointScript waypointsEnabledUAV waypointShow waypointSpeed waypointStatements waypointTimeout waypointTimeoutCurrent waypointType waypointVisible weaponAccessories weaponAccessoriesCargo weaponCargo weaponDirection weaponInertia weaponLowered weapons weaponsItems weaponsItemsCargo weaponState weaponsTurret weightRTD WFSideText wind ",
+literal:"blufor civilian configNull controlNull displayNull east endl false grpNull independent lineBreak locationNull nil objNull opfor pi resistance scriptNull sideAmbientLife sideEmpty sideLogic sideUnknown taskNull teamMemberNull true west"
+},contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.NUMBER_MODE,{
+className:"variable",begin:/\b_+[a-zA-Z]\w*/},{className:"title",
+begin:/[a-zA-Z][a-zA-Z0-9]+_fnc_\w*/},t,a],illegal:/#|^\$ /}}})());
+hljs.registerLanguage("sql_more",(()=>{"use strict";return e=>{
+var t=e.COMMENT("--","$");return{name:"SQL (more)",aliases:["mysql","oracle"],
+disableAutodetect:!0,case_insensitive:!0,illegal:/[<>{}*]/,contains:[{
+beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",
+end:/;/,endsWithParent:!0,keywords:{$pattern:/[\w\.]+/,
+keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",
+literal:"true false null unknown",
+built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varchar2 varying void"
+},contains:[{className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{
+className:"string",begin:'"',end:'"',contains:[{begin:'""'}]},{
+className:"string",begin:"`",end:"`"
+},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]
+},e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]}}})());
+hljs.registerLanguage("sql",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function r(...r){
+return r.map((r=>e(r))).join("")}function t(...r){
+return"("+r.map((r=>e(r))).join("|")+")"}return e=>{
+const n=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],s=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],o=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],c=s,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update   ","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!s.includes(e))),u={
+begin:r(/\b/,t(...c),/\s*\(/),keywords:{built_in:c}};return{name:"SQL",
+case_insensitive:!0,illegal:/[{}]|<\//,keywords:{$pattern:/\b[\w\.]+/,
+keyword:((e,{exceptions:r,when:t}={})=>{const n=t
+;return r=r||[],e.map((e=>e.match(/\|\d+$/)||r.includes(e)?e:n(e)?e+"|0":e))
+})(l,{when:e=>e.length<3}),literal:a,type:i,
+built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"]
+},contains:[{begin:t(...o),keywords:{$pattern:/[\w\.]+/,keyword:l.concat(o),
+literal:a,type:i}},{className:"type",
+begin:t("double precision","large object","with timezone","without timezone")
+},u,{className:"variable",begin:/@[a-z0-9]+/},{className:"string",variants:[{
+begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/,contains:[{
+begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,n,{className:"operator",
+begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/,relevance:0}]}}})());
+hljs.registerLanguage("stan",(()=>{"use strict";return _=>({name:"Stan",
+aliases:["stanfuncs"],keywords:{$pattern:_.IDENT_RE,
+title:["functions","model","data","parameters","quantities","transformed","generated"],
+keyword:["for","in","if","else","while","break","continue","return"].concat(["int","real","vector","ordered","positive_ordered","simplex","unit_vector","row_vector","matrix","cholesky_factor_corr|10","cholesky_factor_cov|10","corr_matrix|10","cov_matrix|10","void"]).concat(["print","reject","increment_log_prob|10","integrate_ode|10","integrate_ode_rk45|10","integrate_ode_bdf|10","algebra_solver"]),
+built_in:["Phi","Phi_approx","abs","acos","acosh","algebra_solver","append_array","append_col","append_row","asin","asinh","atan","atan2","atanh","bernoulli_cdf","bernoulli_lccdf","bernoulli_lcdf","bernoulli_logit_lpmf","bernoulli_logit_rng","bernoulli_lpmf","bernoulli_rng","bessel_first_kind","bessel_second_kind","beta_binomial_cdf","beta_binomial_lccdf","beta_binomial_lcdf","beta_binomial_lpmf","beta_binomial_rng","beta_cdf","beta_lccdf","beta_lcdf","beta_lpdf","beta_rng","binary_log_loss","binomial_cdf","binomial_coefficient_log","binomial_lccdf","binomial_lcdf","binomial_logit_lpmf","binomial_lpmf","binomial_rng","block","categorical_logit_lpmf","categorical_logit_rng","categorical_lpmf","categorical_rng","cauchy_cdf","cauchy_lccdf","cauchy_lcdf","cauchy_lpdf","cauchy_rng","cbrt","ceil","chi_square_cdf","chi_square_lccdf","chi_square_lcdf","chi_square_lpdf","chi_square_rng","cholesky_decompose","choose","col","cols","columns_dot_product","columns_dot_self","cos","cosh","cov_exp_quad","crossprod","csr_extract_u","csr_extract_v","csr_extract_w","csr_matrix_times_vector","csr_to_dense_matrix","cumulative_sum","determinant","diag_matrix","diag_post_multiply","diag_pre_multiply","diagonal","digamma","dims","dirichlet_lpdf","dirichlet_rng","distance","dot_product","dot_self","double_exponential_cdf","double_exponential_lccdf","double_exponential_lcdf","double_exponential_lpdf","double_exponential_rng","e","eigenvalues_sym","eigenvectors_sym","erf","erfc","exp","exp2","exp_mod_normal_cdf","exp_mod_normal_lccdf","exp_mod_normal_lcdf","exp_mod_normal_lpdf","exp_mod_normal_rng","expm1","exponential_cdf","exponential_lccdf","exponential_lcdf","exponential_lpdf","exponential_rng","fabs","falling_factorial","fdim","floor","fma","fmax","fmin","fmod","frechet_cdf","frechet_lccdf","frechet_lcdf","frechet_lpdf","frechet_rng","gamma_cdf","gamma_lccdf","gamma_lcdf","gamma_lpdf","gamma_p","gamma_q","gamma_rng","gaussian_dlm_obs_lpdf","get_lp","gumbel_cdf","gumbel_lccdf","gumbel_lcdf","gumbel_lpdf","gumbel_rng","head","hypergeometric_lpmf","hypergeometric_rng","hypot","inc_beta","int_step","integrate_ode","integrate_ode_bdf","integrate_ode_rk45","inv","inv_Phi","inv_chi_square_cdf","inv_chi_square_lccdf","inv_chi_square_lcdf","inv_chi_square_lpdf","inv_chi_square_rng","inv_cloglog","inv_gamma_cdf","inv_gamma_lccdf","inv_gamma_lcdf","inv_gamma_lpdf","inv_gamma_rng","inv_logit","inv_sqrt","inv_square","inv_wishart_lpdf","inv_wishart_rng","inverse","inverse_spd","is_inf","is_nan","lbeta","lchoose","lgamma","lkj_corr_cholesky_lpdf","lkj_corr_cholesky_rng","lkj_corr_lpdf","lkj_corr_rng","lmgamma","lmultiply","log","log10","log1m","log1m_exp","log1m_inv_logit","log1p","log1p_exp","log2","log_determinant","log_diff_exp","log_falling_factorial","log_inv_logit","log_mix","log_rising_factorial","log_softmax","log_sum_exp","logistic_cdf","logistic_lccdf","logistic_lcdf","logistic_lpdf","logistic_rng","logit","lognormal_cdf","lognormal_lccdf","lognormal_lcdf","lognormal_lpdf","lognormal_rng","machine_precision","matrix_exp","max","mdivide_left_spd","mdivide_left_tri_low","mdivide_right_spd","mdivide_right_tri_low","mean","min","modified_bessel_first_kind","modified_bessel_second_kind","multi_gp_cholesky_lpdf","multi_gp_lpdf","multi_normal_cholesky_lpdf","multi_normal_cholesky_rng","multi_normal_lpdf","multi_normal_prec_lpdf","multi_normal_rng","multi_student_t_lpdf","multi_student_t_rng","multinomial_lpmf","multinomial_rng","multiply_log","multiply_lower_tri_self_transpose","neg_binomial_2_cdf","neg_binomial_2_lccdf","neg_binomial_2_lcdf","neg_binomial_2_log_lpmf","neg_binomial_2_log_rng","neg_binomial_2_lpmf","neg_binomial_2_rng","neg_binomial_cdf","neg_binomial_lccdf","neg_binomial_lcdf","neg_binomial_lpmf","neg_binomial_rng","negative_infinity","normal_cdf","normal_lccdf","normal_lcdf","normal_lpdf","normal_rng","not_a_number","num_elements","ordered_logistic_lpmf","ordered_logistic_rng","owens_t","pareto_cdf","pareto_lccdf","pareto_lcdf","pareto_lpdf","pareto_rng","pareto_type_2_cdf","pareto_type_2_lccdf","pareto_type_2_lcdf","pareto_type_2_lpdf","pareto_type_2_rng","pi","poisson_cdf","poisson_lccdf","poisson_lcdf","poisson_log_lpmf","poisson_log_rng","poisson_lpmf","poisson_rng","positive_infinity","pow","print","prod","qr_Q","qr_R","quad_form","quad_form_diag","quad_form_sym","rank","rayleigh_cdf","rayleigh_lccdf","rayleigh_lcdf","rayleigh_lpdf","rayleigh_rng","reject","rep_array","rep_matrix","rep_row_vector","rep_vector","rising_factorial","round","row","rows","rows_dot_product","rows_dot_self","scaled_inv_chi_square_cdf","scaled_inv_chi_square_lccdf","scaled_inv_chi_square_lcdf","scaled_inv_chi_square_lpdf","scaled_inv_chi_square_rng","sd","segment","sin","singular_values","sinh","size","skew_normal_cdf","skew_normal_lccdf","skew_normal_lcdf","skew_normal_lpdf","skew_normal_rng","softmax","sort_asc","sort_desc","sort_indices_asc","sort_indices_desc","sqrt","sqrt2","square","squared_distance","step","student_t_cdf","student_t_lccdf","student_t_lcdf","student_t_lpdf","student_t_rng","sub_col","sub_row","sum","tail","tan","tanh","target","tcrossprod","tgamma","to_array_1d","to_array_2d","to_matrix","to_row_vector","to_vector","trace","trace_gen_quad_form","trace_quad_form","trigamma","trunc","uniform_cdf","uniform_lccdf","uniform_lcdf","uniform_lpdf","uniform_rng","variance","von_mises_lpdf","von_mises_rng","weibull_cdf","weibull_lccdf","weibull_lcdf","weibull_lpdf","weibull_rng","wiener_lpdf","wishart_lpdf","wishart_rng"]
+},contains:[_.C_LINE_COMMENT_MODE,_.COMMENT(/#/,/$/,{relevance:0,keywords:{
+"meta-keyword":"include"}}),_.COMMENT(/\/\*/,/\*\//,{relevance:0,contains:[{
+className:"doctag",begin:/@(return|param)/}]}),{begin:/<\s*lower\s*=/,
+keywords:"lower"},{begin:/[<,]\s*upper\s*=/,keywords:"upper"},{
+className:"keyword",begin:/\btarget\s*\+=/,relevance:10},{
+begin:"~\\s*("+_.IDENT_RE+")\\s*\\(",
+keywords:["bernoulli","bernoulli_logit","beta","beta_binomial","binomial","binomial_logit","categorical","categorical_logit","cauchy","chi_square","dirichlet","double_exponential","exp_mod_normal","exponential","frechet","gamma","gaussian_dlm_obs","gumbel","hypergeometric","inv_chi_square","inv_gamma","inv_wishart","lkj_corr","lkj_corr_cholesky","logistic","lognormal","multi_gp","multi_gp_cholesky","multi_normal","multi_normal_cholesky","multi_normal_prec","multi_student_t","multinomial","neg_binomial","neg_binomial_2","neg_binomial_2_log","normal","ordered_logistic","pareto","pareto_type_2","poisson","poisson_log","rayleigh","scaled_inv_chi_square","skew_normal","student_t","uniform","von_mises","weibull","wiener","wishart"]
+},{className:"number",variants:[{begin:/\b\d+(?:\.\d*)?(?:[eE][+-]?\d+)?/},{
+begin:/\.\d+(?:[eE][+-]?\d+)?\b/}],relevance:0},{className:"string",begin:'"',
+end:'"',relevance:0}]})})());
+hljs.registerLanguage("stata",(()=>{"use strict";return e=>({name:"Stata",
+aliases:["do","ado"],case_insensitive:!0,
+keywords:"if else in foreach for forv forva forval forvalu forvalue forvalues by bys bysort xi quietly qui capture about ac ac_7 acprplot acprplot_7 adjust ado adopath adoupdate alpha ameans an ano anov anova anova_estat anova_terms anovadef aorder ap app appe appen append arch arch_dr arch_estat arch_p archlm areg areg_p args arima arima_dr arima_estat arima_p as asmprobit asmprobit_estat asmprobit_lf asmprobit_mfx__dlg asmprobit_p ass asse asser assert avplot avplot_7 avplots avplots_7 bcskew0 bgodfrey bias binreg bip0_lf biplot bipp_lf bipr_lf bipr_p biprobit bitest bitesti bitowt blogit bmemsize boot bootsamp bootstrap bootstrap_8 boxco_l boxco_p boxcox boxcox_6 boxcox_p bprobit br break brier bro brow brows browse brr brrstat bs bs_7 bsampl_w bsample bsample_7 bsqreg bstat bstat_7 bstat_8 bstrap bstrap_7 bubble bubbleplot ca ca_estat ca_p cabiplot camat canon canon_8 canon_8_p canon_estat canon_p cap caprojection capt captu captur capture cat cc cchart cchart_7 cci cd censobs_table centile cf char chdir checkdlgfiles checkestimationsample checkhlpfiles checksum chelp ci cii cl class classutil clear cli clis clist clo clog clog_lf clog_p clogi clogi_sw clogit clogit_lf clogit_p clogitp clogl_sw cloglog clonevar clslistarray cluster cluster_measures cluster_stop cluster_tree cluster_tree_8 clustermat cmdlog cnr cnre cnreg cnreg_p cnreg_sw cnsreg codebook collaps4 collapse colormult_nb colormult_nw compare compress conf confi confir confirm conren cons const constr constra constrai constrain constraint continue contract copy copyright copysource cor corc corr corr2data corr_anti corr_kmo corr_smc corre correl correla correlat correlate corrgram cou coun count cox cox_p cox_sw coxbase coxhaz coxvar cprplot cprplot_7 crc cret cretu cretur creturn cross cs cscript cscript_log csi ct ct_is ctset ctst_5 ctst_st cttost cumsp cumsp_7 cumul cusum cusum_7 cutil d|0 datasig datasign datasigna datasignat datasignatu datasignatur datasignature datetof db dbeta de dec deco decod decode deff des desc descr descri describ describe destring dfbeta dfgls dfuller di di_g dir dirstats dis discard disp disp_res disp_s displ displa display distinct do doe doed doedi doedit dotplot dotplot_7 dprobit drawnorm drop ds ds_util dstdize duplicates durbina dwstat dydx e|0 ed edi edit egen eivreg emdef en enc enco encod encode eq erase ereg ereg_lf ereg_p ereg_sw ereghet ereghet_glf ereghet_glf_sh ereghet_gp ereghet_ilf ereghet_ilf_sh ereghet_ip eret eretu eretur ereturn err erro error esize est est_cfexist est_cfname est_clickable est_expand est_hold est_table est_unhold est_unholdok estat estat_default estat_summ estat_vce_only esti estimates etodow etof etomdy ex exi exit expand expandcl fac fact facto factor factor_estat factor_p factor_pca_rotated factor_rotate factormat fcast fcast_compute fcast_graph fdades fdadesc fdadescr fdadescri fdadescrib fdadescribe fdasav fdasave fdause fh_st file open file read file close file filefilter fillin find_hlp_file findfile findit findit_7 fit fl fli flis flist for5_0 forest forestplot form forma format fpredict frac_154 frac_adj frac_chk frac_cox frac_ddp frac_dis frac_dv frac_in frac_mun frac_pp frac_pq frac_pv frac_wgt frac_xo fracgen fracplot fracplot_7 fracpoly fracpred fron_ex fron_hn fron_p fron_tn fron_tn2 frontier ftodate ftoe ftomdy ftowdate funnel funnelplot g|0 gamhet_glf gamhet_gp gamhet_ilf gamhet_ip gamma gamma_d2 gamma_p gamma_sw gammahet gdi_hexagon gdi_spokes ge gen gene gener genera generat generate genrank genstd genvmean gettoken gl gladder gladder_7 glim_l01 glim_l02 glim_l03 glim_l04 glim_l05 glim_l06 glim_l07 glim_l08 glim_l09 glim_l10 glim_l11 glim_l12 glim_lf glim_mu glim_nw1 glim_nw2 glim_nw3 glim_p glim_v1 glim_v2 glim_v3 glim_v4 glim_v5 glim_v6 glim_v7 glm glm_6 glm_p glm_sw glmpred glo glob globa global glogit glogit_8 glogit_p gmeans gnbre_lf gnbreg gnbreg_5 gnbreg_p gomp_lf gompe_sw gomper_p gompertz gompertzhet gomphet_glf gomphet_glf_sh gomphet_gp gomphet_ilf gomphet_ilf_sh gomphet_ip gphdot gphpen gphprint gprefs gprobi_p gprobit gprobit_8 gr gr7 gr_copy gr_current gr_db gr_describe gr_dir gr_draw gr_draw_replay gr_drop gr_edit gr_editviewopts gr_example gr_example2 gr_export gr_print gr_qscheme gr_query gr_read gr_rename gr_replay gr_save gr_set gr_setscheme gr_table gr_undo gr_use graph graph7 grebar greigen greigen_7 greigen_8 grmeanby grmeanby_7 gs_fileinfo gs_filetype gs_graphinfo gs_stat gsort gwood h|0 hadimvo hareg hausman haver he heck_d2 heckma_p heckman heckp_lf heckpr_p heckprob hel help hereg hetpr_lf hetpr_p hetprob hettest hexdump hilite hist hist_7 histogram hlogit hlu hmeans hotel hotelling hprobit hreg hsearch icd9 icd9_ff icd9p iis impute imtest inbase include inf infi infil infile infix inp inpu input ins insheet insp inspe inspec inspect integ inten intreg intreg_7 intreg_p intrg2_ll intrg_ll intrg_ll2 ipolate iqreg ir irf irf_create irfm iri is_svy is_svysum isid istdize ivprob_1_lf ivprob_lf ivprobit ivprobit_p ivreg ivreg_footnote ivtob_1_lf ivtob_lf ivtobit ivtobit_p jackknife jacknife jknife jknife_6 jknife_8 jkstat joinby kalarma1 kap kap_3 kapmeier kappa kapwgt kdensity kdensity_7 keep ksm ksmirnov ktau kwallis l|0 la lab labbe labbeplot labe label labelbook ladder levels levelsof leverage lfit lfit_p li lincom line linktest lis list lloghet_glf lloghet_glf_sh lloghet_gp lloghet_ilf lloghet_ilf_sh lloghet_ip llogi_sw llogis_p llogist llogistic llogistichet lnorm_lf lnorm_sw lnorma_p lnormal lnormalhet lnormhet_glf lnormhet_glf_sh lnormhet_gp lnormhet_ilf lnormhet_ilf_sh lnormhet_ip lnskew0 loadingplot loc loca local log logi logis_lf logistic logistic_p logit logit_estat logit_p loglogs logrank loneway lookfor lookup lowess lowess_7 lpredict lrecomp lroc lroc_7 lrtest ls lsens lsens_7 lsens_x lstat ltable ltable_7 ltriang lv lvr2plot lvr2plot_7 m|0 ma mac macr macro makecns man manova manova_estat manova_p manovatest mantel mark markin markout marksample mat mat_capp mat_order mat_put_rr mat_rapp mata mata_clear mata_describe mata_drop mata_matdescribe mata_matsave mata_matuse mata_memory mata_mlib mata_mosave mata_rename mata_which matalabel matcproc matlist matname matr matri matrix matrix_input__dlg matstrik mcc mcci md0_ md1_ md1debug_ md2_ md2debug_ mds mds_estat mds_p mdsconfig mdslong mdsmat mdsshepard mdytoe mdytof me_derd mean means median memory memsize menl meqparse mer merg merge meta mfp mfx mhelp mhodds minbound mixed_ll mixed_ll_reparm mkassert mkdir mkmat mkspline ml ml_5 ml_adjs ml_bhhhs ml_c_d ml_check ml_clear ml_cnt ml_debug ml_defd ml_e0 ml_e0_bfgs ml_e0_cycle ml_e0_dfp ml_e0i ml_e1 ml_e1_bfgs ml_e1_bhhh ml_e1_cycle ml_e1_dfp ml_e2 ml_e2_cycle ml_ebfg0 ml_ebfr0 ml_ebfr1 ml_ebh0q ml_ebhh0 ml_ebhr0 ml_ebr0i ml_ecr0i ml_edfp0 ml_edfr0 ml_edfr1 ml_edr0i ml_eds ml_eer0i ml_egr0i ml_elf ml_elf_bfgs ml_elf_bhhh ml_elf_cycle ml_elf_dfp ml_elfi ml_elfs ml_enr0i ml_enrr0 ml_erdu0 ml_erdu0_bfgs ml_erdu0_bhhh ml_erdu0_bhhhq ml_erdu0_cycle ml_erdu0_dfp ml_erdu0_nrbfgs ml_exde ml_footnote ml_geqnr ml_grad0 ml_graph ml_hbhhh ml_hd0 ml_hold ml_init ml_inv ml_log ml_max ml_mlout ml_mlout_8 ml_model ml_nb0 ml_opt ml_p ml_plot ml_query ml_rdgrd ml_repor ml_s_e ml_score ml_searc ml_technique ml_unhold mleval mlf_ mlmatbysum mlmatsum mlog mlogi mlogit mlogit_footnote mlogit_p mlopts mlsum mlvecsum mnl0_ mor more mov move mprobit mprobit_lf mprobit_p mrdu0_ mrdu1_ mvdecode mvencode mvreg mvreg_estat n|0 nbreg nbreg_al nbreg_lf nbreg_p nbreg_sw nestreg net newey newey_7 newey_p news nl nl_7 nl_9 nl_9_p nl_p nl_p_7 nlcom nlcom_p nlexp2 nlexp2_7 nlexp2a nlexp2a_7 nlexp3 nlexp3_7 nlgom3 nlgom3_7 nlgom4 nlgom4_7 nlinit nllog3 nllog3_7 nllog4 nllog4_7 nlog_rd nlogit nlogit_p nlogitgen nlogittree nlpred no nobreak noi nois noisi noisil noisily note notes notes_dlg nptrend numlabel numlist odbc old_ver olo olog ologi ologi_sw ologit ologit_p ologitp on one onew onewa oneway op_colnm op_comp op_diff op_inv op_str opr opro oprob oprob_sw oprobi oprobi_p oprobit oprobitp opts_exclusive order orthog orthpoly ou out outf outfi outfil outfile outs outsh outshe outshee outsheet ovtest pac pac_7 palette parse parse_dissim pause pca pca_8 pca_display pca_estat pca_p pca_rotate pcamat pchart pchart_7 pchi pchi_7 pcorr pctile pentium pergram pergram_7 permute permute_8 personal peto_st pkcollapse pkcross pkequiv pkexamine pkexamine_7 pkshape pksumm pksumm_7 pl plo plot plugin pnorm pnorm_7 poisgof poiss_lf poiss_sw poisso_p poisson poisson_estat post postclose postfile postutil pperron pr prais prais_e prais_e2 prais_p predict predictnl preserve print pro prob probi probit probit_estat probit_p proc_time procoverlay procrustes procrustes_estat procrustes_p profiler prog progr progra program prop proportion prtest prtesti pwcorr pwd q\\s qby qbys qchi qchi_7 qladder qladder_7 qnorm qnorm_7 qqplot qqplot_7 qreg qreg_c qreg_p qreg_sw qu quadchk quantile quantile_7 que quer query range ranksum ratio rchart rchart_7 rcof recast reclink recode reg reg3 reg3_p regdw regr regre regre_p2 regres regres_p regress regress_estat regriv_p remap ren rena renam rename renpfix repeat replace report reshape restore ret retu retur return rm rmdir robvar roccomp roccomp_7 roccomp_8 rocf_lf rocfit rocfit_8 rocgold rocplot rocplot_7 roctab roctab_7 rolling rologit rologit_p rot rota rotat rotate rotatemat rreg rreg_p ru run runtest rvfplot rvfplot_7 rvpplot rvpplot_7 sa safesum sample sampsi sav save savedresults saveold sc sca scal scala scalar scatter scm_mine sco scob_lf scob_p scobi_sw scobit scor score scoreplot scoreplot_help scree screeplot screeplot_help sdtest sdtesti se search separate seperate serrbar serrbar_7 serset set set_defaults sfrancia sh she shel shell shewhart shewhart_7 signestimationsample signrank signtest simul simul_7 simulate simulate_8 sktest sleep slogit slogit_d2 slogit_p smooth snapspan so sor sort spearman spikeplot spikeplot_7 spikeplt spline_x split sqreg sqreg_p sret sretu sretur sreturn ssc st st_ct st_hc st_hcd st_hcd_sh st_is st_issys st_note st_promo st_set st_show st_smpl st_subid stack statsby statsby_8 stbase stci stci_7 stcox stcox_estat stcox_fr stcox_fr_ll stcox_p stcox_sw stcoxkm stcoxkm_7 stcstat stcurv stcurve stcurve_7 stdes stem stepwise stereg stfill stgen stir stjoin stmc stmh stphplot stphplot_7 stphtest stphtest_7 stptime strate strate_7 streg streg_sw streset sts sts_7 stset stsplit stsum sttocc sttoct stvary stweib su suest suest_8 sum summ summa summar summari summariz summarize sunflower sureg survcurv survsum svar svar_p svmat svy svy_disp svy_dreg svy_est svy_est_7 svy_estat svy_get svy_gnbreg_p svy_head svy_header svy_heckman_p svy_heckprob_p svy_intreg_p svy_ivreg_p svy_logistic_p svy_logit_p svy_mlogit_p svy_nbreg_p svy_ologit_p svy_oprobit_p svy_poisson_p svy_probit_p svy_regress_p svy_sub svy_sub_7 svy_x svy_x_7 svy_x_p svydes svydes_8 svygen svygnbreg svyheckman svyheckprob svyintreg svyintreg_7 svyintrg svyivreg svylc svylog_p svylogit svymarkout svymarkout_8 svymean svymlog svymlogit svynbreg svyolog svyologit svyoprob svyoprobit svyopts svypois svypois_7 svypoisson svyprobit svyprobt svyprop svyprop_7 svyratio svyreg svyreg_p svyregress svyset svyset_7 svyset_8 svytab svytab_7 svytest svytotal sw sw_8 swcnreg swcox swereg swilk swlogis swlogit swologit swoprbt swpois swprobit swqreg swtobit swweib symmetry symmi symplot symplot_7 syntax sysdescribe sysdir sysuse szroeter ta tab tab1 tab2 tab_or tabd tabdi tabdis tabdisp tabi table tabodds tabodds_7 tabstat tabu tabul tabula tabulat tabulate te tempfile tempname tempvar tes test testnl testparm teststd tetrachoric time_it timer tis tob tobi tobit tobit_p tobit_sw token tokeni tokeniz tokenize tostring total translate translator transmap treat_ll treatr_p treatreg trim trimfill trnb_cons trnb_mean trpoiss_d2 trunc_ll truncr_p truncreg tsappend tset tsfill tsline tsline_ex tsreport tsrevar tsrline tsset tssmooth tsunab ttest ttesti tut_chk tut_wait tutorial tw tware_st two twoway twoway__fpfit_serset twoway__function_gen twoway__histogram_gen twoway__ipoint_serset twoway__ipoints_serset twoway__kdensity_gen twoway__lfit_serset twoway__normgen_gen twoway__pci_serset twoway__qfit_serset twoway__scatteri_serset twoway__sunflower_gen twoway_ksm_serset ty typ type typeof u|0 unab unabbrev unabcmd update us use uselabel var var_mkcompanion var_p varbasic varfcast vargranger varirf varirf_add varirf_cgraph varirf_create varirf_ctable varirf_describe varirf_dir varirf_drop varirf_erase varirf_graph varirf_ograph varirf_rename varirf_set varirf_table varlist varlmar varnorm varsoc varstable varstable_w varstable_w2 varwle vce vec vec_fevd vec_mkphi vec_p vec_p_w vecirf_create veclmar veclmar_w vecnorm vecnorm_w vecrank vecstable verinst vers versi versio version view viewsource vif vwls wdatetof webdescribe webseek webuse weib1_lf weib2_lf weib_lf weib_lf0 weibhet_glf weibhet_glf_sh weibhet_glfa weibhet_glfa_sh weibhet_gp weibhet_ilf weibhet_ilf_sh weibhet_ilfa weibhet_ilfa_sh weibhet_ip weibu_sw weibul_p weibull weibull_c weibull_s weibullhet wh whelp whi which whil while wilc_st wilcoxon win wind windo window winexec wntestb wntestb_7 wntestq xchart xchart_7 xcorr xcorr_7 xi xi_6 xmlsav xmlsave xmluse xpose xsh xshe xshel xshell xt_iis xt_tis xtab_p xtabond xtbin_p xtclog xtcloglog xtcloglog_8 xtcloglog_d2 xtcloglog_pa_p xtcloglog_re_p xtcnt_p xtcorr xtdata xtdes xtfront_p xtfrontier xtgee xtgee_elink xtgee_estat xtgee_makeivar xtgee_p xtgee_plink xtgls xtgls_p xthaus xthausman xtht_p xthtaylor xtile xtint_p xtintreg xtintreg_8 xtintreg_d2 xtintreg_p xtivp_1 xtivp_2 xtivreg xtline xtline_ex xtlogit xtlogit_8 xtlogit_d2 xtlogit_fe_p xtlogit_pa_p xtlogit_re_p xtmixed xtmixed_estat xtmixed_p xtnb_fe xtnb_lf xtnbreg xtnbreg_pa_p xtnbreg_refe_p xtpcse xtpcse_p xtpois xtpoisson xtpoisson_d2 xtpoisson_pa_p xtpoisson_refe_p xtpred xtprobit xtprobit_8 xtprobit_d2 xtprobit_re_p xtps_fe xtps_lf xtps_ren xtps_ren_8 xtrar_p xtrc xtrc_p xtrchh xtrefe_p xtreg xtreg_be xtreg_fe xtreg_ml xtreg_pa_p xtreg_re xtregar xtrere_p xtset xtsf_ll xtsf_llti xtsum xttab xttest0 xttobit xttobit_8 xttobit_p xttrans yx yxview__barlike_draw yxview_area_draw yxview_bar_draw yxview_dot_draw yxview_dropline_draw yxview_function_draw yxview_iarrow_draw yxview_ilabels_draw yxview_normal_draw yxview_pcarrow_draw yxview_pcbarrow_draw yxview_pccapsym_draw yxview_pcscatter_draw yxview_pcspike_draw yxview_rarea_draw yxview_rbar_draw yxview_rbarm_draw yxview_rcap_draw yxview_rcapsym_draw yxview_rconnected_draw yxview_rline_draw yxview_rscatter_draw yxview_rspike_draw yxview_spike_draw yxview_sunflower_draw zap_s zinb zinb_llf zinb_plf zip zip_llf zip_p zip_plf zt_ct_5 zt_hc_5 zt_hcd_5 zt_is_5 zt_iss_5 zt_sho_5 zt_smp_5 ztbase_5 ztcox_5 ztdes_5 ztereg_5 ztfill_5 ztgen_5 ztir_5 ztjoin_5 ztnb ztnb_p ztp ztp_p zts_5 ztset_5 ztspli_5 ztsum_5 zttoct_5 ztvary_5 ztweib_5",
+contains:[{className:"symbol",begin:/`[a-zA-Z0-9_]+'/},{className:"variable",
+begin:/\$\{?[a-zA-Z0-9_]+\}?/},{className:"string",variants:[{
+begin:'`"[^\r\n]*?"\''},{begin:'"[^\r\n"]*"'}]},{className:"built_in",
+variants:[{
+begin:"\\b(abs|acos|asin|atan|atan2|atanh|ceil|cloglog|comb|cos|digamma|exp|floor|invcloglog|invlogit|ln|lnfact|lnfactorial|lngamma|log|log10|max|min|mod|reldif|round|sign|sin|sqrt|sum|tan|tanh|trigamma|trunc|betaden|Binomial|binorm|binormal|chi2|chi2tail|dgammapda|dgammapdada|dgammapdadx|dgammapdx|dgammapdxdx|F|Fden|Ftail|gammaden|gammap|ibeta|invbinomial|invchi2|invchi2tail|invF|invFtail|invgammap|invibeta|invnchi2|invnFtail|invnibeta|invnorm|invnormal|invttail|nbetaden|nchi2|nFden|nFtail|nibeta|norm|normal|normalden|normd|npnchi2|tden|ttail|uniform|abbrev|char|index|indexnot|length|lower|ltrim|match|plural|proper|real|regexm|regexr|regexs|reverse|rtrim|string|strlen|strlower|strltrim|strmatch|strofreal|strpos|strproper|strreverse|strrtrim|strtrim|strupper|subinstr|subinword|substr|trim|upper|word|wordcount|_caller|autocode|byteorder|chop|clip|cond|e|epsdouble|epsfloat|group|inlist|inrange|irecode|matrix|maxbyte|maxdouble|maxfloat|maxint|maxlong|mi|minbyte|mindouble|minfloat|minint|minlong|missing|r|recode|replay|return|s|scalar|d|date|day|dow|doy|halfyear|mdy|month|quarter|week|year|d|daily|dofd|dofh|dofm|dofq|dofw|dofy|h|halfyearly|hofd|m|mofd|monthly|q|qofd|quarterly|tin|twithin|w|weekly|wofd|y|yearly|yh|ym|yofd|yq|yw|cholesky|colnumb|colsof|corr|det|diag|diag0cnt|el|get|hadamard|I|inv|invsym|issym|issymmetric|J|matmissing|matuniform|mreldif|nullmat|rownumb|rowsof|sweep|syminv|trace|vec|vecdiag)(?=\\()"
+}]},e.COMMENT("^[ \t]*\\*.*$",!1),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]
+})})());
+hljs.registerLanguage("step21",(()=>{"use strict";return e=>({
+name:"STEP Part 21",aliases:["p21","step","stp"],case_insensitive:!0,keywords:{
+$pattern:"[A-Z_][A-Z0-9_.]*",keyword:"HEADER ENDSEC DATA"},contains:[{
+className:"meta",begin:"ISO-10303-21;",relevance:10},{className:"meta",
+begin:"END-ISO-10303-21;",relevance:10
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.COMMENT("/\\*\\*!","\\*/"),e.C_NUMBER_MODE,e.inherit(e.APOS_STRING_MODE,{
+illegal:null}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null}),{
+className:"string",begin:"'",end:"'"},{className:"symbol",variants:[{begin:"#",
+end:"\\d+",illegal:"\\W"}]}]})})());
+hljs.registerLanguage("stylus",(()=>{"use strict"
+;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],t=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],i=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],o=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],r=["align-content","align-items","align-self","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","auto","backface-visibility","background","background-attachment","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","clear","clip","clip-path","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","content","counter-increment","counter-reset","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-variant-ligatures","font-variation-settings","font-weight","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inherit","initial","justify-content","left","letter-spacing","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marks","mask","max-height","max-width","min-height","min-width","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","perspective","perspective-origin","pointer-events","position","quotes","resize","right","src","tab-size","table-layout","text-align","text-align-last","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-indent","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","white-space","widows","width","word-break","word-spacing","word-wrap","z-index"].reverse()
+;return n=>{const a=(e=>({IMPORTANT:{className:"meta",begin:"!important"},
+HEXCOLOR:{className:"number",begin:"#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})"},
+ATTRIBUTE_SELECTOR_MODE:{className:"selector-attr",begin:/\[/,end:/\]/,
+illegal:"$",contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]}}))(n),s={
+className:"variable",begin:"\\$"+n.IDENT_RE},l="(?=[.\\s\\n[:,(])";return{
+name:"Stylus",aliases:["styl"],case_insensitive:!1,keywords:"if else for in",
+illegal:"(\\?|(\\bReturn\\b)|(\\bEnd\\b)|(\\bend\\b)|(\\bdef\\b)|;|#\\s|\\*\\s|===\\s|\\||%)",
+contains:[n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,a.HEXCOLOR,{
+begin:"\\.[a-zA-Z][a-zA-Z0-9_-]*(?=[.\\s\\n[:,(])",className:"selector-class"},{
+begin:"#[a-zA-Z][a-zA-Z0-9_-]*(?=[.\\s\\n[:,(])",className:"selector-id"},{
+begin:"\\b("+e.join("|")+")"+l,className:"selector-tag"},{
+className:"selector-pseudo",begin:"&?:("+i.join("|")+")"+l},{
+className:"selector-pseudo",begin:"&?::("+o.join("|")+")"+l
+},a.ATTRIBUTE_SELECTOR_MODE,{className:"keyword",begin:/@media/,starts:{
+end:/[{;}]/,keywords:{$pattern:/[a-z-]+/,keyword:"and or not only",
+attribute:t.join(" ")},contains:[n.CSS_NUMBER_MODE]}},{className:"keyword",
+begin:"@((-(o|moz|ms|webkit)-)?(charset|css|debug|extend|font-face|for|import|include|keyframes|media|mixin|page|warn|while))\\b"
+},s,n.CSS_NUMBER_MODE,{className:"function",
+begin:"^[a-zA-Z][a-zA-Z0-9_-]*\\(.*\\)",illegal:"[\\n]",returnBegin:!0,
+contains:[{className:"title",begin:"\\b[a-zA-Z][a-zA-Z0-9_-]*"},{
+className:"params",begin:/\(/,end:/\)/,
+contains:[a.HEXCOLOR,s,n.APOS_STRING_MODE,n.CSS_NUMBER_MODE,n.QUOTE_STRING_MODE]
+}]},{className:"attribute",begin:"\\b("+r.join("|")+")\\b",starts:{end:/;|$/,
+contains:[a.HEXCOLOR,s,n.APOS_STRING_MODE,n.QUOTE_STRING_MODE,n.CSS_NUMBER_MODE,n.C_BLOCK_COMMENT_MODE,a.IMPORTANT],
+illegal:/\./,relevance:0}}]}}})());
+hljs.registerLanguage("subunit",(()=>{"use strict";return s=>({name:"SubUnit",
+case_insensitive:!0,contains:[{className:"string",begin:"\\[\n(multipart)?",
+end:"\\]\n"},{className:"string",
+begin:"\\d{4}-\\d{2}-\\d{2}(\\s+)\\d{2}:\\d{2}:\\d{2}.\\d+Z"},{
+className:"string",begin:"(\\+|-)\\d+"},{className:"keyword",relevance:10,
+variants:[{
+begin:"^(test|testing|success|successful|failure|error|skip|xfail|uxsuccess)(:?)\\s+(test)?"
+},{begin:"^progress(:?)(\\s+)?(pop|push)?"},{begin:"^tags:"},{begin:"^time:"}]}]
+})})());
+hljs.registerLanguage("swift",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function n(e){return a("(?=",e,")")}
+function a(...n){return n.map((n=>e(n))).join("")}function t(...n){
+return"("+n.map((n=>e(n))).join("|")+")"}
+const i=e=>a(/\b/,e,/\w$/.test(e)?/\b/:/\B/),s=["Protocol","Type"].map(i),u=["init","self"].map(i),c=["Any","Self"],r=["associatedtype",/as\?/,/as!/,"as","break","case","catch","class","continue","convenience","default","defer","deinit","didSet","do","dynamic","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","lazy","let","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],o=["false","nil","true"],l=["assignment","associativity","higherThan","left","lowerThan","none","right"],m=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warn_unqualified_access","#warning"],d=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],p=t(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),F=t(p,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),b=a(p,F,"*"),h=t(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),f=t(h,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),w=a(h,f,"*"),y=a(/[A-Z]/,f,"*"),g=["autoclosure",a(/convention\(/,t("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",a(/objc\(/,w,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","testable","UIApplicationMain","unknown","usableFromInline"],E=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"]
+;return e=>{const p={match:/\s+/,relevance:0},h=e.COMMENT("/\\*","\\*/",{
+contains:["self"]}),v=[e.C_LINE_COMMENT_MODE,h],N={className:"keyword",
+begin:a(/\./,n(t(...s,...u))),end:t(...s,...u),excludeBegin:!0},A={
+match:a(/\./,t(...r)),relevance:0
+},C=r.filter((e=>"string"==typeof e)).concat(["_|0"]),_={variants:[{
+className:"keyword",
+match:t(...r.filter((e=>"string"!=typeof e)).concat(c).map(i),...u)}]},D={
+$pattern:t(/\b\w+/,/#\w+/),keyword:C.concat(m),literal:o},B=[N,A,_],k=[{
+match:a(/\./,t(...d)),relevance:0},{className:"built_in",
+match:a(/\b/,t(...d),/(?=\()/)}],M={match:/->/,relevance:0},S=[M,{
+className:"operator",relevance:0,variants:[{match:b},{match:`\\.(\\.|${F})+`}]
+}],x="([0-9a-fA-F]_*)+",I={className:"number",relevance:0,variants:[{
+match:"\\b(([0-9]_*)+)(\\.(([0-9]_*)+))?([eE][+-]?(([0-9]_*)+))?\\b"},{
+match:`\\b0x(${x})(\\.(${x}))?([pP][+-]?(([0-9]_*)+))?\\b`},{
+match:/\b0o([0-7]_*)+\b/},{match:/\b0b([01]_*)+\b/}]},O=(e="")=>({
+className:"subst",variants:[{match:a(/\\/,e,/[0\\tnr"']/)},{
+match:a(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}]}),T=(e="")=>({className:"subst",
+match:a(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/)}),L=(e="")=>({className:"subst",
+label:"interpol",begin:a(/\\/,e,/\(/),end:/\)/}),P=(e="")=>({begin:a(e,/"""/),
+end:a(/"""/,e),contains:[O(e),T(e),L(e)]}),$=(e="")=>({begin:a(e,/"/),
+end:a(/"/,e),contains:[O(e),L(e)]}),K={className:"string",
+variants:[P(),P("#"),P("##"),P("###"),$(),$("#"),$("##"),$("###")]},j={
+match:a(/`/,w,/`/)},z=[j,{className:"variable",match:/\$\d+/},{
+className:"variable",match:`\\$${f}+`}],q=[{match:/(@|#)available/,
+className:"keyword",starts:{contains:[{begin:/\(/,end:/\)/,keywords:E,
+contains:[...S,I,K]}]}},{className:"keyword",match:a(/@/,t(...g))},{
+className:"meta",match:a(/@/,w)}],U={match:n(/\b[A-Z]/),relevance:0,contains:[{
+className:"type",
+match:a(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,f,"+")
+},{className:"type",match:y,relevance:0},{match:/[?!]+/,relevance:0},{
+match:/\.\.\./,relevance:0},{match:a(/\s+&\s+/,n(y)),relevance:0}]},Z={
+begin:/</,end:/>/,keywords:D,contains:[...v,...B,...q,M,U]};U.contains.push(Z)
+;const G={begin:/\(/,end:/\)/,relevance:0,keywords:D,contains:["self",{
+match:a(w,/\s*:/),keywords:"_|0",relevance:0
+},...v,...B,...k,...S,I,K,...z,...q,U]},H={beginKeywords:"func",contains:[{
+className:"title",match:t(j.match,w,b),endsParent:!0,relevance:0},p]},R={
+begin:/</,end:/>/,contains:[...v,U]},V={begin:/\(/,end:/\)/,keywords:D,
+contains:[{begin:t(n(a(w,/\s*:/)),n(a(w,/\s+/,w,/\s*:/))),end:/:/,relevance:0,
+contains:[{className:"keyword",match:/\b_\b/},{className:"params",match:w}]
+},...v,...B,...S,I,K,...q,U,G],endsParent:!0,illegal:/["']/},W={
+className:"function",match:n(/\bfunc\b/),contains:[H,R,V,p],illegal:[/\[/,/%/]
+},X={className:"function",match:/\b(subscript|init[?!]?)\s*(?=[<(])/,keywords:{
+keyword:"subscript init init? init!",$pattern:/\w+[?!]?/},contains:[R,V,p],
+illegal:/\[|%/},J={beginKeywords:"operator",end:e.MATCH_NOTHING_RE,contains:[{
+className:"title",match:b,endsParent:!0,relevance:0}]},Q={
+beginKeywords:"precedencegroup",end:e.MATCH_NOTHING_RE,contains:[{
+className:"title",match:y,relevance:0},{begin:/{/,end:/}/,relevance:0,
+endsParent:!0,keywords:[...l,...o],contains:[U]}]};for(const e of K.variants){
+const n=e.contains.find((e=>"interpol"===e.label));n.keywords=D
+;const a=[...B,...k,...S,I,K,...z];n.contains=[...a,{begin:/\(/,end:/\)/,
+contains:["self",...a]}]}return{name:"Swift",keywords:D,contains:[...v,W,X,{
+className:"class",beginKeywords:"struct protocol class extension enum",
+end:"\\{",excludeEnd:!0,keywords:D,contains:[e.inherit(e.TITLE_MODE,{
+begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...B]},J,Q,{
+beginKeywords:"import",end:/$/,contains:[...v],relevance:0
+},...B,...k,...S,I,K,...z,...q,U,G]}}})());
+hljs.registerLanguage("taggerscript",(()=>{"use strict";return e=>({
+name:"Tagger Script",contains:[{className:"comment",begin:/\$noop\(/,end:/\)/,
+contains:[{begin:/\(/,end:/\)/,contains:["self",{begin:/\\./}]}],relevance:10},{
+className:"keyword",begin:/\$(?!noop)[a-zA-Z][_a-zA-Z0-9]*/,end:/\(/,
+excludeEnd:!0},{className:"variable",begin:/%[_a-zA-Z0-9:]*/,end:"%"},{
+className:"symbol",begin:/\\./}]})})());
+hljs.registerLanguage("yaml",(()=>{"use strict";return e=>{
+var n="true false yes no null",a="[\\w#;/?:@&=+$,.~*'()[\\]]+",s={
+className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/
+},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",
+variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(s,{
+variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={
+end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},t={begin:/\{/,
+end:/\}/,contains:[l],illegal:"\\n",relevance:0},g={begin:"\\[",end:"\\]",
+contains:[l],illegal:"\\n",relevance:0},b=[{className:"attr",variants:[{
+begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{
+begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",
+relevance:10},{className:"string",
+begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{
+begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,
+relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type",
+begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a
+},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",
+begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",
+relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{
+className:"number",
+begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"
+},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},t,g,s],r=[...b]
+;return r.pop(),r.push(i),l.contains=r,{name:"YAML",case_insensitive:!0,
+aliases:["yml","YAML"],contains:b}}})());
+hljs.registerLanguage("tap",(()=>{"use strict";return e=>({
+name:"Test Anything Protocol",case_insensitive:!0,
+contains:[e.HASH_COMMENT_MODE,{className:"meta",variants:[{
+begin:"^TAP version (\\d+)$"},{begin:"^1\\.\\.(\\d+)$"}]},{begin:/---$/,
+end:"\\.\\.\\.$",subLanguage:"yaml",relevance:0},{className:"number",
+begin:" (\\d+) "},{className:"symbol",variants:[{begin:"^ok"},{begin:"^not ok"}]
+}]})})());
+hljs.registerLanguage("tcl",(()=>{"use strict";function e(...e){
+return e.map((e=>{return(a=e)?"string"==typeof a?a:a.source:null;var a
+})).join("")}return a=>{const t=/[a-zA-Z_][a-zA-Z0-9_]*/,r={className:"number",
+variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]};return{name:"Tcl",
+aliases:["tk"],
+keywords:"after append apply array auto_execok auto_import auto_load auto_mkindex auto_mkindex_old auto_qualify auto_reset bgerror binary break catch cd chan clock close concat continue dde dict encoding eof error eval exec exit expr fblocked fconfigure fcopy file fileevent filename flush for foreach format gets glob global history http if incr info interp join lappend|10 lassign|10 lindex|10 linsert|10 list llength|10 load lrange|10 lrepeat|10 lreplace|10 lreverse|10 lsearch|10 lset|10 lsort|10 mathfunc mathop memory msgcat namespace open package parray pid pkg::create pkg_mkIndex platform platform::shell proc puts pwd read refchan regexp registry regsub|10 rename return safe scan seek set socket source split string subst switch tcl_endOfWord tcl_findLibrary tcl_startOfNextWord tcl_startOfPreviousWord tcl_wordBreakAfter tcl_wordBreakBefore tcltest tclvars tell time tm trace unknown unload unset update uplevel upvar variable vwait while",
+contains:[a.COMMENT(";[ \\t]*#","$"),a.COMMENT("^[ \\t]*#","$"),{
+beginKeywords:"proc",end:"[\\{]",excludeEnd:!0,contains:[{className:"title",
+begin:"[ \\t\\n\\r]+(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*",end:"[ \\t\\n\\r]",
+endsWithParent:!0,excludeEnd:!0}]},{className:"variable",variants:[{
+begin:e(/\$/,(n=/::/,e("(",n,")?")),t,"(::",t,")*")},{
+begin:"\\$\\{(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*",end:"\\}",contains:[r]}]},{
+className:"string",contains:[a.BACKSLASH_ESCAPE],
+variants:[a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},r]};var n}})());
+hljs.registerLanguage("thrift",(()=>{"use strict";return e=>{
+const t="bool byte i16 i32 i64 double string binary";return{name:"Thrift",
+keywords:{
+keyword:"namespace const typedef struct enum service exception void oneway set list map required optional",
+built_in:t,literal:"true false"},
+contains:[e.QUOTE_STRING_MODE,e.NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{
+className:"class",beginKeywords:"struct enum service exception",end:/\{/,
+illegal:/\n/,contains:[e.inherit(e.TITLE_MODE,{starts:{endsWithParent:!0,
+excludeEnd:!0}})]},{begin:"\\b(set|list|map)\\s*<",end:">",keywords:t,
+contains:["self"]}]}}})());
+hljs.registerLanguage("tp",(()=>{"use strict";return O=>{const e={
+className:"number",begin:"[1-9][0-9]*",relevance:0},R={className:"symbol",
+begin:":[^\\]]+"};return{name:"TP",keywords:{
+keyword:"ABORT ACC ADJUST AND AP_LD BREAK CALL CNT COL CONDITION CONFIG DA DB DIV DETECT ELSE END ENDFOR ERR_NUM ERROR_PROG FINE FOR GP GUARD INC IF JMP LINEAR_MAX_SPEED LOCK MOD MONITOR OFFSET Offset OR OVERRIDE PAUSE PREG PTH RT_LD RUN SELECT SKIP Skip TA TB TO TOOL_OFFSET Tool_Offset UF UT UFRAME_NUM UTOOL_NUM UNLOCK WAIT X Y Z W P R STRLEN SUBSTR FINDSTR VOFFSET PROG ATTR MN POS",
+literal:"ON OFF max_speed LPOS JPOS ENABLE DISABLE START STOP RESET"},
+contains:[{className:"built_in",
+begin:"(AR|P|PAYLOAD|PR|R|SR|RSR|LBL|VR|UALM|MESSAGE|UTOOL|UFRAME|TIMER|TIMER_OVERFLOW|JOINT_MAX_SPEED|RESUME_PROG|DIAG_REC)\\[",
+end:"\\]",contains:["self",e,R]},{className:"built_in",
+begin:"(AI|AO|DI|DO|F|RI|RO|UI|UO|GI|GO|SI|SO)\\[",end:"\\]",
+contains:["self",e,O.QUOTE_STRING_MODE,R]},{className:"keyword",
+begin:"/(PROG|ATTR|MN|POS|END)\\b"},{className:"keyword",
+begin:"(CALL|RUN|POINT_LOGIC|LBL)\\b"},{className:"keyword",
+begin:"\\b(ACC|CNT|Skip|Offset|PSPD|RT_LD|AP_LD|Tool_Offset)"},{
+className:"number",
+begin:"\\d+(sec|msec|mm/sec|cm/min|inch/min|deg/sec|mm|in|cm)?\\b",relevance:0
+},O.COMMENT("//","[;$]"),O.COMMENT("!","[;$]"),O.COMMENT("--eg:","$"),O.QUOTE_STRING_MODE,{
+className:"string",begin:"'",end:"'"},O.C_NUMBER_MODE,{className:"variable",
+begin:"\\$[A-Za-z0-9_]+"}]}}})());
+hljs.registerLanguage("twig",(()=>{"use strict";return e=>{
+var a="attribute block constant cycle date dump include max min parent random range source template_from_string",n={
+beginKeywords:a,keywords:{name:a},relevance:0,contains:[{className:"params",
+begin:"\\(",end:"\\)"}]},t={begin:/\|[A-Za-z_]+:?/,
+keywords:"abs batch capitalize column convert_encoding date date_modify default escape filter first format inky_to_html inline_css join json_encode keys last length lower map markdown merge nl2br number_format raw reduce replace reverse round slice sort spaceless split striptags title trim upper url_encode",
+contains:[n]
+},s="apply autoescape block deprecated do embed extends filter flush for from if import include macro sandbox set use verbatim with"
+;return s=s+" "+s.split(" ").map((e=>"end"+e)).join(" "),{name:"Twig",
+aliases:["craftcms"],case_insensitive:!0,subLanguage:"xml",
+contains:[e.COMMENT(/\{#/,/#\}/),{className:"template-tag",begin:/\{%/,
+end:/%\}/,contains:[{className:"name",begin:/\w+/,keywords:s,starts:{
+endsWithParent:!0,contains:[t,n],relevance:0}}]},{className:"template-variable",
+begin:/\{\{/,end:/\}\}/,contains:["self",t,n]}]}}})());
+hljs.registerLanguage("typescript",(()=>{"use strict"
+;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],s=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"])
+;function t(e){return r("(?=",e,")")}function r(...e){return e.map((e=>{
+return(n=e)?"string"==typeof n?n:n.source:null;var n})).join("")}return i=>{
+const c={$pattern:e,
+keyword:n.concat(["type","namespace","typedef","interface","public","private","protected","implements","declare","abstract","readonly"]),
+literal:a,
+built_in:s.concat(["any","void","number","boolean","string","object","never","enum"])
+},o={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},l=(e,n,a)=>{
+const s=e.contains.findIndex((e=>e.label===n))
+;if(-1===s)throw Error("can not find mode to replace");e.contains.splice(s,1,a)
+},b=(i=>{const c=e,o={begin:/<[A-Za-z0-9\\._:-]+/,
+end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{
+const a=e[0].length+e.index,s=e.input[a];"<"!==s?">"===s&&(((e,{after:n})=>{
+const a="</"+e[0].slice(1);return-1!==e.input.indexOf(a,n)})(e,{after:a
+})||n.ignoreMatch()):n.ignoreMatch()}},l={$pattern:e,keyword:n,literal:a,
+built_in:s},b="\\.([0-9](_?[0-9])*)",d="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",g={
+className:"number",variants:[{
+begin:`(\\b(${d})((${b})|\\.)?|(${b}))[eE][+-]?([0-9](_?[0-9])*)\\b`},{
+begin:`\\b(${d})\\b((${b})\\b|\\.)?|(${b})\\b`},{
+begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{
+begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{
+begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{
+begin:"\\b0[0-7]+n?\\b"}],relevance:0},u={className:"subst",begin:"\\$\\{",
+end:"\\}",keywords:l,contains:[]},E={begin:"html`",end:"",starts:{end:"`",
+returnEnd:!1,contains:[i.BACKSLASH_ESCAPE,u],subLanguage:"xml"}},m={
+begin:"css`",end:"",starts:{end:"`",returnEnd:!1,
+contains:[i.BACKSLASH_ESCAPE,u],subLanguage:"css"}},y={className:"string",
+begin:"`",end:"`",contains:[i.BACKSLASH_ESCAPE,u]},_={className:"comment",
+variants:[i.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{
+className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",
+end:"\\}",relevance:0},{className:"variable",begin:c+"(?=\\s*(-)|$)",
+endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]
+}),i.C_BLOCK_COMMENT_MODE,i.C_LINE_COMMENT_MODE]
+},p=[i.APOS_STRING_MODE,i.QUOTE_STRING_MODE,E,m,y,g,i.REGEXP_MODE]
+;u.contains=p.concat({begin:/\{/,end:/\}/,keywords:l,contains:["self"].concat(p)
+});const N=[].concat(_,u.contains),f=N.concat([{begin:/\(/,end:/\)/,keywords:l,
+contains:["self"].concat(N)}]),A={className:"params",begin:/\(/,end:/\)/,
+excludeBegin:!0,excludeEnd:!0,keywords:l,contains:f};return{name:"Javascript",
+aliases:["js","jsx","mjs","cjs"],keywords:l,exports:{PARAMS_CONTAINS:f},
+illegal:/#(?![$_A-z])/,contains:[i.SHEBANG({label:"shebang",binary:"node",
+relevance:5}),{label:"use_strict",className:"meta",relevance:10,
+begin:/^\s*['"]use (strict|asm)['"]/
+},i.APOS_STRING_MODE,i.QUOTE_STRING_MODE,E,m,y,_,g,{
+begin:r(/[{,\n]\s*/,t(r(/(((\/\/.*$)|(\/\*(\*[^/]|[^*])*\*\/))\s*)*/,c+"\\s*:"))),
+relevance:0,contains:[{className:"attr",begin:c+t("\\s*:"),relevance:0}]},{
+begin:"("+i.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",
+keywords:"return throw case",contains:[_,i.REGEXP_MODE,{className:"function",
+begin:"(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+i.UNDERSCORE_IDENT_RE+")\\s*=>",
+returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{
+begin:i.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0
+},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:l,contains:f}]}]
+},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{
+variants:[{begin:"<>",end:"</>"},{begin:o.begin,"on:begin":o.isTrulyOpeningTag,
+end:o.end}],subLanguage:"xml",contains:[{begin:o.begin,end:o.end,skip:!0,
+contains:["self"]}]}],relevance:0},{className:"function",
+beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:l,
+contains:["self",i.inherit(i.TITLE_MODE,{begin:c}),A],illegal:/%/},{
+beginKeywords:"while if switch catch for"},{className:"function",
+begin:i.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",
+returnBegin:!0,contains:[A,i.inherit(i.TITLE_MODE,{begin:c})]},{variants:[{
+begin:"\\."+c},{begin:"\\$"+c}],relevance:0},{className:"class",
+beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{
+beginKeywords:"extends"},i.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,
+end:/[{;]/,excludeEnd:!0,contains:[i.inherit(i.TITLE_MODE,{begin:c}),"self",A]
+},{begin:"(get|set)\\s+(?="+c+"\\()",end:/\{/,keywords:"get set",
+contains:[i.inherit(i.TITLE_MODE,{begin:c}),{begin:/\(\)/},A]},{begin:/\$[(.]/}]
+}})(i)
+;return Object.assign(b.keywords,c),b.exports.PARAMS_CONTAINS.push(o),b.contains=b.contains.concat([o,{
+beginKeywords:"namespace",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",
+end:/\{/,excludeEnd:!0,keywords:"interface extends"
+}]),l(b,"shebang",i.SHEBANG()),l(b,"use_strict",{className:"meta",relevance:10,
+begin:/^\s*['"]use strict['"]/
+}),b.contains.find((e=>"function"===e.className)).relevance=0,Object.assign(b,{
+name:"TypeScript",aliases:["ts","tsx"]}),b}})());
+hljs.registerLanguage("vala",(()=>{"use strict";return e=>({name:"Vala",
+keywords:{
+keyword:"char uchar unichar int uint long ulong short ushort int8 int16 int32 int64 uint8 uint16 uint32 uint64 float double bool struct enum string void weak unowned owned async signal static abstract interface override virtual delegate if while do for foreach else switch case break default return try catch public private protected internal using new this get set const stdout stdin stderr var",
+built_in:"DBus GLib CCode Gee Object Gtk Posix",literal:"false true null"},
+contains:[{className:"class",beginKeywords:"class interface namespace",end:/\{/,
+excludeEnd:!0,illegal:"[^,:\\n\\s\\.]",contains:[e.UNDERSCORE_TITLE_MODE]
+},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"string",begin:'"""',
+end:'"""',relevance:5},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,{
+className:"meta",begin:"^#",end:"$",relevance:2}]})})());
+hljs.registerLanguage("vbnet",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function n(...n){
+return n.map((n=>e(n))).join("")}function t(...n){
+return"("+n.map((n=>e(n))).join("|")+")"}return e=>{
+const a=/\d{1,2}\/\d{1,2}\/\d{4}/,i=/\d{4}-\d{1,2}-\d{1,2}/,s=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,o={
+className:"literal",variants:[{begin:n(/# */,t(i,a),/ *#/)},{
+begin:n(/# */,r,/ *#/)},{begin:n(/# */,s,/ *#/)},{
+begin:n(/# */,t(i,a),/ +/,t(s,r),/ *#/)}]},l=e.COMMENT(/'''/,/$/,{contains:[{
+className:"doctag",begin:/<\/?/,end:/>/}]}),c=e.COMMENT(null,/$/,{variants:[{
+begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]});return{name:"Visual Basic .NET",
+aliases:["vb"],case_insensitive:!0,classNameAliases:{label:"symbol"},keywords:{
+keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield",
+built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort",
+type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort",
+literal:"true false nothing"},
+illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{
+className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/,
+end:/"/,illegal:/\n/,contains:[{begin:/""/}]},o,{className:"number",relevance:0,
+variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/
+},{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{
+begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{
+className:"label",begin:/^\w+:/},l,c,{className:"meta",
+begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/,
+end:/$/,keywords:{
+"meta-keyword":"const disable else elseif enable end externalsource if region then"
+},contains:[c]}]}}})());
+hljs.registerLanguage("vbscript",(()=>{"use strict";function e(e){
+return e?"string"==typeof e?e:e.source:null}function t(...t){
+return t.map((t=>e(t))).join("")}function r(...t){
+return"("+t.map((t=>e(t))).join("|")+")"}return e=>{
+const i="lcase month vartype instrrev ubound setlocale getobject rgb getref string weekdayname rnd dateadd monthname now day minute isarray cbool round formatcurrency conversions csng timevalue second year space abs clng timeserial fixs len asc isempty maths dateserial atn timer isobject filter weekday datevalue ccur isdate instr datediff formatdatetime replace isnull right sgn array snumeric log cdbl hex chr lbound msgbox ucase getlocale cos cdate cbyte rtrim join hour oct typename trim strcomp int createobject loadpicture tan formatnumber mid split  cint sin datepart ltrim sqr time derived eval date formatpercent exp inputbox left ascw chrw regexp cstr err".split(" ")
+;return{name:"VBScript",aliases:["vbs"],case_insensitive:!0,keywords:{
+keyword:"call class const dim do loop erase execute executeglobal exit for each next function if then else on error option explicit new private property let get public randomize redim rem select case set stop sub while wend with end to elseif is or xor and not class_initialize class_terminate default preserve in me byval byref step resume goto",
+built_in:["server","response","request","scriptengine","scriptenginebuildversion","scriptengineminorversion","scriptenginemajorversion"],
+literal:"true false null nothing empty"},illegal:"//",contains:[{
+begin:t(r(...i),"\\s*\\("),relevance:0,keywords:{built_in:i}
+},e.inherit(e.QUOTE_STRING_MODE,{contains:[{begin:'""'}]}),e.COMMENT(/'/,/$/,{
+relevance:0}),e.C_NUMBER_MODE]}}})());
+hljs.registerLanguage("vbscript-html",(()=>{"use strict";return e=>({
+name:"VBScript in HTML",subLanguage:"xml",contains:[{begin:"<%",end:"%>",
+subLanguage:"vbscript"}]})})());
+hljs.registerLanguage("verilog",(()=>{"use strict";return e=>({name:"Verilog",
+aliases:["v","sv","svh"],case_insensitive:!1,keywords:{$pattern:/[\w\$]+/,
+keyword:"accept_on alias always always_comb always_ff always_latch and assert assign assume automatic before begin bind bins binsof bit break buf|0 bufif0 bufif1 byte case casex casez cell chandle checker class clocking cmos config const constraint context continue cover covergroup coverpoint cross deassign default defparam design disable dist do edge else end endcase endchecker endclass endclocking endconfig endfunction endgenerate endgroup endinterface endmodule endpackage endprimitive endprogram endproperty endspecify endsequence endtable endtask enum event eventually expect export extends extern final first_match for force foreach forever fork forkjoin function generate|5 genvar global highz0 highz1 if iff ifnone ignore_bins illegal_bins implements implies import incdir include initial inout input inside instance int integer interconnect interface intersect join join_any join_none large let liblist library local localparam logic longint macromodule matches medium modport module nand negedge nettype new nexttime nmos nor noshowcancelled not notif0 notif1 or output package packed parameter pmos posedge primitive priority program property protected pull0 pull1 pulldown pullup pulsestyle_ondetect pulsestyle_onevent pure rand randc randcase randsequence rcmos real realtime ref reg reject_on release repeat restrict return rnmos rpmos rtran rtranif0 rtranif1 s_always s_eventually s_nexttime s_until s_until_with scalared sequence shortint shortreal showcancelled signed small soft solve specify specparam static string strong strong0 strong1 struct super supply0 supply1 sync_accept_on sync_reject_on table tagged task this throughout time timeprecision timeunit tran tranif0 tranif1 tri tri0 tri1 triand trior trireg type typedef union unique unique0 unsigned until until_with untyped use uwire var vectored virtual void wait wait_order wand weak weak0 weak1 while wildcard wire with within wor xnor xor",
+literal:"null",
+built_in:"$finish $stop $exit $fatal $error $warning $info $realtime $time $printtimescale $bitstoreal $bitstoshortreal $itor $signed $cast $bits $stime $timeformat $realtobits $shortrealtobits $rtoi $unsigned $asserton $assertkill $assertpasson $assertfailon $assertnonvacuouson $assertoff $assertcontrol $assertpassoff $assertfailoff $assertvacuousoff $isunbounded $sampled $fell $changed $past_gclk $fell_gclk $changed_gclk $rising_gclk $steady_gclk $coverage_control $coverage_get $coverage_save $set_coverage_db_name $rose $stable $past $rose_gclk $stable_gclk $future_gclk $falling_gclk $changing_gclk $display $coverage_get_max $coverage_merge $get_coverage $load_coverage_db $typename $unpacked_dimensions $left $low $increment $clog2 $ln $log10 $exp $sqrt $pow $floor $ceil $sin $cos $tan $countbits $onehot $isunknown $fatal $warning $dimensions $right $high $size $asin $acos $atan $atan2 $hypot $sinh $cosh $tanh $asinh $acosh $atanh $countones $onehot0 $error $info $random $dist_chi_square $dist_erlang $dist_exponential $dist_normal $dist_poisson $dist_t $dist_uniform $q_initialize $q_remove $q_exam $async$and$array $async$nand$array $async$or$array $async$nor$array $sync$and$array $sync$nand$array $sync$or$array $sync$nor$array $q_add $q_full $psprintf $async$and$plane $async$nand$plane $async$or$plane $async$nor$plane $sync$and$plane $sync$nand$plane $sync$or$plane $sync$nor$plane $system $display $displayb $displayh $displayo $strobe $strobeb $strobeh $strobeo $write $readmemb $readmemh $writememh $value$plusargs $dumpvars $dumpon $dumplimit $dumpports $dumpportson $dumpportslimit $writeb $writeh $writeo $monitor $monitorb $monitorh $monitoro $writememb $dumpfile $dumpoff $dumpall $dumpflush $dumpportsoff $dumpportsall $dumpportsflush $fclose $fdisplay $fdisplayb $fdisplayh $fdisplayo $fstrobe $fstrobeb $fstrobeh $fstrobeo $swrite $swriteb $swriteh $swriteo $fscanf $fread $fseek $fflush $feof $fopen $fwrite $fwriteb $fwriteh $fwriteo $fmonitor $fmonitorb $fmonitorh $fmonitoro $sformat $sformatf $fgetc $ungetc $fgets $sscanf $rewind $ftell $ferror"
+},contains:[e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE,e.QUOTE_STRING_MODE,{
+className:"number",contains:[e.BACKSLASH_ESCAPE],variants:[{
+begin:"\\b((\\d+'(b|h|o|d|B|H|O|D))[0-9xzXZa-fA-F_]+)"},{
+begin:"\\B(('(b|h|o|d|B|H|O|D))[0-9xzXZa-fA-F_]+)"},{begin:"\\b([0-9_])+",
+relevance:0}]},{className:"variable",variants:[{begin:"#\\((?!parameter).+\\)"
+},{begin:"\\.\\w+",relevance:0}]},{className:"meta",begin:"`",end:"$",keywords:{
+"meta-keyword":"define __FILE__ __LINE__ begin_keywords celldefine default_nettype define else elsif end_keywords endcelldefine endif ifdef ifndef include line nounconnected_drive pragma resetall timescale unconnected_drive undef undefineall"
+},relevance:0}]})})());
+hljs.registerLanguage("vhdl",(()=>{"use strict";return e=>({name:"VHDL",
+case_insensitive:!0,keywords:{
+keyword:"abs access after alias all and architecture array assert assume assume_guarantee attribute begin block body buffer bus case component configuration constant context cover disconnect downto default else elsif end entity exit fairness file for force function generate generic group guarded if impure in inertial inout is label library linkage literal loop map mod nand new next nor not null of on open or others out package parameter port postponed procedure process property protected pure range record register reject release rem report restrict restrict_guarantee return rol ror select sequence severity shared signal sla sll sra srl strong subtype then to transport type unaffected units until use variable view vmode vprop vunit wait when while with xnor xor",
+built_in:"boolean bit character integer time delay_length natural positive string bit_vector file_open_kind file_open_status std_logic std_logic_vector unsigned signed boolean_vector integer_vector std_ulogic std_ulogic_vector unresolved_unsigned u_unsigned unresolved_signed u_signed real_vector time_vector",
+literal:"false true note warning error failure line text side width"},
+illegal:/\{/,
+contains:[e.C_BLOCK_COMMENT_MODE,e.COMMENT("--","$"),e.QUOTE_STRING_MODE,{
+className:"number",
+begin:"\\b(\\d(_|\\d)*#\\w+(\\.\\w+)?#([eE][-+]?\\d(_|\\d)*)?|\\d(_|\\d)*(\\.\\d(_|\\d)*)?([eE][-+]?\\d(_|\\d)*)?)",
+relevance:0},{className:"string",begin:"'(U|X|0|1|Z|W|L|H|-)'",
+contains:[e.BACKSLASH_ESCAPE]},{className:"symbol",
+begin:"'[A-Za-z](_?[A-Za-z0-9])*",contains:[e.BACKSLASH_ESCAPE]}]})})());
+hljs.registerLanguage("vim",(()=>{"use strict";return e=>({name:"Vim Script",
+keywords:{$pattern:/[!#@\w]+/,
+keyword:"N|0 P|0 X|0 a|0 ab abc abo al am an|0 ar arga argd arge argdo argg argl argu as au aug aun b|0 bN ba bad bd be bel bf bl bm bn bo bp br brea breaka breakd breakl bro bufdo buffers bun bw c|0 cN cNf ca cabc caddb cad caddf cal cat cb cc ccl cd ce cex cf cfir cgetb cgete cg changes chd che checkt cl cla clo cm cmapc cme cn cnew cnf cno cnorea cnoreme co col colo com comc comp con conf cope cp cpf cq cr cs cst cu cuna cunme cw delm deb debugg delc delf dif diffg diffo diffp diffpu diffs diffthis dig di dl dell dj dli do doautoa dp dr ds dsp e|0 ea ec echoe echoh echom echon el elsei em en endfo endf endt endw ene ex exe exi exu f|0 files filet fin fina fini fir fix fo foldc foldd folddoc foldo for fu go gr grepa gu gv ha helpf helpg helpt hi hid his ia iabc if ij il im imapc ime ino inorea inoreme int is isp iu iuna iunme j|0 ju k|0 keepa kee keepj lN lNf l|0 lad laddb laddf la lan lat lb lc lch lcl lcs le lefta let lex lf lfir lgetb lgete lg lgr lgrepa lh ll lla lli lmak lm lmapc lne lnew lnf ln loadk lo loc lockv lol lope lp lpf lr ls lt lu lua luad luaf lv lvimgrepa lw m|0 ma mak map mapc marks mat me menut mes mk mks mksp mkv mkvie mod mz mzf nbc nb nbs new nm nmapc nme nn nnoreme noa no noh norea noreme norm nu nun nunme ol o|0 om omapc ome on ono onoreme opt ou ounme ow p|0 profd prof pro promptr pc ped pe perld po popu pp pre prev ps pt ptN ptf ptj ptl ptn ptp ptr pts pu pw py3 python3 py3d py3f py pyd pyf quita qa rec red redi redr redraws reg res ret retu rew ri rightb rub rubyd rubyf rund ru rv sN san sa sal sav sb sbN sba sbf sbl sbm sbn sbp sbr scrip scripte scs se setf setg setl sf sfir sh sim sig sil sl sla sm smap smapc sme sn sni sno snor snoreme sor so spelld spe spelli spellr spellu spellw sp spr sre st sta startg startr star stopi stj sts sun sunm sunme sus sv sw sy synti sync tN tabN tabc tabdo tabe tabf tabfir tabl tabm tabnew tabn tabo tabp tabr tabs tab ta tags tc tcld tclf te tf th tj tl tm tn to tp tr try ts tu u|0 undoj undol una unh unl unlo unm unme uns up ve verb vert vim vimgrepa vi viu vie vm vmapc vme vne vn vnoreme vs vu vunme windo w|0 wN wa wh wi winc winp wn wp wq wqa ws wu wv x|0 xa xmapc xm xme xn xnoreme xu xunme y|0 z|0 ~ Next Print append abbreviate abclear aboveleft all amenu anoremenu args argadd argdelete argedit argglobal arglocal argument ascii autocmd augroup aunmenu buffer bNext ball badd bdelete behave belowright bfirst blast bmodified bnext botright bprevious brewind break breakadd breakdel breaklist browse bunload bwipeout change cNext cNfile cabbrev cabclear caddbuffer caddexpr caddfile call catch cbuffer cclose center cexpr cfile cfirst cgetbuffer cgetexpr cgetfile chdir checkpath checktime clist clast close cmap cmapclear cmenu cnext cnewer cnfile cnoremap cnoreabbrev cnoremenu copy colder colorscheme command comclear compiler continue confirm copen cprevious cpfile cquit crewind cscope cstag cunmap cunabbrev cunmenu cwindow delete delmarks debug debuggreedy delcommand delfunction diffupdate diffget diffoff diffpatch diffput diffsplit digraphs display deletel djump dlist doautocmd doautoall deletep drop dsearch dsplit edit earlier echo echoerr echohl echomsg else elseif emenu endif endfor endfunction endtry endwhile enew execute exit exusage file filetype find finally finish first fixdel fold foldclose folddoopen folddoclosed foldopen function global goto grep grepadd gui gvim hardcopy help helpfind helpgrep helptags highlight hide history insert iabbrev iabclear ijump ilist imap imapclear imenu inoremap inoreabbrev inoremenu intro isearch isplit iunmap iunabbrev iunmenu join jumps keepalt keepmarks keepjumps lNext lNfile list laddexpr laddbuffer laddfile last language later lbuffer lcd lchdir lclose lcscope left leftabove lexpr lfile lfirst lgetbuffer lgetexpr lgetfile lgrep lgrepadd lhelpgrep llast llist lmake lmap lmapclear lnext lnewer lnfile lnoremap loadkeymap loadview lockmarks lockvar lolder lopen lprevious lpfile lrewind ltag lunmap luado luafile lvimgrep lvimgrepadd lwindow move mark make mapclear match menu menutranslate messages mkexrc mksession mkspell mkvimrc mkview mode mzscheme mzfile nbclose nbkey nbsart next nmap nmapclear nmenu nnoremap nnoremenu noautocmd noremap nohlsearch noreabbrev noremenu normal number nunmap nunmenu oldfiles open omap omapclear omenu only onoremap onoremenu options ounmap ounmenu ownsyntax print profdel profile promptfind promptrepl pclose pedit perl perldo pop popup ppop preserve previous psearch ptag ptNext ptfirst ptjump ptlast ptnext ptprevious ptrewind ptselect put pwd py3do py3file python pydo pyfile quit quitall qall read recover redo redir redraw redrawstatus registers resize retab return rewind right rightbelow ruby rubydo rubyfile rundo runtime rviminfo substitute sNext sandbox sargument sall saveas sbuffer sbNext sball sbfirst sblast sbmodified sbnext sbprevious sbrewind scriptnames scriptencoding scscope set setfiletype setglobal setlocal sfind sfirst shell simalt sign silent sleep slast smagic smapclear smenu snext sniff snomagic snoremap snoremenu sort source spelldump spellgood spellinfo spellrepall spellundo spellwrong split sprevious srewind stop stag startgreplace startreplace startinsert stopinsert stjump stselect sunhide sunmap sunmenu suspend sview swapname syntax syntime syncbind tNext tabNext tabclose tabedit tabfind tabfirst tablast tabmove tabnext tabonly tabprevious tabrewind tag tcl tcldo tclfile tearoff tfirst throw tjump tlast tmenu tnext topleft tprevious trewind tselect tunmenu undo undojoin undolist unabbreviate unhide unlet unlockvar unmap unmenu unsilent update vglobal version verbose vertical vimgrep vimgrepadd visual viusage view vmap vmapclear vmenu vnew vnoremap vnoremenu vsplit vunmap vunmenu write wNext wall while winsize wincmd winpos wnext wprevious wqall wsverb wundo wviminfo xit xall xmapclear xmap xmenu xnoremap xnoremenu xunmap xunmenu yank",
+built_in:"synIDtrans atan2 range matcharg did_filetype asin feedkeys xor argv complete_check add getwinposx getqflist getwinposy screencol clearmatches empty extend getcmdpos mzeval garbagecollect setreg ceil sqrt diff_hlID inputsecret get getfperm getpid filewritable shiftwidth max sinh isdirectory synID system inputrestore winline atan visualmode inputlist tabpagewinnr round getregtype mapcheck hasmapto histdel argidx findfile sha256 exists toupper getcmdline taglist string getmatches bufnr strftime winwidth bufexists strtrans tabpagebuflist setcmdpos remote_read printf setloclist getpos getline bufwinnr float2nr len getcmdtype diff_filler luaeval resolve libcallnr foldclosedend reverse filter has_key bufname str2float strlen setline getcharmod setbufvar index searchpos shellescape undofile foldclosed setqflist buflisted strchars str2nr virtcol floor remove undotree remote_expr winheight gettabwinvar reltime cursor tabpagenr finddir localtime acos getloclist search tanh matchend rename gettabvar strdisplaywidth type abs py3eval setwinvar tolower wildmenumode log10 spellsuggest bufloaded synconcealed nextnonblank server2client complete settabwinvar executable input wincol setmatches getftype hlID inputsave searchpair or screenrow line settabvar histadd deepcopy strpart remote_peek and eval getftime submatch screenchar winsaveview matchadd mkdir screenattr getfontname libcall reltimestr getfsize winnr invert pow getbufline byte2line soundfold repeat fnameescape tagfiles sin strwidth spellbadword trunc maparg log lispindent hostname setpos globpath remote_foreground getchar synIDattr fnamemodify cscope_connection stridx winbufnr indent min complete_add nr2char searchpairpos inputdialog values matchlist items hlexists strridx browsedir expand fmod pathshorten line2byte argc count getwinvar glob foldtextresult getreg foreground cosh matchdelete has char2nr simplify histget searchdecl iconv winrestcmd pumvisible writefile foldlevel haslocaldir keys cos matchstr foldtext histnr tan tempname getcwd byteidx getbufvar islocked escape eventhandler remote_send serverlist winrestview synstack pyeval prevnonblank readfile cindent filereadable changenr exp"
+},illegal:/;/,contains:[e.NUMBER_MODE,{className:"string",begin:"'",end:"'",
+illegal:"\\n"},{className:"string",begin:/"(\\"|\n\\|[^"\n])*"/
+},e.COMMENT('"',"$"),{className:"variable",begin:/[bwtglsav]:[\w\d_]*/},{
+className:"function",beginKeywords:"function function!",end:"$",relevance:0,
+contains:[e.TITLE_MODE,{className:"params",begin:"\\(",end:"\\)"}]},{
+className:"symbol",begin:/<[\w-]+>/}]})})());
+hljs.registerLanguage("x86asm",(()=>{"use strict";return s=>({
+name:"Intel x86 Assembly",case_insensitive:!0,keywords:{
+$pattern:"[.%]?"+s.IDENT_RE,
+keyword:"lock rep repe repz repne repnz xaquire xrelease bnd nobnd aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63",
 built_in:"ip eip rip al ah bl bh cl ch dl dh sil dil bpl spl r8b r9b r10b r11b r12b r13b r14b r15b ax bx cx dx si di bp sp r8w r9w r10w r11w r12w r13w r14w r15w eax ebx ecx edx esi edi ebp esp eip r8d r9d r10d r11d r12d r13d r14d r15d rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 cs ds es fs gs ss st st0 st1 st2 st3 st4 st5 st6 st7 mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7 xmm0  xmm1  xmm2  xmm3  xmm4  xmm5  xmm6  xmm7  xmm8  xmm9 xmm10  xmm11 xmm12 xmm13 xmm14 xmm15 xmm16 xmm17 xmm18 xmm19 xmm20 xmm21 xmm22 xmm23 xmm24 xmm25 xmm26 xmm27 xmm28 xmm29 xmm30 xmm31 ymm0  ymm1  ymm2  ymm3  ymm4  ymm5  ymm6  ymm7  ymm8  ymm9 ymm10  ymm11 ymm12 ymm13 ymm14 ymm15 ymm16 ymm17 ymm18 ymm19 ymm20 ymm21 ymm22 ymm23 ymm24 ymm25 ymm26 ymm27 ymm28 ymm29 ymm30 ymm31 zmm0  zmm1  zmm2  zmm3  zmm4  zmm5  zmm6  zmm7  zmm8  zmm9 zmm10  zmm11 zmm12 zmm13 zmm14 zmm15 zmm16 zmm17 zmm18 zmm19 zmm20 zmm21 zmm22 zmm23 zmm24 zmm25 zmm26 zmm27 zmm28 zmm29 zmm30 zmm31 k0 k1 k2 k3 k4 k5 k6 k7 bnd0 bnd1 bnd2 bnd3 cr0 cr1 cr2 cr3 cr4 cr8 dr0 dr1 dr2 dr3 dr8 tr3 tr4 tr5 tr6 tr7 r0 r1 r2 r3 r4 r5 r6 r7 r0b r1b r2b r3b r4b r5b r6b r7b r0w r1w r2w r3w r4w r5w r6w r7w r0d r1d r2d r3d r4d r5d r6d r7d r0h r1h r2h r3h r0l r1l r2l r3l r4l r5l r6l r7l r8l r9l r10l r11l r12l r13l r14l r15l db dw dd dq dt ddq do dy dz resb resw resd resq rest resdq reso resy resz incbin equ times byte word dword qword nosplit rel abs seg wrt strict near far a32 ptr",
-meta:"%define %xdefine %+ %undef %defstr %deftok %assign %strcat %strlen %substr %rotate %elif %else %endif %if %ifmacro %ifctx %ifidn %ifidni %ifid %ifnum %ifstr %iftoken %ifempty %ifenv %error %warning %fatal %rep %endrep %include %push %pop %repl %pathsearch %depend %use %arg %stacksize %local %line %comment %endcomment .nolist __FILE__ __LINE__ __SECT__  __BITS__ __OUTPUT_FORMAT__ __DATE__ __TIME__ __DATE_NUM__ __TIME_NUM__ __UTC_DATE__ __UTC_TIME__ __UTC_DATE_NUM__ __UTC_TIME_NUM__  __PASS__ struc endstruc istruc at iend align alignb sectalign daz nodaz up down zero default option assume public bits use16 use32 use64 default section segment absolute extern global common cpu float __utf16__ __utf16le__ __utf16be__ __utf32__ __utf32le__ __utf32be__ __float8__ __float16__ __float32__ __float64__ __float80m__ __float80e__ __float128l__ __float128h__ __Infinity__ __QNaN__ __SNaN__ Inf NaN QNaN SNaN float8 float16 float32 float64 float80m float80e float128l float128h __FLOAT_DAZ__ __FLOAT_ROUND__ __FLOAT__"},
-contains:[a.COMMENT(";","$",{relevance:0}),{className:"number",variants:[{begin:"\\b(?:([0-9][0-9_]*)?\\.[0-9_]*(?:[eE][+-]?[0-9_]+)?|(0[Xx])?[0-9][0-9_]*\\.?[0-9_]*(?:[pP](?:[+-]?[0-9_]+)?)?)\\b",relevance:0},{begin:"\\$[0-9][0-9A-Fa-f]*",relevance:0},{begin:"\\b(?:[0-9A-Fa-f][0-9A-Fa-f_]*[Hh]|[0-9][0-9_]*[DdTt]?|[0-7][0-7_]*[QqOo]|[0-1][0-1_]*[BbYy])\\b"},{begin:"\\b(?:0[Xx][0-9A-Fa-f_]+|0[DdTt][0-9_]+|0[QqOo][0-7_]+|0[BbYy][0-1_]+)\\b"}]},a.QUOTE_STRING_MODE,{className:"string",variants:[{begin:"'",
-end:"[^\\\\]'"},{begin:"`",end:"[^\\\\]`"}],relevance:0},{className:"symbol",variants:[{begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)"},{begin:"^\\s*%%[A-Za-z0-9_$#@~.?]*:"}],relevance:0},{className:"subst",begin:"%[0-9]+",relevance:0},{className:"subst",begin:"%!S+",relevance:0},{className:"meta",begin:/^\s*\.[\w_-]+/}]}}}());
-hljs.registerLanguage("xl",function(){return function(a){var b={$pattern:/[a-zA-Z][a-zA-Z0-9_?]*/,keyword:"if then else do while until for loop import with is as where when by data constant integer real text name boolean symbol infix prefix postfix block tree",literal:"true false nil",built_in:"in mod rem and or xor not abs sign floor ceil sqrt sin cos tan asin acos atan exp expm1 log log2 log10 log1p pi at text_length text_range text_find text_replace contains page slide basic_slide title_slide title subtitle fade_in fade_out fade_at clear_color color line_color line_width texture_wrap texture_transform texture scale_?x scale_?y scale_?z? translate_?x translate_?y translate_?z? rotate_?x rotate_?y rotate_?z? rectangle circle ellipse sphere path line_to move_to quad_to curve_to theme background contents locally time mouse_?x mouse_?y mouse_buttons ObjectLoader Animate MovieCredits Slides Filters Shading Materials LensFlare Mapping VLCAudioVideo StereoDecoder PointCloud NetworkAccess RemoteControl RegExp ChromaKey Snowfall NodeJS Speech Charts"},
-c={className:"string",begin:'"',end:'"',illegal:"\\n"},d={beginKeywords:"import",end:"$",keywords:b,contains:[c]},e={className:"function",begin:/[a-z][^\n]*->/,returnBegin:!0,end:/->/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,keywords:b}})]};return{name:"XL",aliases:["tao"],keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,{className:"string",begin:"'",end:"'",illegal:"\\n"},{className:"string",begin:"<<",end:">>"},e,d,{className:"number",begin:"[0-9]+#[0-9A-Z_]+(\\.[0-9-A-Z_]+)?#?([Ee][+-]?[0-9]+)?"},
-a.NUMBER_MODE]}}}());
-hljs.registerLanguage("xquery",function(){return function(a){return{name:"XQuery",aliases:["xpath","xq"],case_insensitive:!1,illegal:/(proc)|(abstract)|(extends)|(until)|(#)/,keywords:{$pattern:/[a-zA-Z\$][a-zA-Z0-9_:\-]*/,keyword:"module schema namespace boundary-space preserve no-preserve strip default collation base-uri ordering context decimal-format decimal-separator copy-namespaces empty-sequence except exponent-separator external grouping-separator inherit no-inherit lax minus-sign per-mille percent schema-attribute schema-element strict unordered zero-digit declare import option function validate variable for at in let where order group by return if then else tumbling sliding window start when only end previous next stable ascending descending allowing empty greatest least some every satisfies switch case typeswitch try catch and or to union intersect instance of treat as castable cast map array delete insert into replace value rename copy modify update",type:"item document-node node attribute document element comment namespace namespace-node processing-instruction text construction xs:anyAtomicType xs:untypedAtomic xs:duration xs:time xs:decimal xs:float xs:double xs:gYearMonth xs:gYear xs:gMonthDay xs:gMonth xs:gDay xs:boolean xs:base64Binary xs:hexBinary xs:anyURI xs:QName xs:NOTATION xs:dateTime xs:dateTimeStamp xs:date xs:string xs:normalizedString xs:token xs:language xs:NMTOKEN xs:Name xs:NCName xs:ID xs:IDREF xs:ENTITY xs:integer xs:nonPositiveInteger xs:negativeInteger xs:long xs:int xs:short xs:byte xs:nonNegativeInteger xs:unisignedLong xs:unsignedInt xs:unsignedShort xs:unsignedByte xs:positiveInteger xs:yearMonthDuration xs:dayTimeDuration",
-literal:"eq ne lt le gt ge is self:: child:: descendant:: descendant-or-self:: attribute:: following:: following-sibling:: parent:: ancestor:: ancestor-or-self:: preceding:: preceding-sibling:: NaN"},contains:[{className:"variable",begin:/[\$][\w-:]+/},{className:"built_in",variants:[{begin:/\barray:/,end:/(?:append|filter|flatten|fold\-(?:left|right)|for-each(?:\-pair)?|get|head|insert\-before|join|put|remove|reverse|size|sort|subarray|tail)\b/},{begin:/\bmap:/,end:/(?:contains|entry|find|for\-each|get|keys|merge|put|remove|size)\b/},
-{begin:/\bmath:/,end:/(?:a(?:cos|sin|tan[2]?)|cos|exp(?:10)?|log(?:10)?|pi|pow|sin|sqrt|tan)\b/},{begin:/\bop:/,end:/\(/,excludeEnd:!0},{begin:/\bfn:/,end:/\(/,excludeEnd:!0},{begin:/[^<\/\$:'"-]\b(?:abs|accumulator\-(?:after|before)|adjust\-(?:date(?:Time)?|time)\-to\-timezone|analyze\-string|apply|available\-(?:environment\-variables|system\-properties)|avg|base\-uri|boolean|ceiling|codepoints?\-(?:equal|to\-string)|collation\-key|collection|compare|concat|contains(?:\-token)?|copy\-of|count|current(?:\-)?(?:date(?:Time)?|time|group(?:ing\-key)?|output\-uri|merge\-(?:group|key))?data|dateTime|days?\-from\-(?:date(?:Time)?|duration)|deep\-equal|default\-(?:collation|language)|distinct\-values|document(?:\-uri)?|doc(?:\-available)?|element\-(?:available|with\-id)|empty|encode\-for\-uri|ends\-with|environment\-variable|error|escape\-html\-uri|exactly\-one|exists|false|filter|floor|fold\-(?:left|right)|for\-each(?:\-pair)?|format\-(?:date(?:Time)?|time|integer|number)|function\-(?:arity|available|lookup|name)|generate\-id|has\-children|head|hours\-from\-(?:dateTime|duration|time)|id(?:ref)?|implicit\-timezone|in\-scope\-prefixes|index\-of|innermost|insert\-before|iri\-to\-uri|json\-(?:doc|to\-xml)|key|lang|last|load\-xquery\-module|local\-name(?:\-from\-QName)?|(?:lower|upper)\-case|matches|max|minutes\-from\-(?:dateTime|duration|time)|min|months?\-from\-(?:date(?:Time)?|duration)|name(?:space\-uri\-?(?:for\-prefix|from\-QName)?)?|nilled|node\-name|normalize\-(?:space|unicode)|not|number|one\-or\-more|outermost|parse\-(?:ietf\-date|json)|path|position|(?:prefix\-from\-)?QName|random\-number\-generator|regex\-group|remove|replace|resolve\-(?:QName|uri)|reverse|root|round(?:\-half\-to\-even)?|seconds\-from\-(?:dateTime|duration|time)|snapshot|sort|starts\-with|static\-base\-uri|stream\-available|string\-?(?:join|length|to\-codepoints)?|subsequence|substring\-?(?:after|before)?|sum|system\-property|tail|timezone\-from\-(?:date(?:Time)?|time)|tokenize|trace|trans(?:form|late)|true|type\-available|unordered|unparsed\-(?:entity|text)?\-?(?:public\-id|uri|available|lines)?|uri\-collection|xml\-to\-json|years?\-from\-(?:date(?:Time)?|duration)|zero\-or\-one)\b/},
-{begin:/\blocal:/,end:/\(/,excludeEnd:!0},{begin:/\bzip:/,end:/(?:zip\-file|(?:xml|html|text|binary)\-entry| (?:update\-)?entries)\b/},{begin:/\b(?:util|db|functx|app|xdmp|xmldb):/,end:/\(/,excludeEnd:!0}]},{className:"string",variants:[{begin:/"/,end:/"/,contains:[{begin:/""/,relevance:0}]},{begin:/'/,end:/'/,contains:[{begin:/''/,relevance:0}]}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{className:"comment",begin:"\\(:",end:":\\)",
-relevance:10,contains:[{className:"doctag",begin:"@\\w+"}]},{className:"meta",begin:/%[\w-:]+/},{className:"title",begin:/\bxquery version "[13]\.[01]"\s?(?:encoding ".+")?/,end:/;/},{beginKeywords:"element attribute comment document processing-instruction",end:"{",excludeEnd:!0},{begin:/<([\w\._:\-]+)((\s*.*)=('|").*('|"))?>/,end:/(\/[\w\._:\-]+>)/,subLanguage:"xml",contains:[{begin:"{",end:"}",subLanguage:"xquery"},"self"]}]}}}());
-hljs.registerLanguage("zephir",function(){return function(a){var b={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},c=a.UNDERSCORE_TITLE_MODE,d={variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]};return{name:"Zephir",aliases:["zep"],keywords:"namespace class interface use extends function return abstract final public protected private static deprecated throw try catch Exception echo empty isset instanceof unset let var new const self require if else elseif switch case default do while loop for continue break likely unlikely __LINE__ __FILE__ __DIR__ __FUNCTION__ __CLASS__ __TRAIT__ __METHOD__ __NAMESPACE__ array boolean float double integer object resource string char long unsigned bool int uint ulong uchar true false null undefined",
-contains:[a.C_LINE_COMMENT_MODE,a.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),{className:"string",begin:"<<<['\"]?\\w+['\"]?$",end:"^\\w+;",contains:[a.BACKSLASH_ESCAPE]},{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",beginKeywords:"function fn",end:/[;{]/,excludeEnd:!0,illegal:"\\$|\\[|%",contains:[c,{className:"params",begin:"\\(",end:"\\)",keywords:"namespace class interface use extends function return abstract final public protected private static deprecated throw try catch Exception echo empty isset instanceof unset let var new const self require if else elseif switch case default do while loop for continue break likely unlikely __LINE__ __FILE__ __DIR__ __FUNCTION__ __CLASS__ __TRAIT__ __METHOD__ __NAMESPACE__ array boolean float double integer object resource string char long unsigned bool int uint ulong uchar true false null undefined",
-contains:["self",a.C_BLOCK_COMMENT_MODE,b,d]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},c]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[c]},{beginKeywords:"use",end:";",contains:[c]},{begin:"=>"},b,d]}}}());
+meta:"%define %xdefine %+ %undef %defstr %deftok %assign %strcat %strlen %substr %rotate %elif %else %endif %if %ifmacro %ifctx %ifidn %ifidni %ifid %ifnum %ifstr %iftoken %ifempty %ifenv %error %warning %fatal %rep %endrep %include %push %pop %repl %pathsearch %depend %use %arg %stacksize %local %line %comment %endcomment .nolist __FILE__ __LINE__ __SECT__  __BITS__ __OUTPUT_FORMAT__ __DATE__ __TIME__ __DATE_NUM__ __TIME_NUM__ __UTC_DATE__ __UTC_TIME__ __UTC_DATE_NUM__ __UTC_TIME_NUM__  __PASS__ struc endstruc istruc at iend align alignb sectalign daz nodaz up down zero default option assume public bits use16 use32 use64 default section segment absolute extern global common cpu float __utf16__ __utf16le__ __utf16be__ __utf32__ __utf32le__ __utf32be__ __float8__ __float16__ __float32__ __float64__ __float80m__ __float80e__ __float128l__ __float128h__ __Infinity__ __QNaN__ __SNaN__ Inf NaN QNaN SNaN float8 float16 float32 float64 float80m float80e float128l float128h __FLOAT_DAZ__ __FLOAT_ROUND__ __FLOAT__"
+},contains:[s.COMMENT(";","$",{relevance:0}),{className:"number",variants:[{
+begin:"\\b(?:([0-9][0-9_]*)?\\.[0-9_]*(?:[eE][+-]?[0-9_]+)?|(0[Xx])?[0-9][0-9_]*(\\.[0-9_]*)?(?:[pP](?:[+-]?[0-9_]+)?)?)\\b",
+relevance:0},{begin:"\\$[0-9][0-9A-Fa-f]*",relevance:0},{
+begin:"\\b(?:[0-9A-Fa-f][0-9A-Fa-f_]*[Hh]|[0-9][0-9_]*[DdTt]?|[0-7][0-7_]*[QqOo]|[0-1][0-1_]*[BbYy])\\b"
+},{
+begin:"\\b(?:0[Xx][0-9A-Fa-f_]+|0[DdTt][0-9_]+|0[QqOo][0-7_]+|0[BbYy][0-1_]+)\\b"
+}]},s.QUOTE_STRING_MODE,{className:"string",variants:[{begin:"'",end:"[^\\\\]'"
+},{begin:"`",end:"[^\\\\]`"}],relevance:0},{className:"symbol",variants:[{
+begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)"},{
+begin:"^\\s*%%[A-Za-z0-9_$#@~.?]*:"}],relevance:0},{className:"subst",
+begin:"%[0-9]+",relevance:0},{className:"subst",begin:"%!S+",relevance:0},{
+className:"meta",begin:/^\s*\.[\w_-]+/}]})})());
+hljs.registerLanguage("xl",(()=>{"use strict";return e=>{const t={
+$pattern:/[a-zA-Z][a-zA-Z0-9_?]*/,
+keyword:"if then else do while until for loop import with is as where when by data constant integer real text name boolean symbol infix prefix postfix block tree",
+literal:"true false nil",
+built_in:"in mod rem and or xor not abs sign floor ceil sqrt sin cos tan asin acos atan exp expm1 log log2 log10 log1p pi at text_length text_range text_find text_replace contains page slide basic_slide title_slide title subtitle fade_in fade_out fade_at clear_color color line_color line_width texture_wrap texture_transform texture scale_?x scale_?y scale_?z? translate_?x translate_?y translate_?z? rotate_?x rotate_?y rotate_?z? rectangle circle ellipse sphere path line_to move_to quad_to curve_to theme background contents locally time mouse_?x mouse_?y mouse_buttons ObjectLoader Animate MovieCredits Slides Filters Shading Materials LensFlare Mapping VLCAudioVideo StereoDecoder PointCloud NetworkAccess RemoteControl RegExp ChromaKey Snowfall NodeJS Speech Charts"
+},a={className:"string",begin:'"',end:'"',illegal:"\\n"},n={
+beginKeywords:"import",end:"$",keywords:t,contains:[a]},o={className:"function",
+begin:/[a-z][^\n]*->/,returnBegin:!0,end:/->/,contains:[e.inherit(e.TITLE_MODE,{
+starts:{endsWithParent:!0,keywords:t}})]};return{name:"XL",aliases:["tao"],
+keywords:t,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,{
+className:"string",begin:"'",end:"'",illegal:"\\n"},{className:"string",
+begin:"<<",end:">>"},o,n,{className:"number",
+begin:"[0-9]+#[0-9A-Z_]+(\\.[0-9-A-Z_]+)?#?([Ee][+-]?[0-9]+)?"},e.NUMBER_MODE]}}
+})());
+hljs.registerLanguage("xquery",(()=>{"use strict";return e=>({name:"XQuery",
+aliases:["xpath","xq"],case_insensitive:!1,
+illegal:/(proc)|(abstract)|(extends)|(until)|(#)/,keywords:{
+$pattern:/[a-zA-Z$][a-zA-Z0-9_:-]*/,
+keyword:"module schema namespace boundary-space preserve no-preserve strip default collation base-uri ordering context decimal-format decimal-separator copy-namespaces empty-sequence except exponent-separator external grouping-separator inherit no-inherit lax minus-sign per-mille percent schema-attribute schema-element strict unordered zero-digit declare import option function validate variable for at in let where order group by return if then else tumbling sliding window start when only end previous next stable ascending descending allowing empty greatest least some every satisfies switch case typeswitch try catch and or to union intersect instance of treat as castable cast map array delete insert into replace value rename copy modify update",
+type:"item document-node node attribute document element comment namespace namespace-node processing-instruction text construction xs:anyAtomicType xs:untypedAtomic xs:duration xs:time xs:decimal xs:float xs:double xs:gYearMonth xs:gYear xs:gMonthDay xs:gMonth xs:gDay xs:boolean xs:base64Binary xs:hexBinary xs:anyURI xs:QName xs:NOTATION xs:dateTime xs:dateTimeStamp xs:date xs:string xs:normalizedString xs:token xs:language xs:NMTOKEN xs:Name xs:NCName xs:ID xs:IDREF xs:ENTITY xs:integer xs:nonPositiveInteger xs:negativeInteger xs:long xs:int xs:short xs:byte xs:nonNegativeInteger xs:unisignedLong xs:unsignedInt xs:unsignedShort xs:unsignedByte xs:positiveInteger xs:yearMonthDuration xs:dayTimeDuration",
+literal:"eq ne lt le gt ge is self:: child:: descendant:: descendant-or-self:: attribute:: following:: following-sibling:: parent:: ancestor:: ancestor-or-self:: preceding:: preceding-sibling:: NaN"
+},contains:[{className:"variable",begin:/[$][\w\-:]+/},{className:"built_in",
+variants:[{begin:/\barray:/,
+end:/(?:append|filter|flatten|fold-(?:left|right)|for-each(?:-pair)?|get|head|insert-before|join|put|remove|reverse|size|sort|subarray|tail)\b/
+},{begin:/\bmap:/,
+end:/(?:contains|entry|find|for-each|get|keys|merge|put|remove|size)\b/},{
+begin:/\bmath:/,
+end:/(?:a(?:cos|sin|tan[2]?)|cos|exp(?:10)?|log(?:10)?|pi|pow|sin|sqrt|tan)\b/
+},{begin:/\bop:/,end:/\(/,excludeEnd:!0},{begin:/\bfn:/,end:/\(/,excludeEnd:!0
+},{
+begin:/[^</$:'"-]\b(?:abs|accumulator-(?:after|before)|adjust-(?:date(?:Time)?|time)-to-timezone|analyze-string|apply|available-(?:environment-variables|system-properties)|avg|base-uri|boolean|ceiling|codepoints?-(?:equal|to-string)|collation-key|collection|compare|concat|contains(?:-token)?|copy-of|count|current(?:-)?(?:date(?:Time)?|time|group(?:ing-key)?|output-uri|merge-(?:group|key))?data|dateTime|days?-from-(?:date(?:Time)?|duration)|deep-equal|default-(?:collation|language)|distinct-values|document(?:-uri)?|doc(?:-available)?|element-(?:available|with-id)|empty|encode-for-uri|ends-with|environment-variable|error|escape-html-uri|exactly-one|exists|false|filter|floor|fold-(?:left|right)|for-each(?:-pair)?|format-(?:date(?:Time)?|time|integer|number)|function-(?:arity|available|lookup|name)|generate-id|has-children|head|hours-from-(?:dateTime|duration|time)|id(?:ref)?|implicit-timezone|in-scope-prefixes|index-of|innermost|insert-before|iri-to-uri|json-(?:doc|to-xml)|key|lang|last|load-xquery-module|local-name(?:-from-QName)?|(?:lower|upper)-case|matches|max|minutes-from-(?:dateTime|duration|time)|min|months?-from-(?:date(?:Time)?|duration)|name(?:space-uri-?(?:for-prefix|from-QName)?)?|nilled|node-name|normalize-(?:space|unicode)|not|number|one-or-more|outermost|parse-(?:ietf-date|json)|path|position|(?:prefix-from-)?QName|random-number-generator|regex-group|remove|replace|resolve-(?:QName|uri)|reverse|root|round(?:-half-to-even)?|seconds-from-(?:dateTime|duration|time)|snapshot|sort|starts-with|static-base-uri|stream-available|string-?(?:join|length|to-codepoints)?|subsequence|substring-?(?:after|before)?|sum|system-property|tail|timezone-from-(?:date(?:Time)?|time)|tokenize|trace|trans(?:form|late)|true|type-available|unordered|unparsed-(?:entity|text)?-?(?:public-id|uri|available|lines)?|uri-collection|xml-to-json|years?-from-(?:date(?:Time)?|duration)|zero-or-one)\b/
+},{begin:/\blocal:/,end:/\(/,excludeEnd:!0},{begin:/\bzip:/,
+end:/(?:zip-file|(?:xml|html|text|binary)-entry| (?:update-)?entries)\b/},{
+begin:/\b(?:util|db|functx|app|xdmp|xmldb):/,end:/\(/,excludeEnd:!0}]},{
+className:"string",variants:[{begin:/"/,end:/"/,contains:[{begin:/""/,
+relevance:0}]},{begin:/'/,end:/'/,contains:[{begin:/''/,relevance:0}]}]},{
+className:"number",
+begin:/(\b0[0-7_]+)|(\b0x[0-9a-fA-F_]+)|(\b[1-9][0-9_]*(\.[0-9_]+)?)|[0_]\b/,
+relevance:0},{className:"comment",begin:/\(:/,end:/:\)/,relevance:10,contains:[{
+className:"doctag",begin:/@\w+/}]},{className:"meta",begin:/%[\w\-:]+/},{
+className:"title",begin:/\bxquery version "[13]\.[01]"\s?(?:encoding ".+")?/,
+end:/;/},{
+beginKeywords:"element attribute comment document processing-instruction",
+end:/\{/,excludeEnd:!0},{begin:/<([\w._:-]+)(\s+\S*=('|").*('|"))?>/,
+end:/(\/[\w._:-]+>)/,subLanguage:"xml",contains:[{begin:/\{/,end:/\}/,
+subLanguage:"xquery"},"self"]}]})})());
+hljs.registerLanguage("zephir",(()=>{"use strict";return e=>{const n={
+className:"string",contains:[e.BACKSLASH_ESCAPE],
+variants:[e.inherit(e.APOS_STRING_MODE,{illegal:null
+}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null})]},s=e.UNDERSCORE_TITLE_MODE,a={
+variants:[e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE]
+},i="namespace class interface use extends function return abstract final public protected private static deprecated throw try catch Exception echo empty isset instanceof unset let var new const self require if else elseif switch case default do while loop for continue break likely unlikely __LINE__ __FILE__ __DIR__ __FUNCTION__ __CLASS__ __TRAIT__ __METHOD__ __NAMESPACE__ array boolean float double integer object resource string char long unsigned bool int uint ulong uchar true false null undefined"
+;return{name:"Zephir",aliases:["zep"],keywords:i,
+contains:[e.C_LINE_COMMENT_MODE,e.COMMENT(/\/\*/,/\*\//,{contains:[{
+className:"doctag",begin:/@[A-Za-z]+/}]}),{className:"string",
+begin:/<<<['"]?\w+['"]?$/,end:/^\w+;/,contains:[e.BACKSLASH_ESCAPE]},{
+begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",
+beginKeywords:"function fn",end:/[;{]/,excludeEnd:!0,illegal:/\$|\[|%/,
+contains:[s,{className:"params",begin:/\(/,end:/\)/,keywords:i,
+contains:["self",e.C_BLOCK_COMMENT_MODE,n,a]}]},{className:"class",
+beginKeywords:"class interface",end:/\{/,excludeEnd:!0,illegal:/[:($"]/,
+contains:[{beginKeywords:"extends implements"},s]},{beginKeywords:"namespace",
+end:/;/,illegal:/[.']/,contains:[s]},{beginKeywords:"use",end:/;/,contains:[s]
+},{begin:/=>/},n,a]}}})());
\ No newline at end of file
diff --git a/lib/jackson/BUILD b/lib/jackson/BUILD
index 64f0076..f11b96d 100644
--- a/lib/jackson/BUILD
+++ b/lib/jackson/BUILD
@@ -12,6 +12,7 @@
     name = "jackson-core",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = [
+        "//java/com/google/gerrit/acceptance:__pkg__",
         "//java/com/google/gerrit/elasticsearch:__pkg__",
         "//plugins:__pkg__",
     ],
diff --git a/lib/jetty/BUILD b/lib/jetty/BUILD
index 6417385..86d455f 100644
--- a/lib/jetty/BUILD
+++ b/lib/jetty/BUILD
@@ -4,7 +4,10 @@
     name = "servlet",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
-    exports = ["@jetty-servlet//jar"],
+    exports = [
+        ":util-ajax",
+        "@jetty-servlet//jar",
+    ],
     runtime_deps = [":security"],
 )
 
@@ -69,3 +72,9 @@
     data = ["//lib:LICENSE-Apache2.0"],
     exports = ["@jetty-util//jar"],
 )
+
+java_library(
+    name = "util-ajax",
+    data = ["//lib:LICENSE-Apache2.0"],
+    exports = ["@jetty-util-ajax//jar"],
+)
diff --git a/lib/jgit/BUILD b/lib/jgit/BUILD
deleted file mode 100644
index e69de29..0000000
--- a/lib/jgit/BUILD
+++ /dev/null
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
deleted file mode 100644
index 519d6ed..0000000
--- a/lib/jgit/jgit.bzl
+++ /dev/null
@@ -1,75 +0,0 @@
-load("//tools/bzl:maven_jar.bzl", "MAVEN_CENTRAL", "maven_jar")
-
-_JGIT_VERS = "5.3.9.202012012026-r"
-
-_DOC_VERS = _JGIT_VERS  # Set to _JGIT_VERS unless using a snapshot
-
-JGIT_DOC_URL = "https://archive.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
-
-_JGIT_REPO = MAVEN_CENTRAL  # Leave here even if set to MAVEN_CENTRAL.
-
-# set this to use a local version.
-# "/home/<user>/projects/jgit"
-LOCAL_JGIT_REPO = ""
-
-def jgit_repos():
-    if LOCAL_JGIT_REPO:
-        native.local_repository(
-            name = "jgit",
-            path = LOCAL_JGIT_REPO,
-        )
-        jgit_maven_repos_dev()
-    else:
-        jgit_maven_repos()
-
-def jgit_maven_repos_dev():
-    # Transitive dependencies from JGit's WORKSPACE.
-    maven_jar(
-        name = "hamcrest-library",
-        artifact = "org.hamcrest:hamcrest-library:1.3",
-        sha1 = "4785a3c21320980282f9f33d0d1264a69040538f",
-    )
-    maven_jar(
-        name = "jzlib",
-        artifact = "com.jcraft:jzlib:1.1.1",
-        sha1 = "a1551373315ffc2f96130a0e5704f74e151777ba",
-    )
-
-def jgit_maven_repos():
-    maven_jar(
-        name = "jgit-lib",
-        artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
-        repository = _JGIT_REPO,
-        sha1 = "b6d3af64d2538db1c25ee8cf9f2346fd8663321b",
-    )
-    maven_jar(
-        name = "jgit-servlet",
-        artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
-        repository = _JGIT_REPO,
-        sha1 = "1f68350b98cbbd9a5219ad5827b3aa6c46a15dea",
-    )
-    maven_jar(
-        name = "jgit-archive",
-        artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
-        repository = _JGIT_REPO,
-        sha1 = "7a4bf3ac728274129acf8c13c11b66199808eb20",
-    )
-    maven_jar(
-        name = "jgit-junit",
-        artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
-        repository = _JGIT_REPO,
-        sha1 = "dd8a2b2cd0b65ee00c260d1a1e7ed33aed951c69",
-    )
-
-def jgit_dep(name):
-    mapping = {
-        "@jgit-archive//jar": "@jgit//org.eclipse.jgit.archive:jgit-archive",
-        "@jgit-junit//jar": "@jgit//org.eclipse.jgit.junit:junit",
-        "@jgit-lib//jar": "@jgit//org.eclipse.jgit:jgit",
-        "@jgit-servlet//jar": "@jgit//org.eclipse.jgit.http.server:jgit-servlet",
-    }
-
-    if LOCAL_JGIT_REPO:
-        return mapping[name]
-    else:
-        return name
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUILD b/lib/jgit/org.eclipse.jgit.archive/BUILD
deleted file mode 100644
index 151cd71..0000000
--- a/lib/jgit/org.eclipse.jgit.archive/BUILD
+++ /dev/null
@@ -1,10 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-load("//lib/jgit:jgit.bzl", "jgit_dep")
-
-java_library(
-    name = "jgit-archive",
-    data = ["//lib:LICENSE-jgit"],
-    visibility = ["//visibility:public"],
-    exports = [jgit_dep("@jgit-archive//jar")],
-    runtime_deps = ["//lib/jgit/org.eclipse.jgit:jgit"],
-)
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUILD b/lib/jgit/org.eclipse.jgit.http.server/BUILD
deleted file mode 100644
index fd634a5..0000000
--- a/lib/jgit/org.eclipse.jgit.http.server/BUILD
+++ /dev/null
@@ -1,10 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-load("//lib/jgit:jgit.bzl", "jgit_dep")
-
-java_library(
-    name = "jgit-servlet",
-    data = ["//lib:LICENSE-jgit"],
-    visibility = ["//visibility:public"],
-    exports = [jgit_dep("@jgit-servlet//jar")],
-    runtime_deps = ["//lib/jgit/org.eclipse.jgit:jgit"],
-)
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUILD b/lib/jgit/org.eclipse.jgit.junit/BUILD
deleted file mode 100644
index abc522b..0000000
--- a/lib/jgit/org.eclipse.jgit.junit/BUILD
+++ /dev/null
@@ -1,11 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-load("//lib/jgit:jgit.bzl", "jgit_dep")
-
-java_library(
-    name = "junit",
-    testonly = True,
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = [jgit_dep("@jgit-junit//jar")],
-    runtime_deps = ["//lib/jgit/org.eclipse.jgit:jgit"],
-)
diff --git a/lib/jgit/org.eclipse.jgit/BUILD b/lib/jgit/org.eclipse.jgit/BUILD
deleted file mode 100644
index c1f2607..0000000
--- a/lib/jgit/org.eclipse.jgit/BUILD
+++ /dev/null
@@ -1,20 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-load("//lib/jgit:jgit.bzl", "jgit_dep")
-
-java_library(
-    name = "jgit",
-    data = ["//lib:LICENSE-jgit"],
-    visibility = ["//visibility:public"],
-    exports = [jgit_dep("@jgit-lib//jar")],
-    runtime_deps = [
-        ":javaewah",
-        "//lib/log:api",
-    ],
-)
-
-java_library(
-    name = "javaewah",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@javaewah//jar"],
-)
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index 1597c02..1cd8d52 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -34,44 +34,44 @@
     bower_archive(
         name = "iron-a11y-announcer",
         package = "PolymerElements/iron-a11y-announcer",
-        version = "1.0.6",
-        sha1 = "14aed1e1b300ea344e80362e875919ea3d104dcc",
+        version = "2.1.0",
+        sha1 = "bda12ed6fe7b98a64bf5f70f3e84384053763190",
     )
     bower_archive(
         name = "iron-a11y-keys-behavior",
         package = "PolymerElements/iron-a11y-keys-behavior",
-        version = "1.1.9",
-        sha1 = "f58358ee652c67e6e721364ba50fb77a2ece1465",
+        version = "2.1.1",
+        sha1 = "4c8f303479253301e81c63b8ba7bd4cfb62ddf55",
     )
     bower_archive(
         name = "iron-behaviors",
         package = "PolymerElements/iron-behaviors",
-        version = "1.0.18",
-        sha1 = "e231a1a02b090f5183db917639fdb96cdd0dca18",
+        version = "2.1.1",
+        sha1 = "d2418e886c3237dcbc8d74a956eec367a95cd068",
     )
     bower_archive(
         name = "iron-checked-element-behavior",
         package = "PolymerElements/iron-checked-element-behavior",
-        version = "1.0.6",
-        sha1 = "93ad3554cec119d8c5732d1c722ad113e1866370",
+        version = "2.1.1",
+        sha1 = "822b6c73e349cf5174e3a17aa9b3d2cb823c37ac",
     )
     bower_archive(
         name = "iron-fit-behavior",
         package = "PolymerElements/iron-fit-behavior",
-        version = "1.2.7",
-        sha1 = "01c485fbf898307029bbb72ac7e132db1570a842",
+        version = "2.2.1",
+        sha1 = "7b12bc96bf05f04bbb6ad78a16d6c39758263a14",
     )
     bower_archive(
         name = "iron-flex-layout",
         package = "PolymerElements/iron-flex-layout",
-        version = "1.3.9",
-        sha1 = "d987b924cf29fcfe4b393833e81fdc9f1e268796",
+        version = "2.0.3",
+        sha1 = "c88e9577cabb005ea6d33f35b97d9c39c68f3d9e",
     )
     bower_archive(
         name = "iron-form-element-behavior",
         package = "PolymerElements/iron-form-element-behavior",
-        version = "1.0.7",
-        sha1 = "7b5a79e02cc32f0918725dd26925d0df1e03ed12",
+        version = "2.1.3",
+        sha1 = "634f01cdedd7a616ae025fdcde85c6c5804f6377",
     )
     bower_archive(
         name = "iron-menu-behavior",
@@ -82,20 +82,20 @@
     bower_archive(
         name = "iron-meta",
         package = "PolymerElements/iron-meta",
-        version = "1.1.3",
-        sha1 = "f77eba3f6f6817f10bda33918bde8f963d450041",
+        version = "2.1.1",
+        sha1 = "7985a9f18b6c32d62f5d3870d58d73ef66613cb9",
     )
     bower_archive(
         name = "iron-resizable-behavior",
-        package = "polymerelements/iron-resizable-behavior",
-        version = "1.0.6",
-        sha1 = "719c2a8a1a784f8aefcdeef41fcc2e5a03518d9e",
+        package = "PolymerElements/iron-resizable-behavior",
+        version = "2.1.1",
+        sha1 = "31e32da6880a983da32da21ee3f483525b24e458",
     )
     bower_archive(
         name = "iron-validatable-behavior",
         package = "PolymerElements/iron-validatable-behavior",
-        version = "1.1.2",
-        sha1 = "7111f34ff32e1510131dfbdb1eaa51bfa291e8be",
+        version = "2.1.0",
+        sha1 = "b5dcf3bf4d95b074b74f8170d7122d34ab417daf",
     )
     bower_archive(
         name = "lodash",
@@ -111,15 +111,15 @@
     )
     bower_archive(
         name = "neon-animation",
-        package = "polymerelements/neon-animation",
-        version = "1.2.5",
-        sha1 = "588d289f779d02b21ce5b676e257bbd6155649e8",
+        package = "PolymerElements/neon-animation",
+        version = "2.2.1",
+        sha1 = "865f4252c6306b91609769fefefb4f641361931f",
     )
     bower_archive(
         name = "paper-behaviors",
         package = "PolymerElements/paper-behaviors",
-        version = "1.0.13",
-        sha1 = "a81eab28a952e124c208430e17508d9a1aae4ee7",
+        version = "2.1.1",
+        sha1 = "af59936a9015cda4abcfb235f831090a41faa2c4",
     )
     bower_archive(
         name = "paper-icon-button",
@@ -130,16 +130,22 @@
     bower_archive(
         name = "paper-ripple",
         package = "PolymerElements/paper-ripple",
-        version = "1.0.10",
-        sha1 = "21199db50d02b842da54bd6f4f1d1b10b474e893",
+        version = "2.1.1",
+        sha1 = "d402c8165c6a09d17c12a2b421e69ea54e2fc8ef",
     )
     bower_archive(
         name = "paper-styles",
         package = "PolymerElements/paper-styles",
-        # Basically 1.3.1 but with
-        # https://github.com/PolymerElements/paper-styles/pull/164 applied
-        version = "dd0b13e186b9690d5e74a93f6e51e0835ea60495",
-        sha1 = "f859a8dee403fbb724e8d0cf009db79c6dd61b47",
+        # Basically 2.1.0 but with
+        # https://github.com/PolymerElements/paper-styles/pull/165 applied
+        version = "a6c207e6eee3402fd7a6550e6f9c387ca22ec4c4",
+        sha1 = "6bd17410578b5d4017ccef330393a4b41b1c716e",
+    )
+    bower_archive(
+        name = "shadycss",
+        package = "webcomponents/shadycss",
+        version = "1.9.1",
+        sha1 = "3ef3bd54280ea2d7ce90434620354a2022c8e13d",
     )
     bower_archive(
         name = "sinon-chai",
@@ -160,14 +166,8 @@
         sha1 = "d6c07a0112ab2e9677fe085933744466a89232fb",
     )
     bower_archive(
-        name = "web-animations-js",
-        package = "web-animations/web-animations-js",
-        version = "2.3.1",
-        sha1 = "2ba5548d36188fe54555eaad0a576de4b027661e",
-    )
-    bower_archive(
         name = "webcomponentsjs",
         package = "webcomponents/webcomponentsjs",
-        version = "0.7.24",
-        sha1 = "559227f8ee9db9bfbd81989f24510cc0c1bfc65c",
+        version = "1.3.3",
+        sha1 = "bbad90bd8301a2f2f5e014e750e0c86351579391",
     )
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
index 64ab611..7fd61c7 100644
--- a/lib/js/bower_components.bzl
+++ b/lib/js/bower_components.bzl
@@ -77,7 +77,6 @@
         deps = [
             ":iron-behaviors",
             ":iron-overlay-behavior",
-            ":iron-resizable-behavior",
             ":neon-animation",
             ":polymer",
         ],
@@ -195,11 +194,9 @@
         name = "neon-animation",
         license = "//lib:LICENSE-polymer",
         deps = [
-            ":iron-meta",
             ":iron-resizable-behavior",
             ":iron-selector",
             ":polymer",
-            ":web-animations-js",
         ],
     )
     bower_component(
@@ -331,14 +328,15 @@
     bower_component(
         name = "polymer",
         license = "//lib:LICENSE-polymer",
-        deps = [":webcomponentsjs"],
+        deps = [
+            ":shadycss",
+            ":webcomponentsjs",
+        ],
         seed = True,
     )
     bower_component(
-        name = "promise-polyfill",
-        license = "//lib:LICENSE-promise-polyfill",
-        deps = [":polymer"],
-        seed = True,
+        name = "shadycss",
+        license = "//lib:LICENSE-shadycss",
     )
     bower_component(
         name = "sinon-chai",
@@ -358,10 +356,6 @@
         seed = True,
     )
     bower_component(
-        name = "web-animations-js",
-        license = "//lib:LICENSE-Apache2.0",
-    )
-    bower_component(
         name = "web-component-tester",
         license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
         deps = [
diff --git a/lib/js/npm.bzl b/lib/js/npm.bzl
index 8a9e1ee..5a6a8c0 100644
--- a/lib/js/npm.bzl
+++ b/lib/js/npm.bzl
@@ -1,11 +1,11 @@
 NPM_VERSIONS = {
     "bower": "1.8.8",
     "crisper": "2.0.2",
-    "polymer-bundler": "4.0.2",
+    "polymer-bundler": "4.0.9",
 }
 
 NPM_SHA1S = {
     "bower": "82544be34a33aeae7efb8bdf9905247b2cffa985",
     "crisper": "7183c58cea33632fb036c91cefd1b43e390d22a2",
-    "polymer-bundler": "6b296b6099ab5a0e93ca914cbe93e753f2395910",
+    "polymer-bundler": "c80c9815690d76656d1fa6a231481850b4fa3874",
 }
diff --git a/lib/log/BUILD b/lib/log/BUILD
index b119b94..fa5bc45 100644
--- a/lib/log/BUILD
+++ b/lib/log/BUILD
@@ -5,7 +5,7 @@
     data = ["//lib:LICENSE-slf4j"],
     visibility = [
         "//javatests/com/google/gerrit/elasticsearch:__pkg__",
-        "//lib/jgit/org.eclipse.jgit:__pkg__",
+        "//lib:__pkg__",
         "//plugins:__pkg__",
     ],
     exports = ["@log-api//jar"],
diff --git a/lib/mina/BUILD b/lib/mina/BUILD
index 5ad47cd..2f98ee3 100644
--- a/lib/mina/BUILD
+++ b/lib/mina/BUILD
@@ -6,6 +6,7 @@
     visibility = ["//visibility:public"],
     exports = [
         ":eddsa",
+        "@sshd-common//jar",
         "@sshd-mina//jar",
         "@sshd//jar",
     ],
diff --git a/lib/mockito/BUILD b/lib/mockito/BUILD
index cff36cd..2aaf56d 100644
--- a/lib/mockito/BUILD
+++ b/lib/mockito/BUILD
@@ -8,8 +8,7 @@
 java_library(
     name = "mockito",
     data = ["//lib:LICENSE-mockito"],
-    # Only exposed for plugin tests; core tests should use Easymock
-    visibility = ["//java/com/google/gerrit/acceptance:__pkg__"],
+    visibility = ["//visibility:public"],
     exports = ["@mockito//jar"],
     runtime_deps = [
         ":byte-buddy",
@@ -21,13 +20,13 @@
 java_library(
     name = "byte-buddy",
     data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@byte-buddy//jar"],
+    exports = ["@bytebuddy//jar"],
 )
 
 java_library(
     name = "byte-buddy-agent",
     data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@byte-buddy-agent//jar"],
+    exports = ["@bytebuddy-agent//jar"],
 )
 
 java_library(
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index 6341dc5..d270853 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -26,20 +26,14 @@
 j2objc
 jackson-annotations
 jackson-core
-javassist
 jna
 jruby
 mina-core
 nekohtml
 objenesis
 openid-consumer
-powermock-api-easymock
-powermock-api-support
-powermock-core
-powermock-module-junit4
-powermock-module-junit4-common
-powermock-reflect
 sshd
+sshd-common
 sshd-mina
 testcontainers
 testcontainers-elasticsearch
diff --git a/lib/powermock/BUILD b/lib/powermock/BUILD
deleted file mode 100644
index 39df164..0000000
--- a/lib/powermock/BUILD
+++ /dev/null
@@ -1,69 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-
-java_library(
-    name = "powermock-module-junit4",
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = [
-        ":powermock-module-junit4-common",
-        "//lib:junit",
-        "@powermock-module-junit4//jar",
-    ],
-)
-
-java_library(
-    name = "powermock-module-junit4-common",
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = [
-        ":powermock-reflect",
-        "//lib:junit",
-        "@powermock-module-junit4-common//jar",
-    ],
-)
-
-java_library(
-    name = "powermock-reflect",
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = [
-        "//lib:junit",
-        "//lib/easymock:objenesis",
-        "@powermock-reflect//jar",
-    ],
-)
-
-java_library(
-    name = "powermock-api-easymock",
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = [
-        ":powermock-api-support",
-        "//lib/easymock",
-        "@powermock-api-easymock//jar",
-    ],
-)
-
-java_library(
-    name = "powermock-api-support",
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = [
-        ":powermock-core",
-        ":powermock-reflect",
-        "//lib:junit",
-        "@powermock-api-support//jar",
-    ],
-)
-
-java_library(
-    name = "powermock-core",
-    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
-    visibility = ["//visibility:public"],
-    exports = [
-        ":powermock-reflect",
-        "//lib:javassist",
-        "//lib:junit",
-        "@powermock-core//jar",
-    ],
-)
diff --git a/modules/jgit b/modules/jgit
new file mode 160000
index 0000000..0038627
--- /dev/null
+++ b/modules/jgit
@@ -0,0 +1 @@
+Subproject commit 00386272264f65c41e36406f7c2e9ea6e901276e
diff --git a/package.json b/package.json
index db09e9c..56acf61 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "gerrit",
-  "version": "3.0.7-SNAPSHOT",
+  "version": "3.1.0-SNAPSHOT",
   "description": "Gerrit Code Review",
   "dependencies": {},
   "devDependencies": {
diff --git a/plugins/BUILD b/plugins/BUILD
index b9776ea..5f9c142 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -44,7 +44,7 @@
     "//java/com/google/gerrit/mail",
     "//java/com/google/gerrit/metrics",
     "//java/com/google/gerrit/metrics/dropwizard",
-    "//java/com/google/gerrit/reviewdb:server",
+    "//java/com/google/gerrit/entities",
     "//java/com/google/gerrit/server/api",
     "//java/com/google/gerrit/server/audit",
     "//java/com/google/gerrit/server/cache/mem",
@@ -69,8 +69,8 @@
     "//lib/httpcomponents:httpclient",
     "//lib/httpcomponents:httpcore",
     "//lib/jackson:jackson-core",
-    "//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet",
-    "//lib/jgit/org.eclipse.jgit:jgit",
+    "//lib:jgit-servlet",
+    "//lib:jgit",
     "//lib:jsr305",
     "//lib/log:api",
     "//lib/log:log4j",
@@ -94,7 +94,18 @@
 ]
 
 java_binary(
+    name = "bouncycastle-deploy-env",
+    main_class = "Dummy",
+    runtime_deps = [
+        "//lib/bouncycastle:bcpg",
+        "//lib/bouncycastle:bcpkix",
+        "//lib/bouncycastle:bcprov",
+    ],
+)
+
+java_binary(
     name = "plugin-api",
+    deploy_env = ["bouncycastle-deploy-env"],
     main_class = "Dummy",
     visibility = ["//visibility:public"],
     runtime_deps = [":plugin-lib"],
@@ -121,12 +132,12 @@
         "//antlr3:libquery_parser-src.jar",
         "//java/com/google/gerrit/common:libannotations-src.jar",
         "//java/com/google/gerrit/common:libserver-src.jar",
+        "//java/com/google/gerrit/entities:libentities-src.jar",
         "//java/com/google/gerrit/extensions:libapi-src.jar",
         "//java/com/google/gerrit/httpd:libhttpd-src.jar",
         "//java/com/google/gerrit/index:libindex-src.jar",
         "//java/com/google/gerrit/index:libquery_exception-src.jar",
         "//java/com/google/gerrit/pgm/init/api:libapi-src.jar",
-        "//java/com/google/gerrit/reviewdb:libserver-src.jar",
         "//java/com/google/gerrit/server:libserver-src.jar",
         "//java/com/google/gerrit/server/restapi:librestapi-src.jar",
         "//java/com/google/gerrit/sshd:libsshd-src.jar",
@@ -143,7 +154,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/util/http",
     ],
     pkgs = ["com.google.gerrit"],
diff --git a/plugins/delete-project b/plugins/delete-project
index 542bb81..debaa2c 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 542bb8137e356e8ed89856576499d185a9f5c757
+Subproject commit debaa2c6d66b5a3c1bc95bc09283344c88407d40
diff --git a/plugins/download-commands b/plugins/download-commands
index 41c61bf..1b98be8 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 41c61bf8c1869bff4e0b436f69478c2137d0ca07
+Subproject commit 1b98be8d371e68237182df0f04361017f2be2ea8
diff --git a/plugins/gitiles b/plugins/gitiles
index 0f6ffbe..3438c49 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 0f6ffbecc51a0571c8a77248b2655a834700d6af
+Subproject commit 3438c497cb1ef7711e023b89ba05ea3e973fdd9e
diff --git a/plugins/hooks b/plugins/hooks
index 089687b..5678f93 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 089687bdcc64b003d09a77f00eaa77bb79b15b9c
+Subproject commit 5678f93fa62df1ec2baedc3a407aff71ca96556d
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index d3b2a6e..20bec50 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit d3b2a6eabcb641e952f253e61b927cd1f7f6e30c
+Subproject commit 20bec5084c7b90029b8860cbb2fb9a12928f6979
diff --git a/plugins/replication b/plugins/replication
index 16d00cd..17e8c41 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 16d00cdf95a0e04db345512fcd48c8ec3d867449
+Subproject commit 17e8c41d5e7b83c419a52a6f7da0ad3ade514f76
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index be50378..b9e1e8d 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit be5037839d987a319dac2236e9c1221d4d31848d
+Subproject commit b9e1e8d61a324ca0592bbb7c80c2802618ef5bc9
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 8a3b6fa..4508df9 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 8a3b6faeaebc0f9b7d1af19eb0022b32994e476a
+Subproject commit 4508df9fe31f9fb70f1392173686ebf80a88de3f
diff --git a/plugins/webhooks b/plugins/webhooks
index 8f7a232..11b6fb8 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 8f7a232ef94f60b20c059fff7066d0d1864bec8b
+Subproject commit 11b6fb81b5e4fb0bf3909f862c120d20d4fa9aaf
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 0e9b4bb..1c685ab 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -31,7 +31,7 @@
         "//lib/js:paper-toggle-button",
         "//lib/js:polymer",
         "//lib/js:polymer-resin",
-        "//lib/js:promise-polyfill",
+        "//lib/js:shadycss",
     ],
 )
 
diff --git a/polygerrit-ui/Polymer2.md b/polygerrit-ui/Polymer2.md
new file mode 100644
index 0000000..96bf779
--- /dev/null
+++ b/polygerrit-ui/Polymer2.md
@@ -0,0 +1,15 @@
+## Polymer 2 upgrade
+
+Gerrit is updating to use polymer 2 from polymer 1 by following the [Polymer 2.0 upgrade guide](https://polymer-library.polymer-project.org/2.0/docs/upgrade).
+
+Polymer 2 contains several breaking changes that may affect some of the UI features and plugins. One of the biggest change is to have the shadow DOM enabled. This will affect how you query elements inside of your component, how css style works within and across components, and several other usages.
+
+If you are owner of any plugins, please start following the [Polymer 2.0 upgrade guide](https://polymer-library.polymer-project.org/2.0/docs/upgrade) to migrate your plugins to be polymer 2 ready.
+
+If you notice any issues or need help with anything, don't hesitate to report to us [here](https://bugs.chromium.org/p/gerrit/issues/list).
+
+
+### Related resources
+
+- [Polymer 2.0 upgrade guide](https://polymer-library.polymer-project.org/2.0/docs/upgrade)
+- [Polymer Shadow DOM](https://polymer-library.polymer-project.org/2.0/docs/devguide/shadow-dom)
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 4dbe146..1fbf581 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -1,4 +1,8 @@
-# PolyGerrit
+# Gerrit Polymer Frontend
+
+Follow the
+[setup instructions for Gerrit backend developers](https://gerrit-review.googlesource.com/Documentation/dev-readme.html)
+where applicable.
 
 ## Installing [Bazel](https://bazel.build/)
 
@@ -20,8 +24,8 @@
 brew install npm
 ```
 
-All other platforms: [download from
-nodejs.org](https://nodejs.org/en/download/).
+All other platforms:
+[download from nodejs.org](https://nodejs.org/en/download/).
 
 Various steps below require installing additional npm packages. The full list of
 dependencies can be installed with:
@@ -33,17 +37,12 @@
 It may complain about a missing `typescript@2.3.4` peer dependency, which is
 harmless.
 
-If you're interested in the details, keep reading.
+## Running locally against production data
 
-## Local UI, Production Data
+#### Go server
 
-This is a quick and easy way to test your local changes against real data.
-Unfortunately, you can't sign in, so testing certain features will require
-you to use the "test data" technique described below.
-
-### Running the server
-
-To test the local UI against gerrit-review.googlesource.com:
+To test the local Polymer frontend against gerrit-review.googlesource.com
+simply execute:
 
 ```sh
 ./polygerrit-ui/run-server.sh
@@ -51,39 +50,51 @@
 
 Then visit http://localhost:8081
 
-## Local UI, Test Data
-
-1. [Build Gerrit](https://gerrit-review.googlesource.com/Documentation/dev-bazel.html#_gerrit_development_war_file)
-2. Set up a local test site. Docs
-   [here](https://gerrit-review.googlesource.com/Documentation/linux-quickstart.html) and
-   [here](https://gerrit-review.googlesource.com/Documentation/dev-readme.html#init).
-
-When your project is set up and works using the classic UI, run a test server
-that serves PolyGerrit:
-
-```sh
-bazel build gerrit &&
-  $(bazel info output_base)/external/local_jdk/bin/java -DsourceRoot=/path/to/my/checkout \
-  -jar bazel-bin/gerrit.war daemon --polygerrit-dev \
-  -d ../gerrit_testsite --console-log --show-stack-trace
-```
-
-Serving plugins
-
-> Local dev plugins must be put inside of gerrit/plugins
-
-Loading a single plugin file:
-
-```sh
-./polygerrit-ui/run-server.sh --plugins=plugins/my_plugin/static/my_plugin.js
-```
-
-Loading multiple plugin files:
+This method is based on a
+[simple hand-written Go webserver](https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/server.go).
+Mostly it just switches between serving files locally and proxying the real
+server based on the file name. It also does some basic response rewriting, e.g.
+it patches the `config/server/info` response with plugin information provided on
+the command line:
 
 ```sh
 ./polygerrit-ui/run-server.sh --plugins=plugins/my_plugin/static/my_plugin.js,plugins/my_plugin/static/my_plugin.html
 ```
 
+The biggest draw back of this method is that you cannot log in, so cannot test
+scenarios that require it.
+
+#### MITM Proxy
+
+[MITM Proxy](https://mitmproxy.org/) is an open source product for proxying
+https servers. The
+[contrib/mitm-ui/](https://gerrit.googlesource.com/gerrit/+/master/contrib/mitm-ui/)
+directory contains scripts (and documentation) for using this technology
+(instead of the Go server). These scripts are somewhat experimental and
+unmaintained though.
+
+## Running locally against a Gerrit test site
+
+Set up a local test site once:
+
+1. [Build Gerrit](https://gerrit-review.googlesource.com/Documentation/dev-bazel.html#_gerrit_development_war_file)
+2. [Set up a local test site](https://gerrit-review.googlesource.com/Documentation/dev-readme.html#init).
+3. Optionally [populate](https://gerrit.googlesource.com/gerrit/+/master/contrib/populate-fixture-data.py) your test site with some test data.
+
+For running a locally built Gerrit war against your test instance use
+[this command](https://gerrit-review.googlesource.com/Documentation/dev-readme.html#run_daemon),
+and add the `--polygerrit-dev` option, if you want to serve the Polymer frontend
+directly from the sources in `polygerrit_ui/app/` instead of from the war:
+
+```sh
+$(bazel info output_base)/external/local_jdk/bin/java \
+    -DsourceRoot=$(bazel info workspace) \
+    -jar bazel-bin/gerrit.war daemon \
+    -d $GERRIT_SITE \
+    --console-log \
+    --polygerrit-dev
+```
+
 ## Running Tests
 
 This step requires the `web-component-tester` npm module.
@@ -93,7 +104,7 @@
 
 For daily development you typically only want to run and debug individual tests.
 Run the local [Go proxy server](#go-server) and navigate for example to
-<http://localhost:8081/elements/change/gr-account-entry/gr-account-entry_test.html>.
+<http://localhost:8081/elements/shared/gr-account-entry/gr-account-entry_test.html>.
 Check "Disable cache" in the "Network" tab of Chrome's dev tools, so code
 changes are picked up on "reload".
 
@@ -115,11 +126,6 @@
 WCT_HEADLESS_MODE=1 WCT_ARGS='--verbose -l chrome' ./polygerrit-ui/app/run_test.sh
 ```
 
-Toolchain requirements for headless mode:
-
-* Chrome: 59+
-* web-component-tester: v6.5.0+
-
 ## Style guide
 
 We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml)
@@ -176,11 +182,14 @@
 ```
 
 ## Template Type Safety
-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.
+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.
+- 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.
 
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 3a3c586..5ec0274 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -181,7 +181,7 @@
         "embed/test.html",
         "test/common-test-setup.html",
         ":embed_test_files",
-        ":polygerrit_embed_ui.zip",
+        ":pg_code.zip",
         ":test_components.zip",
     ],
     # Should not run sandboxed.
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
index fec459b..970bfc7 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>async-foreach-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="async-foreach-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
index c21e96f..b61b142 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>base-url-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <script>
   /** @type {string} */
@@ -52,7 +54,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
-        _legacyUndefinedCheck: true,
         behaviors: [
           Gerrit.BaseUrlBehavior,
         ],
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
index 96d4a08..2c513f3 100644
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
@@ -15,10 +15,12 @@
 limitations under the License.
 -->
 <!-- Polymer included for the html import polyfill. -->
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <title>docs-url-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
 <link rel="import" href="docs-url-behavior.html">
 
@@ -38,7 +40,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'docs-url-behavior-element',
-        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.DocsUrlBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
index e445a78..8323ac6 100644
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>dom-util-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="dom-util-behavior.html">
 
@@ -46,7 +48,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
-        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.DomUtilBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.html b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.html
new file mode 100644
index 0000000..b5afab1
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.html
@@ -0,0 +1,55 @@
+<!--
+@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.
+-->
+
+<script>
+(function(window) {
+  'use strict';
+
+  window.Gerrit = window.Gerrit || {};
+
+  /** @polymerBehavior Gerrit.FireBehavior */
+  Gerrit.FireBehavior = {
+    /**
+     * Dispatches a custom event with an optional detail value.
+     *
+     * @param {string} type Name of event type.
+     * @param {*=} detail Detail value containing event-specific
+     *   payload.
+     * @param {{ bubbles: (boolean|undefined), cancelable: (boolean|undefined),
+     *     composed: (boolean|undefined) }=}
+     *  options Object specifying options.  These may include:
+     *  `bubbles` (boolean, defaults to `true`),
+     *  `cancelable` (boolean, defaults to false), and
+     *  `composed` (boolean, defaults to true).
+     * @return {!Event} The new event that was fired.
+     * @override
+     */
+    fire(type, detail, options) {
+      options = options || {};
+      detail = (detail === null || detail === undefined) ? {} : detail;
+      const event = new Event(type, {
+        bubbles: options.bubbles === undefined ? true : options.bubbles,
+        cancelable: Boolean(options.cancelable),
+        composed: options.composed === undefined ? true: options.composed,
+      });
+      event.detail = detail;
+      this.dispatchEvent(event);
+      return event;
+    },
+  };
+})(window);
+</script>
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
index 530c76c..fedefc6 100644
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
@@ -88,10 +88,6 @@
             id: 'owner',
             name: 'Owner',
           },
-          publishDrafts: {
-            id: 'publishDrafts',
-            name: 'Publish Drafts',
-          },
           push: {
             id: 'push',
             name: 'Push',
@@ -138,6 +134,7 @@
      *    object.
      */
     toSortedArray(obj) {
+      if (!obj) { return []; }
       return Object.keys(obj).map(key => {
         return {
           id: key,
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
index 0b37a0d..0d1ee57 100644
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-access-behavior.html">
 
@@ -38,7 +40,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
-        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.AccessBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
index d217577..0285e35 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-admin-nav-behavior.html">
 
@@ -41,7 +43,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
-        _legacyUndefinedCheck: true,
         behaviors: [
           Gerrit.AdminNavBehavior,
         ],
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
index b052d06..791e2af 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-table-behavior.html">
 
@@ -48,7 +50,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
-        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.ChangeTableBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
similarity index 60%
rename from polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html
rename to polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
index 40379e4..3106fc8 100644
--- a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
@@ -15,33 +15,28 @@
 limitations under the License.
 -->
 
+<script src="../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
+
 <script>
 (function(window) {
   'use strict';
 
-  const ANONYMOUS_NAME = 'Anonymous';
-
   window.Gerrit = window.Gerrit || {};
 
-  /** @polymerBehavior Gerrit.AnonymousNameBehavior */
-  Gerrit.AnonymousNameBehavior = {
+  /** @polymerBehavior Gerrit.DisplayNameBehavior */
+  Gerrit.DisplayNameBehavior = {
+    // TODO(dmfilippov) replace DisplayNameBehavior with GrDisplayNameUtils
+
     /**
      * enableEmail when true enables to fallback to using email if
      * the account name is not avilable.
      */
     getUserName(config, account, enableEmail) {
-      if (account && account.name) {
-        return account.name;
-      } else if (account && account.username) {
-        return account.username;
-      } else if (enableEmail && 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 GrDisplayNameUtils.getUserName(config, account, enableEmail);
+    },
 
-      return ANONYMOUS_NAME;
+    getGroupDisplayName(group) {
+      return GrDisplayNameUtils.getGroupDisplayName(group);
     },
   };
 })(window);
diff --git a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
similarity index 69%
rename from polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
rename to polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
index 820d6bc..3d4eca1 100644
--- a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
@@ -17,12 +17,14 @@
 -->
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-anonymous-name-behavior</title>
+<title>gr-display-name-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="gr-anonymous-name-behavior.html">
+<link rel="import" href="gr-display-name-behavior.html">
 
 <test-fixture id="basic">
   <template>
@@ -31,7 +33,7 @@
 </test-fixture>
 
 <script>
-  suite('gr-anonymous-name-behavior tests', () => {
+  suite('gr-display-name-behavior tests', () => {
     let element;
     // eslint-disable-next-line no-unused-vars
     const config = {
@@ -44,9 +46,8 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element-anon',
-        _legacyUndefinedCheck: true,
         behaviors: [
-          Gerrit.AnonymousNameBehavior,
+          Gerrit.DisplayNameBehavior,
         ],
       });
     });
@@ -55,21 +56,21 @@
       element = fixture('basic');
     });
 
-    test('test for it to return name', () => {
+    test('getUserName name only', () => {
       const account = {
         name: 'test-name',
       };
       assert.deepEqual(element.getUserName(config, account, true), 'test-name');
     });
 
-    test('test for it to return username', () => {
+    test('getUserName username only', () => {
       const account = {
         username: 'test-user',
       };
       assert.deepEqual(element.getUserName(config, account, true), 'test-user');
     });
 
-    test('test for it to return email', () => {
+    test('getUserName email only', () => {
       const account = {
         email: 'test-user@test-url.com',
       };
@@ -77,11 +78,11 @@
           'test-user@test-url.com');
     });
 
-    test('test for it not to Anonymous Coward as the anon name', () => {
+    test('getUserName returns not Anonymous Coward as the anon name', () => {
       assert.deepEqual(element.getUserName(config, null, true), 'Anonymous');
     });
 
-    test('test for the config returning the anon name', () => {
+    test('getUserName for the config returning the anon name', () => {
       const config = {
         user: {
           anonymous_coward_name: 'Test Anon',
@@ -89,5 +90,10 @@
       };
       assert.deepEqual(element.getUserName(config, null, true), 'Test Anon');
     });
+
+    test('getGroupDisplayName', () => {
+      assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
+          'Some user name (group)');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
index f6c765f..535483d 100644
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-list-view-behavior.html">
 
@@ -40,7 +42,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
-        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.ListViewBehavior],
       });
     });
@@ -68,10 +69,10 @@
 
     test('getFilterValue', () => {
       let params;
-      assert.equal(element.getFilterValue(params), null);
+      assert.equal(element.getFilterValue(params), '');
 
       params = {filter: null};
-      assert.equal(element.getFilterValue(params), null);
+      assert.equal(element.getFilterValue(params), '');
 
       params = {filter: 'test'};
       assert.equal(element.getFilterValue(params), 'test');
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index a4c1e86..28d6990 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -31,7 +31,7 @@
 
   window.Gerrit = window.Gerrit || {};
 
-  /** @polymerBehavior this */
+  /** @polymerBehavior Gerrit.PatchSetBehavior*/
   Gerrit.PatchSetBehavior = {
     EDIT_NAME: 'edit',
     PARENT_NAME: 'PARENT',
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
index b858c51..3db4084 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -15,10 +15,12 @@
 limitations under the License.
 -->
 <!-- Polymer included for the html import polyfill. -->
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <title>gr-patch-set-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
 <link rel="import" href="gr-patch-set-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index 75c2433..0046290 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -15,10 +15,12 @@
 limitations under the License.
 -->
 <!-- Polymer included for the html import polyfill. -->
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <title>gr-path-list-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
 <link rel="import" href="gr-path-list-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html b/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html
index 2dc070d..2fa9191 100644
--- a/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html
@@ -20,7 +20,7 @@
 
   window.Gerrit = window.Gerrit || {};
 
-  /** @polymerBehavior this */
+  /** @polymerBehavior Gerrit.RepoPluginConfig*/
   Gerrit.RepoPluginConfig = {
     // Should be kept in sync with
     // gerrit/java/com/google/gerrit/extensions/api/projects/ProjectConfigEntryType.java.
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
index 07d3484..0e2e99f 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../elements/shared/gr-tooltip/gr-tooltip.html">
 <script src="../../scripts/rootElement.js"></script>
 
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index 04d8b6e..0bf620f 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -89,7 +89,7 @@
       this._tooltip = tooltip;
       this.listen(window, 'scroll', '_handleWindowScroll');
       this.listen(this, 'mouseleave', '_handleHideTooltip');
-      this.listen(this, 'tap', '_handleHideTooltip');
+      this.listen(this, 'click', '_handleHideTooltip');
     },
 
     _handleHideTooltip(e) {
@@ -101,7 +101,7 @@
 
       this.unlisten(window, 'scroll', '_handleWindowScroll');
       this.unlisten(this, 'mouseleave', '_handleHideTooltip');
-      this.unlisten(this, 'tap', '_handleHideTooltip');
+      this.unlisten(this, 'click', '_handleHideTooltip');
       this.setAttribute('title', this._titleText);
       if (this._tooltip && this._tooltip.parentNode) {
         this._tooltip.parentNode.removeChild(this._tooltip);
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
index 943e000..173c8d4 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
@@ -17,9 +17,11 @@
 -->
 
 <title>tooltip-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-tooltip-behavior.html">
 
@@ -51,7 +53,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'tooltip-behavior-element',
-        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.TooltipBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
index d909e86..73e51d3 100644
--- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
@@ -17,9 +17,11 @@
 -->
 
 <title>gr-url-encoding-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-url-encoding-behavior.html">
 
@@ -40,7 +42,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
-        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.URLEncodingBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
index df7f3cf..3c5a733 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
@@ -96,8 +96,8 @@
 NOTE: doc-only shortcuts will not be customizable in the same way that other
 shortcuts are.
 -->
-<link rel="import" href="../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
 
 <script>
 (function(window) {
@@ -180,6 +180,7 @@
 
     SEARCH: 'SEARCH',
     SEND_REPLY: 'SEND_REPLY',
+    EMOJI_DROPDOWN: 'EMOJI_DROPDOWN',
   };
 
   const _help = new Map();
@@ -292,6 +293,8 @@
       'Show/hide selected inline diff');
 
   _describe(Shortcut.SEND_REPLY, ShortcutSection.REPLY_DIALOG, 'Send reply');
+  _describe(Shortcut.EMOJI_DROPDOWN, ShortcutSection.REPLY_DIALOG,
+      'Emoji dropdown');
 
   // Must be declared outside behavior implementation to be accessed inside
   // behavior functions.
@@ -423,6 +426,9 @@
     }
 
     describeBinding(binding) {
+      if (binding.length === 1) {
+        return [binding];
+      }
       return binding.split(':')[0].split('+').map(part => {
         switch (part) {
           case 'shift':
@@ -457,7 +463,7 @@
 
   window.Gerrit = window.Gerrit || {};
 
-  /** @polymerBehavior KeyboardShortcutBehavior */
+  /** @polymerBehavior Gerrit.KeyboardShortcutBehavior*/
   Gerrit.KeyboardShortcutBehavior = [
     Polymer.IronA11yKeysBehavior,
     {
@@ -506,7 +512,7 @@
       },
 
       // Alias for getKeyboardEvent.
-      /** @return {!(Event|PolymerDomApi|PolymerEventApi)} */
+      /** @return {!Event} */
       getKeyboardEvent(e) {
         return getKeyboardEvent(e);
       },
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
index b4451fe..3183c7e 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="keyboard-shortcut-behavior.html">
 
@@ -50,7 +52,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
-        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.KeyboardShortcutBehavior],
         keyBindings: {
           k: '_handleKey',
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
index 354bedc..85bc6a1 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../base-url-behavior/base-url-behavior.html">
 <script>
 (function(window) {
@@ -97,6 +97,12 @@
 
       // 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) {
@@ -123,8 +129,8 @@
       return this.getBaseUrl() + '/c/' + changeNum;
     },
 
-    changeIsOpen(status) {
-      return status === this.ChangeStatus.NEW;
+    changeIsOpen(change) {
+      return change && change.status === this.ChangeStatus.NEW;
     },
 
     /**
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
index 6af43dc..a77a01f 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <script>
   /** @type {string} */
@@ -54,7 +56,6 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
-        _legacyUndefinedCheck: true,
         behaviors: [
           Gerrit.BaseUrlBehavior,
           Gerrit.RESTClientBehavior,
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
index 6e040a3..ab446f1 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
@@ -17,9 +17,11 @@
 -->
 
 <title>safe-types-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="safe-types-behavior.html">
 
@@ -39,7 +41,6 @@
     suiteSetup(() => {
       Polymer({
         is: 'safe-types-element',
-        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.SafeTypes],
       });
     });
@@ -119,4 +120,4 @@
       });
     });
   });
-</script>
\ No newline at end of file
+</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
index 45bc5f6..ac65360 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
@@ -15,10 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -33,7 +34,7 @@
     <style include="shared-styles">
       :host {
         display: block;
-        margin-bottom: 1em;
+        margin-bottom: var(--spacing-l);
       }
       fieldset {
         border: 1px solid var(--border-color);
@@ -50,13 +51,13 @@
         display: flex;
         justify-content: space-between;
         min-height: 3em;
-        padding: 0 .7em;
+        padding: 0 var(--spacing-m);
       }
       #deletedContainer {
         border-bottom: 0;
       }
       .sectionContent {
-        padding: .7em;
+        padding: var(--spacing-m);
       }
       #editBtn,
       .editing #editBtn.global,
@@ -65,11 +66,11 @@
       #addPermission,
       #deleteBtn,
       .editingRef .name,
-      #editRefInput {
+      .editRefInput {
         display: none;
       }
       .editing #editBtn,
-      .editingRef #editRefInput {
+      .editingRef .editRefInput {
         display: flex;
       }
       .deleted #deletedContainer {
@@ -82,7 +83,7 @@
       }
       .editing #deleteBtn,
       #undoRemoveBtn {
-        padding-right: .7em;
+        padding-right: var(--spacing-m);
       }
     </style>
     <style include="gr-form-styles"></style>
@@ -96,20 +97,26 @@
                 id="editBtn"
                 link
                 class$="[[_computeEditBtnClass(section.id)]]"
-                on-tap="editReference">
+                on-click="editReference">
               <iron-icon id="icon" icon="gr-icons:create"></iron-icon>
             </gr-button>
           </div>
-          <input
-              id="editRefInput"
+          <iron-input
+              class="editRefInput"
               bind-value="{{section.id}}"
-              is="iron-input"
               type="text"
               on-input="_handleValueChange">
+            <input
+                class="editRefInput"
+                bind-value="{{section.id}}"
+                is="iron-input"
+                type="text"
+                on-input="_handleValueChange">
+          </iron-input>
           <gr-button
               link
               id="deleteBtn"
-              on-tap="_handleRemoveReference">Remove</gr-button>
+              on-click="_handleRemoveReference">Remove</gr-button>
         </div><!-- end header -->
         <div class="sectionContent">
           <template
@@ -140,7 +147,7 @@
             <gr-button
                 link
                 id="addBtn"
-                on-tap="_handleAddPermission">Add</gr-button>
+                on-click="_handleAddPermission">Add</gr-button>
           </div>
           <!-- end addPermission -->
         </div><!-- end sectionContent -->
@@ -150,7 +157,7 @@
         <gr-button
             link
             id="undoRemoveBtn"
-            on-tap="_handleUndoRemove">Undo</gr-button>
+            on-click="_handleUndoRemove">Undo</gr-button>
       </div><!-- end deletedContainer -->
     </fieldset>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
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 71f8d26..8178709 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
@@ -39,7 +39,6 @@
 
   Polymer({
     is: 'gr-access-section',
-    _legacyUndefinedCheck: true,
 
     properties: {
       capabilities: Object,
@@ -72,6 +71,11 @@
 
     behaviors: [
       Gerrit.AccessBehavior,
+      /**
+       * Unused in this element, but called by other elements in tests
+       * e.g gr-repo-access_test.
+       */
+      Gerrit.FireBehavior,
     ],
 
     listeners: {
@@ -79,7 +83,15 @@
     },
 
     _updateSection(section) {
-      this._permissions = this.toSortedArray(section.value.permissions);
+      let permissions = this.toSortedArray(section.value.permissions);
+      // We do not care about permissions for global capabilities that are not
+      // currently supported by the server (f.i. capabilities provided by
+      // plugins that are no longer installed).
+      if (section.id === GLOBAL_NAME) {
+        permissions = permissions.filter(
+            p => this.capabilities.hasOwnProperty(p.id));
+      }
+      this._permissions = permissions;
       this._originalId = section.id;
     },
 
@@ -96,7 +108,8 @@
         // For a new section, this is not fired because new permissions and
         // rules have to be added in order to save, modifying the ref is not
         // enough.
-        this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+        this.dispatchEvent(new CustomEvent(
+            'access-modified', {bubbles: true, composed: true}));
       }
       this.section.value.updatedId = this.section.id;
     },
@@ -123,6 +136,9 @@
 
     _computePermissions(name, capabilities, labels) {
       let allPermissions;
+      if (!this.section || !this.section.value) {
+        return [];
+      }
       if (name === GLOBAL_NAME) {
         allPermissions = this.toSortedArray(capabilities);
       } else {
@@ -147,6 +163,7 @@
 
     _computeLabelOptions(labels) {
       const labelOptions = [];
+      if (!labels) { return []; }
       for (const labelName of Object.keys(labels)) {
         labelOptions.push({
           id: 'label-' + labelName,
@@ -200,12 +217,13 @@
 
     _handleRemoveReference() {
       if (this.section.value.added) {
-        this.dispatchEvent(new CustomEvent('added-section-removed',
-            {bubbles: true}));
+        this.dispatchEvent(new CustomEvent(
+            'added-section-removed', {bubbles: true, composed: true}));
       }
       this._deleted = true;
       this.section.value.deleted = true;
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _handleUndoRemove() {
@@ -213,13 +231,19 @@
       delete this.section.value.deleted;
     },
 
+    editRefInput() {
+      return Polymer.dom(this.root).querySelector(Polymer.Element ?
+        'iron-input.editRefInput' :
+        'input[is=iron-input].editRefInput');
+    },
+
     editReference() {
       this._editingRef = true;
-      this.$.editRefInput.focus();
+      this.editRefInput().focus();
     },
 
     _isEditEnabled(canUpload, ownerOf, sectionId) {
-      return canUpload || ownerOf.indexOf(sectionId) >= 0;
+      return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0);
     },
 
     _computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) {
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
index 56f321b..5575014 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-access-section</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-access-section.html">
 
@@ -327,6 +329,24 @@
             default_value: 0,
           },
         };
+        element.capabilities = {
+          accessDatabase: {
+            id: 'accessDatabase',
+            name: 'Access Database',
+          },
+          administrateServer: {
+            id: 'administrateServer',
+            name: 'Administrate Server',
+          },
+          batchChangesLimit: {
+            id: 'batchChangesLimit',
+            name: 'Batch Changes Limit',
+          },
+          createAccount: {
+            id: 'createAccount',
+            name: 'Create Account',
+          },
+        };
       });
       suite('Global section', () => {
         setup(() => {
@@ -340,24 +360,6 @@
               },
             },
           };
-          element.capabilities = {
-            accessDatabase: {
-              id: 'accessDatabase',
-              name: 'Access Database',
-            },
-            administrateServer: {
-              id: 'administrateServer',
-              name: 'Administrate Server',
-            },
-            batchChangesLimit: {
-              id: 'batchChangesLimit',
-              name: 'Batch Changes Limit',
-            },
-            createAccount: {
-              id: 'createAccount',
-              name: 'Create Account',
-            },
-          };
           element._updateSection(element.section);
           flushAsynchronousOperations();
         });
@@ -385,7 +387,6 @@
               },
             },
           };
-          element.capabilities = {};
           element._updateSection(element.section);
           flushAsynchronousOperations();
         });
@@ -455,7 +456,7 @@
               1);
         });
 
-        test('edit section reference', () => {
+        test('edit section reference', done => {
           element.canUpload = true;
           element.ownerOf = [];
           element.section = {id: 'refs/for/bar', value: {permissions: {}}};
@@ -464,14 +465,16 @@
           assert.isTrue(element.$.section.classList.contains('editing'));
           assert.isFalse(element._editingRef);
           MockInteractions.tap(element.$.editBtn);
-          element.$.editRefInput.bindValue='new/ref';
-          flushAsynchronousOperations();
-          assert.equal(element.section.id, 'new/ref');
-          assert.isTrue(element._editingRef);
-          assert.isTrue(element.$.section.classList.contains('editingRef'));
-          element.editing = false;
-          assert.isFalse(element._editingRef);
-          assert.equal(element.section.id, 'refs/for/bar');
+          element.editRefInput().bindValue='new/ref';
+          setTimeout(() => {
+            assert.equal(element.section.id, 'new/ref');
+            assert.isTrue(element._editingRef);
+            assert.isTrue(element.$.section.classList.contains('editingRef'));
+            element.editing = false;
+            assert.isFalse(element._editingRef);
+            assert.equal(element.section.id, 'refs/for/bar');
+            done();
+          });
         });
 
         test('_handleValueChange', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
index ea08d89..dd8758f 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
@@ -15,10 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
@@ -54,7 +54,7 @@
           <template is="dom-repeat" items="[[_shownGroups]]">
             <tr class="table">
               <td class="name">
-                <a href$="[[_computeGroupUrl(item.group_id)]]">[[item.name]]</a>
+                <a href$="[[_computeGroupUrl(item.id)]]">[[item.name]]</a>
               </td>
               <td class="description">[[item.description]]</td>
               <td class="visibleToAll">[[_visibleToAll(item)]]</td>
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 9db2e34..cf2100d 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-admin-group-list',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -68,6 +67,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.ListViewBehavior,
     ],
 
@@ -97,8 +97,13 @@
       }
     },
 
+    /**
+     * Generates groups link (/admin/groups/<uuid>)
+     *
+     * @param {string} id
+     */
     _computeGroupUrl(id) {
-      return Gerrit.Nav.getUrlForGroup(id);
+      return Gerrit.Nav.getUrlForGroup(decodeURIComponent(id));
     },
 
     _getCreateGroupCapability() {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
index 065a757..bd9b30a 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-admin-group-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
@@ -67,6 +69,30 @@
       sandbox.restore();
     });
 
+    test('_computeGroupUrl', () => {
+      let urlStub = sandbox.stub(Gerrit.Nav, 'getUrlForGroup',
+          () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
+
+      let group = {
+        id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5',
+      };
+      assert.equal(element._computeGroupUrl(group),
+          '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
+
+      urlStub.restore();
+
+      urlStub = sandbox.stub(Gerrit.Nav, 'getUrlForGroup',
+          () => '/admin/groups/user/test');
+
+      group = {
+        id: 'user%2Ftest',
+      };
+      assert.equal(element._computeGroupUrl(group),
+          '/admin/groups/user/test');
+
+      urlStub.restore();
+    });
+
     suite('list with groups', () => {
       setup(done => {
         groups = _.times(26, groupGenerator);
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index b6a6d27..c7187a9 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
@@ -56,7 +56,7 @@
         padding: 5px 4px;
       }
       iron-icon {
-        margin: 0 .2em;
+        margin: 0 var(--spacing-xs);
       }
       .breadcrumb {
         align-items: center;
@@ -74,7 +74,7 @@
         display: inline-block;
       }
       main.breadcrumbs:not(.table) {
-        margin-top: 1em;
+        margin-top: var(--spacing-l);
       }
     </style>
     <gr-page-nav class="navStyles">
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 4d964d7..72cca9e 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
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-admin-view',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
@@ -198,10 +197,6 @@
       this.reload();
     },
 
-    _computeSelectedTitle(params) {
-      return this.getSelectedTitle(params.view);
-    },
-
     // TODO (beckysiegel): Update these functions after router abstraction is
     // updated. They are currently copied from gr-dropdown (and should be
     // updated there as well once complete).
@@ -228,6 +223,7 @@
      * @param {string=} opt_detailType
      */
     _computeSelectedClass(itemView, params, opt_detailType) {
+      if (!params) return '';
       // Group params are structured differently from admin params. Compute
       // selected differently for groups.
       // TODO(wyatta): Simplify this when all routes work like group params.
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index ff25377..984be19 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-admin-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-admin-view.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
index 3873083..9d8ee18 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
index 9c0e405..acc76de 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
@@ -25,7 +25,6 @@
 
   Polymer({
     is: 'gr-confirm-delete-item-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -44,6 +43,10 @@
       itemType: String,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     _handleConfirmTap(e) {
       e.preventDefault();
       e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
index d735acb..3292cec 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-delete-item-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-delete-item-dialog.html">
 
@@ -49,19 +51,23 @@
     test('_handleConfirmTap', () => {
       const confirmHandler = sandbox.stub();
       element.addEventListener('confirm', confirmHandler);
-      sandbox.stub(element, '_handleConfirmTap');
+      sandbox.spy(element, '_handleConfirmTap');
       element.$$('gr-dialog').fire('confirm');
       assert.isTrue(confirmHandler.called);
+      assert.isTrue(confirmHandler.calledOnce);
       assert.isTrue(element._handleConfirmTap.called);
+      assert.isTrue(element._handleConfirmTap.calledOnce);
     });
 
     test('_handleCancelTap', () => {
       const cancelHandler = sandbox.stub();
       element.addEventListener('cancel', cancelHandler);
-      sandbox.stub(element, '_handleCancelTap');
+      sandbox.spy(element, '_handleCancelTap');
       element.$$('gr-dialog').fire('cancel');
       assert.isTrue(cancelHandler.called);
+      assert.isTrue(cancelHandler.calledOnce);
       assert.isTrue(element._handleCancelTap.called);
+      assert.isTrue(element._handleCancelTap.calledOnce);
     });
 
     test('_computeItemName function for branches', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
index da1c871..2a95991 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
@@ -15,10 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -42,7 +43,7 @@
       }
       gr-autocomplete {
         --gr-autocomplete: {
-          padding: 0 .15em;
+          padding: 0 var(--spacing-xs);
         }
       }
       .hide {
@@ -69,23 +70,33 @@
       <section class$="[[_computeBranchClass(baseChange)]]">
         <span class="title">Provide base commit sha1 for change</span>
         <span class="value">
-          <input
-              is="iron-input"
-              id="baseCommitInput"
+          <iron-input
               maxlength="40"
               placeholder="(optional)"
               bind-value="{{baseCommit}}">
+            <input
+                is="iron-input"
+                id="baseCommitInput"
+                maxlength="40"
+                placeholder="(optional)"
+                bind-value="{{baseCommit}}">
+          </iron-input>
         </span>
       </section>
       <section>
         <span class="title">Enter topic for new change</span>
         <span class="value">
-          <input
-              is="iron-input"
-              id="tagNameInput"
+          <iron-input
               maxlength="1024"
               placeholder="(optional)"
               bind-value="{{topic}}">
+            <input
+                is="iron-input"
+                id="tagNameInput"
+                maxlength="1024"
+                placeholder="(optional)"
+                bind-value="{{topic}}">
+          </iron-input>
         </span>
       </section>
       <section id="description">
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 5a4c064..e29e5f8 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
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-create-change-dialog',
-    _legacyUndefinedCheck: true,
 
     properties: {
       repoName: String,
@@ -50,6 +49,11 @@
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
+      /**
+       * Unused in this element, but called by other elements in tests
+       * e.g gr-repo-commands_test.
+       */
+      Gerrit.FireBehavior,
       Gerrit.URLEncodingBehavior,
     ],
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
index aa4da68..3a3683f 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-change-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-create-change-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html
index d96a935..8a4287b 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -39,10 +39,12 @@
       <div id="form">
         <section>
           <span class="title">Group name</span>
-          <input
-              is="iron-input"
-              id="groupNameInput"
+          <iron-input
               bind-value="{{_name}}">
+            <input
+                is="iron-input"
+                bind-value="{{_name}}">
+          </iron-input>
         </section>
       </div>
     </div>
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 ec667ee..01aeb43 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-create-group-dialog',
-    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
index 95ffdb1..ebca289 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-group-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-create-group-dialog.html">
 
@@ -51,13 +53,16 @@
       sandbox.restore();
     });
 
-    test('name is updated correctly', () => {
+    test('name is updated correctly', done => {
       assert.isFalse(element.hasNewGroupName);
 
-      element.$.groupNameInput.bindValue = GROUP_NAME;
+      ironInput(element.root).bindValue = GROUP_NAME;
 
-      assert.isTrue(element.hasNewGroupName);
-      assert.deepEqual(element._name, GROUP_NAME);
+      setTimeout(() => {
+        assert.isTrue(element.hasNewGroupName);
+        assert.deepEqual(element._name, GROUP_NAME);
+        done();
+      });
     });
 
     test('test for redirecting to group on successful creation', done => {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
index aa9639b..33153eb 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -45,29 +45,39 @@
     </style>
     <div class="gr-form-styles">
       <div id="form">
-        <section>
+        <section id="itemNameSection">
           <span class="title">[[detailType]] name</span>
-          <input
-              is="iron-input"
-              id="itemNameInput"
+          <iron-input
               placeholder="[[detailType]] Name"
               bind-value="{{_itemName}}">
+            <input
+                is="iron-input"
+                placeholder="[[detailType]] Name"
+                bind-value="{{_itemName}}">
+          </iron-input>
         </section>
-        <section>
+        <section id="itemRevisionSection">
           <span class="title">Initial Revision</span>
-          <input
-              is="iron-input"
-              id="itemRevisionInput"
+          <iron-input
               placeholder="Revision (Branch or SHA-1)"
               bind-value="{{_itemRevision}}">
+            <input
+                is="iron-input"
+                placeholder="Revision (Branch or SHA-1)"
+                bind-value="{{_itemRevision}}">
+          </iron-input>
         </section>
-        <section class$="[[_computeHideItemClass(itemDetail)]]">
+        <section id="itemAnnotationSection"
+                 class$="[[_computeHideItemClass(itemDetail)]]">
           <span class="title">Annotation</span>
-          <input
-              is="iron-input"
-              id="itemAnnotationInput"
+          <iron-input
               placeholder="Annotation (Optional)"
               bind-value="{{_itemAnnotation}}">
+            <input
+                is="iron-input"
+                placeholder="Annotation (Optional)"
+                bind-value="{{_itemAnnotation}}">
+          </iron-input>
         </section>
       </div>
     </div>
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 65bb46d..4e9da90 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
@@ -24,7 +24,6 @@
 
   Polymer({
     is: 'gr-create-pointer-dialog',
-    _legacyUndefinedCheck: true,
 
     properties: {
       detailType: String,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
index 39e200a..08e8213 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-pointer-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-create-pointer-dialog.html">
 
@@ -49,7 +51,7 @@
       sandbox.restore();
     });
 
-    test('branch created', () => {
+    test('branch created', done => {
       sandbox.stub(element.$.restAPI, 'createRepoBranch', () => {
         return Promise.resolve({});
       });
@@ -59,17 +61,18 @@
       element._itemName = 'test-branch';
       element.itemDetail = 'branches';
 
-      element.$.itemNameInput.bindValue = 'test-branch2';
-      element.$.itemRevisionInput.bindValue = 'HEAD';
+      ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
+      ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
 
-      assert.isTrue(element.hasNewItemName);
-
-      assert.equal(element._itemName, 'test-branch2');
-
-      assert.equal(element._itemRevision, 'HEAD');
+      setTimeout(() => {
+        assert.isTrue(element.hasNewItemName);
+        assert.equal(element._itemName, 'test-branch2');
+        assert.equal(element._itemRevision, 'HEAD');
+        done();
+      });
     });
 
-    test('tag created', () => {
+    test('tag created', done => {
       sandbox.stub(element.$.restAPI, 'createRepoTag', () => {
         return Promise.resolve({});
       });
@@ -79,17 +82,18 @@
       element._itemName = 'test-tag';
       element.itemDetail = 'tags';
 
-      element.$.itemNameInput.bindValue = 'test-tag2';
-      element.$.itemRevisionInput.bindValue = 'HEAD';
+      ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
+      ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
 
-      assert.isTrue(element.hasNewItemName);
-
-      assert.equal(element._itemName, 'test-tag2');
-
-      assert.equal(element._itemRevision, 'HEAD');
+      setTimeout(() => {
+        assert.isTrue(element.hasNewItemName);
+        assert.equal(element._itemName, 'test-tag2');
+        assert.equal(element._itemRevision, 'HEAD');
+        done();
+      });
     });
 
-    test('tag created with annotations', () => {
+    test('tag created with annotations', done => {
       sandbox.stub(element.$.restAPI, 'createRepoTag', () => {
         return Promise.resolve({});
       });
@@ -100,17 +104,17 @@
       element._itemAnnotation = 'test-message';
       element.itemDetail = 'tags';
 
-      element.$.itemNameInput.bindValue = 'test-tag2';
-      element.$.itemAnnotationInput.bindValue = 'test-message2';
-      element.$.itemRevisionInput.bindValue = 'HEAD';
+      ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
+      ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
+      ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
 
-      assert.isTrue(element.hasNewItemName);
-
-      assert.equal(element._itemName, 'test-tag2');
-
-      assert.equal(element._itemAnnotation, 'test-message2');
-
-      assert.equal(element._itemRevision, 'HEAD');
+      setTimeout(() => {
+        assert.isTrue(element.hasNewItemName);
+        assert.equal(element._itemName, 'test-tag2');
+        assert.equal(element._itemAnnotation, 'test-message2');
+        assert.equal(element._itemRevision, 'HEAD');
+        done();
+      });
     });
 
     test('_computeHideItemClass returns hideItem if type is branches', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
index b38fab5..d1a2471 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
@@ -41,10 +41,9 @@
         border: none;
         --gr-autocomplete: {
           border: 1px solid var(--border-color);
-          border-radius: 2px;
-          font-size: var(--font-size-normal);
+          border-radius: var(--border-radius);
           height: 2em;
-          padding: 0 .15em;
+          padding: 0 var(--spacing-xs);
           width: 20em;
         }
       }
@@ -54,10 +53,13 @@
       <div id="form">
         <section>
           <span class="title">Repository name</span>
-          <input is="iron-input"
-              id="repoNameInput"
-              autocomplete="on"
-              bind-value="{{_repoConfig.name}}">
+          <iron-input autocomplete="on"
+                      bind-value="{{_repoConfig.name}}">
+            <input is="iron-input"
+                   id="repoNameInput"
+                   autocomplete="on"
+                   bind-value="{{_repoConfig.name}}">
+          </iron-input>
         </section>
         <section>
           <span class="title">Rights inherit from</span>
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 ef7edd4..bb2b5f2 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-create-repo-dialog',
-    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
index 79079f5..7e32c5c 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-repo-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-create-repo-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
index b10c98a..c15f091 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
@@ -16,12 +16,14 @@
 -->
 
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
 
 <dom-module id="gr-group-audit-log">
   <template>
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 966f3c9..8901d4a 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
@@ -21,7 +21,6 @@
 
   Polymer({
     is: 'gr-group-audit-log',
-    _legacyUndefinedCheck: true,
 
     properties: {
       groupId: String,
@@ -33,6 +32,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.ListViewBehavior,
     ],
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
index 59a665b..313d465 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-group-audit-log</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-group-audit-log.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
index bcfc9e1..86f66c4 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
@@ -16,10 +16,10 @@
 -->
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-subpage-styles.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
@@ -43,7 +43,6 @@
       gr-autocomplete {
         width: 20em;
         --gr-autocomplete: {
-          font-size: var(--font-size-normal);
           height: 2em;
           width: 20em;
         }
@@ -91,7 +90,7 @@
             </span>
             <gr-button
                 id="saveGroupMember"
-                on-tap="_handleSavingGroupMember"
+                on-click="_handleSavingGroupMember"
                 disabled="[[!_groupMemberSearchId]]">
               Add
             </gr-button>
@@ -111,7 +110,7 @@
                     <td class="deleteColumn">
                       <gr-button
                           class="deleteMembersButton"
-                          on-tap="_handleDeleteMember">
+                          on-click="_handleDeleteMember">
                         Delete
                       </gr-button>
                     </td>
@@ -133,7 +132,7 @@
             </span>
             <gr-button
                 id="saveIncludedGroups"
-                on-tap="_handleSavingIncludedGroups"
+                on-click="_handleSavingIncludedGroups"
                 disabled="[[!_includedGroupSearchId]]">
               Add
             </gr-button>
@@ -163,7 +162,7 @@
                     <td class="deleteColumn">
                       <gr-button
                           class="deleteIncludedGroupButton"
-                          on-tap="_handleDeleteIncludedGroup">
+                          on-click="_handleDeleteIncludedGroup">
                         Delete
                       </gr-button>
                     </td>
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 b2784e5..7f8e9ac 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,7 +25,6 @@
 
   Polymer({
     is: 'gr-group-members',
-    _legacyUndefinedCheck: true,
 
     properties: {
       groupId: Number,
@@ -66,6 +65,7 @@
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
+      Gerrit.FireBehavior,
       Gerrit.URLEncodingBehavior,
     ],
 
@@ -205,6 +205,7 @@
               this.dispatchEvent(new CustomEvent('show-alert', {
                 detail: {message: SAVING_ERROR_TEXT},
                 bubbles: true,
+                composed: true,
               }));
               return err;
             }
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index 45d5497..bf9113b 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-group-members</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-group-members.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
index b92dc4b..7617c19 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-subpage-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -35,7 +35,7 @@
         content: ' *';
       }
       .inputUpdateBtn {
-        margin-top: .3em;
+        margin-top: var(--spacing-s);
       }
     </style>
     <style include="gr-form-styles"></style>
@@ -51,7 +51,8 @@
             <h3 id="groupUUID">Group UUID</h3>
             <fieldset>
               <gr-copy-clipboard
-                  text="[[_groupConfig.id]]"></gr-copy-clipboard>
+                  id="uuid"
+                  text="[[_getGroupUUID(_groupConfig.id)]]"></gr-copy-clipboard>
             </fieldset>
             <h3 id="groupName" class$="[[_computeHeaderClass(_rename)]]">
               Group Name
@@ -66,7 +67,7 @@
               <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
                 <gr-button
                     id="inputUpdateNameBtn"
-                    on-tap="_handleSaveName"
+                    on-click="_handleSaveName"
                     disabled="[[!_rename]]">
                   Rename Group</gr-button>
               </span>
@@ -86,7 +87,7 @@
               </span>
               <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
                 <gr-button
-                    on-tap="_handleSaveOwner"
+                    on-click="_handleSaveOwner"
                     disabled="[[!_owner]]">
                   Change Owners</gr-button>
               </span>
@@ -104,7 +105,7 @@
               </div>
               <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
                 <gr-button
-                    on-tap="_handleSaveDescription"
+                    on-click="_handleSaveDescription"
                     disabled="[[!_description]]">
                   Save Description
                 </gr-button>
@@ -132,7 +133,7 @@
               </section>
               <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
                 <gr-button
-                    on-tap="_handleSaveOptions"
+                    on-click="_handleSaveOptions"
                     disabled="[[!_options]]">
                   Save Group Options
                 </gr-button>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
index 2d7f9cb..09953fb 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -32,7 +32,6 @@
 
   Polymer({
     is: 'gr-group',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the group name changes.
@@ -89,6 +88,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     observers: [
       '_handleConfigName(_groupConfig.name)',
       '_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)',
@@ -233,5 +236,11 @@
     _computeGroupDisabled(owner, admin, groupIsInternal) {
       return groupIsInternal && (admin || owner) ? false : true;
     },
+
+    _getGroupUUID(id) {
+      if (!id) return;
+
+      return id.match(INTERNAL_GROUP_REGEX) ? id : decodeURIComponent(id);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
index 226f3ab..fc04c02 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-group</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-group.html">
 
@@ -240,5 +242,19 @@
 
       element._loadGroup();
     });
+
+    test('uuid', () => {
+      element._groupConfig = {
+        id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
+      };
+
+      assert.equal(element._groupConfig.id, element.$.uuid.text);
+
+      element._groupConfig = {
+        id: 'user%2Fgroup',
+      };
+
+      assert.equal('user/group', element.$.uuid.text);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
index a5bb5fd..931e2cd 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
@@ -15,9 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-menu-page-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -31,13 +32,13 @@
     <style include="shared-styles">
       :host {
         display: block;
-        margin-bottom: .7em;
+        margin-bottom: var(--spacing-m);
       }
       .header {
         align-items: baseline;
         display: flex;
         justify-content: space-between;
-        margin: .3em .7em;
+        margin: var(--spacing-s) var(--spacing-m);
       }
       .rules {
         background: var(--table-header-background-color);
@@ -48,7 +49,7 @@
         border-bottom: 1px solid var(--border-color);
       }
       .title {
-        margin-bottom: .3em;
+        margin-bottom: var(--spacing-s);
       }
       #addRule,
       #removeBtn {
@@ -60,11 +61,11 @@
       }
       .editing #removeBtn {
         display: block;
-        margin-left: 1.5em;
+        margin-left: var(--spacing-xl);
       }
       .editing #addRule {
         display: block;
-        padding: .7em;
+        padding: var(--spacing-m);
       }
       #deletedContainer,
       .deleted #mainContainer {
@@ -75,7 +76,7 @@
         border: 1px solid var(--border-color);
         display: flex;
         justify-content: space-between;
-        padding: .7em;
+        padding: var(--spacing-m);
       }
       #mainContainer {
         display: block;
@@ -100,7 +101,7 @@
             <gr-button
                 link
                 id="removeBtn"
-                on-tap="_handleRemovePermission">Remove</gr-button>
+                on-click="_handleRemovePermission">Remove</gr-button>
           </div>
         </div><!-- end header -->
         <div class="rules">
@@ -136,7 +137,7 @@
         <gr-button
             link
             id="undoRemoveBtn"
-            on-tap="_handleUndoRemove">Undo</gr-button>
+            on-click="_handleUndoRemove">Undo</gr-button>
       </div><!-- end deletedContainer -->
     </section>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
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 58a006d..75e715b 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -38,7 +38,6 @@
 
   Polymer({
     is: 'gr-permission',
-    _legacyUndefinedCheck: true,
 
     properties: {
       labels: Object,
@@ -78,6 +77,11 @@
 
     behaviors: [
       Gerrit.AccessBehavior,
+      /**
+       * Unused in this element, but called by other elements in tests
+       * e.g gr-access-section_test.
+       */
+      Gerrit.FireBehavior,
     ],
 
     observers: [
@@ -138,17 +142,19 @@
     _handleValueChange() {
       this.permission.value.modified = true;
       // Allows overall access page to know a change has been made.
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _handleRemovePermission() {
       if (this.permission.value.added) {
-        this.dispatchEvent(new CustomEvent('added-permission-removed',
-            {bubbles: true}));
+        this.dispatchEvent(new CustomEvent(
+            'added-permission-removed', {bubbles: true, composed: true}));
       }
       this._deleted = true;
       this.permission.value.deleted = true;
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _handleRulesChanged(changeRecord) {
@@ -177,7 +183,8 @@
     },
 
     _computeLabel(permission, labels) {
-      if (!permission.value.label) { return; }
+      if (!labels || !permission ||
+          !permission.value || !permission.value.label) { return; }
 
       const labelName = permission.value.label;
 
@@ -281,7 +288,8 @@
       value.added = true;
       // See comment above for why we cannot use "this.set(...)" here.
       this.permission.value.rules[groupId] = value;
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _computeHasRange(name) {
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index a432ab0..0c70f9c 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-permission</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-permission.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html
index ca98c50..2761526 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html
@@ -15,10 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -33,22 +33,22 @@
       .existingItems {
         background: var(--table-header-background-color);
         border: 1px solid var(--border-color);
-        border-radius: 2px;
+        border-radius: var(--border-radius);
       }
       gr-button {
         float: right;
-        margin-left: .5em;
+        margin-left: var(--spacing-m);
         width: 4.5em;
       }
       .row {
         align-items: center;
         display: flex;
         justify-content: space-between;
-        padding: .5em 0;
+        padding: var(--spacing-m) 0;
         width: 100%;
       }
       .existingItems .row {
-        padding: .5em;
+        padding: var(--spacing-m);
       }
       .existingItems .row:not(:first-of-type) {
         border-top: 1px solid var(--border-color);
@@ -61,7 +61,7 @@
       }
       .placeholder {
         color: var(--deemphasized-text-color);
-        padding-top: .75em;
+        padding-top: var(--spacing-m);
       }
     </style>
     <div class="wrapper gr-form-styles">
@@ -73,8 +73,8 @@
               <gr-button
                   link
                   disabled$="[[disabled]]"
-                  data-item="[[item]]"
-                  on-tap="_handleDelete">Delete</gr-button>
+                  data-item$="[[item]]"
+                  on-click="_handleDelete">Delete</gr-button>
             </div>
           </template>
         </div>
@@ -83,16 +83,20 @@
         <div class="row placeholder">None configured.</div>
       </template>
       <div class$="row [[_computeShowInputRow(disabled)]]">
-        <input
-            is="iron-input"
-            id="input"
+        <iron-input
             on-keydown="_handleInputKeydown"
-            bind-value="{{_newValue}}"/>
+            bind-value="{{_newValue}}">
+          <input
+              is="iron-input"
+              id="input"
+              on-keydown="_handleInputKeydown"
+              bind-value="{{_newValue}}">
+        </iron-input>
         <gr-button
             id="addButton"
             disabled$="[[!_newValue.length]]"
             link
-            on-tap="_handleAddTap">Add</gr-button>
+            on-click="_handleAddTap">Add</gr-button>
       </div>
     </div>
   </template>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
index ab4d286..bb0d501 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
@@ -67,7 +67,7 @@
     },
 
     _handleDelete(e) {
-      const value = Polymer.dom(e).localTarget.dataItem;
+      const value = Polymer.dom(e).localTarget.dataset.item;
       this._dispatchChanged(
           this.pluginOption.info.values.filter(str => str !== value));
     },
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
index dc3f67e..39e4ddc 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-config-array-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-plugin-config-array-editor.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
index 9be526e..ee5dd83 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
@@ -14,8 +14,9 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
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 d6484d8..1dbfdc8 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-plugin-list',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -65,6 +64,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.ListViewBehavior,
     ],
 
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
index e42e374..2485687 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-plugin-list.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
index b6e56de..ea12908 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
@@ -15,10 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
-<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
+<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 
 <link rel="import" href="../../../styles/gr-menu-page-styles.html">
@@ -48,20 +49,20 @@
         align-items: center;
       }
       .weblink {
-        margin-right: .2em;
+        margin-right: var(--spacing-xs);
       }
       .weblinks.show,
       .referenceContainer {
         display: block;
       }
       .rightsText {
-        margin-right: .3rem;
+        margin-right: var(--spacing-s);
       }
 
       .editing gr-button,
       .admin #editBtn {
         display: inline-block;
-        margin: 1em 0;
+        margin: var(--spacing-l) 0;
       }
       .editing #editInheritFromInput {
         display: inline-block;
@@ -95,16 +96,16 @@
           </template>
         </div>
         <gr-button id="editBtn"
-            on-tap="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
+            on-click="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
         <gr-button id="saveBtn"
             primary
             class$="[[_computeSaveBtnClass(_ownerOf)]]"
-            on-tap="_handleSave"
+            on-click="_handleSave"
             disabled$="[[!_modified]]">Save</gr-button>
         <gr-button id="saveReviewBtn"
             primary
             class$="[[_computeSaveReviewBtnClass(_canUpload)]]"
-            on-tap="_handleSaveForReview"
+            on-click="_handleSaveForReview"
             disabled$="[[!_modified]]">Save for review</gr-button>
         <template
             is="dom-repeat"
@@ -124,7 +125,7 @@
         </template>
         <div class="referenceContainer">
           <gr-button id="addReferenceBtn"
-              on-tap="_handleCreateSection">Add Reference</gr-button>
+              on-click="_handleCreateSection">Add Reference</gr-button>
         </div>
       </div>
     </main>
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 1b8b322..1e6a431 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
@@ -70,7 +70,6 @@
 
   Polymer({
     is: 'gr-repo-access',
-    _legacyUndefinedCheck: true,
 
     properties: {
       repo: {
@@ -118,6 +117,7 @@
     behaviors: [
       Gerrit.AccessBehavior,
       Gerrit.BaseUrlBehavior,
+      Gerrit.FireBehavior,
       Gerrit.URLEncodingBehavior,
     ],
 
@@ -237,7 +237,7 @@
     },
 
     _computeWebLinkClass(weblinks) {
-      return weblinks.length ? 'show' : '';
+      return weblinks && weblinks.length ? 'show' : '';
     },
 
     _computeShowInherit(inheritsFrom) {
@@ -413,8 +413,11 @@
       if (!Object.keys(addRemoveObj.add).length &&
           !Object.keys(addRemoveObj.remove).length &&
           !addRemoveObj.parent) {
-        this.dispatchEvent(new CustomEvent('show-alert',
-            {detail: {message: NOTHING_TO_SAVE}, bubbles: true}));
+        this.dispatchEvent(new CustomEvent('show-alert', {
+          detail: {message: NOTHING_TO_SAVE},
+          bubbles: true,
+          composed: true,
+        }));
         return;
       }
       const obj = {
@@ -450,12 +453,12 @@
     },
 
     _computeSaveBtnClass(ownerOf) {
-      return ownerOf.length < 0 ? 'invisible' : '';
+      return ownerOf && ownerOf.length === 0 ? 'invisible' : '';
     },
 
     _computeMainClass(ownerOf, canUpload, editing) {
       const classList = [];
-      if (ownerOf.length > 0 || canUpload) {
+      if (ownerOf && ownerOf.length > 0 || canUpload) {
         classList.push('admin');
       }
       if (editing) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
index a33b4be..1660088 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-access</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-access.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html
index 7db4e4c..29bc02d 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 
@@ -23,14 +23,15 @@
     <style include="shared-styles">
       :host {
         display: block;
-        margin-bottom: 2em;
+        margin-bottom: var(--spacing-xxl);
       }
     </style>
     <h3>[[title]]</h3>
     <gr-button
         title$="[[tooltip]]"
         disabled$="[[disabled]]"
-        on-tap="_onCommandTap">
+        on-click
+        ="_onCommandTap">
       [[title]]
     </gr-button>
   </template>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
index e0becaf..026c990 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-repo-command',
-    _legacyUndefinedCheck: true,
 
     properties: {
       title: String,
@@ -34,7 +33,8 @@
      */
 
     _onCommandTap() {
-      this.dispatchEvent(new CustomEvent('command-tap', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('command-tap', {bubbles: true, composed: true}));
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
index 9f9ac92..49d8765 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-command</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-command.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
index dba01aa..5089f34 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-subpage-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
index 09c9026..3b4811e 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -28,7 +28,6 @@
 
   Polymer({
     is: 'gr-repo-commands',
-    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
@@ -42,6 +41,10 @@
       _canCreate: Boolean,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     attached() {
       this._loadRepo();
 
@@ -75,8 +78,9 @@
     _handleRunningGC() {
       return this.$.restAPI.runRepoGC(this.repo).then(response => {
         if (response.status === 200) {
-          this.dispatchEvent(new CustomEvent('show-alert',
-              {detail: {message: GC_MESSAGE}, bubbles: true}));
+          this.dispatchEvent(new CustomEvent(
+              'show-alert',
+              {detail: {message: GC_MESSAGE}, bubbles: true, composed: true}));
         }
       });
     },
@@ -101,7 +105,7 @@
           CREATE_CHANGE_SUCCEEDED_MESSAGE :
           CREATE_CHANGE_FAILED_MESSAGE;
         this.dispatchEvent(new CustomEvent('show-alert',
-            {detail: {message}, bubbles: true}));
+            {detail: {message}, bubbles: true, composed: true}));
         if (!change) { return; }
 
         Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff(
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
index 76c65e8..2976923 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-commands</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-commands.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
index 1d49db9..8af3a92 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
@@ -14,7 +14,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -24,7 +25,7 @@
     <style include="shared-styles">
       :host {
         display: block;
-        margin-bottom: 2em;
+        margin-bottom: var(--spacing-xxl);
       }
       .loading #dashboards,
       #loadingContainer {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
index f72e986..71cc571 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-repo-dashboards',
-    _legacyUndefinedCheck: true,
 
     properties: {
       repo: {
@@ -33,6 +32,10 @@
       _dashboards: Array,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     _repoChanged(repo) {
       this._loading = true;
       if (!repo) { return Promise.resolve(); }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
index 94bf5e0..4f76983 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-dashboards</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-dashboards.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
index fccfa6a..2f244f8 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
@@ -17,8 +17,9 @@
 
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -64,15 +65,14 @@
         display: none;
       }
       .revisionEdit gr-button {
-        margin-left: .6em;
+        margin-left: var(--spacing-m);
       }
       .editBtn {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
       }
       .canEdit .revisionEdit{
         align-items: center;
         display: flex;
-        line-height: 1;
       }
       .deleteButton:not(.show) {
         display: none;
@@ -120,23 +120,26 @@
                   </span>
                   <gr-button
                       link
-                      on-tap="_handleEditRevision"
+                      on-click="_handleEditRevision"
                       class="editBtn">
                     edit
                   </gr-button>
-                  <input
-                      is=iron-input
+                  <iron-input
                       bind-value="{{_revisedRef}}"
                       class="editItem">
+                    <input
+                        is="iron-input"
+                        bind-value="{{_revisedRef}}">
+                  </iron-input>
                   <gr-button
                       link
-                      on-tap="_handleCancelRevision"
+                      on-click="_handleCancelRevision"
                       class="cancelBtn editItem">
                     Cancel
                   </gr-button>
                   <gr-button
                       link
-                      on-tap="_handleSaveRevision"
+                      on-click="_handleSaveRevision"
                       class="saveBtn editItem"
                       disabled="[[!_revisedRef]]">
                     Save
@@ -172,7 +175,7 @@
                 <gr-button
                     link
                     class$="deleteButton [[_computeHideDeleteClass(_isOwner, item.can_delete)]]"
-                    on-tap="_handleDeleteItem">
+                    on-click="_handleDeleteItem">
                   Delete
                 </gr-button>
               </td>
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 e4000e0..9052322 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
@@ -26,7 +26,6 @@
 
   Polymer({
     is: 'gr-repo-detail-list',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -85,6 +84,7 @@
 
     behaviors: [
       Gerrit.ListViewBehavior,
+      Gerrit.FireBehavior,
       Gerrit.URLEncodingBehavior,
     ],
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
index 427a78a..44d9b27 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-detail-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-detail-list.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html
index 0490db2..5e82c1e 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html
@@ -14,10 +14,9 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
@@ -30,6 +29,20 @@
   <template>
     <style include="shared-styles"></style>
     <style include="gr-table-styles"></style>
+    <style>
+      .genericList tr td:last-of-type {
+        text-align: left;
+      }
+      .genericList tr th:last-of-type {
+        text-align: left;
+      }
+      .readOnly {
+        text-align: center;
+      }
+      .changesLink, .name, .repositoryBrowser, .readOnly {
+        white-space:nowrap;
+      }
+    </style>
     <gr-list-view
         create-new=[[_createNewCapability]]
         filter="[[_filter]]"
@@ -42,10 +55,10 @@
       <table id="list" class="genericList">
         <tr class="headerRow">
           <th class="name topHeader">Repository Name</th>
-          <th class="description topHeader">Repository Description</th>
-          <th class="changesLink topHeader">Changes</th>
           <th class="repositoryBrowser topHeader">Repository Browser</th>
-          <th class="readOnly topHeader">Read only</th>
+          <th class="changesLink topHeader">Changes</th>
+          <th class="topHeader readOnly">Read only</th>
+          <th class="description topHeader">Repository Description</th>
         </tr>
         <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
           <td>Loading...</td>
@@ -56,8 +69,6 @@
               <td class="name">
                 <a href$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a>
               </td>
-              <td class="description">[[item.description]]</td>
-              <td class="changesLink"><a href$="[[_computeChangesLink(item.name)]]">(view all)</a></td>
               <td class="repositoryBrowser">
                 <template is="dom-repeat"
                     items="[[_computeWeblink(item)]]" as="link">
@@ -65,11 +76,13 @@
                       class="webLink"
                       rel="noopener"
                       target="_blank">
-                    ([[link.name]])
+                    [[link.name]]
                   </a>
                 </template>
               </td>
+              <td class="changesLink"><a href$="[[_computeChangesLink(item.name)]]">view all</a></td>
               <td class="readOnly">[[_readOnly(item)]]</td>
+              <td class="description">[[item.description]]</td>
             </tr>
           </template>
         </tbody>
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 d4eb29b..6f347f0 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-repo-list',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -126,7 +125,9 @@
           .then(repos => {
             // Late response.
             if (filter !== this._filter || !repos) { return; }
-            this._repos = repos;
+            this._repos = repos.filter(repo =>
+              repo.name.toLowerCase().includes(filter.toLowerCase())
+            );
             this._loading = false;
           });
     },
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
index 4bc023f..1300b20 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-list.html">
 
@@ -34,19 +36,22 @@
 </test-fixture>
 
 <script>
-  let counter;
-  const repoGenerator = () => {
+  function createRepo(name, counter) {
     return {
-      id: `test${++counter}`,
+      id: `${name}${counter}`,
+      name: `${name}`,
       state: 'ACTIVE',
       web_links: [
         {
           name: 'diffusion',
-          url: `https://phabricator.example.org/r/project/test${counter}`,
+          url: `https://phabricator.example.org/r/project/${name}${counter}`,
         },
       ],
     };
-  };
+  }
+
+  let counter;
+  const repoGenerator = () => createRepo('test', ++counter);
 
   suite('gr-repo-list tests', () => {
     let element;
@@ -151,6 +156,15 @@
           done();
         });
       });
+
+      test('filter is case insensitive', async () => {
+        const repoStub = sandbox.stub(element.$.restAPI, 'getRepos');
+        const repos = [createRepo('aSDf', 0)];
+        repoStub.withArgs('asdf').returns(Promise.resolve(repos));
+        element._filter = 'asdf';
+        await element._getRepos('asdf', 25, 0);
+        assert.equal(element._repos.length, 1);
+      });
     });
 
     suite('loading', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
index 7f2cbe7..d2093e4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 
 <link rel="import" href="../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
@@ -35,14 +35,14 @@
     <style include="gr-subpage-styles">
       .inherited {
         color: var(--deemphasized-text-color);
-        margin-left: .5em;
+        margin-left: var(--spacing-m);
       }
       section.section:not(.ARRAY) .title {
         align-items: center;
         display: flex;
       }
       section.section.ARRAY .title {
-        padding-top: .75em;
+        padding-top: var(--spacing-m);
       }
     </style>
     <div class="gr-form-styles">
@@ -87,12 +87,18 @@
                 </gr-select>
               </template>
               <template is="dom-if" if="[[_isString(option.info.type)]]">
-                <input
-                    is="iron-input"
-                    value="[[option.info.value]]"
+                <iron-input
+                    bind-value="[[option.info.value]]"
                     on-input="_handleStringChange"
                     data-option-key$="[[option._key]]"
-                    disabled$="[[_computeDisabled(option.info.editable)]]"></input>
+                    disabled$="[[_computeDisabled(option.info.editable)]]">
+                  <input
+                      is="iron-input"
+                      value="[[option.info.value]]"
+                      on-input="_handleStringChange"
+                      data-option-key$="[[option._key]]"
+                      disabled$="[[_computeDisabled(option.info.editable)]]">
+                </iron-input>
               </template>
               <template is="dom-if" if="[[option.info.inherited_value]]">
                 <span class="inherited">
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 6d7677e..bfa8832 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
@@ -71,7 +71,10 @@
       return editable === 'false';
     },
 
-    _computeChecked(value) {
+    /**
+     * @param {string} value - fallback to 'false' if undefined
+     */
+    _computeChecked(value = 'false') {
       return JSON.parse(value);
     },
 
@@ -123,8 +126,8 @@
         notifyPath: `${name}.${notifyPath}`,
       };
 
-      this.dispatchEvent(new CustomEvent(this.PLUGIN_CONFIG_CHANGED,
-          {detail, bubbles: true}));
+      this.dispatchEvent(new CustomEvent(
+          this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true}));
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
index ba0c876..0a6846f 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-plugin-config</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-plugin-config.html">
 
@@ -151,7 +153,8 @@
         const select = element.$$('select');
         assert.ok(select);
         select.value = 'newTest';
-        select.dispatchEvent(new Event('change', {bubbles: true}));
+        select.dispatchEvent(new Event(
+            'change', {bubbles: true, composed: true}));
         flushAsynchronousOperations();
 
         assert.isTrue(buildStub.called);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index 8604ad8..5de77b9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -15,11 +15,13 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
+<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
 <link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-select/gr-select.html">
@@ -270,12 +272,18 @@
               <section>
                 <span class="title">Maximum Git object size limit</span>
                 <span class="value">
-                  <input
-                      id="maxGitObjSizeInput"
+                  <iron-input
+                      id="maxGitObjSizeIronInput"
                       bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
-                      is="iron-input"
                       type="text"
                       disabled$="[[_readOnly]]">
+                    <input
+                        id="maxGitObjSizeInput"
+                        bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
+                        is="iron-input"
+                        type="text"
+                        disabled$="[[_readOnly]]">
+                  </iron-input>
                   <template is="dom-if" if="[[_repoConfig.max_object_size_limit.value]]">
                     effective: [[_repoConfig.max_object_size_limit.value]] bytes
                   </template>
@@ -356,7 +364,7 @@
               </template>
             </div>
             <gr-button
-                on-tap="_handleSaveRepoConfig"
+                on-click="_handleSaveRepoConfig"
                 disabled$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button>
           </fieldset>
           <gr-endpoint-decorator name="repo-config">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index ff630c4..153a137 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -53,7 +53,6 @@
 
   Polymer({
     is: 'gr-repo',
-    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
@@ -109,6 +108,10 @@
       _schemesObj: Object,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     observers: [
       '_handleConfigChanged(_repoConfig.*)',
     ],
@@ -309,6 +312,9 @@
     },
 
     _computeCommands(repo, schemesObj, _selectedScheme) {
+      if (!schemesObj || !repo || !_selectedScheme) {
+        return [];
+      }
       const commands = [];
       let commandObj;
       if (schemesObj.hasOwnProperty(_selectedScheme)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
index 60645bb..4e81565 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo.html">
 
@@ -100,10 +102,12 @@
     const SCHEMES = {http: {}, repo: {}, ssh: {}};
 
     function getFormFields() {
-      const selects = Polymer.dom(element.root).querySelectorAll('select');
-      const textareas =
-          Polymer.dom(element.root).querySelectorAll('iron-autogrow-textarea');
-      const inputs = Polymer.dom(element.root).querySelectorAll('input');
+      const selects = Array.from(
+          Polymer.dom(element.root).querySelectorAll('select'));
+      const textareas = Array.from(
+          Polymer.dom(element.root).querySelectorAll('iron-autogrow-textarea'));
+      const inputs = Array.from(
+          Polymer.dom(element.root).querySelectorAll('input'));
       return inputs.concat(textareas).concat(selects);
     }
 
@@ -362,8 +366,9 @@
               configInputObj.private_by_default;
           element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
               configInputObj.match_author_to_committer_date;
-          element.$.maxGitObjSizeInput.bindValue =
-              configInputObj.max_object_size_limit;
+          const inputElement = Polymer.Element ?
+            element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput;
+          inputElement.bindValue = configInputObj.max_object_size_limit;
           element.$.contributorAgreementSelect.bindValue =
               configInputObj.use_contributor_agreements;
           element.$.useSignedOffBySelect.bindValue =
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
index c8ae650..9820e31 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
@@ -15,22 +15,25 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-select/gr-select.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-rule-editor">
   <template>
     <style include="shared-styles">
       :host {
         border-bottom: 1px solid var(--border-color);
-        padding: .7em;
+        padding: var(--spacing-m);
         display: block;
       }
       #removeBtn {
@@ -44,7 +47,7 @@
         display: flex;
       }
       #options > * {
-        margin-right: .5em;
+        margin-right: var(--spacing-m);
       }
       #mainContainer {
         align-items: baseline;
@@ -146,14 +149,14 @@
       <gr-button
           link
           id="removeBtn"
-          on-tap="_handleRemoveRule">Remove</gr-button>
+          on-click="_handleRemoveRule">Remove</gr-button>
     </div>
     <div
         id="deletedContainer"
         class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
       [[groupName]] was deleted
       <gr-button link
-          id="undoRemoveBtn" on-tap="_handleUndoRemove">Undo</gr-button>
+          id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
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 84c6101..8d94a90 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
@@ -66,7 +66,6 @@
 
   Polymer({
     is: 'gr-rule-editor',
-    _legacyUndefinedCheck: true,
 
     properties: {
       hasRange: Boolean,
@@ -97,6 +96,11 @@
     behaviors: [
       Gerrit.AccessBehavior,
       Gerrit.BaseUrlBehavior,
+      /**
+       * Unused in this element, but called by other elements in tests
+       * e.g gr-permission_test.
+       */
+      Gerrit.FireBehavior,
       Gerrit.URLEncodingBehavior,
     ],
 
@@ -115,11 +119,20 @@
       this._setupValues(this.rule);
     },
 
+    attached() {
+      if (!this.rule) { return; } // Check needed for test purposes.
+      if (!this._originalRuleValues) {
+        // Observer _handleValueChange is called after the ready()
+        // method finishes. Original values must be set later to
+        // avoid set .modified flag to true
+        this._setOriginalRuleValues(this.rule.value);
+      }
+    },
+
     _setupValues(rule) {
       if (!rule.value) {
         this._setDefaultRuleValues();
       }
-      this._setOriginalRuleValues(rule.value);
     },
 
     _computeForce(permission, action) {
@@ -211,12 +224,13 @@
 
     _handleRemoveRule() {
       if (this.rule.value.added) {
-        this.dispatchEvent(new CustomEvent('added-rule-removed',
-            {bubbles: true}));
+        this.dispatchEvent(new CustomEvent(
+            'added-rule-removed', {bubbles: true, composed: true}));
       }
       this._deleted = true;
       this.rule.value.deleted = true;
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _handleUndoRemove() {
@@ -238,7 +252,8 @@
       if (!this._originalRuleValues) { return; }
       this.rule.value.modified = true;
       // Allows overall access page to know a change has been made.
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _setOriginalRuleValues(value) {
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
index 7f5bf6a..6d533af 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-rule-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-rule-editor.html">
 
@@ -200,7 +202,7 @@
     });
 
     suite('already existing generic rule', () => {
-      setup(() => {
+      setup(done => {
         element.group = 'Group Name';
         element.permission = 'submit';
         element.rule = {
@@ -216,6 +218,10 @@
         // by the parent element.
         element._setupValues(element.rule);
         flushAsynchronousOperations();
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -311,7 +317,7 @@
     });
 
     suite('new edit rule', () => {
-      setup(() => {
+      setup(done => {
         element.group = 'Group Name';
         element.permission = 'editTopicName';
         element.rule = {
@@ -321,6 +327,10 @@
         element._setupValues(element.rule);
         flushAsynchronousOperations();
         element.rule.value.added = true;
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -360,7 +370,7 @@
     });
 
     suite('already existing rule with labels', () => {
-      setup(() => {
+      setup(done => {
         element.label = {values: [
           {value: -2, text: 'This shall not be merged'},
           {value: -1, text: 'I would prefer this is not merged as is'},
@@ -382,6 +392,10 @@
         element.section = 'refs/*';
         element._setupValues(element.rule);
         flushAsynchronousOperations();
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -414,7 +428,7 @@
     });
 
     suite('new rule with labels', () => {
-      setup(() => {
+      setup(done => {
         sandbox.spy(element, '_setDefaultRuleValues');
         element.label = {values: [
           {value: -2, text: 'This shall not be merged'},
@@ -432,6 +446,10 @@
         element._setupValues(element.rule);
         flushAsynchronousOperations();
         element.rule.value.added = true;
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -472,7 +490,7 @@
     });
 
     suite('already existing push rule', () => {
-      setup(() => {
+      setup(done => {
         element.group = 'Group Name';
         element.permission = 'push';
         element.rule = {
@@ -485,6 +503,10 @@
         element.section = 'refs/*';
         element._setupValues(element.rule);
         flushAsynchronousOperations();
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -513,7 +535,7 @@
     });
 
     suite('new push rule', () => {
-      setup(() => {
+      setup(done => {
         element.group = 'Group Name';
         element.permission = 'push';
         element.rule = {
@@ -523,6 +545,10 @@
         element._setupValues(element.rule);
         flushAsynchronousOperations();
         element.rule.value.added = true;
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -553,7 +579,7 @@
     });
 
     suite('already existing edit rule', () => {
-      setup(() => {
+      setup(done => {
         element.group = 'Group Name';
         element.permission = 'editTopicName';
         element.rule = {
@@ -566,6 +592,10 @@
         element.section = 'refs/*';
         element._setupValues(element.rule);
         flushAsynchronousOperations();
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index a6dde7a..a6c86bb 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -19,7 +19,7 @@
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
@@ -29,6 +29,8 @@
 <link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html">
 <link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
+<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
 
 <dom-module id="gr-change-list-item">
   <template>
@@ -76,7 +78,7 @@
         display: inline-flex;
       }
       .status .comma {
-        padding-right: .2rem;
+        padding-right: var(--spacing-xs);
       }
       /* Used to hide the leading separator comma for statuses. */
       .status .comma:first-of-type {
@@ -85,7 +87,7 @@
       .size gr-tooltip-content {
         margin: -.4rem -.6rem;
         max-width: 2.5rem;
-        padding: .4rem .6rem;
+        padding: var(--spacing-m) var(--spacing-l);
       }
       a {
         color: inherit;
@@ -97,6 +99,8 @@
       }
       .u-monospace {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
       }
       .u-green {
         color: var(--vote-text-color-recommended);
@@ -104,10 +108,6 @@
       .u-red {
         color: var(--vote-text-color-disliked);
       }
-      .label.u-green:not(.u-monospace),
-      .label.u-red:not(.u-monospace) {
-        font-size: 1.2rem;
-      }
       .u-gray-background {
         background-color: var(--table-header-background-color);
       }
@@ -159,16 +159,14 @@
     <td class="cell owner"
         hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]">
       <gr-account-link
-          account="[[change.owner]]"
-          additional-text="[[_computeAccountStatusString(change.owner)]]"></gr-account-link>
+          account="[[change.owner]]"></gr-account-link>
     </td>
     <td class="cell assignee"
         hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]">
       <template is="dom-if" if="[[change.assignee]]">
         <gr-account-link
             id="assigneeAccountLink"
-            account="[[change.assignee]]"
-            additional-text="[[_computeAccountStatusString(change.assignee)]]"></gr-account-link>
+            account="[[change.assignee]]"></gr-account-link>
       </template>
       <template is="dom-if" if="[[!change.assignee]]">
         <span class="placeholder">--</span>
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 ecc7532..8eb39891 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,7 +26,6 @@
 
   Polymer({
     is: 'gr-change-list-item',
-    _legacyUndefinedCheck: true,
 
     properties: {
       visibleChangeTableColumns: Array,
@@ -40,11 +39,6 @@
         type: String,
         computed: '_computeChangeURL(change)',
       },
-      needsReview: {
-        type: Boolean,
-        reflectToAttribute: true,
-        computed: '_computeItemNeedsReview(change.reviewed)',
-      },
       statuses: {
         type: Array,
         computed: 'changeStatuses(change)',
@@ -78,10 +72,6 @@
       });
     },
 
-    _computeItemNeedsReview(reviewed) {
-      return !reviewed;
-    },
-
     _computeChangeURL(change) {
       return Gerrit.Nav.getUrlForChange(change);
     },
@@ -174,10 +164,6 @@
       return str;
     },
 
-    _computeAccountStatusString(account) {
-      return account && account.status ? `(${account.status})` : '';
-    },
-
     _computeSizeTooltip(change) {
       if (change.insertions + change.deletions === 0 ||
           isNaN(change.insertions + change.deletions)) {
@@ -214,6 +200,7 @@
       this.set('change.reviewed', newVal);
       this.dispatchEvent(new CustomEvent('toggle-reviewed', {
         bubbles: true,
+        composed: true,
         detail: {change: this.change, reviewed: newVal},
       }));
     },
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 35f81bc..afae619 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-list-item</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
@@ -187,14 +189,6 @@
       };
       flushAsynchronousOperations();
       assert.isOk(element.$$('.assignee gr-account-link'));
-      assert.equal(Polymer.dom(element.root)
-          .querySelector('#assigneeAccountLink').additionalText, '(test)');
-    });
-
-    test('_computeAccountStatusString', () => {
-      assert.equal(element._computeAccountStatusString({}), '');
-      assert.equal(element._computeAccountStatusString({status: 'Working'}),
-          '(Working)');
     });
 
     test('TShirt sizing tooltip', () => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index 48d5075..3ac1d5f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -17,7 +17,8 @@
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -35,7 +36,7 @@
       }
       .loading {
         color: var(--deemphasized-text-color);
-        padding: 1em var(--default-horizontal-margin);
+        padding: var(--spacing-l);
       }
       gr-change-list {
         width: 100%;
@@ -67,7 +68,7 @@
       @media only screen and (max-width: 50em) {
         .loading,
         .error {
-          padding: 0 var(--default-horizontal-margin);
+          padding: 0 var(--spacing-l);
         }
       }
     </style>
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 6d05132..cf6da73 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
@@ -18,7 +18,7 @@
   'use strict';
 
   const LookupQueryPatterns = {
-    CHANGE_ID: /^\s*i?[0-9a-f]{8,40}\s*$/i,
+    CHANGE_ID: /^\s*i?[0-9a-f]{7,40}\s*$/i,
     CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g,
   };
 
@@ -31,7 +31,6 @@
 
   Polymer({
     is: 'gr-change-list-view',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -41,6 +40,7 @@
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
+      Gerrit.FireBehavior,
       Gerrit.URLEncodingBehavior,
     ],
 
@@ -162,8 +162,7 @@
           for (const query in LookupQueryPatterns) {
             if (LookupQueryPatterns.hasOwnProperty(query) &&
                 this._query.match(LookupQueryPatterns[query])) {
-              this._replaceCurrentLocation(
-                  Gerrit.Nav.getUrlForChange(changes[0]));
+              Gerrit.Nav.navigateToChange(changes[0]);
               return;
             }
           }
@@ -185,10 +184,6 @@
       });
     },
 
-    _replaceCurrentLocation(url) {
-      window.location.replace(url);
-    },
-
     _getChanges() {
       return this.$.restAPI.getChanges(this._changesPerPage, this._query,
           this._offset);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
index 3911364..54885cc 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-list-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-list-view.html">
 
@@ -194,7 +196,6 @@
 
     suite('query based navigation', () => {
       setup(() => {
-        sandbox.stub(Gerrit.Nav, 'getUrlForChange', () => '/r/c/1');
       });
 
       teardown(done => {
@@ -205,10 +206,11 @@
       });
 
       test('Searching for a change ID redirects to change', done => {
+        const change = {_number: 1};
         sandbox.stub(element, '_getChanges')
-            .returns(Promise.resolve([{_number: 1}]));
-        sandbox.stub(element, '_replaceCurrentLocation', url => {
-          assert.equal(url, '/r/c/1');
+            .returns(Promise.resolve([change]));
+        sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
+          assert.equal(url, change);
           done();
         });
 
@@ -216,10 +218,11 @@
       });
 
       test('Searching for a change num redirects to change', done => {
+        const change = {_number: 1};
         sandbox.stub(element, '_getChanges')
-            .returns(Promise.resolve([{_number: 1}]));
-        sandbox.stub(element, '_replaceCurrentLocation', url => {
-          assert.equal(url, '/r/c/1');
+            .returns(Promise.resolve([change]));
+        sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
+          assert.equal(url, change);
           done();
         });
 
@@ -227,10 +230,11 @@
       });
 
       test('Commit hash redirects to change', done => {
+        const change = {_number: 1};
         sandbox.stub(element, '_getChanges')
-            .returns(Promise.resolve([{_number: 1}]));
-        sandbox.stub(element, '_replaceCurrentLocation', url => {
-          assert.equal(url, '/r/c/1');
+            .returns(Promise.resolve([change]));
+        sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
+          assert.equal(url, change);
           done();
         });
 
@@ -240,7 +244,7 @@
       test('Searching for an invalid change ID searches', () => {
         sandbox.stub(element, '_getChanges')
             .returns(Promise.resolve([]));
-        const stub = sandbox.stub(element, '_replaceCurrentLocation');
+        const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
 
         element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
         flushAsynchronousOperations();
@@ -251,7 +255,7 @@
       test('Change ID with multiple search results searches', () => {
         sandbox.stub(element, '_getChanges')
             .returns(Promise.resolve([{}, {}]));
-        const stub = sandbox.stub(element, '_replaceCurrentLocation');
+        const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
 
         element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
         flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
index ef17baa..699f07a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -20,12 +20,14 @@
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 
 <dom-module id="gr-change-list">
   <template>
@@ -35,6 +37,18 @@
         border-collapse: collapse;
         width: 100%;
       }
+      .section-count-label {
+        color: var(--deemphasized-text-color);
+      }
+      a.section-title:hover {
+        text-decoration: none;
+      }
+      a.section-title:hover .section-count-label {
+        text-decoration: none;
+      }
+      a.section-title:hover .section-name {
+        text-decoration: underline;
+      }
     </style>
     <table id="changeList">
       <tr class="topHeader">
@@ -68,8 +82,9 @@
             <td class="star" hidden$="[[!showStar]]" hidden></td>
             <td class="cell"
                 colspan$="[[_computeColspan(changeTableColumns, labelNames)]]">
-              <a href$="[[_sectionHref(changeSection.query)]]">
-                [[changeSection.name]]
+              <a href$="[[_sectionHref(changeSection.query)]]" class="section-title">
+                <span class="section-name">[[changeSection.name]]</span>
+                <span class="section-count-label">[[changeSection.countLabel]]</span>
               </a>
             </td>
           </tr>
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 587719d..5006f1e 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
@@ -24,7 +24,6 @@
 
   Polymer({
     is: 'gr-change-list',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when next page key shortcut was pressed.
@@ -105,6 +104,7 @@
     behaviors: [
       Gerrit.BaseUrlBehavior,
       Gerrit.ChangeTableBehavior,
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.RESTClientBehavior,
       Gerrit.URLEncodingBehavior,
@@ -158,6 +158,11 @@
     },
 
     _computePreferences(account, preferences) {
+      // Polymer 2: check for undefined
+      if ([account, preferences].some(arg => arg === undefined)) {
+        return;
+      }
+
       this.changeTableColumns = this.columnNames;
 
       if (account) {
@@ -173,6 +178,7 @@
     },
 
     _computeColspan(changeTableColumns, labelNames) {
+      if (!changeTableColumns || !labelNames) return;
       return changeTableColumns.length + labelNames.length +
           NUMBER_FIXED_COLUMNS;
     },
@@ -210,8 +216,19 @@
       this.sections = changes ? [{results: changes}] : [];
     },
 
+    _processQuery(query) {
+      let tokens = query.split(' ');
+      const invalidTokens = ['limit:', 'age:', '-age:'];
+      tokens = tokens.filter(token => {
+        return !invalidTokens.some(invalidToken => {
+          return token.startsWith(invalidToken);
+        });
+      });
+      return tokens.join(' ');
+    },
+
     _sectionHref(query) {
-      return Gerrit.Nav.getUrlForSearchQuery(query);
+      return Gerrit.Nav.getUrlForSearchQuery(this._processQuery(query));
     },
 
     /**
@@ -238,13 +255,13 @@
     _computeItemNeedsReview(account, change, showReviewedState) {
       return showReviewedState && !change.reviewed &&
           !change.work_in_progress &&
-          this.changeIsOpen(change.status) &&
+          this.changeIsOpen(change) &&
           (!account || account._account_id != change.owner._account_id);
     },
 
     _computeItemHighlight(account, change) {
       // Do not show the assignee highlight if the change is not open.
-      if (!change.assignee ||
+      if (!change ||!change.assignee ||
           !account ||
           CLOSED_STATUS.indexOf(change.status) !== -1) {
         return false;
@@ -352,16 +369,16 @@
     },
 
     _getListItems() {
-      // Polymer2: querySelectorAll returns NodeList instead of Array.
       return Array.from(
           Polymer.dom(this.root).querySelectorAll('gr-change-list-item'));
     },
 
     _sectionsChanged() {
       // Flush DOM operations so that the list item elements will be loaded.
-      Polymer.dom.flush();
-      this.$.cursor.stops = this._getListItems();
-      this.$.cursor.moveToStart();
+      Polymer.RenderStatus.afterNextRender(this, () => {
+        this.$.cursor.stops = this._getListItems();
+        this.$.cursor.moveToStart();
+      });
     },
 
     _isOutgoing(section) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index 99741a8..817de6f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="gr-change-list.html">
 
@@ -170,11 +172,11 @@
         {_number: 2},
       ];
       flushAsynchronousOperations();
-      const elementItems = Polymer.dom(element.root).querySelectorAll(
-          'gr-change-list-item');
-      assert.equal(elementItems.length, 3);
+      Polymer.RenderStatus.afterNextRender(element, () => {
+        const elementItems = Polymer.dom(element.root).querySelectorAll(
+            'gr-change-list-item');
+        assert.equal(elementItems.length, 3);
 
-      flush(() => {
         assert.isTrue(elementItems[0].hasAttribute('selected'));
         MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
         assert.equal(element.selectedIndex, 1);
@@ -431,6 +433,59 @@
     });
   });
 
+  suite('dashboard queries', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('query without age and limit unchanged', () => {
+      const query = 'status:closed owner:me';
+      assert.deepEqual(element._processQuery(query), query);
+    });
+
+    test('query with age and limit', () => {
+      const query = 'status:closed age:1week limit:10 owner:me';
+      const expectedQuery = 'status:closed owner:me';
+      assert.deepEqual(element._processQuery(query), expectedQuery);
+    });
+
+    test('query with age', () => {
+      const query = 'status:closed age:1week owner:me';
+      const expectedQuery = 'status:closed owner:me';
+      assert.deepEqual(element._processQuery(query), expectedQuery);
+    });
+
+    test('query with limit', () => {
+      const query = 'status:closed limit:10 owner:me';
+      const expectedQuery = 'status:closed owner:me';
+      assert.deepEqual(element._processQuery(query), expectedQuery);
+    });
+
+    test('query with age as value and not key', () => {
+      const query = 'status:closed random:age';
+      const expectedQuery = 'status:closed random:age';
+      assert.deepEqual(element._processQuery(query), expectedQuery);
+    });
+
+    test('query with limit as value and not key', () => {
+      const query = 'status:closed random:limit';
+      const expectedQuery = 'status:closed random:limit';
+      assert.deepEqual(element._processQuery(query), expectedQuery);
+    });
+
+    test('query with -age key', () => {
+      const query = 'status:closed -age:1week';
+      const expectedQuery = 'status:closed';
+      assert.deepEqual(element._processQuery(query), expectedQuery);
+    });
+  });
+
   suite('gr-change-list sections', () => {
     let element;
     let sandbox;
@@ -442,7 +497,7 @@
 
     teardown(() => { sandbox.restore(); });
 
-    test('keyboard shortcuts', () => {
+    test('keyboard shortcuts', done => {
       element.selectedIndex = 0;
       element.sections = [
         {
@@ -468,42 +523,45 @@
         },
       ];
       flushAsynchronousOperations();
-      const elementItems = Polymer.dom(element.root).querySelectorAll(
-          'gr-change-list-item');
-      assert.equal(elementItems.length, 9);
+      Polymer.RenderStatus.afterNextRender(element, () => {
+        const elementItems = Polymer.dom(element.root).querySelectorAll(
+            'gr-change-list-item');
+        assert.equal(elementItems.length, 9);
 
-      MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
-      assert.equal(element.selectedIndex, 1);
-      MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+        MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+        assert.equal(element.selectedIndex, 1);
+        MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
 
-      const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-      assert.equal(element.selectedIndex, 2);
+        const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+        assert.equal(element.selectedIndex, 2);
 
-      MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
-      assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
-          'Should navigate to /c/2/');
+        MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+        assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
+            'Should navigate to /c/2/');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
-      assert.equal(element.selectedIndex, 1);
-      MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
-      assert.deepEqual(navStub.lastCall.args[0], {_number: 1},
-          'Should navigate to /c/1/');
+        MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+        assert.equal(element.selectedIndex, 1);
+        MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+        assert.deepEqual(navStub.lastCall.args[0], {_number: 1},
+            'Should navigate to /c/1/');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
-      MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
-      MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
-      assert.equal(element.selectedIndex, 4);
-      MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
-      assert.deepEqual(navStub.lastCall.args[0], {_number: 4},
-          'Should navigate to /c/4/');
+        MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+        MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+        MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+        assert.equal(element.selectedIndex, 4);
+        MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+        assert.deepEqual(navStub.lastCall.args[0], {_number: 4},
+            'Should navigate to /c/4/');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
-      const change = element._changeForIndex(element.selectedIndex);
-      assert.equal(change.reviewed, true,
-          'Should mark change as reviewed');
-      MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
-      assert.equal(change.reviewed, false,
-          'Should mark change as unreviewed');
+        MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
+        const change = element._changeForIndex(element.selectedIndex);
+        assert.equal(change.reviewed, true,
+            'Should mark change as reviewed');
+        MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
+        assert.equal(change.reviewed, false,
+            'Should mark change as unreviewed');
+        done();
+      });
     });
 
     test('highlight attribute is updated correctly', () => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
index ecbd67e..e88368d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -30,7 +30,7 @@
       #graphic,
       #help {
         display: inline-block;
-        margin: .5em;
+        margin: var(--spacing-m);
       }
       #graphic #circle {
         align-items: center;
@@ -51,14 +51,14 @@
         text-align: center;
       }
       #help {
-        padding-top: 1.35em;
+        padding-top: var(--spacing-xl);
         vertical-align: top;
       }
       #help h1 {
-        font-size: var(--font-size-large);
+        font-size: var(--font-size-h3);
       }
       #help p {
-        margin-bottom: .6em;
+        margin-bottom: var(--spacing-m);
         max-width: 35em;
       }
       @media only screen and (max-width: 50em) {
@@ -82,7 +82,7 @@
         other git code review tools. Click on the `Create Change' button
         and follow the step by step instructions.
       </p>
-      <gr-button on-tap="_handleCreateTap">Create Change</gr-button>
+      <gr-button on-click="_handleCreateTap">Create Change</gr-button>
     </div>
   </template>
   <script src="gr-create-change-help.js"></script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
index 42d7bd7..19e7a25 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-create-change-help',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the "Create change" button is tapped.
@@ -29,7 +28,8 @@
 
     _handleCreateTap(e) {
       e.preventDefault();
-      this.dispatchEvent(new CustomEvent('create-tap', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('create-tap', {bubbles: true, composed: true}));
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
index 09d95fd..c43d62a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-change-help</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html
index e6d123c..9e86058 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
@@ -25,10 +25,10 @@
     <style include="shared-styles">
       ol {
         list-style: decimal;
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
       }
       p {
-        margin-bottom: .75em;
+        margin-bottom: var(--spacing-m);
       }
       #commandsDialog {
         max-width: 40em;
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
index e4958c5..5abb257 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
@@ -25,7 +25,7 @@
 
   Polymer({
     is: 'gr-create-commands-dialog',
-    _legacyUndefinedCheck: true,
+
     properties: {
       branch: String,
       _createNewCommitCommand: {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
index e00037d..89ad573 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-commands-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-create-commands-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html
index d12d84b..def5228 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-repo-branch-picker/gr-repo-branch-picker.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
index ed87e9e..c2bfbf5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
@@ -26,7 +26,7 @@
 
   Polymer({
     is: 'gr-create-destination-dialog',
-    _legacyUndefinedCheck: true,
+
     properties: {
       _repo: String,
       _branch: String,
@@ -46,9 +46,13 @@
       this.$.createOverlay.close();
     },
 
-    _pickerConfirm() {
+    _pickerConfirm(e) {
       this.$.createOverlay.close();
       const detail = {repo: this._repo, branch: this._branch};
+      // e is a 'confirm' event from gr-dialog. We want to fire a more detailed
+      // 'confirm' event here, so let's stop propagation of the bare event.
+      e.preventDefault();
+      e.stopPropagation();
       this.dispatchEvent(new CustomEvent('confirm', {detail, bubbles: false}));
     },
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
index b0ba8a2b..41475a0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../change-list/gr-change-list/gr-change-list.html">
@@ -38,7 +39,7 @@
       }
       .loading {
         color: var(--deemphasized-text-color);
-        padding: 1em var(--default-horizontal-margin);
+        padding: var(--spacing-l);
       }
       gr-change-list {
         width: 100%;
@@ -52,7 +53,7 @@
         border-bottom: 1px solid var(--border-color);
         display: flex;
         justify-content: space-between;
-        padding: .25em var(--default-horizontal-margin);
+        padding: var(--spacing-xs) var(--spacing-l);
       }
       .banner gr-button {
         --gr-button: {
@@ -67,7 +68,7 @@
       }
       @media only screen and (max-width: 50em) {
         .loading {
-          padding: 0 var(--default-horizontal-margin);
+          padding: 0 var(--spacing-l);
         }
       }
     </style>
@@ -80,7 +81,7 @@
         <gr-button
             class="delete"
             link
-            on-tap="_handleOpenDeleteDialog">Delete All</gr-button>
+            on-click="_handleOpenDeleteDialog">Delete All</gr-button>
       </div>
     </div>
     <div class="loading" hidden$="[[!_loading]]">Loading...</div>
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 5faef08..b762ab3 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
@@ -21,7 +21,6 @@
 
   Polymer({
     is: 'gr-dashboard-view',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -76,6 +75,7 @@
     ],
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.RESTClientBehavior,
     ],
 
@@ -144,12 +144,6 @@
         return Promise.resolve();
       }
 
-      const user = params.user || 'self';
-
-      // NOTE: This method may be called before attachment. Fire title-change
-      // in an async so that attachment to the DOM can take place first.
-      const title = params.title || this._computeTitle(user);
-      this.async(() => this.fire('title-change', {title}));
       return this._reload();
     },
 
@@ -170,11 +164,19 @@
 
       const checkForNewUser = !project && user === 'self';
       return dashboardPromise
-          .then(res => this._fetchDashboardChanges(res, checkForNewUser))
+          .then(res => {
+            if (res && res.title) {
+              this.fire('title-change', {title: res.title});
+            }
+            return this._fetchDashboardChanges(res, checkForNewUser);
+          })
           .then(() => {
             this._maybeShowDraftsBanner();
             this.$.reporting.dashboardDisplayed();
           }).catch(err => {
+            this.fire('title-change', {
+              title: title || this._computeTitle(user),
+            });
             console.warn(err);
           }).then(() => { this._loading = false; });
     },
@@ -196,7 +198,7 @@
             section.query);
 
       if (checkForNewUser) {
-        queries.push('owner:self');
+        queries.push('owner:self limit:1');
       }
 
       return this.$.restAPI.getChanges(null, queries, null, this.options)
@@ -208,6 +210,7 @@
             }
             this._results = changes.map((results, i) => ({
               name: res.sections[i].name,
+              countLabel: this._computeSectionCountLabel(results),
               query: res.sections[i].query,
               results,
               isOutgoing: res.sections[i].isOutgoing,
@@ -217,6 +220,16 @@
           });
     },
 
+    _computeSectionCountLabel(changes) {
+      if (!changes || !changes.length || changes.length == 0) {
+        return '';
+      }
+      const more = changes[changes.length - 1]._more_changes;
+      const numChanges = changes.length;
+      const andMore = more ? ' and more' : '';
+      return `(${numChanges}${andMore})`;
+    },
+
     _computeUserHeaderClass(params) {
       if (!params || !!params.project || !params.user
           || params.user === 'self') {
@@ -248,7 +261,7 @@
       if (!draftSection || !draftSection.results.length) { return; }
 
       const closedChanges = draftSection.results
-          .filter(change => !this.changeIsOpen(change.status));
+          .filter(change => !this.changeIsOpen(change));
       if (!closedChanges.length) { return; }
 
       this._showDraftsBanner = true;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index 54c8ea9..41d4192 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-dashboard-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-dashboard-view.html">
 
@@ -147,6 +149,27 @@
       assert.equal(element._computeTitle('not self'), 'Dashboard for not self');
     });
 
+    suite('_computeSectionCountLabel', () => {
+      test('empty changes dont count label', () => {
+        assert.equal('', element._computeSectionCountLabel([]));
+      });
+
+      test('1 change', () => {
+        assert.equal('(1)',
+            element._computeSectionCountLabel(['1']));
+      });
+
+      test('2 changes', () => {
+        assert.equal('(2)',
+            element._computeSectionCountLabel(['1', '2']));
+      });
+
+      test('1 change and more', () => {
+        assert.equal('(1 and more)',
+            element._computeSectionCountLabel([{_more_changes: true}]));
+      });
+    });
+
     suite('_isViewActive', () => {
       test('nothing happens when user param is falsy', () => {
         element.params = {};
@@ -182,7 +205,7 @@
         return paramsChangedPromise.then(() => {
           assert.isTrue(
               getChangesStub.calledWith(
-                  null, ['1', '2', 'owner:self'], null, element.options));
+                  null, ['1', '2', 'owner:self limit:1'], null, element.options));
         });
       });
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html
index 2394e24..d445185 100644
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../change-list/gr-change-list/gr-change-list.html">
 <link rel="import" href="../gr-create-change-help/gr-create-change-help.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
index 14d0cb0..acc4295 100644
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
@@ -19,7 +19,7 @@
 
   Polymer({
     is: 'gr-embed-dashboard',
-    _legacyUndefinedCheck: true,
+
     properties: {
       account: Object,
       sections: Array,
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
index 2328725..0b4459c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/dashboard-header-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
index 67fbd97..7ae4dab 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
@@ -19,7 +19,7 @@
 
   Polymer({
     is: 'gr-repo-header',
-    _legacyUndefinedCheck: true,
+
     properties: {
       /** @type {?String} */
       repo: {
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
index a561e09..266818e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-header</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-header.html">
 
@@ -44,30 +46,13 @@
 
     teardown(() => { sandbox.restore(); });
 
-    test('loads and clears account info', done => {
-      sandbox.stub(element.$.restAPI, 'getAccountDetails')
-          .returns(Promise.resolve({
-            name: 'foo',
-            email: 'bar',
-            registered_on: '2015-03-12 18:32:08.000000000',
-          }));
-      sandbox.stub(element.$.restAPI, 'getAccountStatus')
-          .returns(Promise.resolve('baz'));
-
-      element.userId = 'foo.bar@baz';
-      flush(() => {
-        assert.isOk(element._accountDetails);
-        assert.isOk(element._status);
-
-        element.userId = null;
-        flush(() => {
-          flushAsynchronousOperations();
-          assert.isNull(element._accountDetails);
-          assert.isNull(element._status);
-
-          done();
-        });
-      });
+    test('repoUrl reset once repo changed', () => {
+      sandbox.stub(Gerrit.Nav, 'getUrlForRepo',
+          repoName => `http://test.com/${repoName}`
+      );
+      assert.equal(element._repoUrl, undefined);
+      element.repo = 'test';
+      assert.equal(element._repoUrl, 'http://test.com/test');
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
index 6accdfc..fed1c12 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
index c7fda2c..6afc169 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -19,7 +19,7 @@
 
   Polymer({
     is: 'gr-user-header',
-    _legacyUndefinedCheck: true,
+
     properties: {
       /** @type {?String} */
       userId: {
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
index c33be3b..e837a5b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-user-header</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-user-header.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
deleted file mode 100644
index c53c111..0000000
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
+++ /dev/null
@@ -1,185 +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.
- */
-(function() {
-  'use strict';
-
-  Polymer({
-    is: 'gr-account-entry',
-    _legacyUndefinedCheck: true,
-
-    /**
-     * Fired when an account is entered.
-     *
-     * @event add
-     */
-
-    /**
-     * When allowAnyInput is true, account-text-changed is fired when input text
-     * changed. This is needed so that the reply dialog's save button can be
-     * enabled for arbitrary cc's, which don't need a 'commit'.
-     *
-     * @event account-text-changed
-     */
-    properties: {
-      allowAnyInput: Boolean,
-      borderless: Boolean,
-      change: Object,
-      filter: Function,
-      placeholder: String,
-      /**
-       * When true, account-entry uses the account suggest API endpoint, which
-       * suggests any account in that Gerrit instance (and does not suggest
-       * groups).
-       *
-       * When false/undefined, account-entry uses the suggest_reviewers API
-       * endpoint, which suggests any account or group in that Gerrit instance
-       * that is not already a reviewer (or is not CCed) on that change.
-       */
-      allowAnyUser: Boolean,
-
-      // suggestFrom = 0 to enable default suggestions.
-      suggestFrom: {
-        type: Number,
-        value: 0,
-      },
-
-      query: {
-        type: Function,
-        value() {
-          return this._getReviewerSuggestions.bind(this);
-        },
-      },
-
-      _config: Object,
-      /** The value of the autocomplete entry. */
-      _inputText: {
-        type: String,
-        observer: '_inputTextChanged',
-      },
-
-      _loggedIn: Boolean,
-    },
-
-    behaviors: [
-      Gerrit.AnonymousNameBehavior,
-    ],
-
-    attached() {
-      this.$.restAPI.getConfig().then(cfg => {
-        this._config = cfg;
-      });
-      this.$.restAPI.getLoggedIn().then(loggedIn => {
-        this._loggedIn = loggedIn;
-      });
-    },
-
-    get focusStart() {
-      return this.$.input.focusStart;
-    },
-
-    focus() {
-      this.$.input.focus();
-    },
-
-    clear() {
-      this.$.input.clear();
-    },
-
-    setText(text) {
-      this.$.input.setText(text);
-    },
-
-    getText() {
-      return this.$.input.text;
-    },
-
-    _handleInputCommit(e) {
-      this.fire('add', {value: e.detail.value});
-      this.$.input.focus();
-    },
-
-    _accountOrAnon(reviewer) {
-      return this.getUserName(this._config, reviewer, false);
-    },
-
-    _inputTextChanged(text) {
-      if (text.length && this.allowAnyInput) {
-        this.dispatchEvent(new CustomEvent('account-text-changed',
-            {bubbles: true}));
-      }
-    },
-
-    _makeSuggestion(reviewer) {
-      let name;
-      let value;
-      const generateStatusStr = function(account) {
-        return account.status ? '(' + account.status + ')' : '';
-      };
-      if (reviewer.account) {
-        // Reviewer is an account suggestion from getChangeSuggestedReviewers.
-        const reviewerName = this._accountOrAnon(reviewer.account);
-        const reviewerEmail = this._reviewerEmail(reviewer.account.email);
-        const reviewerStatus = generateStatusStr(reviewer.account);
-        name = [reviewerName, reviewerEmail, reviewerStatus]
-            .filter(p => p.length > 0).join(' ');
-        value = reviewer;
-      } else if (reviewer.group) {
-        // Reviewer is a group suggestion from getChangeSuggestedReviewers.
-        name = reviewer.group.name + ' (group)';
-        value = reviewer;
-      } else if (reviewer._account_id) {
-        // Reviewer is an account suggestion from getSuggestedAccounts.
-        const reviewerName = this._accountOrAnon(reviewer);
-        const reviewerEmail = this._reviewerEmail(reviewer.email);
-        const reviewerStatus = generateStatusStr(reviewer);
-        name = [reviewerName, reviewerEmail, reviewerStatus]
-            .filter(p => p.length > 0).join(' ');
-        value = {account: reviewer, count: 1};
-      }
-      return {name, value};
-    },
-
-    _getReviewerSuggestions(input) {
-      if (!this.change || !this.change._number || !this._loggedIn) {
-        return Promise.resolve([]);
-      }
-
-      const api = this.$.restAPI;
-      const xhr = this.allowAnyUser ?
-        api.getSuggestedAccounts(`cansee:${this.change._number} ${input}`) :
-        api.getChangeSuggestedReviewers(this.change._number, input);
-
-      return xhr.then(reviewers => {
-        if (!reviewers) { return []; }
-        if (!this.filter) {
-          return reviewers.map(this._makeSuggestion.bind(this));
-        }
-        return reviewers
-            .filter(this.filter)
-            .map(this._makeSuggestion.bind(this));
-      });
-    },
-
-    _reviewerEmail(email) {
-      if (typeof email !== 'undefined') {
-        return '<' + email + '>';
-      }
-
-      return '';
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
deleted file mode 100644
index 724f7da..0000000
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
+++ /dev/null
@@ -1,274 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-account-entry</title>
-
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-account-entry.html">
-
-<script>void(0);</script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-account-entry></gr-account-entry>
-  </template>
-</test-fixture>
-
-<script>
-  suite('gr-account-entry tests', () => {
-    let sandbox;
-    let _nextAccountId = 0;
-    const makeAccount = function(opt_status) {
-      const accountId = ++_nextAccountId;
-      return {
-        _account_id: accountId,
-        name: 'name ' + accountId,
-        email: 'email ' + accountId,
-        status: opt_status,
-      };
-    };
-    let _nextAccountId2 = 0;
-    const makeAccount2 = function(opt_status) {
-      const accountId2 = ++_nextAccountId2;
-      return {
-        _account_id: accountId2,
-        email: 'email ' + accountId2,
-        status: opt_status,
-      };
-    };
-    let _nextAccountId3 = 0;
-    const makeAccount3 = function(opt_status) {
-      const accountId3 = ++_nextAccountId3;
-      return {
-        _account_id: accountId3,
-        name: 'name ' + accountId3,
-        status: opt_status,
-      };
-    };
-
-    let owner;
-    let existingReviewer1;
-    let existingReviewer2;
-    let suggestion1;
-    let suggestion2;
-    let suggestion3;
-    let element;
-
-    setup(done => {
-      owner = makeAccount();
-      existingReviewer1 = makeAccount();
-      existingReviewer2 = makeAccount();
-      suggestion1 = {account: makeAccount()};
-      suggestion2 = {account: makeAccount()};
-      suggestion3 = {
-        group: {
-          id: 'suggested group id',
-          name: 'suggested group',
-        },
-      };
-
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(true); },
-      });
-
-      element = fixture('basic');
-      element.change = {
-        _number: 42,
-        owner,
-        reviewers: {
-          CC: [existingReviewer1],
-          REVIEWER: [existingReviewer2],
-        },
-      };
-      sandbox = sinon.sandbox.create();
-      return flush(done);
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    suite('stubbed values for _getReviewerSuggestions', () => {
-      setup(() => {
-        stub('gr-rest-api-interface', {
-          getChangeSuggestedReviewers() {
-            const redundantSuggestion1 = {account: existingReviewer1};
-            const redundantSuggestion2 = {account: existingReviewer2};
-            const redundantSuggestion3 = {account: owner};
-            return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
-              redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
-          },
-        });
-      });
-
-      test('_makeSuggestion formats account or group accordingly', () => {
-        let account = makeAccount();
-        const account2 = makeAccount2();
-        const account3 = makeAccount3();
-        let suggestion = element._makeSuggestion({account});
-        assert.deepEqual(suggestion, {
-          name: account.name + ' <' + account.email + '>',
-          value: {account},
-        });
-
-        const group = {name: 'test'};
-        suggestion = element._makeSuggestion({group});
-        assert.deepEqual(suggestion, {
-          name: group.name + ' (group)',
-          value: {group},
-        });
-
-        suggestion = element._makeSuggestion(account);
-        assert.deepEqual(suggestion, {
-          name: account.name + ' <' + account.email + '>',
-          value: {account, count: 1},
-        });
-
-        element._config = {
-          user: {
-            anonymous_coward_name: 'Anonymous Coward',
-          },
-        };
-        assert.deepEqual(element._accountOrAnon(account2), 'Anonymous');
-
-        account = makeAccount('OOO');
-
-        suggestion = element._makeSuggestion({account});
-        assert.deepEqual(suggestion, {
-          name: account.name + ' <' + account.email + '> (OOO)',
-          value: {account},
-        });
-
-        suggestion = element._makeSuggestion(account);
-        assert.deepEqual(suggestion, {
-          name: account.name + ' <' + account.email + '> (OOO)',
-          value: {account, count: 1},
-        });
-
-        sandbox.stub(element, '_reviewerEmail',
-            () => { return ''; });
-
-        suggestion = element._makeSuggestion(account3);
-        assert.deepEqual(suggestion, {
-          name: account3.name,
-          value: {account: account3, count: 1},
-        });
-      });
-
-      test('_reviewerEmail', () => {
-        assert.equal(
-            element._reviewerEmail('email@gerritreview.com'),
-            '<email@gerritreview.com>');
-        assert.equal(element._reviewerEmail(undefined), '');
-      });
-
-      test('_getReviewerSuggestions excludes owner+reviewers', done => {
-        element._getReviewerSuggestions().then(reviewers => {
-          // Default is no filtering.
-          assert.equal(reviewers.length, 6);
-
-          // Set up filter that only accepts suggestion1.
-          const accountId = suggestion1.account._account_id;
-          element.filter = function(suggestion) {
-            return suggestion.account &&
-                suggestion.account._account_id === accountId;
-          };
-
-          element._getReviewerSuggestions().then(reviewers => {
-            assert.deepEqual(reviewers, [element._makeSuggestion(suggestion1)]);
-          }).then(done);
-        });
-      });
-
-      test('_getReviewerSuggestions short circuits when logged out', () => {
-        // API call is already stubbed.
-        const xhrSpy = element.$.restAPI.getChangeSuggestedReviewers;
-        element._loggedIn = false;
-        return element._getReviewerSuggestions('').then(() => {
-          assert.isFalse(xhrSpy.called);
-          element._loggedIn = true;
-          return element._getReviewerSuggestions('').then(() => {
-            assert.isTrue(xhrSpy.called);
-          });
-        });
-      });
-    });
-
-    test('allowAnyUser', done => {
-      const suggestReviewerStub =
-          sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
-              .returns(Promise.resolve([]));
-      const suggestAccountStub =
-          sandbox.stub(element.$.restAPI, 'getSuggestedAccounts')
-              .returns(Promise.resolve([]));
-
-      element._getReviewerSuggestions('').then(() => {
-        assert.isTrue(suggestReviewerStub.calledOnce);
-        assert.isTrue(suggestReviewerStub.calledWith(42, ''));
-        assert.isFalse(suggestAccountStub.called);
-        element.allowAnyUser = true;
-
-        element._getReviewerSuggestions('').then(() => {
-          assert.isTrue(suggestReviewerStub.calledOnce);
-          assert.isTrue(suggestAccountStub.calledOnce);
-          assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
-          done();
-        });
-      });
-    });
-    test('account-text-changed fired when input text changed and allowAnyInput',
-        () => {
-          // Spy on query, as that is called when _updateSuggestions proceeds.
-          const changeStub = sandbox.stub();
-          element.allowAnyInput = true;
-          sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
-              .returns(Promise.resolve([]));
-          element.addEventListener('account-text-changed', changeStub);
-          element.$.input.text = 'a';
-          assert.isTrue(changeStub.calledOnce);
-          element.$.input.text = 'ab';
-          assert.isTrue(changeStub.calledTwice);
-        });
-
-    test('account-text-changed not fired when input text changed without ' +
-        'allowAnyUser', () => {
-      // Spy on query, as that is called when _updateSuggestions proceeds.
-      const changeStub = sandbox.stub();
-      sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
-          .returns(Promise.resolve([]));
-      element.addEventListener('account-text-changed', changeStub);
-      element.$.input.text = 'a';
-      assert.isFalse(changeStub.called);
-    });
-
-    test('setText', () => {
-      // Spy on query, as that is called when _updateSuggestions proceeds.
-      const suggestSpy = sandbox.spy(element.$.input, 'query');
-      element.setText('test text');
-      flushAsynchronousOperations();
-
-      assert.equal(element.$.input.$.input.value, 'test text');
-      assert.isFalse(suggestSpy.called);
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 278875e..e12f10d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../admin/gr-create-change-dialog/gr-create-change-dialog.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
@@ -62,13 +62,13 @@
         color: var(--deemphasized-text-color);
       }
       #confirmSubmitDialog .changeSubject {
-        margin: 1em;
+        margin: var(--spacing-l);
         text-align: center;
       }
       iron-icon {
         color: inherit;
         height: 1.2rem;
-        margin-right: .2rem;
+        margin-right: var(--spacing-xs);
         width: 1.2rem;
       }
       gr-button {
@@ -92,7 +92,7 @@
         }
         gr-button {
           --gr-button: {
-            padding: .5em;
+            padding: var(--spacing-m);
             white-space: nowrap;
           }
         }
@@ -101,7 +101,7 @@
           margin: 0;
         }
         #actionLoadingMessage {
-          margin: .5em;
+          margin: var(--spacing-m);
           text-align: center;
         }
         #moreMessage {
@@ -124,11 +124,12 @@
                 link
                 title$="[[action.title]]"
                 has-tooltip="[[_computeHasTooltip(action.title)]]"
+                position-below="true"
                 data-action-key$="[[action.__key]]"
                 data-action-type$="[[action.__type]]"
                 data-label$="[[action.label]]"
                 disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
-                on-tap="_handleActionTap">
+                on-click="_handleActionTap">
                 <iron-icon class$="[[_computeHasIcon(action)]]" icon$="gr-icons:[[action.icon]]"></iron-icon>
               [[action.label]]
             </gr-button>
@@ -144,11 +145,12 @@
                 link
                 title$="[[action.title]]"
                 has-tooltip="[[_computeHasTooltip(action.title)]]"
+                position-below="true"
                 data-action-key$="[[action.__key]]"
                 data-action-type$="[[action.__type]]"
                 data-label$="[[action.label]]"
                 disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
-                on-tap="_handleActionTap">
+                on-click="_handleActionTap">
               <iron-icon class$="[[_computeHasIcon(action)]]" icon$="gr-icons:[[action.icon]]"></iron-icon>
               [[action.label]]
             </gr-button>
@@ -177,7 +179,7 @@
           on-cancel="_handleConfirmDialogCancel"
           branch="[[change.branch]]"
           has-parent="[[hasParent]]"
-          rebase-on-current="[[revisionActions.rebase.rebaseOnCurrent]]"
+          rebase-on-current="[[_computeRebaseOnCurrent(_revisionRebaseAction)]]"
           hidden></gr-confirm-rebase-dialog>
       <gr-confirm-cherrypick-dialog id="confirmCherrypick"
           class="confirmDialog"
@@ -217,7 +219,7 @@
           id="confirmSubmitDialog"
           class="confirmDialog"
           change="[[change]]"
-          action="[[revisionActions.submit]]"
+          action="[[_revisionSubmitAction]]"
           on-cancel="_handleConfirmDialogCancel"
           on-confirm="_handleSubmitConfirm" hidden></gr-confirm-submit-dialog>
       <gr-dialog id="createFollowUpDialog"
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 9182bbc..9c0c38f 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
@@ -50,7 +50,6 @@
     OPTIONAL: 'OPTIONAL',
   };
 
-  // TODO(davido): Add the rest of the change actions.
   const ChangeActions = {
     ABANDON: 'abandon',
     DELETE: '/',
@@ -72,7 +71,6 @@
     WIP: 'wip',
   };
 
-  // TODO(andybons): Add the rest of the revision actions.
   const RevisionActions = {
     CHERRYPICK: 'cherrypick',
     REBASE: 'rebase',
@@ -193,7 +191,6 @@
 
   Polymer({
     is: 'gr-change-actions',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the change should be reloaded.
@@ -269,8 +266,23 @@
       /** @type {?} */
       revisionActions: {
         type: Object,
+        notify: true,
         value() { return {}; },
       },
+      // If property binds directly to [[revisionActions.submit]] it is not
+      // updated when revisionActions doesn't contain submit action.
+      /** @type {?} */
+      _revisionSubmitAction: {
+        type: Object,
+        computed: '_getSubmitAction(revisionActions)',
+      },
+      // If property binds directly to [[revisionActions.rebase]] it is not
+      // updated when revisionActions doesn't contain rebase action.
+      /** @type {?} */
+      _revisionRebaseAction: {
+        type: Object,
+        computed: '_getRebaseAction(revisionActions)',
+      },
       privateByDefault: String,
 
       _loading: {
@@ -394,6 +406,7 @@
     RevisionActions,
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.RESTClientBehavior,
     ],
@@ -415,6 +428,26 @@
       this._handleLoadingComplete();
     },
 
+    _getSubmitAction(revisionActions) {
+      return this._getRevisionAction(revisionActions, 'submit', null);
+    },
+
+    _getRebaseAction(revisionActions) {
+      return this._getRevisionAction(revisionActions, 'rebase', null);
+    },
+
+    _getRevisionAction(revisionActions, actionName, emptyActionValue) {
+      if (!revisionActions) {
+        return undefined;
+      }
+      if (revisionActions[actionName] === undefined) {
+        // Return null to fire an event when reveisionActions was loaded
+        // but doesn't contain actionName. undefined doesn't fire an event
+        return emptyActionValue;
+      }
+      return revisionActions[actionName];
+    },
+
     reload() {
       if (!this.changeNum || !this.latestPatchNum) {
         return Promise.resolve();
@@ -425,6 +458,10 @@
         if (!revisionActions) { return; }
 
         this.revisionActions = revisionActions;
+        this._sendShowRevisionActions({
+          change: this.change,
+          revisionActions,
+        });
         this._handleLoadingComplete();
       }).catch(err => {
         this.fire('show-alert', {message: ERR_REVISION_ACTIONS});
@@ -437,6 +474,13 @@
       Gerrit.awaitPluginsLoaded().then(() => this._loading = false);
     },
 
+    _sendShowRevisionActions(detail) {
+      this.$.jsAPI.handleEvent(
+          this.$.jsAPI.EventType.SHOW_REVISION_ACTIONS,
+          detail
+      );
+    },
+
     _changeChanged() {
       this.reload();
     },
@@ -553,6 +597,15 @@
 
     _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord,
         additionalActionsChangeRecord) {
+      // Polymer 2: check for undefined
+      if ([
+        actionsChangeRecord,
+        revisionActionsChangeRecord,
+        additionalActionsChangeRecord,
+      ].some(arg => arg === undefined)) {
+        return;
+      }
+
       const additionalActions = (additionalActionsChangeRecord &&
           additionalActionsChangeRecord.base) || [];
       this.hidden = this._keyCount(actionsChangeRecord) === 0 &&
@@ -573,14 +626,25 @@
      * @param {string=} actionName
      */
     _deleteAndNotify(actionName) {
-      if (this.actions[actionName]) {
+      if (this.actions && this.actions[actionName]) {
         delete this.actions[actionName];
-        this.notifyPath('actions.' + actionName);
+        // We assign a fake value of 'false' to support Polymer 2
+        // see https://github.com/Polymer/polymer/issues/2631
+        this.notifyPath('actions.' + actionName, false);
       }
     },
 
     _editStatusChanged(editMode, editPatchsetLoaded,
         editBasedOnCurrentPatchSet, disableEdit) {
+      // Polymer 2: check for undefined
+      if ([
+        editMode,
+        editBasedOnCurrentPatchSet,
+        disableEdit,
+      ].some(arg => arg === undefined)) {
+        return;
+      }
+
       if (disableEdit) {
         this._deleteAndNotify('publishEdit');
         this._deleteAndNotify('rebaseEdit');
@@ -589,10 +653,10 @@
         this._deleteAndNotify('edit');
         return;
       }
-      if (editPatchsetLoaded) {
+      if (this.actions && editPatchsetLoaded) {
         // Only show actions that mutate an edit if an actual edit patch set
         // is loaded.
-        if (this.changeIsOpen(this.change.status)) {
+        if (this.changeIsOpen(this.change)) {
           if (editBasedOnCurrentPatchSet) {
             if (!this.actions.publishEdit) {
               this.set('actions.publishEdit', PUBLISH_EDIT);
@@ -614,7 +678,7 @@
         this._deleteAndNotify('deleteEdit');
       }
 
-      if (this.changeIsOpen(this.change.status)) {
+      if (this.actions && this.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) {
@@ -843,7 +907,7 @@
     _handleActionTap(e) {
       e.preventDefault();
       let el = Polymer.dom(e).localTarget;
-      while (el.is !== 'gr-button') {
+      while (el.tagName.toLowerCase() !== 'gr-button') {
         if (!el.parentElement) { return; }
         el = el.parentElement;
       }
@@ -967,8 +1031,9 @@
     },
 
     _calculateDisabled(action, hasKnownChainState) {
-      if (action.__key === 'rebase' && hasKnownChainState === false) {
-        return true;
+      if (action.__key === 'rebase') {
+        // Rebase button is only disabled when change has no parent(s).
+        return hasKnownChainState === false;
       }
       return !action.enabled;
     },
@@ -1003,7 +1068,6 @@
     _handleCherryPickRestApi(conflicts) {
       const el = this.$.confirmCherrypick;
       if (!el.branch) {
-        // TODO(davido): Fix error handling
         this.fire('show-alert', {message: ERR_BRANCH_EMPTY});
         return;
       }
@@ -1307,6 +1371,17 @@
      */
     _computeAllActions(changeActionsRecord, revisionActionsRecord,
         primariesRecord, additionalActionsRecord, change) {
+      // Polymer 2: check for undefined
+      if ([
+        changeActionsRecord,
+        revisionActionsRecord,
+        primariesRecord,
+        additionalActionsRecord,
+        change,
+      ].some(arg => arg === undefined)) {
+        return [];
+      }
+
       const revisionActionValues = this._getActionValues(revisionActionsRecord,
           primariesRecord, additionalActionsRecord, ActionType.REVISION);
       const changeActionValues = this._getActionValues(changeActionsRecord,
@@ -1395,6 +1470,13 @@
       });
     },
 
+    _computeRebaseOnCurrent(revisionRebaseAction) {
+      if (revisionRebaseAction) {
+        return !!revisionRebaseAction.enabled;
+      }
+      return null;
+    },
+
     /**
      * Occasionally, a change created by a change action is not yet knwon to the
      * API for a brief time. Wait for the given change number to be recognized.
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 9803fab..74d262a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-actions</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
@@ -112,6 +114,15 @@
       sandbox.restore();
     });
 
+    test('show-revision-actions event should fire', done => {
+      const spy = sinon.spy(element, '_sendShowRevisionActions');
+      element.reload();
+      flush(() => {
+        assert.isTrue(spy.called);
+        done();
+      });
+    });
+
     test('primary and secondary actions split properly', () => {
       // Submit should be the only primary action.
       assert.equal(element._topLevelPrimaryActions.length, 1);
@@ -352,7 +363,7 @@
 
       action.enabled = false;
       assert.equal(
-          element._calculateDisabled(action, hasKnownChainState), true);
+          element._calculateDisabled(action, hasKnownChainState), false);
     });
 
     test('rebase change', done => {
@@ -1511,4 +1522,69 @@
       assert.equal(reportStub.lastCall.args[0], 'type-key');
     });
   });
+
+  suite('getChangeRevisionActions returns only some actions', () => {
+    let element;
+    let sandbox;
+    let changeRevisionActions;
+
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        getChangeRevisionActions() {
+          return Promise.resolve(changeRevisionActions);
+        },
+        send(method, url, payload) {
+          return Promise.reject(new Error('error'));
+        },
+        getProjectConfig() { return Promise.resolve({}); },
+      });
+
+      sandbox = sinon.sandbox.create();
+      sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
+
+      element = fixture('basic');
+      // getChangeRevisionActions is not called without
+      // set the following properies
+      element.change = {};
+      element.changeNum = '42';
+      element.latestPatchNum = '2';
+
+
+      sandbox.stub(element.$.confirmCherrypick.$.restAPI,
+          'getRepoBranches').returns(Promise.resolve([]));
+      sandbox.stub(element.$.confirmMove.$.restAPI,
+          'getRepoBranches').returns(Promise.resolve([]));
+      return element.reload();
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('confirmSubmitDialog and confirmRebase properties are changed', () => {
+      changeRevisionActions = {};
+      element.reload();
+      assert.strictEqual(element.$.confirmSubmitDialog.action, null);
+      assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
+    });
+
+    test('_computeRebaseOnCurrent', () => {
+      const rebaseAction = {
+        enabled: true,
+        label: 'Rebase',
+        method: 'POST',
+        title: 'Rebase onto tip of branch or parent change',
+      };
+
+      // When rebase is enabled initially, rebaseOnCurrent should be set to
+      // true.
+      assert.isTrue(element._computeRebaseOnCurrent(rebaseAction));
+
+      delete rebaseAction.enabled;
+
+      // When rebase is not enabled initially, rebaseOnCurrent should be set to
+      // false.
+      assert.isFalse(element._computeRebaseOnCurrent(rebaseAction));
+    });
+  });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
index f1d3cd3..b8bea9ca 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-metadata</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html">
 <link rel="import" href="gr-change-metadata.html">
@@ -87,6 +89,7 @@
 
     teardown(() => {
       sandbox.restore();
+      Gerrit._testOnly_resetPlugins();
     });
 
     suite('by default', () => {
@@ -104,7 +107,7 @@
 
     suite('with plugin style', () => {
       setup(done => {
-        Gerrit._resetPlugins();
+        Gerrit._testOnly_resetPlugins();
         const pluginHost = fixture('plugin-host');
         pluginHost.config = {
           plugin: {
@@ -139,7 +142,7 @@
             new URL('test/plugin.html?' + Math.random(),
                 window.location.href).toString());
         sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
-        Gerrit._setPluginsPending([]);
+        Gerrit._loadPlugins([]);
         element = createElement();
       });
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index a09b730..6a92d96 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -15,9 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../../styles/gr-change-metadata-shared-styles.html">
+<link rel="import" href="../../../styles/gr-change-view-integration-shared-styles.html">
 <link rel="import" href="../../../styles/gr-voting-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
@@ -35,38 +37,17 @@
 <link rel="import" href="../gr-change-requirements/gr-change-requirements.html">
 <link rel="import" href="../gr-commit-info/gr-commit-info.html">
 <link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
+<link rel="import" href="../../shared/gr-account-list/gr-account-list.html">
+<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
 
 <dom-module id="gr-change-metadata">
   <template>
+    <style include="gr-change-metadata-shared-styles"></style>
     <style include="shared-styles">
       :host {
         display: table;
       }
-      section {
-        display: table-row;
-      }
-      section:not(:first-of-type) .title,
-      section:not(:first-of-type) .value {
-        padding-top: .5em;
-      }
-      section:not(:first-of-type) {
-        margin-top: 1em;
-      }
-      .title,
-      .value {
-        display: table-cell;
-      }
-      .title {
-        color: var(--deemphasized-text-color);
-        font-weight: var(--font-weight-bold);
-        max-width: 20em;
-        padding-left: var(--metadata-horizontal-padding);
-        padding-right: .5em;
-        word-break: break-word;
-      }
-      .value {
-        padding-right: var(--metadata-horizontal-padding);
-      }
       gr-change-requirements {
         --requirements-horizontal-padding: var(--metadata-horizontal-padding);
       }
@@ -74,7 +55,7 @@
         max-width: 20ch;
         overflow: hidden;
         text-overflow: ellipsis;
-        vertical-align: middle;
+        vertical-align: top;
         white-space: nowrap;
       }
       gr-editable-label {
@@ -99,14 +80,14 @@
         pointer-events: none;
       }
       .hashtagChip {
-        margin-bottom: .5em;
+        margin-bottom: var(--spacing-m);
       }
       #externalStyle {
         display: block;
       }
       .parentList.merge {
         list-style-type: decimal;
-        padding-left: 1em;
+        padding-left: var(--spacing-l);
       }
       .parentList gr-commit-info {
         display: inline-block;
@@ -116,7 +97,7 @@
         display: none;
       }
       .icon {
-        margin: -.25em 0;
+        margin: -3px 0;
       }
       .icon.help,
       .icon.notTrusted {
@@ -133,9 +114,12 @@
         display: inline-block;
       }
       .separatedSection {
-        border-top: 1px solid var(--border-color);
-        margin-top: .5em;
-        padding: .5em 0;
+        margin-top: var(--spacing-l);
+        padding: var(--spacing-m) 0;
+      }
+      .hashtag gr-linked-chip,
+      .topic gr-linked-chip {
+        --linked-chip-text-color: var(--link-color);
       }
     </style>
     <gr-external-style id="externalStyle" name="change-metadata">
@@ -195,9 +179,9 @@
               id="assigneeValue"
               placeholder="Set assignee..."
               accounts="{{_assignee}}"
-              change="[[change]]"
               readonly="[[_computeAssigneeReadOnly(_mutable, change)]]"
-              allow-any-user></gr-account-list>
+              suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
+          </gr-account-list>
         </span>
       </section>
       <section>
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 94b85fd..4fe5ddc 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
@@ -17,17 +17,6 @@
 (function() {
   'use strict';
 
-  const Defs = {};
-
-  /**
-   * @typedef {{
-   *    message: string,
-   *    icon: string,
-   *    class: string,
-   *  }}
-   */
-  Defs.PushCertificateValidation;
-
   const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
 
   const SubmitTypeLabel = {
@@ -61,7 +50,6 @@
 
   Polymer({
     is: 'gr-change-metadata',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the change topic is changed.
@@ -101,7 +89,7 @@
         computed: '_computeHashtagReadOnly(_mutable, change)',
       },
       /**
-       * @type {Defs.PushCertificateValidation}
+       * @type {Gerrit.PushCertificateValidation}
        */
       _pushCertificateValidation: {
         type: Object,
@@ -177,7 +165,7 @@
     },
 
     _computeHideStrategy(change) {
-      return !this.changeIsOpen(change.status);
+      return !this.changeIsOpen(change);
     },
 
     /**
@@ -215,19 +203,21 @@
             this._settingTopic = false;
             this.set(['change', 'topic'], newTopic);
             if (newTopic !== lastTopic) {
-              this.dispatchEvent(
-                  new CustomEvent('topic-changed', {bubbles: true}));
+              this.dispatchEvent(new CustomEvent(
+                  'topic-changed', {bubbles: true, composed: true}));
             }
           });
     },
 
     _showAddTopic(changeRecord, settingTopic) {
-      const hasTopic = !!changeRecord && !!changeRecord.base.topic;
+      const hasTopic = !!changeRecord &&
+          !!changeRecord.base && !!changeRecord.base.topic;
       return !hasTopic && !settingTopic;
     },
 
     _showTopicChip(changeRecord, settingTopic) {
-      const hasTopic = !!changeRecord && !!changeRecord.base.topic;
+      const hasTopic = !!changeRecord &&
+          !!changeRecord.base && !!changeRecord.base.topic;
       return hasTopic && !settingTopic;
     },
 
@@ -241,13 +231,15 @@
         this.set(['change', 'hashtags'], newHashtag);
         if (newHashtag !== lastHashtag) {
           this.dispatchEvent(
-              new CustomEvent('hashtag-changed', {bubbles: true}));
+              new CustomEvent('hashtag-changed', {
+                bubbles: true, composed: true}));
         }
       });
     },
 
     _computeTopicReadOnly(mutable, change) {
       return !mutable ||
+          !change ||
           !change.actions ||
           !change.actions.topic ||
           !change.actions.topic.enabled;
@@ -255,6 +247,7 @@
 
     _computeHashtagReadOnly(mutable, change) {
       return !mutable ||
+          !change ||
           !change.actions ||
           !change.actions.hashtags ||
           !change.actions.hashtags.enabled;
@@ -262,6 +255,7 @@
 
     _computeAssigneeReadOnly(mutable, change) {
       return !mutable ||
+          !change ||
           !change.actions ||
           !change.actions.assignee ||
           !change.actions.assignee.enabled;
@@ -291,11 +285,11 @@
     },
 
     /**
-     * @return {?Defs.PushCertificateValidation} object representing data for
+     * @return {?Gerrit.PushCertificateValidation} object representing data for
      *     the push validation.
      */
     _computePushCertificateValidation(serverConfig, change) {
-      if (!serverConfig || !serverConfig.receive ||
+      if (!change || !serverConfig || !serverConfig.receive ||
           !serverConfig.receive.enable_signed_push) {
         return null;
       }
@@ -348,6 +342,7 @@
     },
 
     _computeBranchURL(project, branch) {
+      if (!this.change || !this.change.status) return '';
       return Gerrit.Nav.getUrlForBranch(branch, project,
           this.change.status == this.ChangeStatus.NEW ? 'open' :
             this.change.status.toLowerCase());
@@ -368,7 +363,7 @@
         target.disabled = false;
         this.set(['change', 'topic'], '');
         this.dispatchEvent(
-            new CustomEvent('topic-changed', {bubbles: true}));
+            new CustomEvent('topic-changed', {bubbles: true, composed: true}));
       }).catch(err => {
         target.disabled = false;
         return;
@@ -407,7 +402,7 @@
      * @return {Object|null} either an accound or null.
      */
     _getNonOwnerRole(change, role) {
-      if (!change.current_revision ||
+      if (!change || !change.current_revision ||
           !change.revisions[change.current_revision]) {
         return null;
       }
@@ -446,13 +441,18 @@
     },
 
     _computeParentsLabel(parents) {
-      return parents.length > 1 ? 'Parents' : 'Parent';
+      return parents && parents.length > 1 ? 'Parents' : 'Parent';
     },
 
     _computeParentListClass(parents, parentIsCurrent) {
+      // Undefined check for polymer 2
+      if (parents === undefined || parentIsCurrent === undefined) {
+        return '';
+      }
+
       return [
         'parentList',
-        parents.length > 1 ? 'merge' : 'nonMerge',
+        parents && parents.length > 1 ? 'merge' : 'nonMerge',
         parentIsCurrent ? 'current' : 'notCurrent',
       ].join(' ');
     },
@@ -467,5 +467,12 @@
       // dom-if.
       this.$$('.topicEditableLabel').open();
     },
+
+    _getReviewerSuggestionsProvider(change) {
+      const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
+          change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
+      provider.init();
+      return provider;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index ce69d45..d60847b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-metadata</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../core/gr-router/gr-router.html">
 <link rel="import" href="gr-change-metadata.html">
@@ -747,7 +749,7 @@
             },
             '0.1',
             'http://some/plugins/url.html');
-        Gerrit._setPluginsCount(0);
+        Gerrit._loadPlugins([]);
         flush(() => {
           assert.strictEqual(hookEl.plugin, plugin);
           assert.strictEqual(hookEl.change, element.change);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/test/plugin.html b/polygerrit-ui/app/elements/change/gr-change-metadata/test/plugin.html
index d0ed4a1..b3aa98f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/test/plugin.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/test/plugin.html
@@ -7,20 +7,22 @@
 </dom-module>
 
 <dom-module id="my-plugin-style">
-  <style>
-    html {
-      --change-metadata-assignee: {
-        display: none;
+  <template>
+    <style>
+      html {
+        --change-metadata-assignee: {
+          display: none;
+        }
+        --change-metadata-label-status: {
+          display: none;
+        }
+        --change-metadata-strategy: {
+          display: none;
+        }
+        --change-metadata-topic: {
+          display: none;
+        }
       }
-      --change-metadata-label-status: {
-        display: none;
-      }
-      --change-metadata-strategy: {
-        display: none;
-      }
-      --change-metadata-topic: {
-        display: none;
-      }
-    }
-  </style>
+    </style>
+  </template>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
index e79bce1..47ff7f7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -34,8 +34,10 @@
       .status {
         color: #FFA62F;
         display: inline-block;
-        font-family: var(--monospace-font-family);
         text-align: center;
+        font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
       }
       .approved.status {
         color: var(--vote-text-color-recommended);
@@ -46,8 +48,8 @@
       iron-icon {
         color: inherit;
       }
-      .name {
-        font-weight: var(--font-weight-bold);
+      .status iron-icon {
+        vertical-align: top;
       }
       section {
         display: table-row;
@@ -57,16 +59,15 @@
       }
       .title {
         min-width: 10em;
-        padding: .75em .5em 0 var(--requirements-horizontal-padding);
-        vertical-align: top;
+        padding: var(--spacing-s) var(--spacing-m) 0 var(--requirements-horizontal-padding);
       }
       .value {
-        padding: .6em .5em 0 0;
-        vertical-align: middle;
+        padding: var(--spacing-s) 0 0 0;
       }
       .title,
       .value {
         display: table-cell;
+        vertical-align: top;
       }
       .hidden {
         display: none;
@@ -75,12 +76,10 @@
         cursor: pointer;
       }
       .showHide .title {
-        border-top: 1px solid var(--border-color);
-        padding-bottom: .5em;
-        padding-top: .5em;
+        padding-bottom: var(--spacing-m);
+        padding-top: var(--spacing-l);
       }
       .showHide .value {
-        border-top: 1px solid var(--border-color);
         padding-top: 0;
         vertical-align: middle;
       }
@@ -89,7 +88,7 @@
         float: right;
       }
       .spacer {
-        height: .5em;
+        height: var(--spacing-m);
       }
     </style>
     <template
@@ -128,7 +127,7 @@
     <section class$="spacer [[_computeShowOptional(_optionalLabels.*)]]"></section>
     <section
         show-bottom-border$="[[_showOptionalLabels]]"
-        on-tap="_handleShowHide"
+        on-click="_handleShowHide"
         class$="showHide [[_computeShowOptional(_optionalLabels.*)]]">
       <div class="title">Other labels</div>
       <div class="value">
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 4717ff9..dfdcd59 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-change-requirements',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
index 3f35158..2ceac39 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-requirements</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-requirements.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 8579762..29f2d83 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -15,11 +15,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/paper-tabs/paper-tabs.html">
+<link rel="import" href="/bower_components/paper-tabs/paper-tabs.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
@@ -42,6 +43,7 @@
 <link rel="import" href="../../shared/revision-info/revision-info.html">
 <link rel="import" href="../gr-change-actions/gr-change-actions.html">
 <link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
+<link rel="import" href="../../shared/gr-icons/gr-icons.html">
 <link rel="import" href="../gr-commit-info/gr-commit-info.html">
 <link rel="import" href="../gr-download-dialog/gr-download-dialog.html">
 <link rel="import" href="../gr-file-list-header/gr-file-list-header.html">
@@ -61,25 +63,25 @@
       }
       .container.loading {
         color: var(--deemphasized-text-color);
-        padding: 1em var(--default-horizontal-margin);
+        padding: var(--spacing-l);
       }
       .header {
         align-items: center;
         background-color: var(--table-header-background-color);
         border-bottom: 1px solid var(--border-color);
         display: flex;
-        padding: .55em var(--default-horizontal-margin);
+        padding: var(--spacing-s) var(--spacing-l);
         z-index: 99;  /* Less than gr-overlay's backdrop */
       }
       .header.editMode {
         background-color: var(--edit-mode-background-color);
       }
       .header .download {
-        margin-right: 1em;
+        margin-right: var(--spacing-l);
       }
       gr-change-status {
         display: initial;
-        margin: .1em .1em .1em .4em;
+        margin: var(--spacing-xxs) var(--spacing-xxs) var(--spacing-xxs) var(--spacing-s);
       }
       gr-change-status:first-child {
         margin-left: 0;
@@ -88,17 +90,16 @@
         align-items: center;
         display: flex;
         flex: 1;
-        font-size: 1.2rem;
+        font-size: var(--font-size-h3);
       }
       .headerTitle .headerSubject {
         font-weight: var(--font-weight-bold);
       }
       #replyBtn {
-        margin-bottom: 1em;
+        margin-bottom: var(--spacing-l);
       }
       gr-change-star {
-        font-size: var(--font-size-normal);
-        margin-right: .25em;
+        margin-right: var(--spacing-xs);
       }
       gr-reply-dialog {
         width: 60em;
@@ -106,6 +107,9 @@
       .changeStatus {
         text-transform: capitalize;
       }
+      .changeInfo {
+        background-color: var(--table-header-background-color);
+      }
       /* Strong specificity here is needed due to
          https://github.com/Polymer/polymer/issues/2531 */
       .container section.changeInfo {
@@ -114,32 +118,33 @@
       .changeId {
         color: var(--deemphasized-text-color);
         font-family: var(--font-family);
-        margin-top: 1em;
+        margin-top: var(--spacing-l);
       }
       .changeMetadata {
-        border-right: 1px solid var(--border-color);
-      }
-      /* Prevent plugin text from overflowing. */
-      #change_plugins {
-        word-break: break-word;
+        /* Limit meta section to half of the screen at max */
+        max-width: 50%;
       }
       .commitMessage {
         font-family: var(--monospace-font-family);
-        margin-right: 1em;
-        margin-bottom: 1em;
-        max-width: var(--commit-message-max-width, 72ch);;
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
+        margin-right: var(--spacing-l);
+        margin-bottom: var(--spacing-l);
+        /* Account for border and padding and rounding errors. */
+        max-width: calc(72ch + 2px + 2*var(--spacing-m) + 0.4px);
       }
       .commitMessage gr-linked-text {
         word-break: break-word;
       }
       #commitMessageEditor {
-        min-width: 72ch;
+        /* Account for border and padding and rounding errors. */
+        min-width: calc(72ch + 2px + 2*var(--spacing-m) + 0.4px);
       }
       .editCommitMessage {
-        margin-top: 1em;
+        margin-top: var(--spacing-l);
+
         --gr-button: {
-          padding-left: 0;
-          padding-right: 0;
+          padding: 5px 0px;
         }
       }
       .changeStatuses,
@@ -166,7 +171,7 @@
       .relatedChanges {
         flex: 1 1 auto;
         overflow: hidden;
-        padding: 1em 0;
+        padding: var(--spacing-l) 0;
       }
       .mobile {
         display: none;
@@ -178,7 +183,7 @@
         border: 0;
         border-top: 1px solid var(--border-color);
         height: 0;
-        margin-bottom: 1em;
+        margin-bottom: var(--spacing-l);
       }
       #commitMessage.collapsed {
         max-height: 36em;
@@ -187,7 +192,7 @@
       #relatedChanges {
       }
       #relatedChanges.collapsed {
-        margin-bottom: 1.1em;
+        margin-bottom: var(--spacing-l);
         max-height: var(--relation-chain-max-height, 2em);
         overflow: hidden;
       }
@@ -195,8 +200,8 @@
         display: flex;
         flex-direction: column;
         flex-shrink: 0;
-        margin: 1em 0;
-        padding: 0 1em;
+        margin: var(--spacing-l) 0;
+        padding: 0 var(--spacing-l);
       }
       .collapseToggleContainer {
         display: flex;
@@ -212,7 +217,7 @@
         display: block;
       }
       #relatedChangesToggle {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
         padding-top: var(--related-change-btn-top-padding, 0);
       }
       .showOnEdit {
@@ -231,12 +236,12 @@
       paper-tabs {
         background-color: var(--table-header-background-color);
         border-top: 1px solid var(--border-color);
-        height: 3rem;
+        height: calc(var(--line-height-normal) + 2*var(--spacing-m));
         --paper-tabs-selection-bar-color: var(--link-color);
       }
       paper-tab {
         box-sizing: border-box;
-        max-width: 15rem;
+        max-width: 12em;
         --paper-tab-ink: var(--link-color);
       }
       gr-thread-list,
@@ -249,14 +254,9 @@
       #uploadHelpOverlay {
         width: 50em;
       }
-      @media screen and (min-width: 80em) {
-        .commitMessage {
-          max-width: var(--commit-message-max-width, 100ch);
-        }
-      }
       #metadata {
-        --metadata-horizontal-padding: 1em;
-        padding-top: 1em;
+        --metadata-horizontal-padding: var(--spacing-l);
+        padding-top: var(--spacing-l);
         width: 100%;
       }
       /* NOTE: If you update this breakpoint, also update the
@@ -266,8 +266,7 @@
           padding: 0;
         }
         #relatedChanges {
-          border-top: 1px solid var(--border-color);
-          padding-top: 1em;
+          padding-top: var(--spacing-l);
         }
         #commitAndRelated {
           flex-direction: column;
@@ -293,14 +292,14 @@
           align-items: flex-start;
           flex-direction: column;
           flex: 1;
-          padding: .5em var(--default-horizontal-margin);
+          padding: var(--spacing-s) var(--spacing-l);
         }
         gr-change-star {
           vertical-align: middle;
         }
         .headerTitle {
           flex-wrap: wrap;
-          font-size: 1.1rem;
+          font-size: var(--font-size-h3);
         }
         .desktop {
           display: none;
@@ -322,16 +321,10 @@
         }
         .commitContainer {
           margin: 0;
-          padding: 1em;
-        }
-        .relatedChanges,
-        .changeMetadata {
-          font-size: var(--font-size-normal);
+          padding: var(--spacing-l);
         }
         .changeMetadata {
-          border-bottom: 1px solid var(--border-color);
-          border-right: none;
-          margin-top: .25em;
+          margin-top: var(--spacing-xs);
           max-width: none;
         }
         #metadata,
@@ -340,7 +333,7 @@
         }
         .commitActions {
           display: block;
-          margin-top: 1em;
+          margin-top: var(--spacing-l);
           width: 100%;
         }
         .commitMessage {
@@ -366,6 +359,7 @@
     <div
         id="mainContent"
         class="container"
+        on-show-checks-table="_handleShowTab"
         hidden$="{{_loading}}">
       <div class$="[[_computeHeaderClass(_editMode)]]">
         <div class="headerTitle">
@@ -405,7 +399,7 @@
               disable-edit="[[disableEdit]]"
               has-parent="[[hasParent]]"
               actions="[[_change.actions]]"
-              revision-actions="[[_currentRevisionActions]]"
+              revision-actions="{{_currentRevisionActions}}"
               change-num="[[_changeNum]]"
               change-status="[[_change.status]]"
               commit-num="[[_commitInfo.commit]]"
@@ -435,10 +429,6 @@
               parent-is-current="[[_parentIsCurrent]]"
               on-show-reply-dialog="_handleShowReplyDialog">
           </gr-change-metadata>
-          <!-- Plugins insert content into following container.
-               Stop-gap until PolyGerrit plugins interface is ready.
-               This will not work with Shadow DOM. -->
-          <div id="change_plugins"></div>
         </div>
         <div id="mainChangeInfo" class="changeInfo-column mainChangeInfo">
           <div id="commitAndRelated" class="hideOnMobileOverlay">
@@ -450,7 +440,7 @@
                     hidden$="[[!_loggedIn]]"
                     primary
                     disabled="[[_replyDisabled]]"
-                    on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
+                    on-click="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
               </div>
               <div
                   id="commitMessage"
@@ -467,7 +457,7 @@
                 </gr-editable-content>
                 <gr-button link
                     class="editCommitMessage"
-                    on-tap="_handleEditCommitMessage"
+                    on-click="_handleEditCommitMessage"
                     hidden$="[[_hideEditCommitMessage]]">Edit</gr-button>
                 <div class="changeId" hidden$="[[!_changeIdCommitMessageError]]">
                   <hr>
@@ -487,7 +477,7 @@
                     link
                     id="commitCollapseToggleButton"
                     class="collapseToggleButton"
-                    on-tap="_toggleCommitCollapsed">
+                    on-click="_toggleCommitCollapsed">
                   [[_computeCollapseText(_commitCollapsed)]]
                 </gr-button>
               </div>
@@ -515,7 +505,7 @@
                     link
                     id="relatedChangesToggleButton"
                     class="collapseToggleButton"
-                    on-tap="_toggleRelatedChangesCollapsed">
+                    on-click="_toggleRelatedChangesCollapsed">
                   [[_computeCollapseText(_relatedChangesCollapsed)]]
                 </gr-button>
               </div>
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 4788999..02cf861 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
@@ -60,11 +60,11 @@
   };
 
   const CHANGE_DATA_TIMING_LABEL = 'ChangeDataLoaded';
+  const CHANGE_RELOAD_TIMING_LABEL = 'ChangeReloaded';
   const SEND_REPLY_TIMING_LABEL = 'SendReply';
 
   Polymer({
     is: 'gr-change-view',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -203,7 +203,6 @@
       _loading: Boolean,
       /** @type {?} */
       _projectConfig: Object,
-      _rebaseOnCurrent: Boolean,
       _replyButtonLabel: {
         type: String,
         value: 'Reply',
@@ -226,7 +225,8 @@
       },
       _changeStatuses: {
         type: String,
-        computed: '_computeChangeStatusChips(_change, _mergeable)',
+        computed:
+          '_computeChangeStatusChips(_change, _mergeable, _submitEnabled)',
       },
       _commitCollapsed: {
         type: Boolean,
@@ -247,8 +247,14 @@
         value: false,
         observer: '_updateToggleContainerClass',
       },
-      _parentIsCurrent: Boolean,
-      _submitEnabled: Boolean,
+      _parentIsCurrent: {
+        type: Boolean,
+        computed: '_isParentCurrent(_currentRevisionActions)',
+      },
+      _submitEnabled: {
+        type: Boolean,
+        computed: '_isSubmitEnabled(_currentRevisionActions)',
+      },
 
       /** @type {?} */
       _mergeable: {
@@ -281,6 +287,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.RESTClientBehavior,
@@ -307,6 +314,7 @@
     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]:
@@ -346,7 +354,7 @@
             !== this._dynamicTabHeaderEndpoints.length) {
           console.warn('Different number of tab headers and tab content.');
         }
-      });
+      }).then(() => this._setPrimaryTab());
 
       this.addEventListener('comment-save', this._handleCommentSave.bind(this));
       this.addEventListener('comment-refresh', this._reloadDrafts.bind(this));
@@ -410,12 +418,31 @@
       this._showMessagesView = this.$.commentTabs.selected === 0;
     },
 
-    _handleFileTabChange() {
+    _handleFileTabChange(e) {
       const selectedIndex = this.$$('#primaryTabs').selected;
       this._showFileTabContent = selectedIndex === 0;
       // Initial tab is the static files list.
-      this._selectedFilesTabPluginEndpoint =
+      const newSelectedTab =
           this._dynamicTabContentEndpoints[selectedIndex - 1];
+      if (newSelectedTab !== this._selectedFilesTabPluginEndpoint) {
+        this._selectedFilesTabPluginEndpoint = newSelectedTab;
+
+        const tabName = this._selectedFilesTabPluginEndpoint || 'files';
+        const source = e && e.type ? e.type : '';
+        this.$.reporting.reportInteraction('tab-changed',
+            `tabname: ${tabName}, source: ${source}`);
+      }
+    },
+
+    _handleShowTab(e) {
+      const idx = this._dynamicTabContentEndpoints.indexOf(e.detail.tab);
+      if (idx === -1) {
+        console.warn(e.detail.tab + ' tab not found');
+        return;
+      }
+      this.$$('#primaryTabs').selected = idx + 1;
+      this.$$('#primaryTabs').scrollIntoView();
+      this.$.reporting.reportInteraction('show-tab', e.detail.tab);
     },
 
     _handleEditCommitMessage(e) {
@@ -452,20 +479,33 @@
       this._editingCommitMessage = false;
     },
 
-    _computeChangeStatusChips(change, mergeable) {
+    _computeChangeStatusChips(change, mergeable, submitEnabled) {
+      // Polymer 2: check for undefined
+      if ([
+        change,
+        mergeable,
+      ].some(arg => arg === undefined)) {
+        // To keep consistent with Polymer 1, we are returning undefined
+        // if not all dependencies are defined
+        return undefined;
+      }
+
       // Show no chips until mergeability is loaded.
-      if (mergeable === null || mergeable === undefined) { return []; }
+      if (mergeable === null) {
+        return [];
+      }
 
       const options = {
         includeDerived: true,
         mergeable: !!mergeable,
-        submitEnabled: this._submitEnabled,
+        submitEnabled: !!submitEnabled,
       };
       return this.changeStatuses(change, options);
     },
 
     _computeHideEditCommitMessage(loggedIn, editing, change, editMode) {
-      if (!loggedIn || editing || change.status === this.ChangeStatus.MERGED ||
+      if (!loggedIn || editing ||
+          (change && change.status === this.ChangeStatus.MERGED) ||
           editMode) {
         return true;
       }
@@ -495,6 +535,7 @@
     },
 
     _computeTotalCommentCounts(unresolvedCount, changeComments) {
+      if (!changeComments) return undefined;
       const draftCount = changeComments.computeDraftCount();
       const unresolvedString = GrCountStringFormatter.computeString(
           unresolvedCount, 'unresolved');
@@ -736,17 +777,21 @@
       });
     },
 
+    _setPrimaryTab() {
+      // Selected has to be set after the paper-tabs are visible because
+      // the selected underline depends on calculations made by the browser.
+      this.$.commentTabs.selected = 0;
+      const primaryTabs = this.$$('#primaryTabs');
+      if (primaryTabs) primaryTabs.selected = 0;
+    },
+
     _performPostLoadTasks() {
       this._maybeShowReplyDialog();
       this._maybeShowRevertDialog();
 
       this._sendShowChangeEvent();
 
-      // Selected has to be set after the paper-tabs are visible because
-      // the selected underline depends on calculations made by the browser.
-      this.$.commentTabs.selected = 0;
-      const primaryTabs = this.$$('#primaryTabs');
-      if (primaryTabs) primaryTabs.selected = 0;
+      this._setPrimaryTab();
 
       this.async(() => {
         if (this.viewState.scrollTop) {
@@ -759,7 +804,12 @@
       });
     },
 
-    _paramsAndChangeChanged(value) {
+    _paramsAndChangeChanged(value, change) {
+      // Polymer 2: check for undefined
+      if ([value, change].some(arg => arg === undefined)) {
+        return;
+      }
+
       // If the change number or patch range is different, then reset the
       // selected file index.
       const patchRangeState = this.viewState.patchRange;
@@ -814,7 +864,8 @@
       Gerrit.awaitPluginsLoaded()
           .then(this._getLoggedIn.bind(this))
           .then(loggedIn => {
-            if (!loggedIn || this._change.status !== this.ChangeStatus.MERGED) {
+            if (!loggedIn || !this._change ||
+                this._change.status !== this.ChangeStatus.MERGED) {
             // Do not display dialog if not logged-in or the change is not
             // merged.
               return;
@@ -856,19 +907,22 @@
     _changeChanged(change) {
       if (!change || !this._patchRange || !this._allPatchSets) { return; }
 
+      // We get the parent first so we keep the original value for basePatchNum
+      // and not the updated value.
       const parent = this._getBasePatchNum(change, this._patchRange);
 
-      this.set('_patchRange.basePatchNum', parent);
       this.set('_patchRange.patchNum', this._patchRange.patchNum ||
               this.computeLatestPatchNum(this._allPatchSets));
 
+      this.set('_patchRange.basePatchNum', parent);
+
       const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
       this.fire('title-change', {title});
     },
 
     /**
      * Gets base patch number, if it is a parent try and decide from
-     * preference weather to default to `auto merge`, `Parent 1` or `PARENT`.
+     * preference whether to default to `auto merge`, `Parent 1` or `PARENT`.
      *
      * @param {Object} change
      * @param {Object} patchRange
@@ -899,8 +953,8 @@
       return 'PARENT';
     },
 
-    _computeShowPrimaryTabs(dynamicTabContentEndpoints) {
-      return dynamicTabContentEndpoints.length > 0;
+    _computeShowPrimaryTabs(dynamicTabHeaderEndpoints) {
+      return dynamicTabHeaderEndpoints && dynamicTabHeaderEndpoints.length > 0;
     },
 
     _computeChangeUrl(change) {
@@ -933,6 +987,11 @@
     },
 
     _computeChangeIdCommitMessageError(commitMessage, change) {
+      // Polymer 2: check for undefined
+      if ([commitMessage, change].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       if (!commitMessage) { return CHANGE_ID_ERROR.MISSING; }
 
       // Find the last match in the commit message:
@@ -987,6 +1046,11 @@
     },
 
     _computeReplyButtonLabel(changeRecord, canStartReview) {
+      // Polymer 2: check for undefined
+      if ([changeRecord, canStartReview].some(arg => arg === undefined)) {
+        return 'Reply';
+      }
+
       if (canStartReview) {
         return 'Start review';
       }
@@ -1152,6 +1216,7 @@
     },
 
     _getProjectConfig() {
+      if (!this._change) return;
       return this.$.restAPI.getProjectConfig(this._change.project).then(
           config => {
             this._projectConfig = config;
@@ -1162,18 +1227,6 @@
       return this.$.restAPI.getPreferences();
     },
 
-    _updateRebaseAction(revisionActions) {
-      if (revisionActions && revisionActions.rebase) {
-        revisionActions.rebase.rebaseOnCurrent =
-            !!revisionActions.rebase.enabled;
-        this._parentIsCurrent = !revisionActions.rebase.enabled;
-        revisionActions.rebase.enabled = true;
-      } else {
-        this._parentIsCurrent = true;
-      }
-      return revisionActions;
-    },
-
     _prepareCommitMsgForLinkify(msg) {
       // TODO(wyatta) switch linkify sequence, see issue 5526.
       // This is a zero-with space. It is added to prevent the linkify library
@@ -1201,7 +1254,7 @@
       if (!this._patchRange.patchNum &&
           change.current_revision === edit.base_revision) {
         change.current_revision = edit.commit.commit;
-        this._patchRange.patchNum = this.EDIT_NAME;
+        this.set('_patchRange.patchNum', this.EDIT_NAME);
         // 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.
@@ -1239,8 +1292,6 @@
               this._latestCommitMessage = null;
             }
 
-            // Update the submit enabled based on current revision.
-            this._submitEnabled = this._isSubmitEnabled(currentRevision);
 
             const lineHeight = getComputedStyle(this).lineHeight;
 
@@ -1257,8 +1308,6 @@
                 currentRevision.commit.commit = latestRevisionSha;
               }
               this._commitInfo = currentRevision.commit;
-              this._currentRevisionActions =
-                      this._updateRebaseAction(currentRevision.actions);
               this._selectedRevision = currentRevision;
               // TODO: Fetch and process files.
             } else {
@@ -1270,9 +1319,17 @@
           });
     },
 
-    _isSubmitEnabled(currentRevision) {
-      return !!(currentRevision.actions && currentRevision.actions.submit &&
-          currentRevision.actions.submit.enabled);
+    _isSubmitEnabled(revisionActions) {
+      return !!(revisionActions && revisionActions.submit &&
+        revisionActions.submit.enabled);
+    },
+
+    _isParentCurrent(revisionActions) {
+      if (revisionActions && revisionActions.rebase) {
+        return !revisionActions.rebase.enabled;
+      } else {
+        return true;
+      }
     },
 
     _getEdit() {
@@ -1282,6 +1339,7 @@
     _getLatestCommitMessage() {
       return this.$.restAPI.getChangeCommitInfo(this._changeNum,
           this.computeLatestPatchNum(this._allPatchSets)).then(commitInfo => {
+        if (!commitInfo) return Promise.resolve();
         this._latestCommitMessage =
                     this._prepareCommitMsgForLinkify(commitInfo.message);
       });
@@ -1349,15 +1407,17 @@
     /**
      * Reload the change.
      *
-     * @param {boolean=} opt_reloadRelatedChanges Reloads the related chanegs
-     *     when true.
+     * @param {boolean=} opt_isLocationChange Reloads the related changes
+     *     when true and ends reporting events that started on location change.
      * @return {Promise} A promise that resolves when the core data has loaded.
      *     Some non-core data loading may still be in-flight when the core data
      *     promise resolves.
      */
-    _reload(opt_reloadRelatedChanges) {
+    _reload(opt_isLocationChange) {
       this._loading = true;
       this._relatedChangesCollapsed = true;
+      this.$.reporting.time(CHANGE_RELOAD_TIMING_LABEL);
+      this.$.reporting.time(CHANGE_DATA_TIMING_LABEL);
 
       // Array to house all promises related to data requests.
       const allDataPromises = [];
@@ -1370,7 +1430,13 @@
       // Resolves when the loading flag is set to false, meaning that some
       // change content may start appearing.
       const loadingFlagSet = detailCompletes
-          .then(() => { this._loading = false; });
+          .then(() => { this._loading = false; })
+          .then(() => {
+            this.$.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL);
+            if (opt_isLocationChange) {
+              this.$.reporting.changeDisplayed();
+            }
+          });
 
       // Resolves when the project config has loaded.
       const projectConfigLoaded = detailCompletes
@@ -1430,20 +1496,20 @@
         coreDataPromise = mergeabilityLoaded;
       }
 
-      if (opt_reloadRelatedChanges) {
+      if (opt_isLocationChange) {
         const relatedChangesLoaded = coreDataPromise
             .then(() => this.$.relatedChanges.reload());
         allDataPromises.push(relatedChangesLoaded);
       }
 
-      this.$.reporting.time(CHANGE_DATA_TIMING_LABEL);
       Promise.all(allDataPromises).then(() => {
         this.$.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL);
-        this.$.reporting.changeFullyLoaded();
+        if (opt_isLocationChange) {
+          this.$.reporting.changeFullyLoaded();
+        }
       });
 
-      return coreDataPromise
-          .then(() => { this.$.reporting.changeDisplayed(); });
+      return coreDataPromise;
     },
 
     /**
@@ -1458,6 +1524,10 @@
     },
 
     _getMergeability() {
+      if (!this._change) {
+        this._mergeable = null;
+        return Promise.resolve();
+      }
       // If the change is closed, it is not mergeable. Note: already merged
       // changes are obviously not mergeable, but the mergeability API will not
       // answer for abandoned changes.
@@ -1595,8 +1665,7 @@
     _computeShowRelatedToggle() {
       // Make sure the max height has been applied, since there is now content
       // to populate.
-      // TODO update to polymer 2.x syntax
-      if (!this.getComputedStyleValue('--relation-chain-max-height')) {
+      if (!util.getComputedStyleValue('--relation-chain-max-height', this)) {
         this._updateRelatedChangeMaxHeight();
       }
       // Prevents showMore from showing when click on related change, since the
@@ -1690,6 +1759,10 @@
     },
 
     _computeEditMode(patchRangeRecord, paramsRecord) {
+      if ([patchRangeRecord, paramsRecord].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       if (paramsRecord.base && paramsRecord.base.edit) { return true; }
 
       const patchRange = patchRangeRecord.base || {};
@@ -1775,7 +1848,7 @@
     },
 
     _computeCurrentRevision(currentRevision, revisions) {
-      return revisions && revisions[currentRevision];
+      return currentRevision && revisions && revisions[currentRevision];
     },
 
     _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index f74160b..e00bab5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="../../edit/gr-edit-constants.html">
 <link rel="import" href="gr-change-view.html">
@@ -79,7 +81,7 @@
       });
       element = fixture('basic');
       sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
-      Gerrit._setPluginsCount(0);
+      Gerrit._loadPlugins([]);
     });
 
     teardown(done => {
@@ -90,9 +92,7 @@
     });
 
     getCustomCssValue = cssParam => {
-      // TODO: Update to be compatible with 2.x when we upgrade from
-      // 1.x to 2.x.
-      return element.getComputedStyleValue(cssParam);
+      return util.getComputedStyleValue(cssParam, element);
     };
 
     test('_handleMessageAnchorTap', () => {
@@ -345,8 +345,10 @@
         ],
       };
       setup(() => {
+        // Fake computeDraftCount as its required for ChangeComments,
+        // see gr-comment-api#reloadDrafts.
         reloadStub = sandbox.stub(element.$.commentAPI, 'reloadDrafts')
-            .returns(Promise.resolve({drafts}));
+            .returns(Promise.resolve({drafts, computeDraftCount: () => 1}));
       });
 
       test('drafts are reloaded when reload-drafts fired', done => {
@@ -530,65 +532,11 @@
       assert.equal(result, 'CC=\u200Btest@google.com');
     }),
 
-    test('_updateRebaseAction', () => {
-      const currentRevisionActions = {
-        cherrypick: {
-          enabled: true,
-          label: 'Cherry Pick',
-          method: 'POST',
-          title: 'cherrypick',
-        },
-        rebase: {
-          enabled: true,
-          label: 'Rebase',
-          method: 'POST',
-          title: 'Rebase onto tip of branch or parent change',
-        },
-      };
-      element._parentIsCurrent = undefined;
-
-      // Rebase enabled should always end up true.
-      // When rebase is enabled initially, rebaseOnCurrent should be set to
-      // true.
-      assert.equal(element._updateRebaseAction(currentRevisionActions),
-          currentRevisionActions);
-
-      assert.isTrue(currentRevisionActions.rebase.enabled);
-      assert.isTrue(currentRevisionActions.rebase.rebaseOnCurrent);
-      assert.isFalse(element._parentIsCurrent);
-
-      delete currentRevisionActions.rebase.enabled;
-
-      // When rebase is not enabled initially, rebaseOnCurrent should be set to
-      // false.
-      assert.equal(element._updateRebaseAction(currentRevisionActions),
-          currentRevisionActions);
-
-      assert.isTrue(currentRevisionActions.rebase.enabled);
-      assert.isFalse(currentRevisionActions.rebase.rebaseOnCurrent);
-      assert.isTrue(element._parentIsCurrent);
-    });
-
     test('_isSubmitEnabled', () => {
       assert.isFalse(element._isSubmitEnabled({}));
-      assert.isFalse(element._isSubmitEnabled({actions: {}}));
-      assert.isFalse(element._isSubmitEnabled({actions: {submit: {}}}));
+      assert.isFalse(element._isSubmitEnabled({submit: {}}));
       assert.isTrue(element._isSubmitEnabled(
-          {actions: {submit: {enabled: true}}}));
-    });
-
-    test('_updateRebaseAction sets _parentIsCurrent on no rebase', () => {
-      const currentRevisionActions = {
-        cherrypick: {
-          enabled: true,
-          label: 'Cherry Pick',
-          method: 'POST',
-          title: 'cherrypick',
-        },
-      };
-      element._parentIsCurrent = undefined;
-      element._updateRebaseAction(currentRevisionActions);
-      assert.isTrue(element._parentIsCurrent);
+          {submit: {enabled: true}}));
     });
 
     test('_reload is called when an approved label is removed', () => {
@@ -1599,32 +1547,44 @@
       sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
 
       // Delete
-      fileList.dispatchEvent(new CustomEvent('file-action-tap',
-          {detail: {action: Actions.DELETE.id, path: 'foo'}, bubbles: true}));
+      fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+        detail: {action: Actions.DELETE.id, path: 'foo'},
+        bubbles: true,
+        composed: true,
+      }));
       flushAsynchronousOperations();
 
       assert.isTrue(controls.openDeleteDialog.called);
       assert.equal(controls.openDeleteDialog.lastCall.args[0], 'foo');
 
       // Restore
-      fileList.dispatchEvent(new CustomEvent('file-action-tap',
-          {detail: {action: Actions.RESTORE.id, path: 'foo'}, bubbles: true}));
+      fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+        detail: {action: Actions.RESTORE.id, path: 'foo'},
+        bubbles: true,
+        composed: true,
+      }));
       flushAsynchronousOperations();
 
       assert.isTrue(controls.openRestoreDialog.called);
       assert.equal(controls.openRestoreDialog.lastCall.args[0], 'foo');
 
       // Rename
-      fileList.dispatchEvent(new CustomEvent('file-action-tap',
-          {detail: {action: Actions.RENAME.id, path: 'foo'}, bubbles: true}));
+      fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+        detail: {action: Actions.RENAME.id, path: 'foo'},
+        bubbles: true,
+        composed: true,
+      }));
       flushAsynchronousOperations();
 
       assert.isTrue(controls.openRenameDialog.called);
       assert.equal(controls.openRenameDialog.lastCall.args[0], 'foo');
 
       // Open
-      fileList.dispatchEvent(new CustomEvent('file-action-tap',
-          {detail: {action: Actions.OPEN.id, path: 'foo'}, bubbles: true}));
+      fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+        detail: {action: Actions.OPEN.id, path: 'foo'},
+        bubbles: true,
+        composed: true,
+      }));
       flushAsynchronousOperations();
 
       assert.isTrue(Gerrit.Nav.getEditUrlForDiff.called);
@@ -1830,5 +1790,50 @@
       MockInteractions.tap(element.$.changeStar.$$('button'));
       assert.isTrue(stub.called);
     });
+
+    suite('gr-reporting tests', () => {
+      setup(() => {
+        element._patchRange = {
+          basePatchNum: 'PARENT',
+          patchNum: 1,
+        };
+        sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve());
+        sandbox.stub(element, '_getProjectConfig').returns(Promise.resolve());
+        sandbox.stub(element, '_reloadComments').returns(Promise.resolve());
+        sandbox.stub(element, '_getMergeability').returns(Promise.resolve());
+        sandbox.stub(element, '_getLatestCommitMessage')
+            .returns(Promise.resolve());
+      });
+
+      test('don\'t report changedDisplayed on reply', done => {
+        const changeDisplayStub =
+          sandbox.stub(element.$.reporting, 'changeDisplayed');
+        const changeFullyLoadedStub =
+          sandbox.stub(element.$.reporting, 'changeFullyLoaded');
+        element._handleReplySent();
+        flush(() => {
+          assert.isFalse(changeDisplayStub.called);
+          assert.isFalse(changeFullyLoadedStub.called);
+          done();
+        });
+      });
+
+      test('report changedDisplayed on _paramsChanged', done => {
+        const changeDisplayStub =
+          sandbox.stub(element.$.reporting, 'changeDisplayed');
+        const changeFullyLoadedStub =
+          sandbox.stub(element.$.reporting, 'changeFullyLoaded');
+        element._paramsChanged({
+          view: Gerrit.Nav.View.CHANGE,
+          changeNum: 101,
+          project: 'test-project',
+        });
+        flush(() => {
+          assert.isTrue(changeDisplayStub.called);
+          assert.isTrue(changeFullyLoadedStub.called);
+          done();
+        });
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index e4183df..a7e65a3 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -18,7 +18,7 @@
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -43,10 +43,10 @@
       }
       .container {
         display: flex;
-        margin: .5em 0;
+        margin: var(--spacing-m) 0;
       }
       .lineNum {
-        margin-right: .5em;
+        margin-right: var(--spacing-m);
         min-width: 10em;
         text-align: right;
       }
@@ -57,7 +57,7 @@
       @media screen and (max-width: 50em) {
         .container {
           flex-direction: column;
-          margin: 0 0 .5em .5em;
+          margin: 0 0 var(--spacing-m) var(--spacing-m);
         }
         .lineNum {
           min-width: initial;
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index b19ca62..42cb976 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -18,7 +18,6 @@
   'use strict';
   Polymer({
     is: 'gr-comment-list',
-    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index 9996abc..c18ae8d 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-comment-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-comment-list.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
index b6c8fcc..902bf41 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
index ddab319..e2fcdff 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-commit-info',
-    _legacyUndefinedCheck: true,
 
     properties: {
       change: Object,
@@ -47,11 +46,21 @@
     },
 
     _computeShowWebLink(change, commitInfo, serverConfig) {
+      // Polymer 2: check for undefined
+      if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       const weblink = this._getWeblink(change, commitInfo, serverConfig);
       return !!weblink && !!weblink.url;
     },
 
     _computeWebLink(change, commitInfo, serverConfig) {
+      // Polymer 2: check for undefined
+      if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       const {url} = this._getWeblink(change, commitInfo, serverConfig) || {};
       return url;
     },
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
index 438a3f3..f271a70 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-commit-info</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../core/gr-router/gr-router.html">
 <link rel="import" href="gr-commit-info.html">
@@ -119,6 +121,7 @@
           },
         ],
       };
+      element.serverConfig = {};
 
       assert.isOk(element._computeShowWebLink(element.change,
           element.commitInfo, element.serverConfig));
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
index 8803eb3..05a2bb2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
@@ -15,8 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -42,6 +43,8 @@
       }
       iron-autogrow-textarea {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
         padding: 0;
         width: 73ch; /* Add a char to account for the border. */
 
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 a371e13..524876e 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-confirm-abandon-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -38,6 +37,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
     ],
 
@@ -55,6 +55,7 @@
 
     _handleConfirmTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this._confirm();
     },
 
@@ -64,6 +65,7 @@
 
     _handleCancelTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('cancel', null, {bubbles: false});
     },
   });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
index 95d5374..cc4b80e 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-abandon-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-abandon-dialog.html">
 
@@ -49,19 +51,26 @@
     test('_handleConfirmTap', () => {
       const confirmHandler = sandbox.stub();
       element.addEventListener('confirm', confirmHandler);
-      sandbox.stub(element, '_confirm');
+      sandbox.spy(element, '_handleConfirmTap');
+      sandbox.spy(element, '_confirm');
       element.$$('gr-dialog').fire('confirm');
       assert.isTrue(confirmHandler.called);
+      assert.isTrue(confirmHandler.calledOnce);
+      assert.isTrue(element._handleConfirmTap.called);
       assert.isTrue(element._confirm.called);
+      assert.isTrue(element._confirm.called);
+      assert.isTrue(element._confirm.calledOnce);
     });
 
     test('_handleCancelTap', () => {
       const cancelHandler = sandbox.stub();
       element.addEventListener('cancel', cancelHandler);
-      sandbox.stub(element, '_handleCancelTap');
+      sandbox.spy(element, '_handleCancelTap');
       element.$$('gr-dialog').fire('cancel');
       assert.isTrue(cancelHandler.called);
+      assert.isTrue(cancelHandler.calledOnce);
       assert.isTrue(element._handleCancelTap.called);
+      assert.isTrue(element._handleCancelTap.calledOnce);
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
index cd196ec..b9e9155 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
index 2e8af0e..a0da331 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
@@ -19,7 +19,10 @@
 
   Polymer({
     is: 'gr-confirm-cherrypick-conflict-dialog',
-    _legacyUndefinedCheck: true,
+
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
 
     /**
      * Fired when the confirm button is pressed.
@@ -35,11 +38,13 @@
 
     _handleConfirmTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('confirm', null, {bubbles: false});
     },
 
     _handleCancelTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('cancel', null, {bubbles: false});
     },
   });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
index 77b102c..f411de4 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-cherrypick-conflict-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-cherrypick-conflict-dialog.html">
 
@@ -47,19 +49,23 @@
     test('_handleConfirmTap', () => {
       const confirmHandler = sandbox.stub();
       element.addEventListener('confirm', confirmHandler);
-      sandbox.stub(element, '_handleConfirmTap');
+      sandbox.spy(element, '_handleConfirmTap');
       element.$$('gr-dialog').fire('confirm');
       assert.isTrue(confirmHandler.called);
+      assert.isTrue(confirmHandler.calledOnce);
       assert.isTrue(element._handleConfirmTap.called);
+      assert.isTrue(element._handleConfirmTap.calledOnce);
     });
 
     test('_handleCancelTap', () => {
       const cancelHandler = sandbox.stub();
       element.addEventListener('cancel', cancelHandler);
-      sandbox.stub(element, '_handleCancelTap');
+      sandbox.spy(element, '_handleCancelTap');
       element.$$('gr-dialog').fire('cancel');
       assert.isTrue(cancelHandler.called);
+      assert.isTrue(cancelHandler.calledOnce);
       assert.isTrue(element._handleCancelTap.called);
+      assert.isTrue(element._handleCancelTap.calledOnce);
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
index 84558ac..8ddcd83 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
@@ -15,8 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
@@ -43,16 +45,16 @@
       .main label,
       .main input[type="text"] {
         display: block;
-        font: inherit;
         width: 100%;
       }
       .main .message {
-        border: groove;
         width: 100%;
       }
       iron-autogrow-textarea {
+        font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
         padding: 0;
-
         --iron-autogrow-textarea: {
           font-family: var(--monospace-font-family);
           width: 72ch;
@@ -77,12 +79,17 @@
         <label for="baseInput">
           Provide base commit sha1 for cherry-pick
         </label>
-        <input
-            is="iron-input"
-            id="baseCommitInput"
+        <iron-input
             maxlength="40"
             placeholder="(optional)"
             bind-value="{{baseCommit}}">
+          <input
+              is="iron-input"
+              id="baseCommitInput"
+              maxlength="40"
+              placeholder="(optional)"
+              bind-value="{{baseCommit}}">
+        </iron-input>
         <label for="messageInput">
           Cherry Pick Commit Message
         </label>
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 bdc0bb2..5278540 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
@@ -21,7 +21,6 @@
 
   Polymer({
     is: 'gr-confirm-cherrypick-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -51,11 +50,24 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     observers: [
       '_computeMessage(changeStatus, commitNum, commitMessage)',
     ],
 
     _computeMessage(changeStatus, commitNum, commitMessage) {
+      // Polymer 2: check for undefined
+      if ([
+        changeStatus,
+        commitNum,
+        commitMessage,
+      ].some(arg => arg === undefined)) {
+        return;
+      }
+
       let newMessage = commitMessage;
 
       if (changeStatus === 'MERGED') {
@@ -66,11 +78,13 @@
 
     _handleConfirmTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('confirm', null, {bubbles: false});
     },
 
     _handleCancelTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('cancel', null, {bubbles: false});
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
index 5c51fe0..22a2aba 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-cherrypick-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-cherrypick-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
index 621ef0a..24c1132 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
@@ -15,8 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
@@ -47,11 +48,9 @@
       .main label,
       .main input[type="text"] {
         display: block;
-        font: inherit;
         width: 100%;
       }
       .main .message {
-        border: groove;
         width: 100%;
       }
       .warning {
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 91600ff..c6b0adf 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
@@ -21,7 +21,6 @@
 
   Polymer({
     is: 'gr-confirm-move-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -47,13 +46,19 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     _handleConfirmTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('confirm', null, {bubbles: false});
     },
 
     _handleCancelTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('cancel', null, {bubbles: false});
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
index e619425..8d6e029 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-move-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-move-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 912bbfa6a..cf2721a 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -41,14 +41,13 @@
       .parentRevisionContainer label,
       .parentRevisionContainer input[type="text"] {
         display: block;
-        font: inherit;
         width: 100%;
       }
       .parentRevisionContainer label {
-        margin-bottom: .2em;
+        margin-bottom: var(--spacing-xs);
       }
       .rebaseOption {
-        margin: .5em 0;
+        margin: var(--spacing-m) 0;
       }
     </style>
     <gr-dialog
@@ -63,7 +62,7 @@
           <input id="rebaseOnParentInput"
               name="rebaseOptions"
               type="radio"
-              on-tap="_handleRebaseOnParent">
+              on-click="_handleRebaseOnParent">
           <label id="rebaseOnParentLabel" for="rebaseOnParentInput">
             Rebase on parent change
           </label>
@@ -78,7 +77,7 @@
               name="rebaseOptions"
               type="radio"
               disabled$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]"
-              on-tap="_handleRebaseOnTip">
+              on-click="_handleRebaseOnTip">
           <label id="rebaseOnTipLabel" for="rebaseOnTipInput">
             Rebase on top of the [[branch]]
             branch<span hidden$="[[!hasParent]]">
@@ -94,7 +93,7 @@
           <input id="rebaseOnOtherInput"
               name="rebaseOptions"
               type="radio"
-              on-tap="_handleRebaseOnOther">
+              on-click="_handleRebaseOnOther">
           <label id="rebaseOnOtherLabel" for="rebaseOnOtherInput">
             Rebase on a specific change, ref, or commit <span hidden$="[[!hasParent]]">
               (breaks relation chain)
@@ -107,7 +106,7 @@
               query="[[_query]]"
               no-debounce
               text="{{_text}}"
-              on-tap="_handleEnterChangeNumberTap"
+              on-click="_handleEnterChangeNumberClick"
               allow-non-suggested-values
               placeholder="Change number, ref, or commit hash">
           </gr-autocomplete>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
index 77bc798..54ce271 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-confirm-rebase-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -120,6 +119,7 @@
 
     _handleConfirmTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.dispatchEvent(new CustomEvent('confirm',
           {detail: {base: this._getSelectedBase()}}));
       this._text = '';
@@ -127,6 +127,7 @@
 
     _handleCancelTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.dispatchEvent(new CustomEvent('cancel'));
       this._text = '';
     },
@@ -135,7 +136,7 @@
       this.$.parentInput.focus();
     },
 
-    _handleEnterChangeNumberTap() {
+    _handleEnterChangeNumberClick() {
       this.$.rebaseOnOtherInput.checked = true;
     },
 
@@ -144,6 +145,11 @@
      * the corresponding value to be submitted.
      */
     _updateSelectedOption(rebaseOnCurrent, hasParent) {
+      // Polymer 2: check for undefined
+      if ([rebaseOnCurrent, hasParent].some(arg => arg === undefined)) {
+        return;
+      }
+
       if (this._displayParentOption(rebaseOnCurrent, hasParent)) {
         this.$.rebaseOnParentInput.checked = true;
       } else if (this._displayTipOption(rebaseOnCurrent, hasParent)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index c6e9ec4..cd5b130 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-rebase-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-rebase-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
index 9e5f1de..2e1e6ae 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -15,10 +15,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 
 <dom-module id="gr-confirm-revert-dialog">
   <template>
@@ -37,6 +39,8 @@
       }
       iron-autogrow-textarea {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
         padding: 0;
         width: 73ch; /* Add a char to account for the border. */
 
@@ -53,15 +57,17 @@
         on-cancel="_handleCancelTap">
       <div class="header" slot="header">Revert Merged Change</div>
       <div class="main" slot="main">
-        <label for="messageInput">
-          Revert Commit Message
-        </label>
-        <iron-autogrow-textarea
-            id="messageInput"
-            class="message"
-            autocomplete="on"
-            max-rows="15"
-            bind-value="{{message}}"></iron-autogrow-textarea>
+        <gr-endpoint-decorator name="confirm-revert-change">
+          <label for="messageInput">
+            Revert Commit Message
+          </label>
+          <iron-autogrow-textarea
+              id="messageInput"
+              class="message"
+              autocomplete="on"
+              max-rows="15"
+              bind-value="{{message}}"></iron-autogrow-textarea>
+        </gr-endpoint-decorator>
       </div>
     </gr-dialog>
   </template>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 93e21c7..662b64c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-confirm-revert-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -40,6 +39,10 @@
       message: String,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     populateRevertMessage(message, commitHash) {
       // Figure out what the revert title should be.
       const originalTitle = message.split('\n')[0];
@@ -56,11 +59,13 @@
 
     _handleConfirmTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('confirm', null, {bubbles: false});
     },
 
     _handleCancelTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('cancel', null, {bubbles: false});
     },
   });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
index c5a1bde..6e41555 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-revert-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-revert-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
index b9c32ed..02385cf 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
@@ -15,11 +15,13 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
+<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-confirm-submit-dialog">
@@ -29,7 +31,7 @@
         min-width: 40em;
       }
       p {
-        margin-bottom: 1em;
+        margin-bottom: var(--spacing-l);
       }
       @media screen and (max-width: 50em) {
         #dialog {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
index 282b394..8c93778 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-confirm-submit-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -61,11 +60,13 @@
 
     _handleConfirmTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.dispatchEvent(new CustomEvent('confirm', {bubbles: false}));
     },
 
     _handleCancelTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.dispatchEvent(new CustomEvent('cancel', {bubbles: false}));
     },
   });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
index dd61d1c..56a9485 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-submit-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="gr-confirm-submit-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
index 28d25d2..e20bbd7 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -15,8 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -31,7 +32,7 @@
       }
       section {
         display: flex;
-        padding: .5em 1.5em;
+        padding: var(--spacing-m) var(--spacing-xl);
       }
       section:not(:first-of-type) {
         border-top: 1px solid var(--border-color);
@@ -39,7 +40,7 @@
       .flexContainer {
         display: flex;
         justify-content: space-between;
-        padding-top: .75em;
+        padding-top: var(--spacing-m);
       }
       .footer {
         justify-content: flex-end;
@@ -52,15 +53,15 @@
       }
       .patchFiles,
       .archivesContainer {
-        padding-bottom: .5em;
+        padding-bottom: var(--spacing-m);
       }
       .patchFiles {
-        margin-right: 2em;
+        margin-right: var(--spacing-xxl);
       }
       .patchFiles a,
       .archives a {
         display: inline-block;
-        margin-right: 1em;
+        margin-right: var(--spacing-l);
       }
       .patchFiles a:last-of-type,
       .archives a:last-of-type {
@@ -120,7 +121,7 @@
       <span class="closeButtonContainer">
         <gr-button id="closeButton"
             link
-            on-tap="_handleCloseTap">Close</gr-button>
+            on-click="_handleCloseTap">Close</gr-button>
       </span>
     </section>
   </template>
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 b6c155e..b297a14 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-download-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
@@ -48,6 +47,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.RESTClientBehavior,
     ],
@@ -70,16 +70,17 @@
 
     _computeDownloadCommands(change, patchNum, _selectedScheme) {
       let commandObj;
+      if (!change) return [];
       for (const rev of Object.values(change.revisions || {})) {
         if (this.patchNumEquals(rev._number, patchNum) &&
-            rev.fetch.hasOwnProperty(_selectedScheme)) {
+            rev && rev.fetch && rev.fetch.hasOwnProperty(_selectedScheme)) {
           commandObj = rev.fetch[_selectedScheme].commands;
           break;
         }
       }
       const commands = [];
       for (const title in commandObj) {
-        if (!commandObj.hasOwnProperty(title)) { continue; }
+        if (!commandObj || !commandObj.hasOwnProperty(title)) { continue; }
         commands.push({
           title,
           command: commandObj[title],
@@ -116,6 +117,10 @@
      * @return {string} Not sure why there was a mismatch
      */
     _computeDownloadLink(change, patchNum, opt_zip) {
+      // Polymer 2: check for undefined
+      if ([change, patchNum].some(arg => arg === undefined)) {
+        return '';
+      }
       return this.changeBaseURL(change.project, change._number, patchNum) +
           '/patch?' + (opt_zip ? 'zip' : 'download');
     },
@@ -129,6 +134,11 @@
      * @return {string}
      */
     _computeDownloadFilename(change, patchNum, opt_zip) {
+      // Polymer 2: check for undefined
+      if ([change, patchNum].some(arg => arg === undefined)) {
+        return '';
+      }
+
       let shortRev = '';
       for (const rev in change.revisions) {
         if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
@@ -140,6 +150,10 @@
     },
 
     _computeHidePatchFile(change, patchNum) {
+      // Polymer 2: check for undefined
+      if ([change, patchNum].some(arg => arg === undefined)) {
+        return false;
+      }
       for (const rev of Object.values(change.revisions || {})) {
         if (this.patchNumEquals(rev._number, patchNum)) {
           const parentLength = rev.commit && rev.commit.parents ?
@@ -151,11 +165,20 @@
     },
 
     _computeArchiveDownloadLink(change, patchNum, format) {
+      // Polymer 2: check for undefined
+      if ([change, patchNum, format].some(arg => arg === undefined)) {
+        return '';
+      }
       return this.changeBaseURL(change.project, change._number, patchNum) +
           '/archive?format=' + format;
     },
 
     _computeSchemes(change, patchNum) {
+      // Polymer 2: check for undefined
+      if ([change, patchNum].some(arg => arg === undefined)) {
+        return [];
+      }
+
       for (const rev of Object.values(change.revisions || {})) {
         if (this.patchNumEquals(rev._number, patchNum)) {
           const fetch = rev.fetch;
@@ -175,6 +198,7 @@
 
     _handleCloseTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('close', null, {bubbles: false});
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index 214363b..82574808 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-download-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-download-dialog.html">
 
@@ -144,7 +146,8 @@
     });
 
     test('anchors use download attribute', () => {
-      const anchors = Polymer.dom(element.root).querySelectorAll('a');
+      const anchors = Array.from(
+          Polymer.dom(element.root).querySelectorAll('a'));
       assert.isTrue(!anchors.some(a => !a.hasAttribute('download')));
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index f7d90eb..9146125 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
@@ -29,6 +30,7 @@
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
 <link rel="import" href="../gr-file-list-constants.html">
+<link rel="import" href="../gr-commit-info/gr-commit-info.html">
 
 <dom-module id="gr-file-list-header">
   <template>
@@ -47,7 +49,7 @@
         background-color: var(--table-header-background-color);
         border-top: 1px solid var(--border-color);
         display: flex;
-        padding: 6px var(--default-horizontal-margin);
+        padding: var(--spacing-s) var(--spacing-l);
       }
       .patchInfo-left {
         align-items: baseline;
@@ -143,7 +145,7 @@
         margin-right: -5px;
       }
       .fileViewActionsLabel {
-        margin-right: .2rem;
+        margin-right: var(--spacing-xs);
       }
       @media screen and (max-width: 50em) {
         .patchInfo-header .desktop {
@@ -216,28 +218,28 @@
         <span class$="[[_computeUploadHelpContainerClass(change, account)]]">
           <gr-button link
               class="upload"
-              on-tap="_handleUploadTap">Update Change</gr-button>
+              on-click="_handleUploadTap">Update Change</gr-button>
         </span>
         <span class="downloadContainer desktop">
           <gr-button link
               class="download"
-              on-tap="_handleDownloadTap">Download</gr-button>
+              on-click="_handleDownloadTap">Download</gr-button>
         </span>
         <span class$="includedInContainer [[_hideIncludedIn(change)]] desktop">
           <gr-button link
               class="includedIn"
-              on-tap="_handleIncludedInTap">Included In</gr-button>
+              on-click="_handleIncludedInTap">Included In</gr-button>
         </span>
         <template is="dom-if"
             if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
           <gr-button
               id="expandBtn"
               link
-              on-tap="_expandAllDiffs">Expand All</gr-button>
+              on-click="_expandAllDiffs">Expand All</gr-button>
           <gr-button
               id="collapseBtn"
               link
-              on-tap="_collapseAllDiffs">Collapse All</gr-button>
+              on-click="_collapseAllDiffs">Collapse All</gr-button>
         </template>
         <template is="dom-if"
             if="[[!_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
@@ -261,7 +263,7 @@
                 has-tooltip
                 title="Diff preferences"
                 class="prefsButton desktop"
-                on-tap="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
+                on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
           </span>
         </div>
       </div>
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 8321879..d6fbfc4 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
@@ -23,7 +23,6 @@
 
   Polymer({
     is: 'gr-file-list-header',
-    _legacyUndefinedCheck: true,
 
     /**
      * @event expand-diffs
@@ -94,6 +93,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.PatchSetBehavior,
     ],
 
@@ -132,10 +132,20 @@
     },
 
     _computeDescriptionReadOnly(loggedIn, change, account) {
+      // Polymer 2: check for undefined
+      if ([loggedIn, change, account].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       return !(loggedIn && (account._account_id === change.owner._account_id));
     },
 
     _computePatchSetDescription(change, patchNum) {
+      // Polymer 2: check for undefined
+      if ([change, patchNum].some(arg => arg === undefined)) {
+        return;
+      }
+
       const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
       this._patchsetDescription = (rev && rev.description) ?
         rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
@@ -217,6 +227,7 @@
 
     _handleDownloadTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.dispatchEvent(
           new CustomEvent('open-download-dialog', {bubbles: false}));
     },
@@ -239,6 +250,7 @@
 
     _handleUploadTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.dispatchEvent(
           new CustomEvent('open-upload-help-dialog', {bubbles: false}));
     },
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index adfeeb4..ac626ab 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-file-list-header</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="gr-file-list-header.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index f9ee54f..649aa53 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -15,9 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/async-foreach-behavior/async-foreach-behavior.html">
 <link rel="import" href="../../../behaviors/dom-util-behavior/dom-util-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -47,8 +48,8 @@
         align-items: center;
         border-top: 1px solid var(--border-color);
         display: flex;
-        min-height: 2.25em;
-        padding: .2em var(--default-horizontal-margin) .2em calc(var(--default-horizontal-margin) - .35rem);
+        min-height: calc(var(--line-height-normal) + 2*var(--spacing-s));
+        padding: var(--spacing-xs) var(--spacing-l) var(--spacing-xs) calc(var(--spacing-l) - .35rem);
       }
       :host(.loading) .row {
         opacity: .5;
@@ -115,16 +116,13 @@
         cursor: pointer;
         flex: 1;
         text-decoration: none;
-        white-space: nowrap;
+        /* Wrap it into multiple lines if too long. */
+        white-space: normal;
+        word-break: break-word;
       }
       .path:hover :first-child {
         text-decoration: underline;
       }
-      .path,
-      .path div {
-        overflow: hidden;
-        text-overflow: ellipsis;
-      }
       .oldPath {
         color: var(--deemphasized-text-color);
       }
@@ -137,16 +135,18 @@
         min-width: 7.5em;
       }
       .comments {
-        padding-left: 2em;
-        min-width: 20em;
+        padding-left: var(--spacing-l);
+        min-width: 7.5em;
       }
       .row:not(.header-row) .stats,
       .total-stats {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
         display: flex;
       }
       .sizeBars {
-        margin-left: .5em;
+        margin-left: var(--spacing-m);
         min-width: 7em;
         text-align: center;
       }
@@ -165,18 +165,18 @@
         color: var(--vote-text-color-disliked);
         text-align: left;
         min-width: 4em;
-        padding-left: 0.5em;
+        padding-left: var(--spacing-s);
       }
       .drafts {
         color: #C62828;
         font-weight: var(--font-weight-bold);
       }
       .show-hide {
-        margin-left: .35em;
+        margin-left: var(--spacing-s);
         width: 1.9em;
       }
       .fileListButton {
-        margin: .5em;
+        margin: var(--spacing-m);
       }
       .totalChanges {
         justify-content: flex-end;
@@ -200,15 +200,11 @@
       .truncatedFileName {
         display: none;
       }
-      .expanded .fullFileName {
-        white-space: normal;
-        word-wrap: break-word;
-      }
       .mobile {
         display: none;
       }
       .reviewed {
-        margin-left: 2em;
+        margin-left: var(--spacing-xxl);
         width: 15em;
       }
       .reviewed label {
@@ -234,7 +230,7 @@
       }
       .reviewedLabel {
         color: var(--deemphasized-text-color);
-        margin-right: 1em;
+        margin-right: var(--spacing-l);
         opacity: 0;
       }
       .reviewedLabel.isReviewed {
@@ -247,10 +243,12 @@
       .markReviewed,
       .pathLink {
         display: inline-block;
-        margin: -.2em 0;
-        padding: .4em 0;
+        margin: -2px 0;
+        padding: var(--spacing-s) 0;
       }
-      @media screen and (max-width: 50em) {
+
+      /** small screen breakpoint: 768px */
+      @media screen and (max-width: 55em) {
         .desktop {
           display: none;
         }
@@ -285,7 +283,7 @@
     </style>
     <div
         id="container"
-        on-tap="_handleFileListTap">
+        on-click="_handleFileListClick">
       <div class="header-row row">
         <div class="status"></div>
         <div class="path">File</div>
@@ -498,7 +496,7 @@
       <gr-button
           class="fileListButton"
           id="incrementButton"
-          link on-tap="_incrementNumFilesShown">
+          link on-click="_incrementNumFilesShown">
         [[_computeIncrementText(numFilesShown, _files)]]
       </gr-button>
       <gr-tooltip-content
@@ -508,7 +506,7 @@
         <gr-button
             class="fileListButton"
             id="showAllButton"
-            link on-tap="_showAllFiles">
+            link on-click="_showAllFiles">
           [[_computeShowAllText(_files)]]
         </gr-button><!--
   --></gr-tooltip-content>
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 8dba99d..8d9b905 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
@@ -41,29 +41,8 @@
     U: 'Unchanged',
   };
 
-  const Defs = {};
-
-  /**
-   * Object containing layout values to be used in rendering size-bars.
-   * `max{Inserted,Deleted}` represent the largest values of the
-   * `lines_inserted` and `lines_deleted` fields of the files respectively. The
-   * `max{Addition,Deletion}Width` represent the width of the graphic allocated
-   * to the insertion or deletion side respectively. Finally, the
-   * `deletionOffset` value represents the x-position for the deletion bar.
-   *
-   * @typedef {{
-   *    maxInserted: number,
-   *    maxDeleted: number,
-   *    maxAdditionWidth: number,
-   *    maxDeletionWidth: number,
-   *    deletionOffset: number,
-   * }}
-   */
-  Defs.LayoutStats;
-
   Polymer({
     is: 'gr-file-list',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a draft refresh should get triggered
@@ -148,7 +127,7 @@
 
       _shownFiles: {
         type: Array,
-        computed: '_computeFilesShown(numFilesShown, _files.*)',
+        computed: '_computeFilesShown(numFilesShown, _files)',
       },
 
       /**
@@ -166,7 +145,7 @@
         type: Boolean,
         observer: '_loadingChanged',
       },
-      /** @type {Defs.LayoutStats|undefined} */
+      /** @type {Gerrit.LayoutStats|undefined} */
       _sizeBarLayout: {
         type: Object,
         computed: '_computeSizeBarLayout(_shownFiles.*)',
@@ -203,6 +182,7 @@
     behaviors: [
       Gerrit.AsyncForeachBehavior,
       Gerrit.DomUtilBehavior,
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.PathListBehavior,
@@ -332,7 +312,6 @@
     },
 
     get diffs() {
-      // Polymer2: querySelectorAll returns NodeList instead of Array.
       return Array.from(
           Polymer.dom(this.root).querySelectorAll('gr-diff-host'));
     },
@@ -512,7 +491,7 @@
 
       this.set(['_files', index, 'isReviewed'], reviewed);
       if (index < this._shownFiles.length) {
-        this.set(['_shownFiles', index, 'isReviewed'], reviewed);
+        this.notifyPath(`_shownFiles.${index}.isReviewed`);
       }
 
       this._saveReviewedState(path, reviewed);
@@ -562,7 +541,7 @@
      * Handle all events from the file list dom-repeat so event handleers don't
      * have to get registered for potentially very long lists.
      */
-    _handleFileListTap(e) {
+    _handleFileListClick(e) {
       // Traverse upwards to find the row element if the target is not the row.
       let row = e.target;
       while (!row.classList.contains('row') && row.parentElement) {
@@ -800,6 +779,11 @@
     },
 
     _computeDiffURL(change, patchNum, basePatchNum, path, editMode) {
+      // Polymer 2: check for undefined
+      if ([change, patchNum, basePatchNum, path, editMode]
+          .some(arg => arg === undefined)) {
+        return;
+      }
       // TODO(kaspern): Fix editing for commit messages and merge lists.
       if (editMode && path !== this.COMMIT_MESSAGE_PATH &&
           path !== this.MERGE_LIST_PATH) {
@@ -860,8 +844,19 @@
     },
 
     _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) {
+      // Polymer 2: check for undefined
+      if ([
+        filesByPath,
+        changeComments,
+        patchRange,
+        reviewed,
+        loading,
+      ].some(arg => arg === undefined)) {
+        return;
+      }
+
       // Await all promises resolving from reload. @See Issue 9057
-      if (loading) { return; }
+      if (loading || !changeComments) { return; }
 
       const commentedPaths = changeComments.getPaths(patchRange);
       const files = Object.assign({}, filesByPath);
@@ -879,10 +874,15 @@
     },
 
     _computeFilesShown(numFilesShown, files) {
+      // Polymer 2: check for undefined
+      if ([numFilesShown, files].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       const previousNumFilesShown = this._shownFiles ?
         this._shownFiles.length : 0;
 
-      const filesShown = files.base.slice(0, numFilesShown);
+      const filesShown = files.slice(0, numFilesShown);
       this.fire('files-shown-changed', {length: filesShown.length});
 
       // Start the timer for the rendering work hwere because this is where the
@@ -904,12 +904,13 @@
     },
 
     _filesChanged() {
-      Polymer.dom.flush();
-      // Polymer2: querySelectorAll returns NodeList instead of Array.
-      const files = Array.from(
-          Polymer.dom(this.root).querySelectorAll('.file-row'));
-      this.$.fileCursor.stops = files;
-      this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
+      if (this._files && this._files.length > 0) {
+        Polymer.dom.flush();
+        const files = Array.from(
+            Polymer.dom(this.root).querySelectorAll('.file-row'));
+        this.$.fileCursor.stops = files;
+        this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
+      }
     },
 
     _incrementNumFilesShown() {
@@ -947,6 +948,11 @@
     },
 
     _computePatchSetDescription(revisions, patchNum) {
+      // Polymer 2: check for undefined
+      if ([revisions, patchNum].some(arg => arg === undefined)) {
+        return '';
+      }
+
       const rev = this.getRevisionByPatchNum(revisions, patchNum);
       return (rev && rev.description) ?
         rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
@@ -1178,7 +1184,8 @@
     /**
      * Compute size bar layout values from the file list.
      *
-     * @return {Defs.LayoutStats|undefined}
+     * @return {Gerrit.LayoutStats|undefined}
+     *
      */
     _computeSizeBarLayout(shownFilesRecord) {
       if (!shownFilesRecord || !shownFilesRecord.base) { return undefined; }
@@ -1214,7 +1221,7 @@
      * Get the width of the addition bar for a file.
      *
      * @param {Object} file
-     * @param {Defs.LayoutStats} stats
+     * @param {Gerrit.LayoutStats} stats
      * @return {number}
      */
     _computeBarAdditionWidth(file, stats) {
@@ -1232,7 +1239,7 @@
      * Get the x-offset of the addition bar for a file.
      *
      * @param {Object} file
-     * @param {Defs.LayoutStats} stats
+     * @param {Gerrit.LayoutStats} stats
      * @return {number}
      */
     _computeBarAdditionX(file, stats) {
@@ -1244,7 +1251,7 @@
      * Get the width of the deletion bar for a file.
      *
      * @param {Object} file
-     * @param {Defs.LayoutStats} stats
+     * @param {Gerrit.LayoutStats} stats
      * @return {number}
      */
     _computeBarDeletionWidth(file, stats) {
@@ -1261,7 +1268,8 @@
     /**
      * Get the x-offset of the deletion bar for a file.
      *
-     * @param {Defs.LayoutStats} stats
+     * @param {Gerrit.LayoutStats} stats
+     *
      * @return {number}
      */
     _computeBarDeletionX(stats) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index fffac8e..4c102a2 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-file-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
 <script src="../../../scripts/util.js"></script>
 
@@ -815,18 +817,18 @@
       assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
       assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
 
-      const tapSpy = sandbox.spy(element, '_handleFileListTap');
+      const clickSpy = sandbox.spy(element, '_handleFileListClick');
       MockInteractions.tap(markReviewLabel);
       assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
       assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
       assert.equal(markReviewLabel.textContent, 'MARK REVIEWED');
-      assert.isTrue(tapSpy.lastCall.args[0].defaultPrevented);
+      assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
 
       MockInteractions.tap(markReviewLabel);
       assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
       assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
       assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
-      assert.isTrue(tapSpy.lastCall.args[0].defaultPrevented);
+      assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
     });
 
     test('_computeFileStatusLabel', () => {
@@ -834,7 +836,7 @@
       assert.equal(element._computeFileStatusLabel('M'), 'Modified');
     });
 
-    test('_handleFileListTap', () => {
+    test('_handleFileListClick', () => {
       element._filesByPath = {
         '/COMMIT_MSG': {},
         'f1.txt': {},
@@ -846,7 +848,7 @@
         patchNum: '2',
       };
 
-      const tapSpy = sandbox.spy(element, '_handleFileListTap');
+      const clickSpy = sandbox.spy(element, '_handleFileListClick');
       const reviewStub = sandbox.stub(element, '_reviewFile');
       const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
 
@@ -856,26 +858,26 @@
       // Click on the expand button, resulting in _togglePathExpanded being
       // called and not resulting in a call to _reviewFile.
       row.querySelector('div.show-hide').click();
-      assert.isTrue(tapSpy.calledOnce);
+      assert.isTrue(clickSpy.calledOnce);
       assert.isTrue(toggleExpandSpy.calledOnce);
       assert.isFalse(reviewStub.called);
 
       // Click inside the diff. This should result in no additional calls to
       // _togglePathExpanded or _reviewFile.
       Polymer.dom(element.root).querySelector('gr-diff-host').click();
-      assert.isTrue(tapSpy.calledTwice);
+      assert.isTrue(clickSpy.calledTwice);
       assert.isTrue(toggleExpandSpy.calledOnce);
       assert.isFalse(reviewStub.called);
 
       // Click the reviewed checkbox, resulting in a call to _reviewFile, but
       // no additional call to _togglePathExpanded.
       row.querySelector('.markReviewed').click();
-      assert.isTrue(tapSpy.calledThrice);
+      assert.isTrue(clickSpy.calledThrice);
       assert.isTrue(toggleExpandSpy.calledOnce);
       assert.isTrue(reviewStub.calledOnce);
     });
 
-    test('_handleFileListTap editMode', () => {
+    test('_handleFileListClick editMode', () => {
       element._filesByPath = {
         '/COMMIT_MSG': {},
         'f1.txt': {},
@@ -888,12 +890,12 @@
       };
       element.editMode = true;
       flushAsynchronousOperations();
-      const tapSpy = sandbox.spy(element, '_handleFileListTap');
+      const clickSpy = sandbox.spy(element, '_handleFileListClick');
       const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
 
-      // Tap the edit controls. Should be ignored by _handleFileListTap.
+      // Tap the edit controls. Should be ignored by _handleFileListClick.
       MockInteractions.tap(element.$$('.editFileControls'));
-      assert.isTrue(tapSpy.calledOnce);
+      assert.isTrue(clickSpy.calledOnce);
       assert.isFalse(toggleExpandSpy.called);
     });
 
@@ -1211,22 +1213,6 @@
       assert.isFalse(element.classList.contains('loading'));
     });
 
-    test('no execute _computeDiffURL before patchNum is knwon', done => {
-      const urlStub = sandbox.stub(element, '_computeDiffURL');
-      element.change = {_number: 123};
-      element.patchRange = {patchNum: undefined, basePatchNum: 'PARENT'};
-      element._filesByPath = {'foo/bar.cpp': {}};
-      element.editMode = false;
-      flush(() => {
-        assert.isFalse(urlStub.called);
-        element.set('patchRange.patchNum', 4);
-        flush(() => {
-          assert.isTrue(urlStub.called);
-          done();
-        });
-      });
-    });
-
     suite('size bars', () => {
       test('_computeSizeBarLayout', () => {
         assert.isUndefined(element._computeSizeBarLayout(null));
@@ -1713,7 +1699,9 @@
 
       // Commit message should not have edit controls.
       const editControls =
-          Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)')
+          Array.from(
+              Polymer.dom(element.root)
+                  .querySelectorAll('.row:not(.header-row)'))
               .map(row => row.querySelector('gr-edit-file-controls'));
       assert.isTrue(editControls[0].classList.contains('invisible'));
     });
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
index b824f1c..0afd214 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
@@ -15,8 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-included-in-dialog">
@@ -27,44 +30,41 @@
         display: block;
         max-height: 80vh;
         overflow-y: auto;
-        padding: 4.5em 1em 1em 1em;
+        padding: 4.5em var(--spacing-l) var(--spacing-l) var(--spacing-l);
       }
       header {
         background-color: var(--dialog-background-color);
         border-bottom: 1px solid var(--border-color);
         left: 0;
-        padding: 1em;
+        padding: var(--spacing-l);
         position: absolute;
         right: 0;
         top: 0;
       }
       #title {
         display: inline-block;
-        font-size: 1.2rem;
-        margin-top: .2em;
-      }
-      h2 {
-        font-size: 1rem;
+        font-size: var(--font-size-h3);
+        margin-top: var(--spacing-xs);
       }
       #filterInput {
         display: inline-block;
         float: right;
-        margin: 0 1em;
-        padding: .2em;
+        margin: 0 var(--spacing-l);
+        padding: var(--spacing-xs);
       }
       .closeButtonContainer {
         float: right;
       }
       ul {
-        margin-bottom: 1em;
+        margin-bottom: var(--spacing-l);
       }
       ul li {
         border: 1px solid var(--border-color);
-        border-radius: .2em;
+        border-radius: var(--border-radius);
         background: var(--chip-background-color);
         display: inline-block;
-        margin: 0 .2em .4em .2em;
-        padding: .2em .4em;
+        margin: 0 var(--spacing-xs) var(--spacing-s) var(--spacing-xs);
+        padding: var(--spacing-xs) var(--spacing-s);
       }
       .loading.loaded {
         display: none;
@@ -75,13 +75,19 @@
       <span class="closeButtonContainer">
         <gr-button id="closeButton"
             link
-            on-tap="_handleCloseTap">Close</gr-button>
+            on-click="_handleCloseTap">Close</gr-button>
       </span>
-      <input
-          id="filterInput"
+      <iron-input
+        id="filterInput"
+        placeholder="Filter"
+        bind-value="{{_filterText}}"
+      >
+        <input
           is="iron-input"
           placeholder="Filter"
-          on-bind-value-changed="_onFilterChanged">
+          bind-value="{{_filterText}}"
+        />
+      </iron-input>
     </header>
     <div class$="[[_computeLoadingClass(_loaded)]]">Loading...</div>
     <template
@@ -89,7 +95,7 @@
         items="[[_computeGroups(_includedIn, _filterText)]]"
         as="group">
       <div>
-        <h2>[[group.title]]:</h2>
+        <span>[[group.title]]:</span>
         <ul>
           <template is="dom-repeat" items="[[group.items]]">
             <li>[[item]]</li>
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
index 7755a60..9d9f654 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-included-in-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
@@ -45,6 +44,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     loadData() {
       if (!this.changeNum) { return; }
       this._filterText = '';
@@ -62,7 +65,9 @@
     },
 
     _computeGroups(includedIn, filterText) {
-      if (!includedIn) { return []; }
+      if (!includedIn || filterText === undefined) {
+        return [];
+      }
 
       const filter = item => !filterText.length ||
           item.toLowerCase().indexOf(filterText.toLowerCase()) !== -1;
@@ -84,17 +89,12 @@
 
     _handleCloseTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('close', null, {bubbles: false});
     },
 
     _computeLoadingClass(loaded) {
       return loaded ? 'loading loaded' : 'loading';
     },
-
-    _onFilterChanged() {
-      this.debounce('filter-change', () => {
-        this._filterText = this.$.filterInput.bindValue;
-      }, 100);
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
index 539011a..ad2ad5a 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-included-in-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-included-in-dialog.html">
 
@@ -80,5 +82,20 @@
         {title: 'Tags', items: ['v2.0', 'v2.1']},
       ]);
     });
+
+    test('_computeGroups with .bindValue', done => {
+      element.$.filterInput.bindValue = 'stable-3.2';
+      const includedIn = {branches: [], tags: []};
+      includedIn.branches.push('master', 'stable-3.2');
+
+      setTimeout(() => {
+        const filterText = element._filterText;
+        assert.deepEqual(element._computeGroups(includedIn, filterText), [
+          {title: 'Branches', items: ['stable-3.2']},
+        ]);
+
+        done();
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
index 27c5baa..220546b 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-selector/iron-selector.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../../styles/gr-voting-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -28,12 +28,12 @@
       .labelContainer {
         align-items: center;
         display: flex;
-        margin-bottom: .5em;
+        margin-bottom: var(--spacing-m);
       }
       .labelName {
         display: inline-block;
         flex: 0 0 auto;
-        margin-right: .5em;
+        margin-right: var(--spacing-m);
         min-width: 7em;
         text-align: left;
         width: 20%;
@@ -47,7 +47,7 @@
       .selectedValueText {
         color: var(--deemphasized-text-color);
         font-style: italic;
-        margin: 0 .5em 0 .5em;
+        margin: 0 var(--spacing-m);
       }
       .selectedValueText.hidden {
         display: none;
@@ -60,7 +60,7 @@
         --gr-button: {
           background-color: var(--button-background-color, var(--table-header-background-color));
           color: var(--primary-text-color);
-          padding: .2em .85em;
+          padding: var(--spacing-xs) var(--spacing-m);
           @apply --vote-chip-styles;
         }
       }
@@ -108,6 +108,7 @@
           <span class="placeholder" data-label$="[[label.name]]"></span>
         </template>
         <iron-selector
+            id="labelSelector"
             attr-for-selected="value"
             selected="[[_computeLabelValue(labels, permittedLabels, label)]]"
             hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index 9b5847d..76e6e64 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-label-score-row',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when any label is changed.
@@ -66,7 +65,7 @@
     },
 
     get _ironSelector() {
-      return this.$$('iron-selector');
+      return this.$ && this.$.labelSelector;
     },
 
     _computeBlankItems(permittedLabels, label, side) {
@@ -97,19 +96,30 @@
     },
 
     _computeButtonClass(value, index, totalItems) {
-      if (value < 0 && index === 0) {
-        return 'min';
-      } else if (value < 0) {
-        return 'negative';
-      } else if (value > 0 && index === totalItems - 1) {
-        return 'max';
-      } else if (value > 0) {
-        return 'positive';
+      const classes = [];
+      if (value === this.selectedValue) {
+        classes.push('iron-selected');
       }
-      return 'neutral';
+
+      if (value < 0 && index === 0) {
+        classes.push('min');
+      } else if (value < 0) {
+        classes.push('negative');
+      } else if (value > 0 && index === totalItems - 1) {
+        classes.push('max');
+      } else if (value > 0) {
+        classes.push('positive');
+      } else {
+        classes.push('neutral');
+      }
+
+      return classes.join(' ');
     },
 
     _computeLabelValue(labels, permittedLabels, label) {
+      if ([labels, permittedLabels, label].some(arg => arg === undefined)) {
+        return null;
+      }
       if (!labels[label.name]) { return null; }
       const labelValue = this._getLabelValue(labels, permittedLabels, label);
       const len = permittedLabels[label.name] != null ?
@@ -133,11 +143,12 @@
       const name = e.target.selectedItem.name;
       const value = e.target.selectedItem.getAttribute('value');
       this.dispatchEvent(new CustomEvent(
-          'labels-changed', {detail: {name, value}, bubbles: true}));
+          'labels-changed',
+          {detail: {name, value}, bubbles: true, composed: true}));
     },
 
     _computeAnyPermittedLabelValues(permittedLabels, label) {
-      return permittedLabels.hasOwnProperty(label) &&
+      return permittedLabels && permittedLabels.hasOwnProperty(label) &&
         permittedLabels[label].length;
     },
 
@@ -147,6 +158,11 @@
     },
 
     _computePermittedLabelValues(permittedLabels, label) {
+      // Polymer 2: check for undefined
+      if ([permittedLabels, label].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       return permittedLabels[label];
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
index 1e4d471..519fbb8 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-label-score-row</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-label-score-row.html">
@@ -106,7 +108,7 @@
     test('label picker', () => {
       const labelsChangedHandler = sandbox.stub();
       element.addEventListener('labels-changed', labelsChangedHandler);
-      assert.ok(element.$$('iron-selector'));
+      assert.ok(element.$.labelSelector);
       MockInteractions.tap(element.$$(
           'gr-button[value="-1"]'));
       flushAsynchronousOperations();
@@ -155,9 +157,9 @@
 
     test('correct item is selected', () => {
       // 1 should be the value of the selected item
-      assert.strictEqual(element.$$('iron-selector').selected, '+1');
+      assert.strictEqual(element.$.labelSelector.selected, '+1');
       assert.strictEqual(
-          element.$$('iron-selector').selectedItem
+          element.$.labelSelector.selectedItem
               .textContent.trim(), '+1');
       assert.strictEqual(
           element.$.selectedValueLabel.textContent.trim(), 'good');
@@ -234,7 +236,7 @@
           default_value: 0,
         },
       };
-      const selector = element.$$('iron-selector');
+      const selector = element.$.labelSelector;
       element.set('label', {name: 'Verified', value: ' 0'});
       flushAsynchronousOperations();
       assert.strictEqual(selector.selected, ' 0');
@@ -251,21 +253,21 @@
         ],
       };
       flushAsynchronousOperations();
-      assert.isOk(element.$$('iron-selector'));
-      assert.isFalse(element.$$('iron-selector').hidden);
+      assert.isOk(element.$.labelSelector);
+      assert.isFalse(element.$.labelSelector.hidden);
 
       element.permittedLabels = {};
       flushAsynchronousOperations();
-      assert.isOk(element.$$('iron-selector'));
-      assert.isTrue(element.$$('iron-selector').hidden);
+      assert.isOk(element.$.labelSelector);
+      assert.isTrue(element.$.labelSelector.hidden);
 
       element.permittedLabels = {Verified: []};
       flushAsynchronousOperations();
-      assert.isOk(element.$$('iron-selector'));
-      assert.isTrue(element.$$('iron-selector').hidden);
+      assert.isOk(element.$.labelSelector);
+      assert.isTrue(element.$.labelSelector.hidden);
     });
 
-    test('asymetrical labels', () => {
+    test('asymetrical labels', done => {
       element.permittedLabels = {
         'Code-Review': [
           '-2',
@@ -279,30 +281,35 @@
           '+1',
         ],
       };
-      flushAsynchronousOperations();
-      assert.strictEqual(element.$$('iron-selector')
-          .items.length, 2);
-      assert.strictEqual(Polymer.dom(element.root).
-          querySelectorAll('.placeholder').length, 3);
+      flush(() => {
+        assert.strictEqual(element.$.labelSelector
+            .items.length, 2);
+        assert.strictEqual(
+            Polymer.dom(element.root).querySelectorAll('.placeholder').length,
+            3);
 
-      element.permittedLabels = {
-        'Code-Review': [
-          ' 0',
-          '+1',
-        ],
-        'Verified': [
-          '-2',
-          '-1',
-          ' 0',
-          '+1',
-          '+2',
-        ],
-      };
-      flushAsynchronousOperations();
-      assert.strictEqual(element.$$('iron-selector')
-          .items.length, 5);
-      assert.strictEqual(Polymer.dom(element.root).
-          querySelectorAll('.placeholder').length, 0);
+        element.permittedLabels = {
+          'Code-Review': [
+            ' 0',
+            '+1',
+          ],
+          'Verified': [
+            '-2',
+            '-1',
+            ' 0',
+            '+1',
+            '+2',
+          ],
+        };
+        flush(() => {
+          assert.strictEqual(element.$.labelSelector
+              .items.length, 5);
+          assert.strictEqual(
+              Polymer.dom(element.root).querySelectorAll('.placeholder').length,
+              0);
+          done();
+        });
+      });
     });
 
     test('default_value', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
index 7dd4c76..c607a9f 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-label-score-row/gr-label-score-row.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -28,6 +28,9 @@
         text-align: center;
         width: 100%;
       }
+      gr-label-score-row.no-access {
+        display: var(--label-no-access-display, initial);
+      }
       @media only screen and (max-width: 25em) {
         :host {
           text-align: center;
@@ -36,6 +39,7 @@
     </style>
     <template is="dom-repeat" items="[[_labels]]" as="label">
       <gr-label-score-row
+          class$="[[_computeLabelAccessClass(label.name, permittedLabels)]]"
           label="[[label]]"
           name="[[label.name]]"
           labels="[[change.labels]]"
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
index eaf39bc..dffba3e 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
@@ -19,7 +19,7 @@
 
   Polymer({
     is: 'gr-label-scores',
-    _legacyUndefinedCheck: true,
+
     properties: {
       _labels: {
         type: Array,
@@ -82,7 +82,12 @@
       return null;
     },
 
-    _computeLabels(labelRecord) {
+    _computeLabels(labelRecord, account) {
+      // Polymer 2: check for undefined
+      if ([labelRecord, account].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       const labelsObj = labelRecord.base;
       if (!labelsObj) { return []; }
       return Object.keys(labelsObj).sort().map(key => {
@@ -115,5 +120,19 @@
     _changeIsMerged(changeStatus) {
       return changeStatus === 'MERGED';
     },
+
+    /**
+     * @param {string|undefined} label
+     * @param {Object|undefined} permittedLabels
+     * @return {string}
+     */
+    _computeLabelAccessClass(label, permittedLabels) {
+      if (label == null || permittedLabels == null) {
+        return '';
+      }
+
+      return permittedLabels.hasOwnProperty(label) &&
+        permittedLabels[label].length ? 'access' : 'no-access';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
index 187c0a6..b8d471c 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-label-scores</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-label-scores.html">
 
@@ -135,6 +137,25 @@
       });
     });
 
+    test('_computeLabelAccessClass undefined case', () => {
+      assert.strictEqual(
+          element._computeLabelAccessClass(undefined, undefined), '');
+      assert.strictEqual(
+          element._computeLabelAccessClass('', undefined), '');
+      assert.strictEqual(
+          element._computeLabelAccessClass(undefined, {}), '');
+    });
+
+    test('_computeLabelAccessClass has access', () => {
+      assert.strictEqual(
+          element._computeLabelAccessClass('foo', {foo: ['']}), 'access');
+    });
+
+    test('_computeLabelAccessClass no access', () => {
+      assert.strictEqual(
+          element._computeLabelAccessClass('zap', {foo: ['']}), 'no-access');
+    });
+
     test('changes in label score are reflected in _labels', () => {
       element.change = {
         _number: '123',
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index d3a46fe..8317e2f 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -15,9 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-icon/iron-icon.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../shared/gr-account-label/gr-account-label.html">
+<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
@@ -36,16 +38,17 @@
         display: block;
         position: relative;
         cursor: pointer;
+        overflow-y: hidden;
       }
       :host(.expanded) {
         cursor: auto;
       }
       :host > div {
-        padding: 0 var(--default-horizontal-margin);
+        padding: 0 var(--spacing-l);
       }
       gr-avatar {
         position: absolute;
-        left: var(--default-horizontal-margin);
+        left: var(--spacing-l);
       }
       .collapsed .contentContainer {
         align-items: baseline;
@@ -54,11 +57,11 @@
         white-space: nowrap;
       }
       .contentContainer {
-        margin-left: calc(var(--default-horizontal-margin) + 2.5em);
-        padding: 10px 0;
+        margin-left: calc(var(--spacing-l) + 2.5em);
+        padding: var(--spacing-m) 0;
       }
       .showAvatar.collapsed .contentContainer {
-        margin-left: calc(var(--default-horizontal-margin) + 1.75em);
+        margin-left: calc(var(--spacing-l) + 1.75em);
       }
       .hideAvatar.collapsed .contentContainer,
       .hideAvatar.expanded .contentContainer {
@@ -67,17 +70,17 @@
       .showAvatar.collapsed .contentContainer,
       .hideAvatar.collapsed .contentContainer,
       .hideAvatar.expanded .contentContainer {
-        padding: .75em 0;
+        padding: var(--spacing-m) 0;
       }
       .collapsed gr-avatar {
-        top: .5em;
-        height: 1.75em;
-        width: 1.75em;
+        top: var(--spacing-m);
+        height: var(--line-height-normal);
+        width: var(--line-height-normal);
       }
       .expanded gr-avatar {
-        top: 12px;
-        height: 2.5em;
-        width: 2.5em;
+        top: var(--spacing-l);
+        height: var(--line-height-h1);
+        width: var(--line-height-h1);
       }
       .name {
         font-weight: var(--font-weight-bold);
@@ -111,7 +114,7 @@
       }
       .collapsed .content {
         flex: 1;
-        margin-right: .25em;
+        margin-right: var(--spacing-xs);
         min-width: 0;
         overflow: hidden;
         text-overflow: ellipsis;
@@ -122,15 +125,15 @@
       .collapsed .author {
         overflow: hidden;
         color: var(--primary-text-color);
-        margin-right: .4em;
+        margin-right: var(--spacing-s);
       }
       .expanded .author {
         cursor: pointer;
-        margin-bottom: .4em;
+        margin-bottom: var(--spacing-s);
       }
       .dateContainer {
         position: absolute;
-        right: var(--default-horizontal-margin);
+        right: var(--spacing-l);
         top: 10px;
       }
       span.date {
@@ -141,17 +144,18 @@
       }
       .dateContainer iron-icon {
         cursor: pointer;
+        vertical-align: top;
       }
       .replyContainer {
-        padding: .5em 0 0 0;
+        padding: var(--spacing-m) 0 0 0;
       }
       .score {
         border: 1px solid rgba(0,0,0,.12);
-        border-radius: 3px;
+        border-radius: var(--border-radius);
         color: var(--primary-text-color);
         display: inline-block;
-        margin: -.1em 0;
-        padding: 0 .1em;
+        margin: -1px 0;
+        padding: 0 var(--spacing-xxs);
       }
       .score.negative {
         background-color: var(--vote-color-disliked);
@@ -174,7 +178,7 @@
     <div class$="[[_computeClass(_expanded, showAvatar, message)]]">
       <gr-avatar account="[[author]]" image-size="100"></gr-avatar>
       <div class="contentContainer">
-        <div class="author" on-tap="_handleAuthorTap">
+        <div class="author" on-click="_handleAuthorClick">
           <span hidden$="[[!showOnBehalfOf]]">
             <span class="name">[[message.real_author.name]]</span>
             on behalf of
@@ -198,7 +202,7 @@
                 config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
             <template is="dom-if" if="[[_expanded]]">
               <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
-                <gr-button link small on-tap="_handleReplyTap">Reply</gr-button>
+                <gr-button link small on-click="_handleReplyTap">Reply</gr-button>
               </div>
               <gr-comment-list
                   comments="[[comments]]"
@@ -233,7 +237,7 @@
             </span>
           </template>
           <template is="dom-if" if="[[message.id]]">
-            <span class="date" on-tap="_handleAnchorTap">
+            <span class="date" on-click="_handleAnchorClick">
               <gr-date-formatter
                   has-tooltip
                   show-date-and-time
@@ -242,7 +246,7 @@
           </template>
           <iron-icon
               id="expandToggle"
-              on-tap="_toggleExpanded"
+              on-click="_toggleExpanded"
               title="Toggle expanded state"
               icon="[[_computeExpandToggleIcon(_expanded)]]">
         </span>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 7be1c5d..06bbc93 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-message',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when this message's reply link is tapped.
@@ -37,7 +36,7 @@
      */
 
     listeners: {
-      tap: '_handleTap',
+      click: '_handleClick',
     },
 
     properties: {
@@ -103,6 +102,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     observers: [
       '_updateExpandedClass(message.expanded)',
     ],
@@ -139,7 +142,7 @@
     },
 
     _computeShowReplyButton(message, loggedIn) {
-      return !!message.message && loggedIn &&
+      return message && !!message.message && loggedIn &&
           !this._computeIsAutomated(message);
     },
 
@@ -147,13 +150,13 @@
       return expanded;
     },
 
-    _handleTap(e) {
+    _handleClick(e) {
       if (this.message.expanded) { return; }
       e.stopPropagation();
       this.set('message.expanded', true);
     },
 
-    _handleAuthorTap(e) {
+    _handleAuthorClick(e) {
       if (!this.message.expanded) { return; }
       e.stopPropagation();
       this.set('message.expanded', false);
@@ -187,6 +190,10 @@
     },
 
     _computeScoreClass(score, labelExtremes) {
+      // Polymer 2: check for undefined
+      if ([score, labelExtremes].some(arg => arg === undefined)) {
+        return '';
+      }
       const classes = [];
       if (score.value > 0) {
         classes.push('positive');
@@ -212,10 +219,11 @@
       return classes.join(' ');
     },
 
-    _handleAnchorTap(e) {
+    _handleAnchorClick(e) {
       e.preventDefault();
       this.dispatchEvent(new CustomEvent('message-anchor-tap', {
         bubbles: true,
+        composed: true,
         detail: {id: this.message.id},
       }));
     },
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 5664429..a90e3ab 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-message</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-message.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
index 80708a1..d71beb4 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../gr-message/gr-message.html">
@@ -38,10 +38,10 @@
         display: flex;
         justify-content: space-between;
         min-height: 3.2em;
-        padding: .5em var(--default-horizontal-margin);
+        padding: var(--spacing-s) var(--spacing-l);
       }
       #messageControlsContainer {
-        padding: 0 var(--default-horizontal-margin);
+        padding: 0 var(--spacing-l);
       }
       .highlighted {
         animation: 3s fadeOut;
@@ -58,7 +58,7 @@
         justify-content: center;
       }
       #messageControlsContainer gr-button {
-        padding: 0.4em 0;
+        padding: var(--spacing-s) 0;
       }
       .container {
         align-items: center;
@@ -78,14 +78,14 @@
         <gr-button
             id="collapse-messages"
             link
-            on-tap="_handleExpandCollapseTap">
+            on-click="_handleExpandCollapseTap">
           [[_computeExpandCollapseMessage(_expanded)]]
         </gr-button>
       </div>
     <span
         id="messageControlsContainer"
         hidden$="[[_computeShowHideTextHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
-      <gr-button id="oldMessagesBtn" link on-tap="_handleShowAllTap">
+      <gr-button id="oldMessagesBtn" link on-click="_handleShowAllTap">
           [[_computeNumMessagesText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
       </gr-button>
       <span
@@ -93,7 +93,7 @@
           hidden$="[[_computeIncrementHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
         <span class="transparent separator"></span>
         <gr-button id="incrementMessagesBtn" link
-            on-tap="_handleIncrementShownMessages">
+            on-click="_handleIncrementShownMessages">
           [[_computeIncrementText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
         </gr-button>
       </span>
@@ -109,7 +109,7 @@
           hide-automated="[[_hideAutomated]]"
           project-name="[[projectName]]"
           show-reply-button="[[showReplyButtons]]"
-          on-message-anchor-tap="_handleAnchorTap"
+          on-message-anchor-tap="_handleAnchorClick"
           label-extremes="[[_labelExtremes]]"
           data-message-id$="[[message.id]]"></gr-message>
     </template>
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 d1cd08d..5652eca 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
@@ -27,7 +27,6 @@
 
   Polymer({
     is: 'gr-messages-list',
-    _legacyUndefinedCheck: true,
 
     properties: {
       changeNum: Number,
@@ -117,6 +116,11 @@
     },
 
     _computeItems(messages, reviewerUpdates) {
+      // Polymer 2: check for undefined
+      if ([messages, reviewerUpdates].some(arg => arg === undefined)) {
+        return [];
+      }
+
       messages = messages || [];
       reviewerUpdates = reviewerUpdates || [];
       let mi = 0;
@@ -156,10 +160,18 @@
     },
 
     _expandedChanged(exp) {
-      for (let i = 0; i < this._processedMessages.length; i++) {
-        this._processedMessages[i].expanded = exp;
-        if (i < this._visibleMessages.length) {
-          this.set(['_visibleMessages', i, 'expanded'], exp);
+      if (this._processedMessages) {
+        for (let i = 0; i < this._processedMessages.length; i++) {
+          this._processedMessages[i].expanded = exp;
+        }
+      }
+      // _visibleMessages is a subarray of _processedMessages
+      // _processedMessages contains all items from _visibleMessages
+      // At this point all _visibleMessages.expanded values are set,
+      // and notifyPath must be used to notify Polymer about changes.
+      if (this._visibleMessages) {
+        for (let i = 0; i < this._visibleMessages.length; i++) {
+          this.notifyPath(`_visibleMessages.${i}.expanded`);
         }
       }
     },
@@ -190,7 +202,7 @@
       this.handleExpandCollapse(!this._expanded);
     },
 
-    _handleAnchorTap(e) {
+    _handleAnchorClick(e) {
       this.scrollToMessage(e.detail.id);
     },
 
@@ -220,6 +232,9 @@
      * @return {!Object} Hash of arrays of comments, filename as key.
      */
     _computeCommentsForMessage(changeComments, message) {
+      if ([changeComments, message].some(arg => arg === undefined)) {
+        return [];
+      }
       const comments = changeComments.getAllPublishedComments();
       if (message._index === undefined || !comments || !this.messages) {
         return [];
@@ -268,8 +283,13 @@
      * more visible messages in the list.
      */
     _getDelta(visibleMessages, messages, hideAutomated) {
+      if ([visibleMessages, messages].some(arg => arg === undefined)) {
+        return 0;
+      }
+
       let delta = MESSAGES_INCREMENT;
       const msgsRemaining = messages.length - visibleMessages.length;
+
       if (hideAutomated) {
         let counter = 0;
         let i;
@@ -286,6 +306,10 @@
      * exist in _visibleMessages.
      */
     _numRemaining(visibleMessages, messages, hideAutomated) {
+      if ([visibleMessages, messages].some(arg => arg === undefined)) {
+        return 0;
+      }
+
       if (hideAutomated) {
         return this._getHumanMessages(messages).length -
             this._getHumanMessages(visibleMessages).length;
@@ -308,6 +332,10 @@
 
     _computeShowHideTextHidden(visibleMessages, messages,
         hideAutomated) {
+      if ([visibleMessages, messages].some(arg => arg === undefined)) {
+        return 0;
+      }
+
       if (hideAutomated) {
         messages = this._getHumanMessages(messages);
         visibleMessages = this._getHumanMessages(visibleMessages);
@@ -331,7 +359,9 @@
     },
 
     _processedMessagesChanged(messages) {
-      this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES);
+      if (messages) {
+        this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES);
+      }
     },
 
     _computeNumMessagesText(visibleMessages, messages,
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index 9572de4..315403e 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-messages-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
index 30ebc08..696ffdf 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -14,8 +14,9 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
@@ -29,7 +30,7 @@
         display: block;
       }
       h3 {
-        margin: .5em 0 0;
+        margin: var(--spacing-m) 0 0;
       }
       section {
         margin-bottom: 1.4em; /* Same as line height for collapse purposes */
@@ -74,7 +75,7 @@
       .status {
         color: var(--deemphasized-text-color);
         font-weight: var(--font-weight-bold);
-        margin-left: .25em;
+        margin-left: var(--spacing-xs);
       }
       .notCurrent {
         color: #e65100;
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 07f6e20..9103f4f 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-related-changes-list',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a new section is loaded so that the change view can determine
@@ -78,6 +77,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.RESTClientBehavior,
     ],
@@ -121,7 +121,7 @@
       ];
 
       // Get conflicts if change is open and is mergeable.
-      if (this.changeIsOpen(this.change.status) && this.mergeable) {
+      if (this.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.
@@ -208,6 +208,9 @@
 
     _computeChangeContainerClass(currentChange, relatedChange) {
       const classes = ['changeContainer'];
+      if ([relatedChange, currentChange].some(arg => arg === undefined)) {
+        return classes;
+      }
       if (this._changesEqual(relatedChange, currentChange)) {
         classes.push('thisChange');
       }
@@ -244,6 +247,9 @@
      * @return {number}
      */
     _getChangeNumber(change) {
+      // Default to 0 if change property is not defined.
+      if (!change) return 0;
+
       if (change.hasOwnProperty('_change_number')) {
         return change._change_number;
       }
@@ -294,6 +300,17 @@
 
     _resultsChanged(related, submittedTogether, conflicts,
         cherryPicks, sameTopic) {
+      // Polymer 2: check for undefined
+      if ([
+        related,
+        submittedTogether,
+        conflicts,
+        cherryPicks,
+        sameTopic,
+      ].some(arg => arg === undefined)) {
+        return;
+      }
+
       const results = [
         related && related.changes,
         submittedTogether && submittedTogether.changes,
@@ -316,8 +333,14 @@
     },
 
     _computeConnectedRevisions(change, patchNum, relatedChanges) {
+      // Polymer 2: check for undefined
+      if ([change, patchNum, relatedChanges].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       const connected = [];
       let changeRevision;
+      if (!change) { return []; }
       for (const rev in change.revisions) {
         if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
           changeRevision = rev;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index c82bc31..f04d40e 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-related-changes-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-related-changes-list.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index 692cb93..3632348 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reply-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html">
@@ -118,18 +120,18 @@
       // resolving.
       sandbox.stub(element, '_purgeReviewersPendingRemove');
 
-      element.$$('#ccs').$.entry.setText('test');
+      element.$.ccs.$.entry.setText('test');
       MockInteractions.tap(element.$$('gr-button.send'));
       assert.isFalse(sendStub.called);
       flushAsynchronousOperations();
 
-      element.$$('#ccs').$.entry.setText('test@test.test');
+      element.$.ccs.$.entry.setText('test@test.test');
       MockInteractions.tap(element.$$('gr-button.send'));
       assert.isTrue(sendStub.called);
     });
 
     test('lgtm plugin', done => {
-      Gerrit._resetPlugins();
+      Gerrit._testOnly_resetPlugins();
       const pluginHost = fixture('plugin-host');
       pluginHost.config = {
         plugin: {
@@ -149,7 +151,8 @@
           flush(() => {
             const textarea = element.$.textarea.getNativeTextarea();
             textarea.value = 'LGTM';
-            textarea.dispatchEvent(new CustomEvent('input', {bubbles: true}));
+            textarea.dispatchEvent(new CustomEvent(
+                'input', {bubbles: true, composed: true}));
             const labelScoreRows = Polymer.dom(element.$.labelScores.root)
                 .querySelector('gr-label-score-row[name="Code-Review"]');
             const selectedBtn = Polymer.dom(labelScoreRows.root)
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index f8d43cb..e836ccc 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -15,12 +15,13 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
@@ -31,9 +32,12 @@
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../gr-account-list/gr-account-list.html">
+<link rel="import" href="../../shared/gr-account-list/gr-account-list.html">
 <link rel="import" href="../gr-label-scores/gr-label-scores.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../change/gr-comment-list/gr-comment-list.html">
+<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
 
 <dom-module id="gr-reply-dialog">
   <template>
@@ -57,7 +61,7 @@
       section {
         border-top: 1px solid var(--border-color);
         flex-shrink: 0;
-        padding: .5em 1.5em;
+        padding: var(--spacing-m) var(--spacing-xl);
         width: 100%;
       }
       .actions {
@@ -70,7 +74,7 @@
         z-index: 1;
       }
       .actions .right gr-button {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
       }
       .peopleContainer,
       .labelsContainer {
@@ -82,13 +86,13 @@
       }
       .peopleList {
         display: flex;
-        padding-top: .1em;
+        padding-top: var(--spacing-xxs);
       }
       .peopleListLabel {
         color: var(--deemphasized-text-color);
-        margin-top: .2em;
+        margin-top: var(--spacing-xs);
         min-width: 7em;
-        padding-right: .5em;
+        padding-right: var(--spacing-m);
       }
       gr-account-list {
         display: flex;
@@ -97,11 +101,11 @@
         min-height: 1.8em;
       }
       #reviewerConfirmationOverlay {
-        padding: 1em;
+        padding: var(--spacing-l);
         text-align: center;
       }
       .reviewerConfirmationButtons {
-        margin-top: 1em;
+        margin-top: var(--spacing-l);
       }
       .groupName {
         font-weight: var(--font-weight-bold);
@@ -124,14 +128,14 @@
       }
       .previewContainer gr-formatted-text {
         background: var(--table-header-background-color);
-        padding: 1em;
+        padding: var(--spacing-l);
       }
       .draftsContainer h3 {
-        margin-top: .25em;
+        margin-top: var(--spacing-xs);
       }
       #checkingStatusLabel,
       #notLatestLabel {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
       }
       #checkingStatusLabel {
         color: var(--deemphasized-text-color);
@@ -149,8 +153,8 @@
       }
       #pluginMessage {
         color: var(--deemphasized-text-color);
-        margin-left: 1em;
-        margin-bottom: .5em;
+        margin-left: var(--spacing-l);
+        margin-bottom: var(--spacing-m);
       }
       #pluginMessage:empty {
         display: none;
@@ -164,11 +168,11 @@
               id="reviewers"
               accounts="{{_reviewers}}"
               removable-values="[[change.removable_reviewers]]"
-              change="[[change]]"
               filter="[[filterReviewerSuggestion]]"
               pending-confirmation="{{_reviewerPendingConfirmation}}"
               placeholder="Add reviewer..."
-              on-account-text-changed="_handleAccountTextEntry">
+              on-account-text-changed="_handleAccountTextEntry"
+              suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
           </gr-account-list>
         </div>
         <div class="peopleList">
@@ -176,12 +180,12 @@
           <gr-account-list
               id="ccs"
               accounts="{{_ccs}}"
-              change="[[change]]"
               filter="[[filterCCSuggestion]]"
               pending-confirmation="{{_ccPendingConfirmation}}"
               allow-any-input
               placeholder="Add CC..."
-              on-account-text-changed="_handleAccountTextEntry">
+              on-account-text-changed="_handleAccountTextEntry"
+              suggestions-provider="[[_getCcSuggestionsProvider(change)]]">
           </gr-account-list>
         </div>
         <gr-overlay
@@ -201,8 +205,8 @@
             Are you sure you want to add them all?
           </div>
           <div class="reviewerConfirmationButtons">
-            <gr-button on-tap="_confirmPendingReviewer">Yes</gr-button>
-            <gr-button on-tap="_cancelPendingReviewer">No</gr-button>
+            <gr-button on-click="_confirmPendingReviewer">Yes</gr-button>
+            <gr-button on-click="_cancelPendingReviewer">No</gr-button>
           </div>
         </gr-overlay>
       </section>
@@ -273,7 +277,7 @@
                 class="action save"
                 has-tooltip
                 title="[[_saveTooltip]]"
-                on-tap="_saveTapHandler">Save</gr-button>
+                on-click="_saveTapHandler">Save</gr-button>
           </template>
           <span
               id="checkingStatusLabel"
@@ -284,7 +288,7 @@
               id="notLatestLabel"
               hidden$="[[!_isState(knownLatestState, 'not-latest')]]">
             [[_computePatchSetWarning(patchNum, _labelsChanged)]]
-            <gr-button link on-tap="_reload">Reload</gr-button>
+            <gr-button link on-click="_reload">Reload</gr-button>
           </span>
         </div>
         <div class="right">
@@ -292,7 +296,7 @@
               link
               id="cancelButton"
               class="action cancel"
-              on-tap="_cancelTapHandler">Cancel</gr-button>
+              on-click="_cancelTapHandler">Cancel</gr-button>
           <gr-button
               id="sendButton"
               link
@@ -301,7 +305,7 @@
               class="action send"
               has-tooltip
               title$="[[_computeSendButtonTooltip(canBeStarted)]]"
-              on-tap="_sendTapHandler">[[_sendButtonLabel]]</gr-button>
+              on-click="_sendTapHandler">[[_sendButtonLabel]]</gr-button>
         </div>
       </section>
     </div>
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 43f5e8f..da19e62 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
@@ -48,17 +48,12 @@
     SEND: 'Send reply',
   };
 
-  // TODO(logan): Remove once the fix for issue 6841 is stable on
-  // googlesource.com.
-  const START_REVIEW_MESSAGE = 'This change is ready for review.';
-
   const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.';
 
   const SEND_REPLY_TIMING_LABEL = 'SendReply';
 
   Polymer({
     is: 'gr-reply-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a reply is successfully sent.
@@ -220,12 +215,9 @@
 
     FocusTarget,
 
-    // TODO(logan): Remove once the fix for issue 6841 is stable on
-    // googlesource.com.
-    START_REVIEW_MESSAGE,
-
     behaviors: [
       Gerrit.BaseUrlBehavior,
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.RESTClientBehavior,
@@ -314,31 +306,37 @@
     },
 
     _ccsChanged(splices) {
-      if (splices && splices.indexSplices) {
-        this._reviewersMutated = true;
-        this._processReviewerChange(splices.indexSplices, ReviewerTypes.CC);
-      }
+      this._reviewerTypeChanged(splices, ReviewerTypes.CC);
     },
 
     _reviewersChanged(splices) {
+      this._reviewerTypeChanged(splices, ReviewerTypes.REVIEWER);
+    },
+
+    _reviewerTypeChanged(splices, reviewerType) {
       if (splices && splices.indexSplices) {
         this._reviewersMutated = true;
         this._processReviewerChange(splices.indexSplices,
-            ReviewerTypes.REVIEWER);
+            reviewerType);
         let key;
         let index;
         let account;
-        // Remove any accounts that already exist as a CC.
+        // Remove any accounts that already exist as a CC for reviewer
+        // or vice versa.
+        const isReviewer = ReviewerTypes.REVIEWER === reviewerType;
         for (const splice of splices.indexSplices) {
-          for (const addedKey of splice.addedKeys) {
-            account = this.get(`_reviewers.${addedKey}`);
+          for (let i = 0; i < splice.addedCount; i++) {
+            account = splice.object[splice.index + i];
             key = this._accountOrGroupKey(account);
-            index = this._ccs.findIndex(
+            const array = isReviewer ? this._ccs : this._reviewers;
+            index = array.findIndex(
                 account => this._accountOrGroupKey(account) === key);
             if (index >= 0) {
-              this.splice('_ccs', index, 1);
+              this.splice(isReviewer ? '_ccs' : '_reviewers', index, 1);
+              const moveFrom = isReviewer ? 'CC' : 'reviewer';
+              const moveTo = isReviewer ? 'reviewer' : 'CC';
               const message = (account.name || account.email || key) +
-                  ' moved from CC to reviewer.';
+                  ` moved from ${moveFrom} to ${moveTo}.`;
               this.fire('show-alert', {message});
             }
           }
@@ -390,9 +388,6 @@
      *
      * @param {!Object} account
      * @param {string} type
-     *
-     * * TODO(beckysiegel) submit Polymer PR
-     * @suppress {checkTypes}
      */
     _removeAccount(account, type) {
       if (account._pendingAdd) { return; }
@@ -404,7 +399,7 @@
         const reviewers = this.change.reviewers[type] || [];
         for (let i = 0; i < reviewers.length; i++) {
           if (reviewers[i]._account_id == account._account_id) {
-            this.splice(['change', 'reviewers', type], i, 1);
+            this.splice(`change.reviewers.${type}`, i, 1);
             break;
           }
         }
@@ -447,7 +442,7 @@
         }
         return this._mapReviewer(reviewer);
       });
-      const ccsEl = this.$$('#ccs');
+      const ccsEl = this.$.ccs;
       if (ccsEl) {
         for (let reviewer of ccsEl.additions()) {
           if (reviewer.account) {
@@ -461,13 +456,6 @@
 
       this.disabled = true;
 
-      if (obj.ready && !obj.message) {
-        // TODO(logan): The server currently doesn't send email in this case.
-        // Insert a dummy message to force an email to be sent. Remove this
-        // once the fix for issue 6841 is stable on googlesource.com.
-        obj.message = START_REVIEW_MESSAGE;
-      }
-
       const errFn = this._handle400Error.bind(this);
       return this._saveReview(obj, errFn).then(response => {
         if (!response) {
@@ -480,18 +468,10 @@
           return {};
         }
 
-        // TODO(logan): Remove once the required API changes are live and stable
-        // on googlesource.com.
-        return this._maybeSetReady(startReview, response).catch(err => {
-          // We catch error here because we still want to treat this as a
-          // successful review.
-          console.error('error setting ready:', err);
-        }).then(() => {
-          this.draft = '';
-          this._includeComments = true;
-          this.fire('send', null, {bubbles: false});
-          return accountAdditions;
-        });
+        this.draft = '';
+        this._includeComments = true;
+        this.fire('send', null, {bubbles: false});
+        return accountAdditions;
       }).then(result => {
         this.disabled = false;
         return result;
@@ -501,32 +481,6 @@
       });
     },
 
-    /**
-     * Returns a promise resolving to true if review was successfully posted,
-     * false otherwise.
-     *
-     * TODO(logan): Remove this once the required API changes are live and
-     * stable on googlesource.com.
-     */
-    _maybeSetReady(startReview, response) {
-      return this.$.restAPI.getResponseObject(response).then(result => {
-        if (!startReview || result.ready) {
-          return Promise.resolve();
-        }
-        // We don't have confirmation that review was started, so attempt to
-        // start review explicitly.
-        return this.$.restAPI.startReview(
-            this.change._number, null, response => {
-              // If we see a 409 response code, then that means the server
-              // *does* support moving from WIP->ready when posting a
-              // review. Only alert user for non-409 failures.
-              if (response.status !== 409) {
-                this.fire('server-error', {response});
-              }
-            });
-      });
-    },
-
     _focusOn(section) {
       // Safeguard- always want to focus on something.
       if (!section || section === FocusTarget.ANY) {
@@ -540,7 +494,7 @@
         const reviewerEntry = this.$.reviewers.focusStart;
         reviewerEntry.async(reviewerEntry.focus);
       } else if (section === FocusTarget.CCS) {
-        const ccEntry = this.$$('#ccs').focusStart;
+        const ccEntry = this.$.ccs.focusStart;
         ccEntry.async(ccEntry.focus);
       }
     },
@@ -571,31 +525,31 @@
       //
       this.disabled = false;
 
-      if (response.status !== 400) {
-        // This is all restAPI does when there is no custom error handling.
-        this.fire('server-error', {response});
-        return response;
-      }
-
-      // Process the response body, format a better error message, and fire
-      // an event for gr-event-manager to display.
-      const jsonPromise = this.$.restAPI.getResponseObject(response);
+      // Using response.clone() here, because getResponseObject() and
+      // potentially the generic error handler will want to call text() on the
+      // response object, which can only be done once per object.
+      const jsonPromise = this.$.restAPI.getResponseObject(response.clone());
       return jsonPromise.then(result => {
-        const errors = [];
-        for (const state of ['reviewers', 'ccs']) {
-          if (!result.hasOwnProperty(state)) { continue; }
-          for (const reviewer of Object.values(result[state])) {
-            if (reviewer.error) {
-              errors.push(reviewer.error);
+        // Only perform custom error handling for 400s and a parseable
+        // ReviewResult response.
+        if (response.status === 400 && result) {
+          const errors = [];
+          for (const state of ['reviewers', 'ccs']) {
+            if (!result.hasOwnProperty(state)) { continue; }
+            for (const reviewer of Object.values(result[state])) {
+              if (reviewer.error) {
+                errors.push(reviewer.error);
+              }
             }
           }
+          response = {
+            ok: false,
+            status: response.status,
+            text() { return Promise.resolve(errors.join(', ')); },
+          };
         }
-        response = {
-          ok: false,
-          status: response.status,
-          text() { return Promise.resolve(errors.join(', ')); },
-        };
         this.fire('server-error', {response});
+        return null; // Means that the error has been handled.
       });
     },
 
@@ -622,6 +576,11 @@
     },
 
     _changeUpdated(changeRecord, owner) {
+      // Polymer 2: check for undefined
+      if ([changeRecord, owner].some(arg => arg === undefined)) {
+        return;
+      }
+
       this._rebuildReviewerArrays(changeRecord.base, owner);
     },
 
@@ -712,7 +671,7 @@
 
     _saveTapHandler(e) {
       e.preventDefault();
-      if (!this.$$('#ccs').submitEntryText()) {
+      if (!this.$.ccs.submitEntryText()) {
         // Do not proceed with the save if there is an invalid email entry in
         // the text field of the CC entry.
         return;
@@ -728,7 +687,7 @@
     },
 
     _submit() {
-      if (!this.$$('#ccs').submitEntryText()) {
+      if (!this.$.ccs.submitEntryText()) {
         // Do not proceed with the send if there is an invalid email entry in
         // the text field of the CC entry.
         return;
@@ -736,6 +695,7 @@
       if (this._sendDisabled) {
         this.dispatchEvent(new CustomEvent('show-alert', {
           bubbles: true,
+          composed: true,
           detail: {message: EMPTY_REPLY_MESSAGE},
         }));
         return;
@@ -743,6 +703,13 @@
       return this.send(this._includeComments, this.canBeStarted)
           .then(keepReviewers => {
             this._purgeReviewersPendingRemove(false, keepReviewers);
+          })
+          .catch(err => {
+            this.dispatchEvent(new CustomEvent('show-error', {
+              bubbles: true,
+              composed: true,
+              detail: {message: `Error submitting review ${err}`},
+            }));
           });
     },
 
@@ -763,7 +730,7 @@
 
     _confirmPendingReviewer() {
       if (this._ccPendingConfirmation) {
-        this.$$('#ccs').confirmGroup(this._ccPendingConfirmation.group);
+        this.$.ccs.confirmGroup(this._ccPendingConfirmation.group);
         this._focusOn(FocusTarget.CCS);
       } else {
         this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group);
@@ -831,7 +798,8 @@
 
     _reload() {
       // Load the current change without any patch range.
-      location.href = this.getBaseUrl() + '/c/' + this.change._number;
+      Gerrit.Nav.navigateToChange(this.change);
+      this.cancel();
     },
 
     _computeSendButtonLabel(canBeStarted) {
@@ -848,6 +816,19 @@
 
     _computeSendButtonDisabled(buttonLabel, drafts, text, reviewersMutated,
         labelsChanged, includeComments, disabled) {
+      // Polymer 2: check for undefined
+      if ([
+        buttonLabel,
+        drafts,
+        text,
+        reviewersMutated,
+        labelsChanged,
+        includeComments,
+        disabled,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       if (disabled) { return true; }
       if (buttonLabel === ButtonLabels.START_REVIEW) { return false; }
       const hasDrafts = includeComments && Object.keys(drafts).length;
@@ -869,5 +850,19 @@
     _sendDisabledChanged(sendDisabled) {
       this.dispatchEvent(new CustomEvent('send-disabled-changed'));
     },
+
+    _getReviewerSuggestionsProvider(change) {
+      const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
+          change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
+      provider.init();
+      return provider;
+    },
+
+    _getCcSuggestionsProvider(change) {
+      const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
+          change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC);
+      provider.init();
+      return provider;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 5577c1b..d8d49cf 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reply-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-reply-dialog.html">
 
@@ -33,6 +35,25 @@
 </test-fixture>
 
 <script>
+  function cloneableResponse(status, text) {
+    return {
+      ok: false,
+      status,
+      text() {
+        return Promise.resolve(text);
+      },
+      clone() {
+        return {
+          ok: false,
+          status,
+          text() {
+            return Promise.resolve(text);
+          },
+        };
+      },
+    };
+  }
+
   suite('gr-reply-dialog tests', () => {
     let element;
     let changeNum;
@@ -256,17 +277,19 @@
       });
     });
 
-    test('setlabelValue', () => {
+    test('setlabelValue', done => {
       element._account = {_account_id: 1};
-      flushAsynchronousOperations();
-      const label = 'Verified';
-      const value = '+1';
-      element.setLabelValue(label, value);
-      flushAsynchronousOperations();
-      const labels = element.$.labelScores.getLabelValues();
-      assert.deepEqual(labels, {
-        'Code-Review': 0,
-        'Verified': 1,
+      flush(() => {
+        const label = 'Verified';
+        const value = '+1';
+        element.setLabelValue(label, value);
+
+        const labels = element.$.labelScores.getLabelValues();
+        assert.deepEqual(labels, {
+          'Code-Review': 0,
+          'Verified': 1,
+        });
+        done();
       });
     });
 
@@ -289,6 +312,26 @@
       });
     }
 
+    function isFocusInsideElement(element) {
+      // In Polymer 2 focused element either <paper-input> or nested
+      // native input <input> element depending on the current focus
+      // in browser window.
+      // For example, the focus is changed if the developer console
+      // get a focus.
+      let activeElement = getActiveElement();
+      while (activeElement) {
+        if (activeElement === element) {
+          return true;
+        }
+        if (activeElement.parentElement) {
+          activeElement = activeElement.parentElement;
+        } else {
+          activeElement = activeElement.getRootNode().host;
+        }
+      }
+      return false;
+    }
+
     function testConfirmationDialog(done, cc) {
       const yesButton =
           element.$$('.reviewerConfirmationButtons gr-button:first-child');
@@ -342,10 +385,11 @@
         assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
 
         // We should be focused on account entry input.
-        assert.equal(getActiveElement().id, 'input');
+        assert.isTrue(
+            isFocusInsideElement(element.$.reviewers.$.entry.$.input.$.input));
 
         // No reviewer/CC should have been added.
-        assert.equal(element.$$('#ccs').additions().length, 0);
+        assert.equal(element.$.ccs.additions().length, 0);
         assert.equal(element.$.reviewers.additions().length, 0);
 
         // Reopen confirmation dialog.
@@ -370,7 +414,7 @@
       }).then(() => {
         assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
         const additions = cc ?
-          element.$$('#ccs').additions() :
+          element.$.ccs.additions() :
           element.$.reviewers.additions();
         assert.deepEqual(
             additions,
@@ -387,7 +431,13 @@
             ]);
 
         // We should be focused on account entry input.
-        assert.equal(getActiveElement().id, 'input');
+        if (cc) {
+          assert.isTrue(
+              isFocusInsideElement(element.$.ccs.$.entry.$.input.$.input));
+        } else {
+          assert.isTrue(
+              isFocusInsideElement(element.$.reviewers.$.entry.$.input.$.input));
+        }
       }).then(done);
     }
 
@@ -409,10 +459,10 @@
     test('_reviewersMutated when account-text-change is fired from ccs', () => {
       flushAsynchronousOperations();
       assert.isFalse(element._reviewersMutated);
-      assert.isTrue(element.$$('#ccs').allowAnyInput);
+      assert.isTrue(element.$.ccs.allowAnyInput);
       assert.isFalse(element.$$('#reviewers').allowAnyInput);
-      element.$$('#ccs').dispatchEvent(new CustomEvent('account-text-changed',
-          {bubbles: true}));
+      element.$.ccs.dispatchEvent(new CustomEvent('account-text-changed',
+          {bubbles: true, composed: true}));
       assert.isTrue(element._reviewersMutated);
     });
 
@@ -471,11 +521,7 @@
       sandbox.stub(window, 'fetch', () => {
         const text = '....{"reviewers":{"id1":{"error":"first error"}},' +
           '"ccs":{"id2":{"error":"second error"}}}';
-        return Promise.resolve({
-          ok: false,
-          status: 400,
-          text() { return Promise.resolve(text); },
-        });
+        return Promise.resolve(cloneableResponse(400, text));
       });
 
       element.addEventListener('server-error', event => {
@@ -493,6 +539,27 @@
       flush(() => { element.send(); });
     });
 
+    test('non-json 400 is treated as a normal server-error', done => {
+      sandbox.stub(window, 'fetch', () => {
+        const text = 'Comment validation error!';
+        return Promise.resolve(cloneableResponse(400, text));
+      });
+
+      element.addEventListener('server-error', event => {
+        if (event.target !== element) {
+          return;
+        }
+        event.detail.response.text().then(body => {
+          assert.equal(body, 'Comment validation error!');
+          done();
+        });
+      });
+
+      // Async tick is needed because iron-selector content is distributed and
+      // distributed content requires an observer to be set up.
+      flush(() => { element.send(); });
+    });
+
     test('filterReviewerSuggestion', () => {
       const owner = makeAccount();
       const reviewer1 = makeAccount();
@@ -528,7 +595,7 @@
       const textareaStub = sandbox.stub(element.$.textarea, 'async');
       const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart,
           'async');
-      const ccStub = sandbox.stub(element.$$('#ccs').focusStart, 'async');
+      const ccStub = sandbox.stub(element.$.ccs.focusStart, 'async');
       element._focusOn();
       assert.equal(element._chooseFocusTarget.callCount, 1);
       assert.deepEqual(textareaStub.callCount, 1);
@@ -695,6 +762,40 @@
       assert.deepEqual(element._reviewersPendingRemove.CC, [cc1, cc4, cc3]);
     });
 
+    test('moving from reviewer to cc', () => {
+      element._reviewersPendingRemove = {
+        CC: [],
+        REVIEWER: [],
+      };
+      flushAsynchronousOperations();
+
+      const reviewer1 = makeAccount();
+      const reviewer2 = makeAccount();
+      const reviewer3 = makeAccount();
+      const cc1 = makeAccount();
+      const cc2 = makeAccount();
+      const cc3 = makeAccount();
+      const cc4 = makeAccount();
+      element._reviewers = [reviewer1, reviewer2, reviewer3];
+      element._ccs = [cc1, cc2, cc3, cc4];
+      element.push('_ccs', reviewer1);
+      flushAsynchronousOperations();
+
+      assert.deepEqual(element._reviewers,
+          [reviewer2, reviewer3]);
+      assert.deepEqual(element._ccs, [cc1, cc2, cc3, cc4, reviewer1]);
+      assert.deepEqual(element._reviewersPendingRemove.REVIEWER, [reviewer1]);
+
+      element.push('_ccs', reviewer3, reviewer2);
+      flushAsynchronousOperations();
+
+      assert.deepEqual(element._reviewers, []);
+      assert.deepEqual(element._ccs,
+          [cc1, cc2, cc3, cc4, reviewer1, reviewer3, reviewer2]);
+      assert.deepEqual(element._reviewersPendingRemove.REVIEWER,
+          [reviewer1, reviewer3, reviewer2]);
+    });
+
     test('migrate reviewers between states', done => {
       element._reviewersPendingRemove = {
         CC: [],
@@ -702,7 +803,7 @@
       };
       flushAsynchronousOperations();
       const reviewers = element.$.reviewers;
-      const ccs = element.$$('#ccs');
+      const ccs = element.$.ccs;
       const reviewer1 = makeAccount();
       const reviewer2 = makeAccount();
       const cc1 = makeAccount();
@@ -801,60 +902,50 @@
       const error1 = 'error 1';
       const error2 = 'error 2';
       const error3 = 'error 3';
-      const response = {
-        status: 400,
-        text() {
-          return Promise.resolve(')]}\'' + JSON.stringify({
-            reviewers: {
-              username1: {
-                input: 'user 1',
-                error: error1,
-              },
-              username2: {
-                input: 'user 2',
-                error: error2,
-              },
-            },
-            ccs: {
-              username3: {
-                input: 'user 3',
-                error: error3,
-              },
-            },
-          }));
+      const text = ')]}\'' + JSON.stringify({
+        reviewers: {
+          username1: {
+            input: 'user 1',
+            error: error1,
+          },
+          username2: {
+            input: 'user 2',
+            error: error2,
+          },
         },
-      };
+        ccs: {
+          username3: {
+            input: 'user 3',
+            error: error3,
+          },
+        },
+      });
       element.addEventListener('server-error', e => {
         e.detail.response.text().then(text => {
           assert.equal(text, [error1, error2, error3].join(', '));
           done();
         });
       });
-      element._handle400Error(response);
+      element._handle400Error(cloneableResponse(400, text));
     });
 
     test('_handle400Error CCs only', done => {
       const error1 = 'error 1';
-      const response = {
-        status: 400,
-        text() {
-          return Promise.resolve(')]}\'' + JSON.stringify({
-            ccs: {
-              username1: {
-                input: 'user 1',
-                error: error1,
-              },
-            },
-          }));
+      const text = ')]}\'' + JSON.stringify({
+        ccs: {
+          username1: {
+            input: 'user 1',
+            error: error1,
+          },
         },
-      };
+      });
       element.addEventListener('server-error', e => {
         e.detail.response.text().then(text => {
           assert.equal(text, error1);
           done();
         });
       });
-      element._handle400Error(response);
+      element._handle400Error(cloneableResponse(400, text));
     });
 
     test('fires height change when the drafts load', done => {
@@ -898,21 +989,6 @@
           assert.isFalse(startReviewStub.called);
         });
       });
-
-      test('fall back to start review against old backend', () => {
-        stubSaveReview(review => {
-          return {}; // old backend won't set ready: true
-        });
-
-        return element.send(true, true).then(() => {
-          assert.isTrue(startReviewStub.called);
-        }).then(() => {
-          startReviewStub.reset();
-          return element.send(true, false);
-        }).then(() => {
-          assert.isFalse(startReviewStub.called);
-        });
-      });
     });
 
     suite('start review and save buttons', () => {
@@ -938,28 +1014,9 @@
       });
     });
 
-    test('dummy message to force email on start review', () => {
-      stubSaveReview(review => {
-        assert.equal(review.message, element.START_REVIEW_MESSAGE);
-        return {ready: true};
-      });
-      return element.send(true, true);
-    });
-
     test('buttons disabled until all API calls are resolved', () => {
       stubSaveReview(review => {
-        return {}; // old backend won't set ready: true
-      });
-      // Check that element is disabled asynchronously after the setReady
-      // promise is returned. The element should not be reenabled until
-      // that promise is resolved.
-      sandbox.stub(element, '_maybeSetReady', (startReview, response) => {
-        return new Promise(resolve => {
-          Polymer.Base.async(() => {
-            assert.isTrue(element.disabled);
-            resolve();
-          });
-        });
+        return {ready: true};
       });
       return element.send(true, true).then(() => {
         assert.isFalse(element.disabled);
@@ -979,11 +1036,6 @@
         assert.isFalse(element.disabled);
       }
 
-      function assertDialogClosed() {
-        assert.strictEqual('', element.draft);
-        assert.isFalse(element.disabled);
-      }
-
       test('error occurs in _saveReview', () => {
         stubSaveReview(review => {
           throw expectedError;
@@ -994,46 +1046,6 @@
         });
       });
 
-      test('error occurs during startReview', () => {
-        stubSaveReview(review => {
-          return {}; // old backend won't set ready: true
-        });
-        const errorStub = sandbox.stub(
-            console, 'error', (msg, err) => undefined);
-        sandbox.stub(element.$.restAPI, 'startReview', () => {
-          throw expectedError;
-        });
-        return element.send(true, true).then(() => {
-          assertDialogClosed();
-          assert.isTrue(
-              errorStub.calledWith('error setting ready:', expectedError));
-        });
-      });
-
-      test('non-ok response received by startReview', () => {
-        stubSaveReview(review => {
-          return {}; // old backend won't set ready: true
-        });
-        sandbox.stub(element.$.restAPI, 'startReview', (c, b, f) => {
-          f({status: 500});
-        });
-        return element.send(true, true).then(() => {
-          assertDialogClosed();
-        });
-      });
-
-      test('409 response received by startReview', () => {
-        stubSaveReview(review => {
-          return {}; // old backend won't set ready: true
-        });
-        sandbox.stub(element.$.restAPI, 'startReview', (c, b, f) => {
-          f({status: 409});
-        });
-        return element.send(true, true).then(() => {
-          assertDialogClosed();
-        });
-      });
-
       suite('pending diff drafts?', () => {
         test('yes', () => {
           const promise = mockPromise();
@@ -1065,20 +1077,84 @@
 
     test('_computeSendButtonDisabled', () => {
       const fn = element._computeSendButtonDisabled.bind(element);
-      assert.isFalse(fn('Start review'));
-      assert.isTrue(fn('Send', {}, '', false, false, false));
+      assert.isFalse(fn(
+          /* buttonLabel= */ 'Start review',
+          /* drafts= */ {},
+          /* text= */ '',
+          /* reviewersMutated= */ false,
+          /* labelsChanged= */ false,
+          /* includeComments= */ false,
+          /* disabled= */ false
+      ));
+      assert.isTrue(fn(
+          /* buttonLabel= */ 'Send',
+          /* drafts= */ {},
+          /* text= */ '',
+          /* reviewersMutated= */ false,
+          /* labelsChanged= */ false,
+          /* includeComments= */ false,
+          /* disabled= */ false
+      ));
       // Mock nonempty comment draft array, with seding comments.
-      assert.isFalse(fn('Send', {file: ['draft']}, '', false, false, true));
+      assert.isFalse(fn(
+          /* buttonLabel= */ 'Send',
+          /* drafts= */ {file: ['draft']},
+          /* text= */ '',
+          /* reviewersMutated= */ false,
+          /* labelsChanged= */ false,
+          /* includeComments= */ true,
+          /* disabled= */ false
+      ));
       // Mock nonempty comment draft array, without seding comments.
-      assert.isTrue(fn('Send', {file: ['draft']}, '', false, false, false));
+      assert.isTrue(fn(
+          /* buttonLabel= */ 'Send',
+          /* drafts= */ {file: ['draft']},
+          /* text= */ '',
+          /* reviewersMutated= */ false,
+          /* labelsChanged= */ false,
+          /* includeComments= */ false,
+          /* disabled= */ false
+      ));
       // Mock nonempty change message.
-      assert.isFalse(fn('Send', {}, 'test', false, false, false));
+      assert.isFalse(fn(
+          /* buttonLabel= */ 'Send',
+          /* drafts= */ {},
+          /* text= */ 'test',
+          /* reviewersMutated= */ false,
+          /* labelsChanged= */ false,
+          /* includeComments= */ false,
+          /* disabled= */ false
+      ));
       // Mock reviewers mutated.
-      assert.isFalse(fn('Send', {}, '', true, false, false));
+      assert.isFalse(fn(
+          /* buttonLabel= */ 'Send',
+          /* drafts= */ {},
+          /* text= */ '',
+          /* reviewersMutated= */ true,
+          /* labelsChanged= */ false,
+          /* includeComments= */ false,
+          /* disabled= */ false
+      ));
       // Mock labels changed.
-      assert.isFalse(fn('Send', {}, '', false, true, false));
+      assert.isFalse(fn(
+          /* buttonLabel= */ 'Send',
+          /* drafts= */ {},
+          /* text= */ '',
+          /* reviewersMutated= */ false,
+          /* labelsChanged= */ true,
+          /* includeComments= */ false,
+          /* disabled= */ false
+      ));
       // Whole dialog is disabled.
-      assert.isTrue(fn('Send', {}, '', false, true, false, true));
+      assert.isTrue(fn(
+          /* buttonLabel= */ 'Send',
+          /* drafts= */ {},
+          /* text= */ '',
+          /* reviewersMutated= */ false,
+          /* labelsChanged= */ true,
+          /* includeComments= */ false,
+          /* disabled= */ true
+      ));
     });
 
     test('_submit blocked when no mutations exist', () => {
@@ -1092,7 +1168,7 @@
       MockInteractions.tap(element.$$('gr-button.send'));
       assert.isFalse(sendStub.called);
 
-      element.diffDrafts = {test: true};
+      element.diffDrafts = {test: [{val: true}]};
       flushAsynchronousOperations();
 
       MockInteractions.tap(element.$$('gr-button.send'));
@@ -1104,7 +1180,7 @@
       // computed to false.
       element.diffDrafts = {};
       assert.equal(element.getFocusStops().end, element.$.cancelButton);
-      element.diffDrafts = {test: true};
+      element.diffDrafts = {test: [{val: true}]};
       assert.equal(element.getFocusStops().end, element.$.sendButton);
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 73e8bea..a5875ab1 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -32,53 +32,35 @@
         opacity: .8;
         pointer-events: none;
       }
-      .autocompleteContainer {
-        position: relative;
-      }
-      .hiddenReviewers {
-        margin-top: .3em;
-      }
-      .inputContainer {
-        display: flex;
-        margin-top: .25em;
-      }
-      .inputContainer input {
-        flex: 1;
-        font: inherit;
-      }
-      gr-account-chip {
-        margin-top: .3em;
+      .container > :not(:first-child) {
+        margin-top: var(--spacing-s);
       }
       gr-button {
         --gr-button: {
-          padding-left: 0;
-          padding-right: 0;
-        }
-      }
-      @media screen and (max-width: 50em), screen and (min-width: 75em) {
-        gr-account-chip:first-of-type {
-          margin-top: 0;
+          padding: 0px 0px;
         }
       }
     </style>
-    <template is="dom-repeat" items="[[_displayedReviewers]]" as="reviewer">
-      <gr-account-chip class="reviewer" account="[[reviewer]]"
-          on-remove="_handleRemove"
-          additional-text="[[_computeReviewerTooltip(reviewer, change)]]"
-          removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]">
-      </gr-account-chip>
-    </template>
-    <gr-button
-        class="hiddenReviewers"
-        link
-        hidden$="[[!_hiddenReviewerCount]]"
-        on-tap="_handleViewAll">and [[_hiddenReviewerCount]] more</gr-button>
-    <div class="controlsContainer" hidden$="[[!mutable]]">
+    <div class="container">
+      <template is="dom-repeat" items="[[_displayedReviewers]]" as="reviewer">
+        <gr-account-chip class="reviewer" account="[[reviewer]]"
+            on-remove="_handleRemove"
+            additional-text="[[_computeReviewerTooltip(reviewer, change)]]"
+            removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]">
+        </gr-account-chip>
+      </template>
       <gr-button
+          class="hiddenReviewers"
           link
-          id="addReviewer"
-          class="addReviewer"
-          on-tap="_handleAddTap">[[_addLabel]]</gr-button>
+          hidden$="[[!_hiddenReviewerCount]]"
+          on-click="_handleViewAll">and [[_hiddenReviewerCount]] more</gr-button>
+      <div class="controlsContainer" hidden$="[[!mutable]]">
+        <gr-button
+            link
+            id="addReviewer"
+            class="addReviewer"
+            on-click="_handleAddTap">[[_addLabel]]</gr-button>
+      </div>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index 4f1b051..c94625d 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-reviewer-list',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the "Add reviewer..." button is tapped.
@@ -75,6 +74,10 @@
       _xhrPromise: Object,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     observers: [
       '_reviewersChanged(change.reviewers.*, change.owner)',
     ],
@@ -163,6 +166,11 @@
     },
 
     _reviewersChanged(changeRecord, owner) {
+      // Polymer 2: check for undefined
+      if ([changeRecord, owner].some(arg => arg === undefined)) {
+        return;
+      }
+
       let result = [];
       const reviewers = changeRecord.base;
       for (const key in reviewers) {
@@ -180,10 +188,10 @@
         return reviewer._account_id != owner._account_id;
       });
 
-      // If there is one more than the max reviewers, don't show the 'show
-      // more' button, because it takes up just as much space.
+      // If there is one or two more than the max reviewers, don't show the
+      // 'show more' button, because it takes up just as much space.
       if (this.maxReviewersDisplayed &&
-          this._reviewers.length > this.maxReviewersDisplayed + 1) {
+          this._reviewers.length > this.maxReviewersDisplayed + 2) {
         this._displayedReviewers =
           this._reviewers.slice(0, this.maxReviewersDisplayed);
       } else {
@@ -192,6 +200,11 @@
     },
 
     _computeHiddenCount(reviewers, displayedReviewers) {
+      // Polymer 2: check for undefined
+      if ([reviewers, displayedReviewers].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       return reviewers.length - displayedReviewers.length;
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 81827fd..6936a04 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reviewer-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-reviewer-list.html">
 
@@ -212,10 +214,10 @@
       assert.isTrue(element.$$('.hiddenReviewers').hidden);
     });
 
-    test('show all reviewers button with 7 reviewers', () => {
+    test('show all reviewers button with 8 reviewers', () => {
       const reviewers = [];
       element.maxReviewersDisplayed = 5;
-      for (let i = 0; i < 7; i++) {
+      for (let i = 0; i < 8; i++) {
         reviewers.push(
             {email: i+'reviewer@google.com', name: 'reviewer-' + i});
       }
@@ -229,9 +231,9 @@
           CC: reviewers,
         },
       };
-      assert.equal(element._hiddenReviewerCount, 2);
+      assert.equal(element._hiddenReviewerCount, 3);
       assert.equal(element._displayedReviewers.length, 5);
-      assert.equal(element._reviewers.length, 7);
+      assert.equal(element._reviewers.length, 8);
       assert.isFalse(element.$$('.hiddenReviewers').hidden);
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
index 4d8e5ae..4c82d2a 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html">
 
@@ -26,11 +26,11 @@
       #threads {
         display: block;
         min-height: 20rem;
-        padding: 1rem;
+        padding: var(--spacing-l);
       }
       gr-comment-thread {
         display: block;
-        margin-bottom: .5rem;
+        margin-bottom: var(--spacing-m);
         max-width: 80ch;
       }
       .header {
@@ -41,7 +41,7 @@
         display: flex;
         justify-content: left;
         min-height: 3.2em;
-        padding: .5em var(--default-horizontal-margin);
+        padding: var(--spacing-m) var(--spacing-l);
       }
       .toggleItem.draftToggle {
         display: none;
@@ -52,7 +52,7 @@
       .toggleItem {
         align-items: center;
         display: flex;
-        margin-right: 1rem;
+        margin-right: var(--spacing-l);
       }
       .draftsOnly:not(.unresolvedOnly) gr-comment-thread[has-draft],
       .unresolvedOnly:not(.draftsOnly) gr-comment-thread[unresolved],
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index 317685b..747a47a 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -25,7 +25,6 @@
 
   Polymer({
     is: 'gr-thread-list',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
@@ -95,12 +94,35 @@
     },
 
     _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly) {
+      // Polymer 2: check for undefined
+      if ([
+        sortedThreads,
+        unresolvedOnly,
+        draftsOnly,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       return sortedThreads.filter(c => {
         if (draftsOnly) {
           return c.hasDraft;
         } else if (unresolvedOnly) {
           return c.unresolved;
         } else {
+          const comments = c && c.thread && c.thread.comments;
+          let robotComment = false;
+          let humanReplyToRobotComment = false;
+          comments.forEach(comment => {
+            if (comment.robot_id) {
+              robotComment = true;
+            } else if (robotComment) {
+              // Robot comment exists and human comment exists after it
+              humanReplyToRobotComment = true;
+            }
+          });
+          if (robotComment) {
+            return humanReplyToRobotComment ? c : false;
+          }
           return c;
         }
       }).map(threadInfo => threadInfo.thread);
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
index 792644e..ff65aa8 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-thread-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-thread-list.html">
 
@@ -168,6 +170,68 @@
           rootId: 'zcf0b9fa_fe1a5f62',
           start_datetime: '2018-02-09 18:49:18.000000000',
         },
+        {
+          comments: [
+            {
+              __path: '/COMMIT_MSG',
+              author: {
+                _account_id: 1000000,
+                name: 'user',
+                username: 'user',
+              },
+              patch_set: 4,
+              id: 'rc1',
+              line: 5,
+              updated: '2019-02-08 18:49:18.000000000',
+              message: 'test',
+              unresolved: true,
+              robot_id: 'rc1',
+            },
+          ],
+          patchNum: 4,
+          path: '/COMMIT_MSG',
+          line: 5,
+          rootId: 'rc1',
+          start_datetime: '2019-02-08 18:49:18.000000000',
+        },
+        {
+          comments: [
+            {
+              __path: '/COMMIT_MSG',
+              author: {
+                _account_id: 1000000,
+                name: 'user',
+                username: 'user',
+              },
+              patch_set: 4,
+              id: 'rc2',
+              line: 5,
+              updated: '2019-03-08 18:49:18.000000000',
+              message: 'test',
+              unresolved: true,
+              robot_id: 'rc2',
+            },
+            {
+              __path: '/COMMIT_MSG',
+              author: {
+                _account_id: 1000000,
+                name: 'user',
+                username: 'user',
+              },
+              patch_set: 4,
+              id: 'c2_1',
+              line: 5,
+              updated: '2019-03-08 18:49:18.000000000',
+              message: 'test',
+              unresolved: true,
+            },
+          ],
+          patchNum: 4,
+          path: '/COMMIT_MSG',
+          line: 5,
+          rootId: 'rc2',
+          start_datetime: '2019-03-08 18:49:18.000000000',
+        },
       ];
       flushAsynchronousOperations();
       threadElements = Polymer.dom(element.root)
@@ -192,46 +256,69 @@
     });
 
     test('_computeSortedThreads', () => {
-      assert.equal(element._sortedThreads.length, 5);
+      assert.equal(element._sortedThreads.length, 7);
       // Draft and unresolved
       assert.equal(element._sortedThreads[0].thread.rootId,
           'ecf0b9fa_fe1a5f62');
-      // unresolved
+      // Unresolved robot comment
       assert.equal(element._sortedThreads[1].thread.rootId,
+          'rc2');
+      // Unresolved robot comment
+      assert.equal(element._sortedThreads[2].thread.rootId,
+          'rc1');
+      // unresolved
+      assert.equal(element._sortedThreads[3].thread.rootId,
           'scaddf38_44770ec1');
       // unresolved
-      assert.equal(element._sortedThreads[2].thread.rootId,
+      assert.equal(element._sortedThreads[4].thread.rootId,
           '8caddf38_44770ec1');
       // resolved and draft
-      assert.equal(element._sortedThreads[3].thread.rootId,
+      assert.equal(element._sortedThreads[5].thread.rootId,
           'zcf0b9fa_fe1a5f62');
       // resolved
-      assert.equal(element._sortedThreads[4].thread.rootId,
+      assert.equal(element._sortedThreads[6].thread.rootId,
           '09a9fb0a_1484e6cf');
     });
 
+    test('filtered threads do not contain robot comments without reply', () => {
+      const thread = element.threads.find(thread => thread.rootId === 'rc1');
+      assert.equal(element._filteredThreads.includes(thread), false);
+    });
+
+    test('filtered threads contains robot comments with reply', () => {
+      const thread = element.threads.find(thread => thread.rootId === 'rc2');
+      assert.equal(element._filteredThreads.includes(thread), true);
+    });
+
+
     test('thread removal', () => {
-      threadElements[1].fire('thread-discard', {rootId: 'scaddf38_44770ec1'});
+      threadElements[1].fire('thread-discard', {rootId: 'rc2'});
       flushAsynchronousOperations();
-      assert.equal(element._sortedThreads.length, 4);
+      assert.equal(element._sortedThreads.length, 6);
       assert.equal(element._sortedThreads[0].thread.rootId,
           'ecf0b9fa_fe1a5f62');
-      // unresolved
+      // Unresolved robot comment
       assert.equal(element._sortedThreads[1].thread.rootId,
+          'rc1');
+      // unresolved
+      assert.equal(element._sortedThreads[2].thread.rootId,
+          'scaddf38_44770ec1');
+      // unresolved
+      assert.equal(element._sortedThreads[3].thread.rootId,
           '8caddf38_44770ec1');
       // resolved and draft
-      assert.equal(element._sortedThreads[2].thread.rootId,
+      assert.equal(element._sortedThreads[4].thread.rootId,
           'zcf0b9fa_fe1a5f62');
       // resolved
-      assert.equal(element._sortedThreads[3].thread.rootId,
+      assert.equal(element._sortedThreads[5].thread.rootId,
           '09a9fb0a_1484e6cf');
     });
 
-    test('toggle unresolved only shows unressolved comments', () => {
+    test('toggle unresolved only shows unresolved comments', () => {
       MockInteractions.tap(element.$.unresolvedToggle);
       flushAsynchronousOperations();
       assert.equal(Polymer.dom(element.root)
-          .querySelectorAll('gr-comment-thread').length, 3);
+          .querySelectorAll('gr-comment-thread').length, 5);
     });
 
     test('toggle drafts only shows threads with draft comments', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
index 792c300..e3cee56 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
@@ -32,11 +33,11 @@
         width: 100%;
       }
       ol {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
         list-style: decimal;
       }
       p {
-        margin-bottom: .75em;
+        margin-bottom: var(--spacing-m);
       }
     </style>
     <gr-dialog
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
index df96be2..092204a 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
@@ -29,7 +29,6 @@
 
   Polymer({
     is: 'gr-upload-help-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
@@ -58,6 +57,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     attached() {
       this.$.restAPI.getLoggedIn().then(loggedIn => {
         if (loggedIn) {
@@ -73,11 +76,21 @@
 
     _handleCloseTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('close', null, {bubbles: false});
     },
 
     _computeFetchCommand(revision, preferredDownloadCommand,
         preferredDownloadScheme) {
+      // Polymer 2: check for undefined
+      if ([
+        revision,
+        preferredDownloadCommand,
+        preferredDownloadScheme,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       if (!revision) { return; }
       if (!revision || !revision.fetch) { return; }
 
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
index a5a6e76..577b978 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-upload-help-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-upload-help-dialog.html">
 
@@ -73,31 +75,40 @@
         assert.isUndefined(element._computeFetchCommand({fetch: {}}));
       });
 
+      test('not all defined', () => {
+        assert.isUndefined(
+            element._computeFetchCommand(testRev, undefined, ''));
+        assert.isUndefined(
+            element._computeFetchCommand(testRev, '', undefined));
+        assert.isUndefined(
+            element._computeFetchCommand(undefined, '', ''));
+      });
+
       test('insufficiently defined scheme', () => {
         assert.isUndefined(
-            element._computeFetchCommand(testRev, undefined, 'badscheme'));
+            element._computeFetchCommand(testRev, '', 'badscheme'));
 
         const rev = Object.assign({}, testRev);
         rev.fetch = Object.assign({}, testRev.fetch, {nocmds: {commands: {}}});
         assert.isUndefined(
-            element._computeFetchCommand(rev, undefined, 'nocmds'));
+            element._computeFetchCommand(rev, '', 'nocmds'));
 
         rev.fetch.nocmds.commands.unsupported = 'unsupported';
         assert.isUndefined(
-            element._computeFetchCommand(rev, undefined, 'nocmds'));
+            element._computeFetchCommand(rev, '', 'nocmds'));
       });
 
       test('default scheme and command', () => {
-        const cmd = element._computeFetchCommand(testRev);
+        const cmd = element._computeFetchCommand(testRev, '', '');
         assert.isTrue(cmd === 'http checkout' || cmd === 'ssh pull');
       });
 
       test('default command', () => {
         assert.strictEqual(
-            element._computeFetchCommand(testRev, undefined, 'http'),
+            element._computeFetchCommand(testRev, '', 'http'),
             'http checkout');
         assert.strictEqual(
-            element._computeFetchCommand(testRev, undefined, 'ssh'),
+            element._computeFetchCommand(testRev, '', 'ssh'),
             'ssh pull');
       });
 
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 2c2fb4e..5152ef9 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -15,18 +15,19 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
 
 <dom-module id="gr-account-dropdown">
   <template>
     <style include="shared-styles">
       gr-dropdown {
-        padding: 0 .5em;
+        padding: 0 var(--spacing-m);
         --gr-button: {
           color: var(--header-text-color);
         }
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 af4510d..7cbe988 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
@@ -21,7 +21,6 @@
 
   Polymer({
     is: 'gr-account-dropdown',
-    _legacyUndefinedCheck: true,
 
     properties: {
       account: Object,
@@ -58,7 +57,7 @@
     },
 
     behaviors: [
-      Gerrit.AnonymousNameBehavior,
+      Gerrit.DisplayNameBehavior,
     ],
 
     detached() {
@@ -66,6 +65,11 @@
     },
 
     _getLinks(switchAccountUrl, path) {
+      // Polymer 2: check for undefined
+      if ([switchAccountUrl, path].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       const links = [{name: 'Settings', url: '/settings/'}];
       if (switchAccountUrl) {
         const replacements = {path};
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
index fe63a3e..e29faa8 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-dropdown</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-account-dropdown.html">
 
@@ -78,11 +80,15 @@
     });
 
     test('switch account', () => {
+      // Missing params.
+      assert.isUndefined(element._getLinks());
+      assert.isUndefined(element._getLinks(null));
+
       // No switch account link.
-      assert.equal(element._getLinks(null).length, 2);
+      assert.equal(element._getLinks(null, '').length, 2);
 
       // Unparameterized switch account link.
-      let links = element._getLinks('/switch-account');
+      let links = element._getLinks('/switch-account', '');
       assert.equal(links.length, 3);
       assert.deepEqual(links[1], {
         name: 'Switch account',
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html
index f8bf33c..09b928e 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
index 5679408..8d3b58e 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-error-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the dismiss button is pressed.
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
index e2c314b..648f8be 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-error-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-error-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
index 4ca106e..048d392 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
@@ -16,7 +16,8 @@
 -->
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../core/gr-error-dialog/gr-error-dialog.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../shared/gr-alert/gr-alert.html">
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 38dfecb..5865e3c 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
@@ -27,10 +27,10 @@
 
   Polymer({
     is: 'gr-error-manager',
-    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
+      Gerrit.FireBehavior,
     ],
 
     properties: {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index 88e3efd..9140c17 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-error-manager</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-error-manager.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html
index 2ff7953..a863276 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-key-binding-display">
@@ -24,10 +24,10 @@
       .key {
         background-color: var(--chip-background-color);
         border: 1px solid var(--border-color);
-        border-radius: 3px;
+        border-radius: var(--border-radius);
         display: inline-block;
         font-weight: var(--font-weight-bold);
-        padding: .1em .5em;
+        padding: var(--spacing-xxs) var(--spacing-m);
         text-align: center;
       }
     </style>
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
index e8c6479..89d1091 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-key-binding-display',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {Array<string>} */
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
index 0361d76..39c8af8 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-key-binding-display</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-key-binding-display.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index e3552cc..5494f62 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../gr-key-binding-display/gr-key-binding-display.html">
@@ -30,11 +31,11 @@
         overflow-y: auto;
       }
       header{
-        padding: 1em;
+        padding: var(--spacing-l);
       }
       main {
         display: flex;
-        padding: 0 2em 2em;
+        padding: 0 var(--spacing-xxl) var(--spacing-xxl);
       }
       header {
         align-items: center;
@@ -43,18 +44,18 @@
         justify-content: space-between;
       }
       table:last-of-type {
-        margin-left: 3em;
+        margin-left: var(--spacing-xxl);
       }
       td {
-        padding: .2em 0;
+        padding: var(--spacing-xs) 0;
       }
       td:first-child {
-        padding-right: .5em;
+        padding-right: var(--spacing-m);
         text-align: right;
       }
       .header {
         font-weight: var(--font-weight-bold);
-        padding-top: 1em;
+        padding-top: var(--spacing-l);
       }
       .modifier {
         font-weight: normal;
@@ -62,7 +63,7 @@
     </style>
     <header>
       <h3>Keyboard shortcuts</h3>
-      <gr-button link on-tap="_handleCloseTap">Close</gr-button>
+      <gr-button link on-click="_handleCloseTap">Close</gr-button>
     </header>
     <main>
       <table>
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 f206db1..4bc6e11 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
@@ -21,7 +21,6 @@
 
   Polymer({
     is: 'gr-keyboard-shortcuts-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
@@ -51,6 +50,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
     ],
 
@@ -70,6 +70,7 @@
 
     _handleCloseTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('close', null, {bubbles: false});
     },
 
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
index 50579dd..1a3d6c7 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-key-binding-display</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-keyboard-shortcuts-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index e552cda..d29858e 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -14,10 +14,11 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
@@ -45,7 +46,6 @@
       .bigTitle:hover {
         text-decoration: underline;
       }
-      /* TODO (viktard): Clean-up after chromium-style migrates to component. */
       .titleText::before {
         background-image: var(--header-icon);
         background-size: var(--header-icon-size) var(--header-icon-size);
@@ -62,7 +62,7 @@
       }
       ul {
         list-style: none;
-        padding-left: 1em;
+        padding-left: var(--spacing-l);
       }
       .links > li {
         cursor: default;
@@ -86,16 +86,16 @@
         justify-content: flex-end;
       }
       .rightItems gr-endpoint-decorator:not(:empty) {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
       }
       gr-smart-search {
         flex-grow: 1;
-        margin-left: .5em;
+        margin: 0 var(--spacing-m);
         max-width: 500px;
       }
       gr-dropdown,
       .browse {
-        padding: .6em .5em;
+        padding: var(--spacing-m);
       }
       gr-dropdown {
         --gr-dropdown-item: {
@@ -103,7 +103,7 @@
         }
       }
       .settingsButton {
-        margin-left: .5em;
+        margin-left: var(--spacing-m);
       }
       .browse {
         color: var(--header-text-color);
@@ -128,13 +128,13 @@
       .accountContainer {
         align-items: center;
         display: flex;
-        margin: 0 -.5em 0 .5em;
+        margin: 0 calc(0 - var(--spacing-m)) 0 var(--spacing-m);
         overflow: hidden;
         text-overflow: ellipsis;
         white-space: nowrap;
       }
       .loginButton, .registerButton {
-        padding: .5em 1em;
+        padding: var(--spacing-m) var(--spacing-l);
       }
       .dropdown-trigger {
         text-decoration: none;
@@ -160,7 +160,7 @@
       }
       @media screen and (max-width: 50em) {
         .bigTitle {
-          font-size: var(--font-size-large);
+          font-size: var(--font-size-h3);
           font-weight: var(--font-weight-bold);
         }
         gr-smart-search,
@@ -173,10 +173,10 @@
           display: inline-flex;
         }
         .accountContainer {
-          margin-left: .5em !important;
+          margin-left: var(--spacing-m) !important;
         }
         gr-dropdown {
-          padding: .5em 0 .5em .5em;
+          padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-m);
         }
       }
     </style>
@@ -188,16 +188,16 @@
       </a>
       <ul class="links">
         <template is="dom-repeat" items="[[_links]]" as="linkGroup">
-          <li class$="[[linkGroup.class]]">
-          <gr-dropdown
-              link
-              down-arrow
-              items = [[linkGroup.links]]
-              horizontal-align="left">
-            <span class="linksTitle" id="[[linkGroup.title]]">
-              [[linkGroup.title]]
-            </span>
-          </gr-dropdown>
+          <li class$="[[_computeLinkGroupClass(linkGroup)]]">
+            <gr-dropdown
+                link
+                down-arrow
+                items = [[linkGroup.links]]
+                horizontal-align="left">
+              <span class="linksTitle" id="[[linkGroup.title]]">
+                [[linkGroup.title]]
+              </span>
+            </gr-dropdown>
           </li>
         </template>
       </ul>
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 69fc89f..ab9b70e 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
@@ -71,7 +71,6 @@
 
   Polymer({
     is: 'gr-main-header',
-    _legacyUndefinedCheck: true,
 
     hostAttributes: {
       role: 'banner',
@@ -138,6 +137,7 @@
       Gerrit.AdminNavBehavior,
       Gerrit.BaseUrlBehavior,
       Gerrit.DocsUrlBehavior,
+      Gerrit.FireBehavior,
     ],
 
     observers: [
@@ -180,6 +180,17 @@
     },
 
     _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) {
+      // Polymer 2: check for undefined
+      if ([
+        defaultLinks,
+        userLinks,
+        adminLinks,
+        topMenus,
+        docBaseUrl,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       const links = defaultLinks.map(menu => {
         return {
           title: menu.title,
@@ -213,7 +224,7 @@
         });
         if (m.name in topMenuLinks) {
           items.forEach(link => { topMenuLinks[m.name].push(link); });
-        } else {
+        } else if (items.length > 0) {
           links.push({
             title: m.name,
             links: topMenuLinks[m.name] = items,
@@ -318,7 +329,16 @@
 
     _onMobileSearchTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('mobile-search', null, {bubbles: false});
     },
+
+    _computeLinkGroupClass(linkGroup) {
+      if (linkGroup && linkGroup.class) {
+        return linkGroup.class;
+      }
+
+      return '';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index 89b3908..3309aa5 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-main-header</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-main-header.html">
 
@@ -109,22 +111,34 @@
       }];
 
       // When no admin links are passed, it should use the default.
-      assert.deepEqual(element._computeLinks(defaultLinks, [], adminLinks, []),
-          defaultLinks.concat({
-            title: 'Browse',
-            links: adminLinks,
-          }));
-      assert.deepEqual(
-          element._computeLinks(defaultLinks, userLinks, adminLinks, []),
-          defaultLinks.concat([
-            {
-              title: 'Your',
-              links: userLinks,
-            },
-            {
-              title: 'Browse',
-              links: adminLinks,
-            }]));
+      assert.deepEqual(element._computeLinks(
+          defaultLinks,
+          /* userLinks= */[],
+          adminLinks,
+          /* topMenus= */[],
+          /* docBaseUrl= */ ''
+      ),
+      defaultLinks.concat({
+        title: 'Browse',
+        links: adminLinks,
+      }));
+      assert.deepEqual(element._computeLinks(
+          defaultLinks,
+          userLinks,
+          adminLinks,
+          /* topMenus= */[],
+          /* docBaseUrl= */ ''
+      ),
+      defaultLinks.concat([
+        {
+          title: 'Your',
+          links: userLinks,
+        },
+        {
+          title: 'Browse',
+          links: adminLinks,
+        }])
+      );
     });
 
     test('documentation links', () => {
@@ -166,7 +180,13 @@
           url: 'https://gerrit/plugins/plugin-manager/static/index.html',
         }],
       }];
-      assert.deepEqual(element._computeLinks([], [], adminLinks, topMenus), [{
+      assert.deepEqual(element._computeLinks(
+          /* defaultLinks= */ [],
+          /* userLinks= */ [],
+          adminLinks,
+          topMenus,
+          /* baseDocUrl= */ ''
+      ), [{
         title: 'Browse',
         links: adminLinks,
       },
@@ -196,7 +216,13 @@
           url: '/plugins/myplugin/index.html',
         }],
       }];
-      assert.deepEqual(element._computeLinks([], [], adminLinks, topMenus), [{
+      assert.deepEqual(element._computeLinks(
+          /* defaultLinks= */ [],
+          /* userLinks= */ [],
+          adminLinks,
+          topMenus,
+          /* baseDocUrl= */ ''
+      ), [{
         title: 'Browse',
         links: adminLinks,
       },
@@ -229,7 +255,13 @@
           url: 'https://gerrit/plugins/plugin-manager/static/create.html',
         }],
       }];
-      assert.deepEqual(element._computeLinks([], [], adminLinks, topMenus), [{
+      assert.deepEqual(element._computeLinks(
+          /* defaultLinks= */ [],
+          /* userLinks= */ [],
+          adminLinks,
+          topMenus,
+          /* baseDocUrl= */ ''
+      ), [{
         title: 'Browse',
         links: adminLinks,
       }, {
@@ -260,7 +292,13 @@
           url: 'https://gerrit/plugins/plugin-manager/static/index.html',
         }],
       }];
-      assert.deepEqual(element._computeLinks(defaultLinks, [], [], topMenus), [{
+      assert.deepEqual(element._computeLinks(
+          defaultLinks,
+          /* userLinks= */ [],
+          /* adminLinks= */ [],
+          topMenus,
+          /* baseDocUrl= */ ''
+      ), [{
         title: 'Faves',
         links: defaultLinks[0].links.concat([{
           name: 'Manage',
@@ -285,7 +323,13 @@
           url: 'https://gerrit/plugins/plugin-manager/static/index.html',
         }],
       }];
-      assert.deepEqual(element._computeLinks([], userLinks, [], topMenus), [{
+      assert.deepEqual(element._computeLinks(
+          /* defaultLinks= */ [],
+          userLinks,
+          /* adminLinks= */ [],
+          topMenus,
+          /* baseDocUrl= */ ''
+      ), [{
         title: 'Your',
         links: userLinks.concat([{
           name: 'Manage',
@@ -310,7 +354,13 @@
           url: 'https://gerrit/plugins/plugin-manager/static/index.html',
         }],
       }];
-      assert.deepEqual(element._computeLinks([], [], adminLinks, topMenus), [{
+      assert.deepEqual(element._computeLinks(
+          /* defaultLinks= */ [],
+          /* userLinks= */ [],
+          adminLinks,
+          topMenus,
+          /* baseDocUrl= */ ''
+      ), [{
         title: 'Browse',
         links: adminLinks.concat([{
           name: 'Manage',
@@ -348,4 +398,4 @@
       assert.equal(element._registerText, 'Sign up');
     });
   });
-</script>
+      </script>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index ce20127..c8bf6c1 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -115,6 +115,7 @@
         query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' +
             'is:open -is:ignored',
         hideIfEmpty: true,
+        suffixForDashboard: 'limit:25',
       },
       {
         // WIP open changes owned by viewing user. This section is omitted when
@@ -123,6 +124,7 @@
         query: 'is:open owner:${user} is:wip',
         selfOnly: true,
         hideIfEmpty: true,
+        suffixForDashboard: 'limit:25',
       },
       {
         // Non-WIP open changes owned by viewed user. Filter out changes ignored
@@ -130,6 +132,7 @@
         name: 'Outgoing reviews',
         query: 'is:open owner:${user} -is:wip -is:ignored',
         isOutgoing: true,
+        suffixForDashboard: 'limit:25',
       },
       {
         // Non-WIP open changes not owned by the viewed user, that the viewed user
@@ -138,12 +141,14 @@
         name: 'Incoming reviews',
         query: 'is:open -owner:${user} -is:wip -is:ignored ' +
             '(reviewer:${user} OR assignee:${user})',
+        suffixForDashboard: 'limit:25',
       },
       {
         // Non-WIP open changes the viewed user is CCed on. Changes ignored by the
         // viewing user are filtered out.
         name: 'CCed on',
         query: 'is:open -is:wip -is:ignored cc:${user}',
+        suffixForDashboard: 'limit:10',
       },
       {
         name: 'Recently closed',
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
index 2f72338..73ce86a 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-navigation</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector.js
deleted file mode 100644
index 28c46f4..0000000
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector.js
+++ /dev/null
@@ -1,61 +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.
- */
-(function() {
-  'use strict';
-
-  const JANK_SLEEP_TIME_MS = 1000;
-
-  const GrJankDetector = {
-    // Slowdowns counter.
-    jank: 0,
-    fps: 0,
-    _lastFrameTime: 0,
-
-    start() {
-      this._requestAnimationFrame(this._detect.bind(this));
-    },
-
-    _requestAnimationFrame(callback) {
-      window.requestAnimationFrame(callback);
-    },
-
-    _detect(now) {
-      if (this._lastFrameTime === 0) {
-        this._lastFrameTime = now;
-        this.fps = 0;
-        this._requestAnimationFrame(this._detect.bind(this));
-        return;
-      }
-      const fpsNow = 1000/(now - this._lastFrameTime);
-      this._lastFrameTime = now;
-      // Calculate moving average within last 3 measurements.
-      this.fps = this.fps === 0 ? fpsNow : ((this.fps * 2 + fpsNow) / 3);
-      if (this.fps > 10) {
-        this._requestAnimationFrame(this._detect.bind(this));
-      } else {
-        this.jank++;
-        console.warn('JANK', this.jank);
-        this._lastFrameTime = 0;
-        window.setTimeout(
-            () => this._requestAnimationFrame(this._detect.bind(this)),
-            JANK_SLEEP_TIME_MS);
-      }
-    },
-  };
-
-  window.GrJankDetector = GrJankDetector;
-})();
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector_test.html
deleted file mode 100644
index 6faeec1..0000000
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector_test.html
+++ /dev/null
@@ -1,78 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-jank-detector</title>
-
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<script src="gr-jank-detector.js"></script>
-
-<script>
-  suite('gr-jank-detector tests', () => {
-    let sandbox;
-    let clock;
-    let instance;
-
-    const NOW_TIME = 100;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      clock = sinon.useFakeTimers(NOW_TIME);
-      instance = GrJankDetector;
-      instance._lastFrameTime = 0;
-      sandbox.stub(instance, '_requestAnimationFrame');
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('start() installs frame callback', () => {
-      sandbox.stub(instance, '_detect');
-      instance._requestAnimationFrame.callsArg(0);
-      instance.start();
-      assert.isTrue(instance._detect.calledOnce);
-    });
-
-    test('measures fps', () => {
-      instance._detect(10);
-      instance._detect(30);
-      assert.equal(instance.fps, 50);
-    });
-
-    test('detects jank', () => {
-      let now = 10;
-      instance._detect(now);
-      const fastFrame = () => instance._detect(now += 20);
-      const slowFrame = () => instance._detect(now += 300);
-      fastFrame();
-      assert.equal(instance.jank, 0);
-      _.times(4, slowFrame);
-      assert.equal(instance.jank, 0);
-      instance._requestAnimationFrame.reset();
-      slowFrame();
-      assert.equal(instance.jank, 1);
-      assert.isFalse(instance._requestAnimationFrame.called);
-      clock.tick(1000);
-      assert.isTrue(instance._requestAnimationFrame.called);
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
index 935de6b..0ba8a22 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -15,9 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-reporting">
-  <script src="gr-jank-detector.js"></script>
   <script src="gr-reporting.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 0947d77..276c137 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -49,14 +49,6 @@
     STARTED_HIDDEN: 'hidden',
   };
 
-  // Frame rate related constants.
-  const JANK = {
-    TYPE: 'lifecycle',
-    CATEGORY: 'UI Latency',
-    // Reported events - alphabetize below.
-    COUNT: 'Jank count',
-  };
-
   // Navigation reporting constants.
   const NAVIGATION = {
     TYPE: 'nav-report',
@@ -78,24 +70,33 @@
     CHANGE_DISPLAYED: 'ChangeDisplayed',
     CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
     DASHBOARD_DISPLAYED: 'DashboardDisplayed',
+    DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent',
     DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
+    DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded',
     FILE_LIST_DISPLAYED: 'FileListDisplayed',
     PLUGINS_LOADED: 'PluginsLoaded',
     STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
     STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded',
     STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
+    STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent',
     STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
+    STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded',
     STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed',
     WEB_COMPONENTS_READY: 'WebComponentsReady',
+    METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded',
   };
 
   const STARTUP_TIMERS = {};
   STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0;
+  STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0;
   STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0;
   STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0;
   STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0;
+  STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0;
   STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
+  STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0;
   STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0;
+  STARTUP_TIMERS[TIMING.APP_STARTED] = 0;
   // WebComponentsReady timer is triggered from gr-router.
   STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0;
 
@@ -106,6 +107,9 @@
 
   const pending = [];
 
+  const loadedPlugins = [];
+  const detectedExtensions = [];
+
   const onError = function(oldOnError, msg, url, line, column, error) {
     if (oldOnError) {
       oldOnError(msg, url, line, column, error);
@@ -113,7 +117,13 @@
     if (error) {
       line = line || error.lineNumber;
       column = column || error.columnNumber;
-      msg = msg || error.toString();
+      let shortenedErrorStack = msg;
+      if (error.stack) {
+        const errorStackLines = error.stack.split('\n');
+        shortenedErrorStack = errorStackLines.slice(0,
+            Math.min(3, errorStackLines.length)).join('\n');
+      }
+      msg = shortenedErrorStack || error.toString();
     }
     const payload = {
       url,
@@ -138,13 +148,10 @@
   };
   catchErrors();
 
-  GrJankDetector.start();
-
   // The Polymer pass of JSCompiler requires this to be reassignable
   // eslint-disable-next-line prefer-const
   let GrReporting = Polymer({
     is: 'gr-reporting',
-    _legacyUndefinedCheck: true,
 
     properties: {
       category: String,
@@ -173,9 +180,15 @@
         !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED);
     },
 
+    _isMetricsPluginLoaded() {
+      return this._arePluginsLoaded() || this._baselines &&
+        !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED);
+    },
+
     reporter(...args) {
-      const report = (this._arePluginsLoaded() && !pending.length) ?
+      const report = (this._isMetricsPluginLoaded() && !pending.length) ?
         this.defaultReporter : this.cachingReporter;
+      args.splice(4, 0, loadedPlugins, detectedExtensions);
       report.apply(this, args);
     },
 
@@ -186,23 +199,33 @@
      * @param {string} category
      * @param {string} eventName
      * @param {string|number} eventValue
+     * @param {Array} plugins
+     * @param {Array} extensions
      * @param {boolean|undefined} opt_noLog If true, the event will not be
      *     logged to the JS console.
      */
-    defaultReporter(type, category, eventName, eventValue, opt_noLog) {
+    defaultReporter(type, category, eventName, eventValue,
+        loadedPlugins, detectedExtensions, opt_noLog) {
       const detail = {
         type,
         category,
         name: eventName,
         value: eventValue,
       };
+      if (category === TIMING.CATEGORY_UI_LATENCY) {
+        detail.loadedPlugins = loadedPlugins;
+        detail.detectedExtensions = detectedExtensions;
+      }
       document.dispatchEvent(new CustomEvent(type, {detail}));
       if (opt_noLog) { return; }
       if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
-        console.error(eventValue.error || eventName);
+        console.error(eventValue && eventValue.error || eventName);
       } else {
-        console.log(eventName + (eventValue !== undefined ?
-          (': ' + eventValue) : ''));
+        if (eventValue !== undefined) {
+          console.log(`Reporting: ${eventName}: ${eventValue}`);
+        } else {
+          console.log(`Reporting: ${eventName}`);
+        }
       }
     },
 
@@ -214,22 +237,27 @@
      * @param {string} category
      * @param {string} eventName
      * @param {string|number} eventValue
+     * @param {Array} plugins
+     * @param {Array} extensions
      * @param {boolean|undefined} opt_noLog If true, the event will not be
      *     logged to the JS console.
      */
-    cachingReporter(type, category, eventName, eventValue, opt_noLog) {
+    cachingReporter(type, category, eventName, eventValue,
+        plugins, extensions, opt_noLog) {
       if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
-        console.error(eventValue.error || eventName);
+        console.error(eventValue && eventValue.error || eventName);
       }
-      if (this._arePluginsLoaded()) {
+      if (this._isMetricsPluginLoaded()) {
         if (pending.length) {
           for (const args of pending.splice(0)) {
             this.reporter(...args);
           }
         }
-        this.reporter(type, category, eventName, eventValue, opt_noLog);
+        this.reporter(type, category, eventName, eventValue,
+            plugins, extensions, opt_noLog);
       } else {
-        pending.push([type, category, eventName, eventValue, opt_noLog]);
+        pending.push([type, category, eventName, eventValue,
+          plugins, extensions, opt_noLog]);
       }
     },
 
@@ -237,8 +265,7 @@
      * User-perceived app start time, should be reported when the app is ready.
      */
     appStarted(hidden) {
-      this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY,
-          TIMING.APP_STARTED, this.now());
+      this.timeEnd(TIMING.APP_STARTED);
       if (hidden) {
         this.reporter(PAGE_VISIBILITY.TYPE, PAGE_VISIBILITY.CATEGORY,
             PAGE_VISIBILITY.STARTED_HIDDEN);
@@ -261,18 +288,15 @@
     },
 
     beforeLocationChanged() {
-      if (GrJankDetector.jank > 0) {
-        this.reporter(
-            JANK.TYPE, JANK.CATEGORY, JANK.COUNT, GrJankDetector.jank);
-        GrJankDetector.jank = 0;
-      }
       for (const prop of Object.keys(this._baselines)) {
         delete this._baselines[prop];
       }
       this.time(TIMER.CHANGE_DISPLAYED);
       this.time(TIMER.CHANGE_LOAD_FULL);
       this.time(TIMER.DASHBOARD_DISPLAYED);
+      this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
       this.time(TIMER.DIFF_VIEW_DISPLAYED);
+      this.time(TIMER.DIFF_VIEW_LOAD_FULL);
       this.time(TIMER.FILE_LIST_DISPLAYED);
     },
 
@@ -313,6 +337,23 @@
       }
     },
 
+    diffViewFullyLoaded() {
+      if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
+        this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL);
+      } else {
+        this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
+      }
+    },
+
+    diffViewContentDisplayed() {
+      if (this._baselines.hasOwnProperty(
+          TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) {
+        this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED);
+      } else {
+        this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
+      }
+    },
+
     fileListDisplayed() {
       if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
         this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED);
@@ -323,6 +364,16 @@
 
     reportExtension(name) {
       this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name);
+      if (!detectedExtensions.includes(name)) {
+        detectedExtensions.push(name);
+      }
+    },
+
+    pluginLoaded(name) {
+      if (name.startsWith('metrics-')) {
+        this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
+      }
+      loadedPlugins.push(name);
     },
 
     pluginsLoaded(pluginsList) {
@@ -336,6 +387,7 @@
      */
     time(name) {
       this._baselines[name] = this.now();
+      window.performance.mark(`${name}-start`);
     },
 
     /**
@@ -344,8 +396,18 @@
     timeEnd(name) {
       if (!this._baselines.hasOwnProperty(name)) { return; }
       const baseTime = this._baselines[name];
-      this._reportTiming(name, this.now() - baseTime);
       delete this._baselines[name];
+      this._reportTiming(name, this.now() - baseTime);
+
+      // Finalize the interval. Either from a registered start mark or
+      // the navigation start time (if baseTime is 0).
+      if (baseTime !== 0) {
+        window.performance.measure(name, `${name}-start`);
+      } else {
+        // Microsft Edge does not handle the 2nd param correctly
+        // (if undefined).
+        window.performance.measure(name);
+      }
     },
 
     /**
@@ -465,7 +527,7 @@
 
     reportErrorDialog(message) {
       this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY,
-          'ErrorDialog: ' + message);
+          'ErrorDialog: ' + message, {error: new Error(message)});
     },
   });
 
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index 29e70ea..4c561a2 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reporting</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-reporting.html">
 
@@ -93,11 +95,7 @@
     test('beforeLocationChanged', () => {
       element._baselines['garbage'] = 'monster';
       sandbox.stub(element, 'time');
-      GrJankDetector.jank = 42;
       element.beforeLocationChanged();
-      assert.equal(GrJankDetector.jank, 0);
-      assert.isTrue(element.reporter.calledWithExactly(
-          'lifecycle', 'UI Latency', 'Jank count', 42));
       assert.isTrue(element.time.calledWithExactly('DashboardDisplayed'));
       assert.isTrue(element.time.calledWithExactly('ChangeDisplayed'));
       assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded'));
@@ -193,7 +191,6 @@
       assert.isTrue(element.reporter.calledOnce);
       assert.throws(() => {
         timer.end();
-        done();
       }, 'Timer for "foo-bar" already ended.');
     });
 
@@ -263,8 +260,8 @@
       test('pluginsLoaded reports time', () => {
         sandbox.stub(element, 'now').returns(42);
         element.pluginsLoaded();
-        assert.isTrue(element.defaultReporter.calledWithExactly(
-            'timing-report', 'UI Latency', 'PluginsLoaded', 42, undefined
+        assert.isTrue(element.defaultReporter.calledWith(
+            'timing-report', 'UI Latency', 'PluginsLoaded', 42
         ));
       });
 
@@ -285,6 +282,24 @@
         assert.isTrue(element.defaultReporter.called);
       });
 
+      test('reports plugins in timing events', () => {
+        element.pluginsLoaded = [];
+        sandbox.stub(element, 'now').returns(42);
+        element.pluginLoaded('metrics-xyz1');
+        // element.pluginLoaded('foo');
+        element.time('timeAction');
+        element.timeEnd('timeAction');
+        assert.isTrue(element.defaultReporter.getCall(1).calledWith(
+            'timing-report', 'UI Latency', 'timeAction', 0,
+            ['metrics-xyz1']
+        ));
+      });
+
+      test('reports if metrics plugin xyz is loaded', () => {
+        element.pluginLoaded('metrics-xyz');
+        assert.isTrue(element.defaultReporter.called);
+      });
+
       test('reports cached events preserving order', () => {
         element.time('foo');
         element.time('bar');
@@ -334,6 +349,7 @@
 
       test('is reported', () => {
         const error = new Error('bar');
+        error.stack = undefined;
         emulateThrow('bar', 'http://url', 4, 2, error);
         assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
         const payload = reporter.lastCall.args[3];
@@ -345,6 +361,15 @@
         });
       });
 
+      test('is reported with 3 lines of stack', () => {
+        const error = new Error('bar');
+        emulateThrow('bar', 'http://url', 4, 2, error);
+        const expectedStack = error.stack.split('\n').slice(0, 3)
+            .join('\n');
+        assert.isTrue(reporter.calledWith('error', 'exception',
+            expectedStack));
+      });
+
       test('prevent default event handler', () => {
         assert.isTrue(emulateThrow());
       });
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
index 68ddef6..71a5832 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -14,9 +14,10 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
@@ -28,6 +29,6 @@
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-reporting id="reporting"></gr-reporting>
   </template>
-  <script src="../../../bower_components/page/page.js"></script>
+  <script src="/bower_components/page/page.js"></script>
   <script src="gr-router.js"></script>
 </dom-module>
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 2e1f5b8..738a89b 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -200,7 +200,9 @@
     const reporting = document.createElement('gr-reporting');
 
     window.addEventListener('load', () => {
-      reporting.pageLoaded();
+      setTimeout(() => {
+        reporting.pageLoaded();
+      }, 0);
     });
 
     window.addEventListener('WebComponentsReady', () => {
@@ -210,7 +212,6 @@
 
   Polymer({
     is: 'gr-router',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _app: {
@@ -228,6 +229,7 @@
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
+      Gerrit.FireBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.URLEncodingBehavior,
     ],
@@ -238,7 +240,17 @@
     },
 
     _setParams(params) {
-      this._app.params = params;
+      this._appElement().params = params;
+    },
+
+    _appElement() {
+      // In Polymer2 you have to reach through the shadow root of the app
+      // element. This obviously breaks encapsulation.
+      // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element
+      // explicitly in app, or by delegating to it.
+      return document.getElementById('app-element') ||
+          document.getElementById('app').shadowRoot.getElementById(
+              'app-element');
     },
 
     _redirect(url) {
@@ -1404,9 +1416,7 @@
       }
     },
 
-    // TODO fix this so it properly redirects
-    // to /settings#Agreements (Scrolls down)
-    _handleAgreementsRoute(data) {
+    _handleAgreementsRoute() {
       this._redirect('/settings/#Agreements');
     },
 
@@ -1505,7 +1515,7 @@
       // Note: the app's 404 display is tightly-coupled with catching 404
       // network responses, so we simulate a 404 response status to display it.
       // TODO: Decouple the gr-app error view from network responses.
-      this._app.dispatchEvent(new CustomEvent('page-error',
+      this._appElement().dispatchEvent(new CustomEvent('page-error',
           {detail: {response: {status: 404}}}));
     },
   });
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index 168dc28..5c7addc 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-router</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-router.html">
 
@@ -671,11 +673,12 @@
       });
 
       test('_handleDefaultRoute on first load', () => {
-        element._app = {dispatchEvent: sinon.stub()};
+        const appElementStub = {dispatchEvent: sinon.stub()};
+        element._appElement = () => appElementStub;
         element._handleDefaultRoute();
-        assert.isTrue(element._app.dispatchEvent.calledOnce);
+        assert.isTrue(appElementStub.dispatchEvent.calledOnce);
         assert.equal(
-            element._app.dispatchEvent.lastCall.args[0].detail.response.status,
+            appElementStub.dispatchEvent.lastCall.args[0].detail.response.status,
             404);
       });
 
@@ -691,7 +694,8 @@
         sandbox.stub(window, 'page');
         element._startRouter();
 
-        element._app = {dispatchEvent: sinon.stub()};
+        const appElementStub = {dispatchEvent: sinon.stub()};
+        element._appElement = () => appElementStub;
         element._handleDefaultRoute();
 
         onExit('', () => {}); // we left page;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index 3a48213..0cdef8c 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -17,7 +17,7 @@
 
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -30,11 +30,10 @@
       gr-autocomplete {
         background-color: var(--view-background-color);
         border: 1px solid var(--border-color);
-        border-radius: 2px;
+        border-radius: var(--border-radius);
         flex: 1;
-        font: inherit;
         outline: none;
-        padding: .25em;
+        padding: var(--spacing-xs);
       }
     </style>
     <form>
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 c877ac4..0030bab 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
@@ -60,7 +60,6 @@
     'is:merged',
     'is:open',
     'is:owner',
-    'is:pending',
     'is:private',
     'is:reviewed',
     'is:reviewer',
@@ -90,7 +89,6 @@
     'status:closed',
     'status:merged',
     'status:open',
-    'status:pending',
     'status:reviewed',
     'topic:',
     'tr:',
@@ -106,7 +104,6 @@
 
   Polymer({
     is: 'gr-search-bar',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a search is committed
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index b162828..a4927c3 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-search-bar</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="gr-search-bar.html">
 <script src="../../../scripts/util.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
index 4c98068..c4ae41b 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
@@ -14,9 +14,9 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
+<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-search-bar/gr-search-bar.html">
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 65141aa..2446486 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
@@ -23,7 +23,6 @@
 
   Polymer({
     is: 'gr-smart-search',
-    _legacyUndefinedCheck: true,
 
     properties: {
       searchQuery: String,
@@ -49,7 +48,7 @@
     },
 
     behaviors: [
-      Gerrit.AnonymousNameBehavior,
+      Gerrit.DisplayNameBehavior,
     ],
 
     attached() {
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
index af0fc3c..a70eb7c 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-smart-search</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-smart-search.html">
 
diff --git a/polygerrit-ui/app/elements/gr-app-it_test.html b/polygerrit-ui/app/elements/custom-dark-theme_test.html
similarity index 62%
copy from polygerrit-ui/app/elements/gr-app-it_test.html
copy to polygerrit-ui/app/elements/custom-dark-theme_test.html
index 2601aeb..4cf35f1 100644
--- a/polygerrit-ui/app/elements/gr-app-it_test.html
+++ b/polygerrit-ui/app/elements/custom-dark-theme_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-app-it_test</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../test/common-test-setup.html"/>
-<link rel="import" href="gr-app.html">
+<link rel="import" href="./gr-app.html">
 
 <script>void(0);</script>
 
@@ -33,7 +35,7 @@
 </test-fixture>
 
 <script>
-  suite('gr-app integration tests', () => {
+  suite('gr-app custom dark theme tests', () => {
     let sandbox;
     let element;
 
@@ -61,13 +63,22 @@
         getVersion() { return Promise.resolve(42); },
         getLoggedIn() { return Promise.resolve(false); },
       });
+
+      window.localStorage.setItem('dark-theme', 'true');
+
       element = fixture('element');
 
-      const importSpy = sandbox.spy(element.$.externalStyle, '_import');
+      const importSpy = sandbox.spy(
+          element.$['app-element'].$.externalStyleForAll,
+          '_import');
+      const importForThemeSpy = sandbox.spy(
+          element.$['app-element'].$.externalStyleForTheme,
+          '_import');
       Gerrit.awaitPluginsLoaded().then(() => {
-        Promise.all(importSpy.returnValues).then(() => {
-          flush(done);
-        });
+        Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
+            .then(() => {
+              flush(done);
+            });
       });
     });
 
@@ -75,18 +86,16 @@
       sandbox.restore();
     });
 
-    test('applies --primary-text-color', () => {
+    test('applies the right theme', () => {
       assert.equal(
-          element.getComputedStyleValue('--primary-text-color'), '#F00BAA');
-    });
-
-    test('applies --header-background-color', () => {
-      assert.equal(element.getComputedStyleValue('--header-background-color'),
-          '#F01BAA');
-    });
-    test('applies --footer-background-color', () => {
-      assert.equal(element.getComputedStyleValue('--footer-background-color'),
-          '#F02BAA');
+          util.getComputedStyleValue('--primary-text-color', element),
+          'red');
+      assert.equal(
+          util.getComputedStyleValue('--header-background-color', element),
+          'black');
+      assert.equal(
+          util.getComputedStyleValue('--footer-background-color', element),
+          'yellow');
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/gr-app-it_test.html b/polygerrit-ui/app/elements/custom-light-theme_test.html
similarity index 64%
rename from polygerrit-ui/app/elements/gr-app-it_test.html
rename to polygerrit-ui/app/elements/custom-light-theme_test.html
index 2601aeb..e346af5 100644
--- a/polygerrit-ui/app/elements/gr-app-it_test.html
+++ b/polygerrit-ui/app/elements/custom-light-theme_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-app-it_test</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../test/common-test-setup.html"/>
 <link rel="import" href="gr-app.html">
 
@@ -33,7 +35,7 @@
 </test-fixture>
 
 <script>
-  suite('gr-app integration tests', () => {
+  suite('gr-app custom light theme tests', () => {
     let sandbox;
     let element;
 
@@ -61,13 +63,22 @@
         getVersion() { return Promise.resolve(42); },
         getLoggedIn() { return Promise.resolve(false); },
       });
+
+      window.localStorage.removeItem('dark-theme');
+
       element = fixture('element');
 
-      const importSpy = sandbox.spy(element.$.externalStyle, '_import');
+      const importSpy = sandbox.spy(
+          element.$['app-element'].$.externalStyleForAll,
+          '_import');
+      const importForThemeSpy = sandbox.spy(
+          element.$['app-element'].$.externalStyleForTheme,
+          '_import');
       Gerrit.awaitPluginsLoaded().then(() => {
-        Promise.all(importSpy.returnValues).then(() => {
-          flush(done);
-        });
+        Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
+            .then(() => {
+              flush(done);
+            });
       });
     });
 
@@ -75,17 +86,15 @@
       sandbox.restore();
     });
 
-    test('applies --primary-text-color', () => {
+    test('applies the right theme', () => {
       assert.equal(
-          element.getComputedStyleValue('--primary-text-color'), '#F00BAA');
-    });
-
-    test('applies --header-background-color', () => {
-      assert.equal(element.getComputedStyleValue('--header-background-color'),
+          util.getComputedStyleValue('--primary-text-color', element),
+          '#F00BAA');
+      assert.equal(
+          util.getComputedStyleValue('--header-background-color', element),
           '#F01BAA');
-    });
-    test('applies --footer-background-color', () => {
-      assert.equal(element.getComputedStyleValue('--footer-background-color'),
+      assert.equal(
+          util.getComputedStyleValue('--footer-background-color', element),
           '#F02BAA');
     });
   });
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js
index 7bf71f5..b7994e6 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'comment-api-mock',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _changeComments: Object,
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html
index c31bd1166..317e9e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
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 37491d2..1e8158d 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
@@ -19,35 +19,6 @@
 
   const PARENT = 'PARENT';
 
-  const Defs = {};
-
-  /**
-   * @typedef {{
-   *    basePatchNum: (string|number),
-   *    patchNum: (number),
-   * }}
-   */
-  Defs.patchRange;
-
-  /**
-   * @typedef {{
-   *    changeNum: number,
-   *    path: string,
-   *    patchRange: !Defs.patchRange,
-   *    projectConfig: (Object|undefined),
-   * }}
-   */
-  Defs.commentMeta;
-
-  /**
-   * @typedef {{
-   *    meta: !Defs.commentMeta,
-   *    left: !Array,
-   *    right: !Array,
-   * }}
-   */
-  Defs.commentsBySide;
-
   /**
    * Construct a change comments object, which can be data-bound to child
    * elements of that which uses the gr-comment-api.
@@ -92,7 +63,7 @@
    * Paths with comments are mapped to true, whereas paths without comments
    * are not mapped.
    *
-   * @param {Defs.patchRange=} opt_patchRange The patch-range object containing
+   * @param {Gerrit.PatchRange=} opt_patchRange The patch-range object containing
    *     patchNum and basePatchNum properties to represent the range.
    * @return {!Object}
    */
@@ -251,17 +222,26 @@
    * arrays of comments in on either side of the patch range for that path.
    *
    * @param {!string} path
-   * @param {!Defs.patchRange} patchRange The patch-range object containing patchNum
+   * @param {!Gerrit.PatchRange} patchRange The patch-range object containing patchNum
    *     and basePatchNum properties to represent the range.
    * @param {Object=} opt_projectConfig Optional project config object to
    *     include in the meta sub-object.
-   * @return {!Defs.commentsBySide}
+   * @return {!Gerrit.CommentsBySide}
    */
   ChangeComments.prototype.getCommentsBySideForPath = function(path,
       patchRange, opt_projectConfig) {
-    const comments = this.comments[path] || [];
-    const drafts = this.drafts[path] || [];
-    const robotComments = this.robotComments[path] || [];
+    let comments = [];
+    let drafts = [];
+    let robotComments = [];
+    if (this.comments && this.comments[path]) {
+      comments = this.comments[path];
+    }
+    if (this.drafts && this.drafts[path]) {
+      drafts = this.drafts[path];
+    }
+    if (this.robotComments && this.robotComments[path]) {
+      robotComments = this.robotComments[path];
+    }
 
     drafts.forEach(d => { d.__draft = true; });
 
@@ -430,7 +410,7 @@
    * given patch range.
    *
    * @param {!Object} comment
-   * @param {!Defs.patchRange} range
+   * @param {!Gerrit.PatchRange} range
    * @return {boolean}
    */
   ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) {
@@ -462,7 +442,7 @@
    * given patch range.
    *
    * @param {!Object} comment
-   * @param {!Defs.patchRange} range
+   * @param {!Gerrit.PatchRange} range
    * @return {boolean}
    */
   ChangeComments.prototype._isInRevisionOfPatchRange = function(comment,
@@ -475,7 +455,7 @@
    * Whether the given comment should be included in the given patch range.
    *
    * @param {!Object} comment
-   * @param {!Defs.patchRange} range
+   * @param {!Gerrit.PatchRange} range
    * @return {boolean|undefined}
    */
   ChangeComments.prototype._isInPatchRange = function(comment, range) {
@@ -485,7 +465,6 @@
 
   Polymer({
     is: 'gr-comment-api',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _changeComments: Object,
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
index 1ca2a69..47181f9 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-comment-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="./gr-comment-api.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html
index 56a6fb9..549bf43 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html
@@ -15,10 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <dom-module id="gr-coverage-layer">
   <template>
   </template>
+  <script src="../../../types/types.js"></script>
   <script src="../gr-diff-highlight/gr-annotation.js"></script>
   <script src="gr-coverage-layer.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
index e8d6900..3d9c172 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
@@ -17,27 +17,6 @@
 (function() {
   'use strict';
 
-  /** @enum {string} */
-  Gerrit.CoverageType = {
-    /**
-     * start_character and end_character of the range will be ignored for this
-     * type.
-     */
-    COVERED: 'COVERED',
-    /**
-     * start_character and end_character of the range will be ignored for this
-     * type.
-     */
-    NOT_COVERED: 'NOT_COVERED',
-    PARTIALLY_COVERED: 'PARTIALLY_COVERED',
-    /**
-     * You don't have to use this. If there is no coverage information for a
-     * range, then it implicitly means NOT_INSTRUMENTED. start_character and
-     * end_character of the range will be ignored for this type.
-     */
-    NOT_INSTRUMENTED: 'NOT_INSTRUMENTED',
-  };
-
   const TOOLTIP_MAP = new Map([
     [Gerrit.CoverageType.COVERED, 'Covered by tests.'],
     [Gerrit.CoverageType.NOT_COVERED, 'Not covered by tests.'],
@@ -45,15 +24,6 @@
     [Gerrit.CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'],
   ]);
 
-  /**
-   * @typedef {{
-   *   side: string,
-   *   type: Gerrit.CoverageType,
-   *   code_range: Gerrit.Range,
-   * }}
-   */
-  Gerrit.CoverageRange;
-
   Polymer({
     is: 'gr-coverage-layer',
 
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
index edd88a2..45a67e1 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-coverage-layer</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <script src="../gr-diff/gr-diff-line.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index 11bea8c..283b7fd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -95,7 +95,7 @@
         image._width = imageEl.naturalWidth;
         this._updateImageLabel(section, className, image);
       }.bind(this);
-      imageEl.src = 'data:' + image.type + ';base64, ' + image.body;
+      imageEl.setAttribute('src', `data:${image.type};base64, ${image.body}`);
       imageEl.addEventListener('error', () => {
         imageEl.remove();
         td.textContent = '[Image failed to load]';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
index 1ef278f..bb590ba 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
@@ -35,6 +35,9 @@
     if (group.dueToRebase) {
       sectionEl.classList.add('dueToRebase');
     }
+    if (group.ignoredWhitespaceOnly) {
+      sectionEl.classList.add('ignoredWhitespaceOnly');
+    }
     const pairs = group.getSideBySidePairs();
     for (let i = 0; i < pairs.length; i++) {
       sectionEl.appendChild(this._createRow(sectionEl, pairs[i].left,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
index 6be2097..144cc56 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -35,9 +35,18 @@
     if (group.dueToRebase) {
       sectionEl.classList.add('dueToRebase');
     }
+    if (group.ignoredWhitespaceOnly) {
+      sectionEl.classList.add('ignoredWhitespaceOnly');
+    }
 
     for (let i = 0; i < group.lines.length; ++i) {
-      sectionEl.appendChild(this._createRow(sectionEl, group.lines[i]));
+      const line = group.lines[i];
+      // If only whitespace has changed and the settings ask for whitespace to
+      // be ignored, only render the right-side line in unified diff mode.
+      if (group.ignoredWhitespaceOnly && line.type == GrDiffLine.Type.REMOVE) {
+        continue;
+      }
+      sectionEl.appendChild(this._createRow(sectionEl, line));
     }
     return sectionEl;
   };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
new file mode 100644
index 0000000..19e017d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>GrDiffBuilderUnified</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="../../../scripts/util.js"></script>
+<script src="../gr-diff/gr-diff-line.js"></script>
+<script src="../gr-diff/gr-diff-group.js"></script>
+<script src="../gr-diff-highlight/gr-annotation.js"></script>
+<script src="gr-diff-builder.js"></script>
+<script src="gr-diff-builder-unified.js"></script>
+
+<script>void(0);</script>
+
+<script>
+  suite('GrDiffBuilderUnified tests', () => {
+    let prefs;
+    let outputEl;
+    let diffBuilder;
+
+    setup(()=> {
+      prefs = {
+        line_length: 10,
+        show_tabs: true,
+        tab_size: 4,
+      };
+      outputEl = document.createElement('div');
+      diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []);
+    });
+
+    suite('buildSectionElement for BOTH group', () => {
+      let lines;
+      let group;
+
+      setup(() => {
+        lines = [
+          new GrDiffLine(GrDiffLine.Type.BOTH, 1, 2),
+          new GrDiffLine(GrDiffLine.Type.BOTH, 2, 3),
+          new GrDiffLine(GrDiffLine.Type.BOTH, 3, 4),
+        ];
+        lines[0].text = 'def hello_world():';
+        lines[1].text = '  print "Hello World";';
+        lines[2].text = '  return True';
+
+        group = new GrDiffGroup(GrDiffGroup.Type.BOTH, lines);
+      });
+
+      test('creates the section', () => {
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        assert.isTrue(sectionEl.classList.contains('section'));
+        assert.isTrue(sectionEl.classList.contains('both'));
+      });
+
+      test('creates each unchanged row once', () => {
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+        assert.equal(rowEls.length, 3);
+
+        assert.equal(
+            rowEls[0].querySelector('.lineNum.left').textContent,
+            lines[0].beforeNumber);
+        assert.equal(
+            rowEls[0].querySelector('.lineNum.right').textContent,
+            lines[0].afterNumber);
+        assert.equal(
+            rowEls[0].querySelector('.content').textContent, lines[0].text);
+
+        assert.equal(
+            rowEls[1].querySelector('.lineNum.left').textContent,
+            lines[1].beforeNumber);
+        assert.equal(
+            rowEls[1].querySelector('.lineNum.right').textContent,
+            lines[1].afterNumber);
+        assert.equal(
+            rowEls[1].querySelector('.content').textContent, lines[1].text);
+
+        assert.equal(
+            rowEls[2].querySelector('.lineNum.left').textContent,
+            lines[2].beforeNumber);
+        assert.equal(
+            rowEls[2].querySelector('.lineNum.right').textContent,
+            lines[2].afterNumber);
+        assert.equal(
+            rowEls[2].querySelector('.content').textContent, lines[2].text);
+      });
+    });
+
+    suite('buildSectionElement for DELTA group', () => {
+      let lines;
+      let group;
+
+      setup(() => {
+        lines = [
+          new GrDiffLine(GrDiffLine.Type.REMOVE, 1),
+          new GrDiffLine(GrDiffLine.Type.REMOVE, 2),
+          new GrDiffLine(GrDiffLine.Type.ADD, 2),
+          new GrDiffLine(GrDiffLine.Type.ADD, 3),
+        ];
+        lines[0].text = 'def hello_world():';
+        lines[1].text = '  print "Hello World"';
+        lines[2].text = 'def hello_universe()';
+        lines[3].text = '  print "Hello Universe"';
+
+        group = new GrDiffGroup(GrDiffGroup.Type.DELTA, lines);
+      });
+
+      test('creates the section', () => {
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        assert.isTrue(sectionEl.classList.contains('section'));
+        assert.isTrue(sectionEl.classList.contains('delta'));
+      });
+
+      test('creates the section with class if ignoredWhitespaceOnly', () => {
+        group.ignoredWhitespaceOnly = true;
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
+      });
+
+      test('creates the section with class if dueToRebase', () => {
+        group.dueToRebase = true;
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        assert.isTrue(sectionEl.classList.contains('dueToRebase'));
+      });
+
+      test('creates first the removed and then the added rows', () => {
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+        assert.equal(rowEls.length, 4);
+
+        assert.equal(
+            rowEls[0].querySelector('.lineNum.left').textContent,
+            lines[0].beforeNumber);
+        assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
+        assert.equal(
+            rowEls[0].querySelector('.content').textContent, lines[0].text);
+
+        assert.equal(
+            rowEls[1].querySelector('.lineNum.left').textContent,
+            lines[1].beforeNumber);
+        assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
+        assert.equal(
+            rowEls[1].querySelector('.content').textContent, lines[1].text);
+
+        assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
+        assert.equal(
+            rowEls[2].querySelector('.lineNum.right').textContent,
+            lines[2].afterNumber);
+        assert.equal(
+            rowEls[2].querySelector('.content').textContent, lines[2].text);
+
+        assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
+        assert.equal(
+            rowEls[3].querySelector('.lineNum.right').textContent,
+            lines[3].afterNumber);
+        assert.equal(
+            rowEls[3].querySelector('.content').textContent, lines[3].text);
+      });
+
+      test('creates only the added rows if only ignored whitespace', () => {
+        group.ignoredWhitespaceOnly = true;
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+        assert.equal(rowEls.length, 2);
+
+        assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
+        assert.equal(
+            rowEls[0].querySelector('.lineNum.right').textContent,
+            lines[2].afterNumber);
+        assert.equal(
+            rowEls[0].querySelector('.content').textContent, lines[2].text);
+
+        assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
+        assert.equal(
+            rowEls[1].querySelector('.lineNum.right').textContent,
+            lines[3].afterNumber);
+        assert.equal(
+            rowEls[1].querySelector('.content').textContent, lines[3].text);
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index d42d8c1..40fbe3c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -14,12 +14,11 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../gr-coverage-layer/gr-coverage-layer.html">
 <link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
 <link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
-<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
 
 <dom-module id="gr-diff-builder">
   <template>
@@ -29,9 +28,6 @@
     <gr-ranged-comment-layer
         id="rangeLayer"
         comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer>
-    <gr-syntax-layer
-        id="syntaxLayer"
-        diff="[[diff]]"></gr-syntax-layer>
     <gr-coverage-layer
         id="coverageLayerLeft"
         coverage-ranges="[[_leftCoverageRanges]]"
@@ -43,7 +39,6 @@
     <gr-diff-processor
         id="processor"
         groups="{{_groups}}"></gr-diff-processor>
-    <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   </template>
   <script src="../../../scripts/util.js"></script>
   <script src="../gr-diff/gr-diff-line.js"></script>
@@ -63,18 +58,10 @@
         UNIFIED: 'UNIFIED_DIFF',
       };
 
-      // If any line of the diff is more than the character limit, then disable
-      // syntax highlighting for the entire file.
-      const SYNTAX_MAX_LINE_LENGTH = 500;
-
-      // Disable syntax highlighting if the overall diff is too large.
-      const SYNTAX_MAX_DIFF_LENGTH = 20000;
-
       const TRAILING_WHITESPACE_PATTERN = /\s+$/;
 
       Polymer({
         is: 'gr-diff-builder',
-        _legacyUndefinedCheck: true,
 
         /**
          * Fired when the diff begins rendering.
@@ -83,21 +70,13 @@
          */
 
         /**
-         * Fired when the diff finishes rendering text content and starts
-         * syntax highlighting.
+         * Fired when the diff finishes rendering text content.
          *
          * @event render-content
          */
 
-        /**
-         * Fired when the diff finishes syntax highlighting.
-         *
-         * @event render-syntax
-         */
-
         properties: {
           diff: Object,
-          diffPath: String,
           changeNum: String,
           patchNum: String,
           viewMode: String,
@@ -138,8 +117,16 @@
            * @type {?Object}
            */
           _cancelableRenderPromise: Object,
+          layers: {
+            type: Array,
+            value: [],
+          },
         },
 
+        behaviors: [
+          Gerrit.FireBehavior,
+        ],
+
         get diffElement() {
           return this.queryEffectiveChildren('#diffTable');
         },
@@ -163,11 +150,10 @@
           // attached before plugins are installed.
           this._setupAnnotationLayers();
 
-          this.$.syntaxLayer.enabled = prefs.syntax_highlighting;
           this._showTabs = !!prefs.show_tabs;
           this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
 
-          // Stop the processor and syntax layer (if they're running).
+          // Stop the processor if it's running.
           this.cancel();
 
           this._builder = this._getDiffBuilder(this.diff, prefs);
@@ -180,7 +166,8 @@
 
           const isBinary = !!(this.isImageDiff || this.diff.binary);
 
-          this.dispatchEvent(new CustomEvent('render-start', {bubbles: true}));
+          this.dispatchEvent(new CustomEvent(
+              'render-start', {bubbles: true, composed: true}));
           this._cancelableRenderPromise = util.makeCancelable(
               this.$.processor.process(this.diff.content, isBinary)
                   .then(() => {
@@ -188,17 +175,7 @@
                       this._builder.renderDiff();
                     }
                     this.dispatchEvent(new CustomEvent('render-content',
-                        {bubbles: true}));
-
-                    if (this._diffTooLargeForSyntax()) {
-                      this.$.syntaxLayer.enabled = false;
-                    }
-
-                    return this.$.syntaxLayer.process();
-                  })
-                  .then(() => {
-                    this.dispatchEvent(
-                        new CustomEvent('render-syntax', {bubbles: true}));
+                        {bubbles: true, composed: true}));
                   }));
           return this._cancelableRenderPromise
               .finally(() => { this._cancelableRenderPromise = null; })
@@ -211,7 +188,6 @@
         _setupAnnotationLayers() {
           const layers = [
             this._createTrailingWhitespaceLayer(),
-            this.$.syntaxLayer,
             this._createIntralineLayer(),
             this._createTabIndicatorLayer(),
             this.$.rangeLayer,
@@ -219,12 +195,9 @@
             this.$.coverageLayerRight,
           ];
 
-          // Get layers from plugins (if any).
-          for (const pluginLayer of this.$.jsAPI.getDiffLayers(
-              this.diffPath, this.changeNum, this.patchNum)) {
-            layers.push(pluginLayer);
+          if (this.layers) {
+            layers.push(...this.layers);
           }
-
           this._layers = layers;
         },
 
@@ -289,7 +262,7 @@
           const contextIndex = groups.findIndex(group =>
             group.element === sectionEl
           );
-          groups.splice(...[contextIndex, 1].concat(newGroups));
+          groups.splice(contextIndex, 1, ...newGroups);
 
           for (const newGroup of newGroups) {
             this._builder.emitGroup(newGroup, sectionEl);
@@ -301,7 +274,6 @@
 
         cancel() {
           this.$.processor.cancel();
-          this.$.syntaxLayer.cancel();
           if (this._cancelableRenderPromise) {
             this._cancelableRenderPromise.cancel();
             this._cancelableRenderPromise = null;
@@ -314,7 +286,7 @@
           this.dispatchEvent(new CustomEvent('show-alert', {
             detail: {
               message,
-            }, bubbles: true}));
+            }, bubbles: true, composed: true}));
           throw Error(`Invalid preference value: ${pref}`);
         },
 
@@ -440,45 +412,10 @@
           };
         },
 
-        /**
-         * @return {boolean} whether any of the lines in _groups are longer
-         * than SYNTAX_MAX_LINE_LENGTH.
-         */
-        _anyLineTooLong() {
-          return this._groups.reduce((acc, group) => {
-            return acc || group.lines.reduce((acc, line) => {
-              return acc || line.text.length >= SYNTAX_MAX_LINE_LENGTH;
-            }, false);
-          }, false);
-        },
-
-        _diffTooLargeForSyntax() {
-          return this._anyLineTooLong() ||
-              this.getDiffLength() > SYNTAX_MAX_DIFF_LENGTH;
-        },
-
         setBlame(blame) {
           if (!this._builder || !blame) { return; }
           this._builder.setBlame(blame);
         },
-
-        /**
-         * Get the approximate length of the diff as the sum of the maximum
-         * length of the chunks.
-         *
-         * @return {number}
-         */
-        getDiffLength() {
-          return this.diff.content.reduce((sum, sec) => {
-            if (sec.hasOwnProperty('ab')) {
-              return sum + sec.ab.length;
-            } else {
-              return sum + Math.max(
-                  sec.hasOwnProperty('a') ? sec.a.length : 0,
-                  sec.hasOwnProperty('b') ? sec.b.length : 0);
-            }
-          }, 0);
-        },
       });
     })();
   </script>
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 65a56f0..54303f6 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
@@ -115,18 +115,6 @@
     group.element = element;
   };
 
-  GrDiffBuilder.prototype.renderSection = function(element) {
-    for (let i = 0; i < this.groups.length; i++) {
-      const group = this.groups[i];
-      if (group.element === element) {
-        const newElement = this.buildSectionElement(group);
-        group.element.parentElement.replaceChild(newElement, group.element);
-        group.element = newElement;
-        break;
-      }
-    }
-  };
-
   GrDiffBuilder.prototype.getGroupsByLineRange = function(
       startLine, endLine, opt_side) {
     const groups = [];
@@ -233,57 +221,38 @@
         group => { return group.element; });
   };
 
-  // TODO(wyatta): Move this completely into the processor.
-  GrDiffBuilder.prototype._insertContextGroups = function(groups, lines,
-      hiddenRange) {
-    const linesBeforeCtx = lines.slice(0, hiddenRange[0]);
-    const hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
-    const linesAfterCtx = lines.slice(hiddenRange[1]);
-
-    if (linesBeforeCtx.length > 0) {
-      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
-    }
-
-    const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
-    ctxLine.contextGroup =
-        new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines);
-    groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
-        [ctxLine]));
-
-    if (linesAfterCtx.length > 0) {
-      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx));
-    }
-  };
-
   GrDiffBuilder.prototype._createContextControl = function(section, line) {
-    if (!line.contextGroup || !line.contextGroup.lines.length) {
-      return null;
-    }
+    if (!line.contextGroups) return null;
+
+    const numLines =
+        line.contextGroups[line.contextGroups.length - 1].lineRange.left.end -
+        line.contextGroups[0].lineRange.left.start + 1;
+
+    if (numLines === 0) return null;
 
     const td = this._createElement('td');
-    const showPartialLinks =
-        line.contextGroup.lines.length > PARTIAL_CONTEXT_AMOUNT;
+    const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
 
     if (showPartialLinks) {
       td.appendChild(this._createContextButton(
-          GrDiffBuilder.ContextButtonType.ABOVE, section, line));
+          GrDiffBuilder.ContextButtonType.ABOVE, section, line, numLines));
       td.appendChild(document.createTextNode(' - '));
     }
 
     td.appendChild(this._createContextButton(
-        GrDiffBuilder.ContextButtonType.ALL, section, line));
+        GrDiffBuilder.ContextButtonType.ALL, section, line, numLines));
 
     if (showPartialLinks) {
       td.appendChild(document.createTextNode(' - '));
       td.appendChild(this._createContextButton(
-          GrDiffBuilder.ContextButtonType.BELOW, section, line));
+          GrDiffBuilder.ContextButtonType.BELOW, section, line, numLines));
     }
 
     return td;
   };
 
-  GrDiffBuilder.prototype._createContextButton = function(type, section, line) {
-    const contextLines = line.contextGroup.lines;
+  GrDiffBuilder.prototype._createContextButton = function(type, section, line,
+      numLines) {
     const context = PARTIAL_CONTEXT_AMOUNT;
 
     const button = this._createElement('gr-button', 'showContext');
@@ -291,20 +260,20 @@
     button.setAttribute('no-uppercase', true);
 
     let text;
-    const groups = []; // The groups that replace this one if tapped.
+    let groups = []; // The groups that replace this one if tapped.
 
     if (type === GrDiffBuilder.ContextButtonType.ALL) {
-      text = 'Show ' + contextLines.length + ' common line';
-      if (contextLines.length > 1) { text += 's'; }
-      groups.push(line.contextGroup);
+      text = 'Show ' + numLines + ' common line';
+      if (numLines > 1) { text += 's'; }
+      groups.push(...line.contextGroups);
     } else if (type === GrDiffBuilder.ContextButtonType.ABOVE) {
       text = '+' + context + '↑';
-      this._insertContextGroups(groups, contextLines,
-          [context, contextLines.length]);
+      groups = GrDiffGroup.hideInContextControl(line.contextGroups,
+          context, numLines);
     } else if (type === GrDiffBuilder.ContextButtonType.BELOW) {
       text = '+' + context + '↓';
-      this._insertContextGroups(groups, contextLines,
-          [0, contextLines.length - context]);
+      groups = GrDiffGroup.hideInContextControl(line.contextGroups,
+          0, numLines - context);
     }
 
     Polymer.dom(button).textContent = text;
@@ -337,8 +306,6 @@
       return td;
     } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
       td.classList.add('contextLineNum');
-      td.setAttribute('data-value', '@@');
-      td.textContent = '@@';
     } else if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
       td.classList.add('lineNum');
       td.setAttribute('data-value', number);
@@ -353,6 +320,12 @@
     if (line.type !== GrDiffLine.Type.BLANK) {
       td.classList.add('content');
     }
+
+    // If intraline info is not available, the entire line will be
+    // considered as changed and marked as dark red / green color
+    if (!line.hasIntralineInfo) {
+      td.classList.add('no-intraline-info');
+    }
     td.classList.add(line.type);
 
     const lineLimit =
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index a8db47a..42414b7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-builder</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 <script src="../gr-diff/gr-diff-line.js"></script>
@@ -28,13 +30,14 @@
 <script src="../gr-diff-highlight/gr-annotation.js"></script>
 <script src="gr-diff-builder.js"></script>
 
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
 <link rel="import" href="gr-diff-builder.html">
 
 <script>void(0);</script>
 
 <test-fixture id="basic">
-  <template>
+  <template is="dom-template">
     <gr-diff-builder>
       <table id="diffTable"></table>
     </gr-diff-builder>
@@ -88,26 +91,37 @@
     });
 
     test('context control buttons', () => {
-      const section = {};
-      const line = {contextGroup: {lines: []}};
-
       // Create 10 lines.
+      const lines = [];
       for (let i = 0; i < 10; i++) {
-        line.contextGroup.lines.push('lorem upsum');
+        const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+        line.beforeNumber = i + 1;
+        line.afterNumber = i + 1;
+        line.text = 'lorem upsum';
+        lines.push(line);
       }
 
+      const contextLine = {
+        contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)],
+      };
+
+      const section = {};
       // Does not include +10 buttons when there are fewer than 11 lines.
-      let td = builder._createContextControl(section, line);
+      let td = builder._createContextControl(section, contextLine);
       let buttons = td.querySelectorAll('gr-button.showContext');
 
       assert.equal(buttons.length, 1);
       assert.equal(Polymer.dom(buttons[0]).textContent, 'Show 10 common lines');
 
       // Add another line.
-      line.contextGroup.lines.push('lorem upsum');
+      const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+      line.text = 'lorem upsum';
+      line.beforeNumber = 11;
+      line.afterNumber = 11;
+      contextLine.contextGroups[0].addLine(line);
 
       // Includes +10 buttons when there are at least 11 lines.
-      td = builder._createContextControl(section, line);
+      td = builder._createContextControl(section, contextLine);
       buttons = td.querySelectorAll('gr-button.showContext');
 
       assert.equal(buttons.length, 3);
@@ -577,31 +591,39 @@
       });
     });
 
-    suite('layers from plugins', () => {
+    suite('layers', () => {
       let element;
       let initialLayersCount;
-
+      let withLayerCount;
       setup(() => {
+        const layers = [];
         element = fixture('basic');
+        element.layers = layers;
         element._showTrailingWhitespace = true;
         element._setupAnnotationLayers();
         initialLayersCount = element._layers.length;
       });
 
-      test('no plugin layers', () => {
-        const getDiffLayersStub = sinon.stub(element.$.jsAPI, 'getDiffLayers')
-            .returns([]);
+      test('no layers', () => {
         element._setupAnnotationLayers();
-        assert.isTrue(getDiffLayersStub.called);
         assert.equal(element._layers.length, initialLayersCount);
       });
 
-      test('with plugin layers', () => {
-        const getDiffLayersStub = sinon.stub(element.$.jsAPI, 'getDiffLayers')
-            .returns([{}, {}]);
-        element._setupAnnotationLayers();
-        assert.isTrue(getDiffLayersStub.called);
-        assert.equal(element._layers.length, initialLayersCount + 2);
+      suite('with layers', () => {
+        const layers = [{}, {}];
+        setup(() => {
+          element = fixture('basic');
+          element.layers = layers;
+          element._showTrailingWhitespace = true;
+          element._setupAnnotationLayers();
+          withLayerCount = element._layers.length;
+        });
+        test('with layers', () => {
+          element._setupAnnotationLayers();
+          assert.equal(element._layers.length, withLayerCount);
+          assert.equal(initialLayersCount + layers.length,
+              withLayerCount);
+        });
       });
     });
 
@@ -712,7 +734,6 @@
         element.viewMode = 'SIDE_BY_SIDE';
         processStub = sandbox.stub(element.$.processor, 'process')
             .returns(Promise.resolve());
-        sandbox.stub(element, '_anyLineTooLong').returns(true);
         keyLocations = {left: {}, right: {}};
         prefs = {
           line_length: 10,
@@ -802,15 +823,6 @@
         element.render(keyLocations, prefs).then(done);
       });
 
-      test('renderSection', () => {
-        let section = outputEl.querySelector('stub:nth-of-type(2)');
-        const prevInnerHTML = section.innerHTML;
-        section.innerHTML = 'wiped';
-        element._builder.renderSection(section);
-        section = outputEl.querySelector('stub:nth-of-type(2)');
-        assert.equal(section.innerHTML, prevInnerHTML);
-      });
-
       test('addColumns is called', done => {
         element.render(keyLocations, {}).then(done);
         assert.isTrue(element._builder.addColumns.called);
@@ -841,37 +853,14 @@
               .map(c => { return c.args[0].type; });
           assert.include(firedEventTypes, 'render-start');
           assert.include(firedEventTypes, 'render-content');
-          assert.include(firedEventTypes, 'render-syntax');
-          done();
-        });
-      });
-
-      test('rendering normal-sized diff does not disable syntax', () => {
-        assert.isTrue(element.$.syntaxLayer.enabled);
-      });
-
-      test('rendering large diff disables syntax', done => {
-        // Before it renders, set the first diff line to 500 '*' characters.
-        element.diff.content[0].a = [new Array(501).join('*')];
-        const prefs = {
-          line_length: 10,
-          show_tabs: true,
-          tab_size: 4,
-          context: -1,
-          syntax_highlighting: true,
-        };
-        element.render(keyLocations, prefs).then(() => {
-          assert.isFalse(element.$.syntaxLayer.enabled);
           done();
         });
       });
 
       test('cancel', () => {
         const processorCancelStub = sandbox.stub(element.$.processor, 'cancel');
-        const syntaxCancelStub = sandbox.stub(element.$.syntaxLayer, 'cancel');
         element.cancel();
         assert.isTrue(processorCancelStub.called);
-        assert.isTrue(syntaxCancelStub.called);
       });
     });
 
@@ -900,10 +889,6 @@
         });
       });
 
-      test('getDiffLength', () => {
-        assert.equal(element.getDiffLength(diff), 52);
-      });
-
       test('getContentByLine', () => {
         let actual;
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
index c24574e..99d0498 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 
 <dom-module id="gr-diff-cursor">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index 1cfd5e7..6ddb390 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -37,7 +37,6 @@
 
   Polymer({
     is: 'gr-diff-cursor',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -65,7 +64,10 @@
       /**
        * If set, the cursor will attempt to move to the line number (instead of
        * the first chunk) the next time the diff renders. It is set back to null
-       * when used.
+       * when used. It should be only used if you want the line to be focused
+       * after initialization of the component and page should scroll
+       * to that position. This parameter should be set at most for one gr-diff
+       * element in the page.
        *
        * @type {?number}
        */
@@ -136,11 +138,11 @@
       }
     },
 
-    moveToNextChunk() {
+    moveToNextChunk(opt_clipToTop) {
       this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this),
           target => {
             return target.parentNode.scrollHeight;
-          });
+          }, opt_clipToTop);
       this._fixSide();
     },
 
@@ -192,16 +194,20 @@
     },
 
     getTargetDiffElement() {
-      // Find the parent diff element of the cursor row.
-      for (let diff = this.diffRow; diff; diff = diff.parentElement) {
-        if (diff.tagName === 'GR-DIFF') { return diff; }
+      if (!this.diffRow) return null;
+
+      const hostOwner = Polymer.dom(/** @type {Node} */ (this.diffRow))
+          .getOwnerRoot();
+      if (hostOwner && hostOwner.host &&
+          hostOwner.host.tagName === 'GR-DIFF') {
+        return hostOwner.host;
       }
       return null;
     },
 
     moveToFirstChunk() {
       this.$.cursorManager.moveToStart();
-      this.moveToNextChunk();
+      this.moveToNextChunk(true);
     },
 
     reInitCursor() {
@@ -224,8 +230,12 @@
 
     handleDiffUpdate() {
       this._updateStops();
-
       if (!this.diffRow) {
+        // does not scroll during init unless requested
+        const scrollingBehaviorForInit = this.initialLineNumber ?
+          ScrollBehavior.KEEP_VISIBLE :
+          ScrollBehavior.NEVER;
+        this._scrollBehavior = scrollingBehaviorForInit;
         this.reInitCursor();
       }
       this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
@@ -296,7 +306,7 @@
     },
 
     _rowHasThread(row) {
-      return row.querySelector('.comment-thread');
+      return row.querySelector('.thread-group');
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index f111378..1c1100d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -18,14 +18,17 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-cursor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../gr-diff/gr-diff.html">
 <link rel="import" href="./gr-diff-cursor.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
 
 <script>void(0);</script>
@@ -207,40 +210,55 @@
       assert.equal(cursorElement.side, 'left');
     });
 
-    test('initialLineNumber disabled', done => {
+    test('initialLineNumber not provided', done => {
+      let scrollBehaviorDuringMove;
       const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber');
-      const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk');
+      const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk',
+          () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
 
       function renderHandler() {
         diffElement.removeEventListener('render', renderHandler);
         assert.isFalse(moveToNumStub.called);
         assert.isTrue(moveToChunkStub.called);
+        assert.equal(scrollBehaviorDuringMove, 'never');
+        assert.equal(cursorElement._scrollBehavior, 'keep-visible');
         done();
       }
       diffElement.addEventListener('render', renderHandler);
       diffElement._diffChanged(mockDiffResponse.diffResponse);
     });
 
-    test('initialLineNumber enabled', done => {
-      const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber');
+    test('initialLineNumber provided', done => {
+      let scrollBehaviorDuringMove;
+      const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber',
+          () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
       const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk');
-
       function renderHandler() {
         diffElement.removeEventListener('render', renderHandler);
         assert.isFalse(moveToChunkStub.called);
         assert.isTrue(moveToNumStub.called);
         assert.equal(moveToNumStub.lastCall.args[0], 10);
         assert.equal(moveToNumStub.lastCall.args[1], 'right');
+        assert.equal(scrollBehaviorDuringMove, 'keep-visible');
+        assert.equal(cursorElement._scrollBehavior, 'keep-visible');
         done();
       }
       diffElement.addEventListener('render', renderHandler);
-
       cursorElement.initialLineNumber = 10;
       cursorElement.side = 'right';
 
       diffElement._diffChanged(mockDiffResponse.diffResponse);
     });
 
+    test('getTargetDiffElement', () => {
+      cursorElement.initialLineNumber = 1;
+      assert.isTrue(!!cursorElement.diffRow);
+      assert.equal(
+          cursorElement.getTargetDiffElement(),
+          diffElement
+      );
+    });
+
     test('getAddress', () => {
       // It should initialize to the first chunk: line 5 of the revision.
       assert.deepEqual(cursorElement.getAddress(),
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
index c07d370..c1bf3ed 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-annotation</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="gr-annotation.js"></script>
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
index c912a16..3b17190 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
@@ -14,10 +14,11 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
-<link rel="import" href="../gr-selection-action-box/gr-selection-action-box.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../gr-selection-action-box/gr-selection-action-box.html">
 
 <dom-module id="gr-diff-highlight">
   <template>
@@ -25,14 +26,6 @@
       :host {
         position: relative;
       }
-      .contentWrapper ::content .range {
-        background-color: var(--diff-highlight-range-color);
-        display: inline;
-      }
-      .contentWrapper ::content .rangeHighlight {
-        background-color: var(--diff-highlight-range-hover-color);
-        display: inline;
-      }
       gr-selection-action-box {
         /**
          * Needs z-index to apear above wrapped content, since it's inseted
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index c820668..b159b51 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-diff-highlight',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {!Array<!Gerrit.HoveredRange>} */
@@ -36,6 +35,10 @@
       _cachedDiffBuilder: Object,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     listeners: {
       'comment-thread-mouseleave': '_handleCommentThreadMouseleave',
       'comment-thread-mouseenter': '_handleCommentThreadMouseenter',
@@ -160,6 +163,11 @@
      * })|null|!Object}
      */
     _getNormalizedRange(selection) {
+      /* On Safari the ShadowRoot.getSelection() isn't there and the only thing
+         we can get is a single Range */
+      if (selection instanceof Range) {
+        return this._normalizeRange(selection);
+      }
       const rangeCount = selection.rangeCount;
       if (rangeCount === 0) {
         return null;
@@ -320,12 +328,19 @@
     },
 
     _handleSelection(selection, isMouseUp) {
+      /* On Safari, the selection events may return a null range that should
+         be ignored */
+      if (!selection) {
+        return;
+      }
       const normalizedRange = this._getNormalizedRange(selection);
       if (!this._isRangeValid(normalizedRange)) {
         this._removeActionBox();
         return;
       }
-      const domRange = selection.getRangeAt(0);
+      /* On Safari the ShadowRoot.getSelection() isn't there and the only thing
+         we can get is a single Range */
+      const domRange = selection instanceof Range ? selection:selection.getRangeAt(0);
       const start = normalizedRange.start;
       const end = normalizedRange.end;
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 2e50fdb..c929e1e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-highlight</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-diff-highlight.html">
 
@@ -92,7 +94,7 @@
 
         <tbody class="section contextControl">
           <tr class="diff-row side-by-side" left-type="contextControl" right-type="contextControl">
-            <td class="left contextLineNum" data-value="@@"></td>
+            <td class="left contextLineNum"></td>
             <td>
               <gr-button>+10↑</gr-button>
               -
@@ -100,7 +102,7 @@
               -
               <gr-button>+10↓</gr-button>
             </td>
-            <td class="right contextLineNum" data-value="@@"></td>
+            <td class="right contextLineNum"></td>
             <td>
               <gr-button>+10↑</gr-button>
               -
@@ -179,8 +181,8 @@
         element.commentRanges = [{side: 'right'}];
 
         sandbox.stub(element, 'set');
-        threadEl.dispatchEvent(
-            new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
+        threadEl.dispatchEvent(new CustomEvent(
+            'comment-thread-mouseenter', {bubbles: true, composed: true}));
         assert.isFalse(element.set.called);
       });
 
@@ -204,8 +206,8 @@
         }}];
 
         sandbox.stub(element, 'set');
-        threadEl.dispatchEvent(
-            new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
+        threadEl.dispatchEvent(new CustomEvent(
+            'comment-thread-mouseenter', {bubbles: true, composed: true}));
         assert.isTrue(element.set.called);
         const args = element.set.lastCall.args;
         assert.deepEqual(args[0], ['commentRanges', 0, 'hovering']);
@@ -221,8 +223,8 @@
         element.commentRanges = [{side: 'right'}];
 
         sandbox.stub(element, 'set');
-        threadEl.dispatchEvent(
-            new CustomEvent('comment-thread-mouseleave', {bubbles: true}));
+        threadEl.dispatchEvent(new CustomEvent(
+            'comment-thread-mouseleave', {bubbles: true, composed: true}));
         assert.isFalse(element.set.called);
       });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
index 927c759..cb482b2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
@@ -54,7 +54,7 @@
       if (element.nodeName === '#text') {
         element = element.parentElement;
       }
-      while (!element.classList.contains('contentText')) {
+      while (element && !element.classList.contains('contentText')) {
         if (element.parentElement === null) {
           return target;
         }
@@ -80,7 +80,7 @@
         if (n === child) {
           break;
         }
-        if (n.childNodes && n.childNodes.length !== 0) {
+        if (n && n.childNodes && n.childNodes.length !== 0) {
           const arr = [];
           for (const childNode of n.childNodes) {
             arr.push(childNode);
@@ -102,7 +102,9 @@
      * @return {number} The length of the text.
      */
     _getLength(node) {
-      return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length;
+      return node
+        ? node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length
+        : 0;
     },
   };
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
index 53ce6e6..7d60f13 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
@@ -15,13 +15,15 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="../gr-diff/gr-diff.html">
+<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
 
 <dom-module id="gr-diff-host">
   <template>
@@ -48,7 +50,13 @@
         revision-image=[[_revisionImage]]
         coverage-ranges="[[_coverageRanges]]"
         blame="[[_blame]]"
-        diff="[[diff]]"></gr-diff>
+        layers="[[_layers]]"
+        diff="[[diff]]">
+    </gr-diff>
+    <gr-syntax-layer
+        id="syntaxLayer"
+        enabled="[[_syntaxHighlightingEnabled]]"
+        diff="[[diff]]"></gr-syntax-layer>
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-reporting id="reporting" category="diff"></gr-reporting>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index 9760d50..2dd19a0 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
@@ -35,6 +35,16 @@
     SYNTAX: 'Diff Syntax Render',
   };
 
+  // Disable syntax highlighting if the overall diff is too large.
+  const SYNTAX_MAX_DIFF_LENGTH = 20000;
+
+  // If any line of the diff is more than the character limit, then disable
+  // syntax highlighting for the entire file.
+  const SYNTAX_MAX_LINE_LENGTH = 500;
+
+  // 120 lines is good enough threshold for full-sized window viewport
+  const NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT = 120;
+
   const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE';
 
   /**
@@ -66,7 +76,6 @@
    */
   Polymer({
     is: 'gr-diff-host',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user selects a line.
@@ -111,7 +120,9 @@
       commitRange: Object,
       filesWeblinks: {
         type: Object,
-        value() { return {}; },
+        value() {
+          return {};
+        },
         notify: true,
       },
       hidden: {
@@ -194,9 +205,7 @@
       },
 
       /**
-       * TODO(brohlfs): Replace Object type by Gerrit.CoverageRange.
-       *
-       * @type {!Array<!Object>}
+       * @type {!Array<!Gerrit.CoverageRange>}
        */
       _coverageRanges: {
         type: Array,
@@ -209,9 +218,21 @@
         type: Number,
         computed: '_computeParentIndex(patchRange.*)',
       },
+
+      _syntaxHighlightingEnabled: {
+        type: Boolean,
+        computed:
+          '_isSyntaxHighlightingEnabled(prefs.*, diff)',
+      },
+
+      _layers: {
+        type: Array,
+        value: [],
+      },
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.PatchSetBehavior,
     ],
 
@@ -229,7 +250,6 @@
 
       'render-start': '_handleRenderStart',
       'render-content': '_handleRenderContent',
-      'render-syntax': '_handleRenderSyntax',
 
       'normalize-range': '_handleNormalizeRange',
     },
@@ -237,6 +257,7 @@
     observers: [
       '_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' +
           ' noRenderOnPrefsChange)',
+      '_syntaxHighlightingChanged(noRenderOnPrefsChange, prefs.*)',
     ],
 
     ready() {
@@ -251,34 +272,35 @@
       });
     },
 
-    /** @return {!Promise} */
-    reload() {
+    /**
+     * @param {boolean=} haveParamsChanged ends reporting events that started
+     * on location change.
+     * @return {!Promise}
+     **/
+    reload(haveParamsChanged) {
       this._loading = true;
       this._errorMessage = null;
       const whitespaceLevel = this._getIgnoreWhitespace();
 
-      this._coverageRanges = [];
-      const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this;
-      this.$.jsAPI.getCoverageRanges(changeNum, path, basePatchNum, patchNum).
-          then(coverageRanges => {
-            if (changeNum !== this.changeNum ||
-                path !== this.path ||
-                basePatchNum !== this.patchRange.basePatchNum ||
-                patchNum !== this.patchRange.patchNum) {
-              return;
-            }
-            this._coverageRanges = coverageRanges;
-          }).catch(err => {
-            console.warn('Loading coverage ranges failed: ', err);
-          });
+      const layers = [this.$.syntaxLayer];
+      // Get layers from plugins (if any).
+      for (const pluginLayer of this.$.jsAPI.getDiffLayers(
+          this.path, this.changeNum, this.patchNum)) {
+        layers.push(pluginLayer);
+      }
+      this._layers = layers;
 
+      if (haveParamsChanged) {
+        // We listen on render viewport only on DiffPage (on paramsChanged)
+        this._listenToViewportRender();
+      }
+
+      this._coverageRanges = [];
+      this._getCoverageData();
       const diffRequest = this._getDiff()
           .then(diff => {
             this._loadedWhitespaceLevel = whitespaceLevel;
             this._reportDiff(diff);
-            if (this._getIgnoreWhitespace() !== WHITESPACE_IGNORE_NONE) {
-              return this._translateChunksToIgnore(diff);
-            }
             return diff;
           })
           .catch(e => {
@@ -293,6 +315,8 @@
         return this._loadDiffAssets(diff);
       });
 
+      // Not waiting for coverage ranges intentionally as
+      // plugin loading should not block the content rendering
       return Promise.all([diffRequest, assetRequest])
           .then(results => {
             const diff = results[0];
@@ -301,8 +325,20 @@
             }
             this.filesWeblinks = this._getFilesWeblinks(diff);
             return new Promise(resolve => {
-              const callback = () => {
-                resolve();
+              const callback = event => {
+                const needsSyntaxHighlighting = event.detail
+                      && event.detail.contentRendered;
+                if (needsSyntaxHighlighting) {
+                  this.$.reporting.time(TimingLabel.SYNTAX);
+                  this.$.syntaxLayer.process().then(() => {
+                    this.$.reporting.timeEnd(TimingLabel.SYNTAX);
+                    this.$.reporting.timeEnd(TimingLabel.TOTAL);
+                    resolve();
+                  });
+                } else {
+                  this.$.reporting.timeEnd(TimingLabel.TOTAL);
+                  resolve();
+                }
                 this.removeEventListener('render', callback);
               };
               this.addEventListener('render', callback);
@@ -315,8 +351,53 @@
           .then(() => { this._loading = false; });
     },
 
+    _getCoverageData() {
+      const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this;
+      this.$.jsAPI.getCoverageAnnotationApi().
+          then(coverageAnnotationApi => {
+            if (!coverageAnnotationApi) return;
+            const provider = coverageAnnotationApi.getCoverageProvider();
+            return provider(changeNum, path, basePatchNum, patchNum)
+                .then(coverageRanges => {
+                  if (!coverageRanges ||
+                    changeNum !== this.changeNum ||
+                    path !== this.path ||
+                    basePatchNum !== this.patchRange.basePatchNum ||
+                    patchNum !== this.patchRange.patchNum) {
+                    return;
+                  }
+
+                  const existingCoverageRanges = this._coverageRanges;
+                  this._coverageRanges = coverageRanges;
+
+                  // Notify with existing coverage ranges
+                  // in case there is some existing coverage data that needs to be removed
+                  existingCoverageRanges.forEach(range => {
+                    coverageAnnotationApi.notify(
+                        path,
+                        range.code_range.start_line,
+                        range.code_range.end_line,
+                        range.side);
+                  });
+
+                  // Notify with new coverage data
+                  coverageRanges.forEach(range => {
+                    coverageAnnotationApi.notify(
+                        path,
+                        range.code_range.start_line,
+                        range.code_range.end_line,
+                        range.side);
+                  });
+                });
+          }).catch(err => {
+            console.warn('Loading coverage ranges failed: ', err);
+          });
+    },
+
     _getFilesWeblinks(diff) {
-      if (!this.commitRange) { return {}; }
+      if (!this.commitRange) {
+        return {};
+      }
       return {
         meta_a: Gerrit.Nav.getFileWebLinks(
             this.projectName, this.commitRange.baseCommit, this.path,
@@ -375,7 +456,6 @@
      * @return {!Array<!HTMLElement>}
      */
     getThreadEls() {
-      // Polymer2: querySelectorAll returns NodeList instead of Array.
       return Array.from(
           Polymer.dom(this.$.diff).querySelectorAll('.comment-thread'));
     },
@@ -444,7 +524,9 @@
      * Report info about the diff response.
      */
     _reportDiff(diff) {
-      if (!diff || !diff.content) { return; }
+      if (!diff || !diff.content) {
+        return;
+      }
 
       // Count the delta lines stemming from normal deltas, and from
       // due_to_rebase deltas.
@@ -672,7 +754,7 @@
      * @param {!Gerrit.Range=} range
      * @return {?Node}
      */
-    _getThreadEl(lineNum, commentSide, range=undefined) {
+    _getThreadEl(lineNum, commentSide, range = undefined) {
       let line;
       if (commentSide === GrDiffBuilder.Side.LEFT) {
         line = {beforeNumber: lineNum};
@@ -737,50 +819,6 @@
         matchers.some(matcher => matcher(threadEl)));
     },
 
-    /**
-     * Take a diff that was loaded with a ignore-whitespace other than
-     * IGNORE_NONE, and convert delta chunks labeled as common into shared
-     * chunks.
-     *
-     * @param {!Object} diff
-     * @returns {!Object}
-     */
-    _translateChunksToIgnore(diff) {
-      const newDiff = Object.assign({}, diff);
-      const mergedContent = [];
-
-      // Was the last chunk visited a shared chunk?
-      let lastWasShared = false;
-
-      for (const chunk of diff.content) {
-        if (lastWasShared && chunk.common && chunk.b) {
-          // The last chunk was shared and this chunk should be ignored, so
-          // add its revision content to the previous chunk.
-          mergedContent[mergedContent.length - 1].ab.push(...chunk.b);
-        } else if (chunk.common && !chunk.b) {
-          // If the chunk should be ignored, but it doesn't have revision
-          // content, then drop it and continue without updating lastWasShared.
-          continue;
-        } else if (lastWasShared && chunk.ab) {
-          // Both the last chunk and the current chunk are shared. Merge this
-          // chunk's shared content into the previous shared content.
-          mergedContent[mergedContent.length - 1].ab.push(...chunk.ab);
-        } else if (!lastWasShared && chunk.common && chunk.b) {
-          // If the previous chunk was not shared, but this one should be
-          // ignored, then add it as a shared chunk.
-          mergedContent.push({ab: chunk.b});
-        } else {
-          // Otherwise add the chunk as is.
-          mergedContent.push(chunk);
-        }
-
-        lastWasShared = !!mergedContent[mergedContent.length - 1].ab;
-      }
-
-      newDiff.content = mergedContent;
-      return newDiff;
-    },
-
     _getIgnoreWhitespace() {
       if (!this.prefs || !this.prefs.ignore_whitespace) {
         return WHITESPACE_IGNORE_NONE;
@@ -788,14 +826,42 @@
       return this.prefs.ignore_whitespace;
     },
 
-    _whitespaceChanged(preferredWhitespaceLevel, loadedWhitespaceLevel,
+    _whitespaceChanged(
+        preferredWhitespaceLevel, loadedWhitespaceLevel,
         noRenderOnPrefsChange) {
+      // Polymer 2: check for undefined
+      if ([
+        preferredWhitespaceLevel,
+        loadedWhitespaceLevel,
+        noRenderOnPrefsChange,
+      ].some(arg => arg === undefined)) {
+        return;
+      }
+
       if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
           !noRenderOnPrefsChange) {
         this.reload();
       }
     },
 
+    _syntaxHighlightingChanged(noRenderOnPrefsChange, prefsChangeRecord) {
+      // Polymer 2: check for undefined
+      if ([
+        noRenderOnPrefsChange,
+        prefsChangeRecord,
+      ].some(arg => arg === undefined)) {
+        return;
+      }
+
+      if (prefsChangeRecord.path !== 'prefs.syntax_highlighting') {
+        return;
+      }
+
+      if (!noRenderOnPrefsChange) {
+        this.reload();
+      }
+    },
+
     /**
      * @param {Object} patchRangeRecord
      * @return {number|null}
@@ -841,8 +907,8 @@
     },
 
     _handleCommentSaveOrDiscard() {
-      this.dispatchEvent(new CustomEvent('diff-comments-modified',
-          {bubbles: true}));
+      this.dispatchEvent(new CustomEvent(
+          'diff-comments-modified', {bubbles: true, composed: true}));
     },
 
     _removeComment(comment) {
@@ -877,6 +943,42 @@
           item => item.__draftID === comment.__draftID);
     },
 
+    _isSyntaxHighlightingEnabled(preferenceChangeRecord, diff) {
+      if (!preferenceChangeRecord ||
+          !preferenceChangeRecord.base ||
+          !preferenceChangeRecord.base.syntax_highlighting ||
+          !diff) {
+        return false;
+      }
+      return !this._anyLineTooLong(diff) &&
+          this.$.diff.getDiffLength(diff) <= SYNTAX_MAX_DIFF_LENGTH;
+    },
+
+    /**
+     * @return {boolean} whether any of the lines in diff are longer
+     * than SYNTAX_MAX_LINE_LENGTH.
+     */
+    _anyLineTooLong(diff) {
+      if (!diff) return false;
+      return diff.content.some(section => {
+        const lines = section.ab ?
+          section.ab :
+          (section.a || []).concat(section.b || []);
+        return lines.some(line => line.length >= SYNTAX_MAX_LINE_LENGTH);
+      });
+    },
+
+    _listenToViewportRender() {
+      const renderUpdateListener = start => {
+        if (start > NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT) {
+          this.$.reporting.diffViewDisplayed();
+          this.$.syntaxLayer.removeListener(renderUpdateListener);
+        }
+      };
+
+      this.$.syntaxLayer.addListener(renderUpdateListener);
+    },
+
     _handleRenderStart() {
       this.$.reporting.time(TimingLabel.TOTAL);
       this.$.reporting.time(TimingLabel.CONTENT);
@@ -884,12 +986,7 @@
 
     _handleRenderContent() {
       this.$.reporting.timeEnd(TimingLabel.CONTENT);
-      this.$.reporting.time(TimingLabel.SYNTAX);
-    },
-
-    _handleRenderSyntax() {
-      this.$.reporting.timeEnd(TimingLabel.SYNTAX);
-      this.$.reporting.timeEnd(TimingLabel.TOTAL);
+      this.$.reporting.diffViewContentDisplayed();
     },
 
     _handleNormalizeRange(event) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index 6e7c239..94b2f7d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="gr-diff-host.html">
@@ -49,7 +51,6 @@
         time: sandbox.stub(),
         timeEnd: sandbox.stub(),
       });
-
       element = fixture('basic');
     });
 
@@ -57,6 +58,22 @@
       sandbox.restore();
     });
 
+
+    suite('plugin layers', () => {
+      const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
+      setup(() => {
+        stub('gr-js-api-interface', {
+          getDiffLayers() { return pluginLayers; },
+        });
+        element = fixture('basic');
+      });
+      test('plugin layers requested', () => {
+        element.patchRange = {};
+        element.reload();
+        assert(element.$.jsAPI.getDiffLayers.called);
+      });
+    });
+
     suite('handle comment-update', () => {
       setup(() => {
         sandbox.stub(element, '_commentsChanged');
@@ -250,7 +267,15 @@
       element.path = 'some/path';
       element.projectName = 'Some project';
       const threadEls = threads.map(
-          thread => element._createThreadElement(thread));
+          thread => {
+            const threadEl = element._createThreadElement(thread);
+            // Polymer 2 doesn't fire ready events and doesn't execute
+            // observers if element is not added to the Dom.
+            // See https://github.com/Polymer/old-docs-site/issues/2322
+            // and https://github.com/Polymer/polymer/issues/4526
+            element._attachThreadElement(threadEl);
+            return threadEl;
+          });
       assert.equal(threadEls.length, 2);
       assert.equal(threadEls[0].rootId, 4711);
       assert.equal(threadEls[1].rootId, 42);
@@ -269,7 +294,7 @@
     suite('render reporting', () => {
       test('starts total and content timer on render-start', done => {
         element.dispatchEvent(
-            new CustomEvent('render-start', {bubbles: true}));
+            new CustomEvent('render-start', {bubbles: true, composed: true}));
         assert.isTrue(element.$.reporting.time.calledWithExactly(
             'Diff Total Render'));
         assert.isTrue(element.$.reporting.time.calledWithExactly(
@@ -277,24 +302,82 @@
         done();
       });
 
-      test('ends content and starts syntax timer on render-content', done => {
+      test('ends content timer on render-content', () => {
         element.dispatchEvent(
-            new CustomEvent('render-content', {bubbles: true}));
-        assert.isTrue(element.$.reporting.time.calledWithExactly(
-            'Diff Syntax Render'));
+            new CustomEvent('render-content', {bubbles: true, composed: true}));
         assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
             'Diff Content Render'));
-        done();
       });
 
-      test('ends total and syntax timer on render-syntax', done => {
-        element.dispatchEvent(
-            new CustomEvent('render-syntax', {bubbles: true}));
-        assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
-            'Diff Total Render'));
-        assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
-            'Diff Syntax Render'));
-        done();
+      test('ends total and syntax timer after syntax layer processing', done => {
+        let notifySyntaxProcessed;
+        sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
+            resolve => {
+              notifySyntaxProcessed = resolve;
+            }));
+        sandbox.stub(element.$.restAPI, 'getDiff').returns(
+            Promise.resolve({content: []}));
+        element.patchRange = {};
+        element.$.restAPI.getDiffPreferences().then(prefs => {
+          element.prefs = prefs;
+          return element.reload();
+        });
+        // Multiple cascading microtasks are scheduled.
+        setTimeout(() => {
+          notifySyntaxProcessed();
+          // Assert after the notification task is processed.
+          Promise.resolve().then(() => {
+            assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+                'Diff Total Render'));
+            assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+                'Diff Syntax Render'));
+            assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+                'StartupDiffViewOnlyContent'));
+            done();
+          });
+        });
+      });
+
+      test('ends total timer w/ no syntax layer processing', done => {
+        sandbox.stub(element.$.restAPI, 'getDiff').returns(
+            Promise.resolve({content: []}));
+        element.patchRange = {};
+        element.reload();
+        // Multiple cascading microtasks are scheduled.
+        setTimeout(() => {
+          assert.isTrue(element.$.reporting.timeEnd.calledOnce);
+          assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+              'Diff Total Render'));
+          done();
+        });
+      });
+
+      test('completes reload promise after syntax layer processing', done => {
+        let notifySyntaxProcessed;
+        sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
+            resolve => {
+              notifySyntaxProcessed = resolve;
+            }));
+        sandbox.stub(element.$.restAPI, 'getDiff').returns(
+            Promise.resolve({content: []}));
+        element.patchRange = {};
+        let reloadComplete = false;
+        element.$.restAPI.getDiffPreferences().then(prefs => {
+          element.prefs = prefs;
+          return element.reload();
+        }).then(() => {
+          reloadComplete = true;
+        });
+        // Multiple cascading microtasks are scheduled.
+        setTimeout(() => {
+          assert.isFalse(reloadComplete);
+          notifySyntaxProcessed();
+          // Assert after the notification task is processed.
+          setTimeout(() => {
+            assert.isTrue(reloadComplete);
+            done();
+          });
+        });
       });
     });
 
@@ -303,6 +386,7 @@
 
       // Stub the network calls into requests that never resolve.
       sandbox.stub(element, '_getDiff', () => new Promise(() => {}));
+      element.patchRange = {};
 
       element.reload();
       assert.isTrue(cancelStub.called);
@@ -366,6 +450,7 @@
             (changeNum, basePatchNum, patchNum, path, onErr) => {
               onErr(error);
             });
+        element.patchRange = {};
         return element.reload().then(() => {
           assert.isTrue(onErrStub.calledOnce);
         });
@@ -723,6 +808,7 @@
 
     test('delegates cancel()', () => {
       const stub = sandbox.stub(element.$.diff, 'cancel');
+      element.patchRange = {};
       element.reload();
       assert.isTrue(stub.calledOnce);
       assert.equal(stub.lastCall.args.length, 0);
@@ -1241,9 +1327,11 @@
 
       const l = document.createElement('div');
       l.setAttribute('comment-side', 'left');
+      l.setAttribute('line-num', 'FILE');
 
       const r = document.createElement('div');
       r.setAttribute('comment-side', 'right');
+      r.setAttribute('line-num', 'FILE');
 
       const threadEls = [l, r];
       assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
@@ -1256,86 +1344,154 @@
           Gerrit.DiffSide.RIGHT), [r]);
     });
 
-    suite('_translateChunksToIgnore', () => {
-      let content;
-
+    suite('syntax layer with syntax_highlighting on', () => {
       setup(() => {
-        content = [
-          {ab: ['one', 'two']},
-          {a: ['three'], b: ['different three']},
-          {b: ['four']},
-          {ab: ['five', 'six']},
-          {a: ['seven']},
-          {ab: ['eight', 'nine']},
-        ];
+        const prefs = {
+          line_length: 10,
+          show_tabs: true,
+          tab_size: 4,
+          context: -1,
+          syntax_highlighting: true,
+        };
+        element.patchRange = {};
+        element.prefs = prefs;
       });
 
-      test('does nothing to unmarked diff', () => {
-        assert.deepEqual(element._translateChunksToIgnore({content}),
-            {content});
+      test('gr-diff-host provides syntax highlighting layer to gr-diff', () => {
+        element.reload();
+        assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
       });
 
-      test('merges marked delta chunk', () => {
-        content[1].common = true;
-        assert.deepEqual(element._translateChunksToIgnore({content}), {
-          content: [
-            {ab: ['one', 'two', 'different three']},
-            {b: ['four']},
-            {ab: ['five', 'six']},
-            {a: ['seven']},
-            {ab: ['eight', 'nine']},
-          ],
+      test('rendering normal-sized diff does not disable syntax', () => {
+        element.diff = {
+          content: [{
+            a: ['foo'],
+          }],
+        };
+        assert.isTrue(element.$.syntaxLayer.enabled);
+      });
+
+      test('rendering large diff disables syntax', () => {
+        // Before it renders, set the first diff line to 500 '*' characters.
+        element.diff = {
+          content: [{
+            a: [new Array(501).join('*')],
+          }],
+        };
+        assert.isFalse(element.$.syntaxLayer.enabled);
+      });
+
+      test('starts syntax layer processing on render event', done => {
+        sandbox.stub(element.$.syntaxLayer, 'process').returns(Promise.resolve());
+        sandbox.stub(element.$.restAPI, 'getDiff').returns(
+            Promise.resolve({content: []}));
+        element.reload();
+        setTimeout(() => {
+          element.dispatchEvent(
+              new CustomEvent('render', {bubbles: true, composed: true}));
+          assert.isTrue(element.$.syntaxLayer.process.called);
+          done();
+        });
+      });
+    });
+
+    suite('syntax layer with syntax_highlgihting off', () => {
+      setup(() => {
+        const prefs = {
+          line_length: 10,
+          show_tabs: true,
+          tab_size: 4,
+          context: -1,
+        };
+        element.diff = {
+          content: [{
+            a: ['foo'],
+          }],
+        };
+        element.patchRange = {};
+        element.prefs = prefs;
+      });
+
+      test('gr-diff-host provides syntax highlighting layer', () => {
+        element.reload();
+        assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
+      });
+
+      test('syntax layer should be disabled', () => {
+        assert.isFalse(element.$.syntaxLayer.enabled);
+      });
+
+      test('still disabled for large diff', () => {
+        // Before it renders, set the first diff line to 500 '*' characters.
+        element.diff = {
+          content: [{
+            a: [new Array(501).join('*')],
+          }],
+        };
+        assert.isFalse(element.$.syntaxLayer.enabled);
+      });
+    });
+
+    suite('coverage layer', () => {
+      let notifyStub;
+      setup(() => {
+        notifyStub = sinon.stub();
+        stub('gr-js-api-interface', {
+          getCoverageAnnotationApi() {
+            return Promise.resolve({
+              notify: notifyStub,
+              getCoverageProvider() {
+                return () => Promise.resolve([
+                  {
+                    type: 'COVERED',
+                    side: 'right',
+                    code_range: {
+                      start_line: 1,
+                      end_line: 2,
+                    },
+                  },
+                  {
+                    type: 'NOT_COVERED',
+                    side: 'right',
+                    code_range: {
+                      start_line: 3,
+                      end_line: 4,
+                    },
+                  },
+                ]);
+              },
+            });
+          },
+        });
+        element = fixture('basic');
+        const prefs = {
+          line_length: 10,
+          show_tabs: true,
+          tab_size: 4,
+          context: -1,
+        };
+        element.diff = {
+          content: [{
+            a: ['foo'],
+          }],
+        };
+        element.patchRange = {};
+        element.prefs = prefs;
+      });
+
+      test('getCoverageAnnotationApi should be called', done => {
+        element.reload();
+        flush(() => {
+          assert.isTrue(element.$.jsAPI.getCoverageAnnotationApi.calledOnce);
+          done();
         });
       });
 
-      test('merges marked addition chunk', () => {
-        content[2].common = true;
-        assert.deepEqual(element._translateChunksToIgnore({content}), {
-          content: [
-            {ab: ['one', 'two']},
-            {a: ['three'], b: ['different three']},
-            {ab: ['four', 'five', 'six']},
-            {a: ['seven']},
-            {ab: ['eight', 'nine']},
-          ],
-        });
-      });
-
-      test('merges multiple marked delta', () => {
-        content[1].common = true;
-        content[2].common = true;
-        assert.deepEqual(element._translateChunksToIgnore({content}), {
-          content: [
-            {ab: ['one', 'two', 'different three', 'four', 'five', 'six']},
-            {a: ['seven']},
-            {ab: ['eight', 'nine']},
-          ],
-        });
-      });
-
-      test('marked deletion chunks are omitted', () => {
-        content[4].common = true;
-        assert.deepEqual(element._translateChunksToIgnore({content}), {
-          content: [
-            {ab: ['one', 'two']},
-            {a: ['three'], b: ['different three']},
-            {b: ['four']},
-            {ab: ['five', 'six', 'eight', 'nine']},
-          ],
-        });
-      });
-
-      test('marked deltas can start shared chunks', () => {
-        content[0] = {a: ['one'], b: ['two'], common: true};
-        assert.deepEqual(element._translateChunksToIgnore({content}), {
-          content: [
-            {ab: ['two']},
-            {a: ['three'], b: ['different three']},
-            {b: ['four']},
-            {ab: ['five', 'six']},
-            {a: ['seven']},
-            {ab: ['eight', 'nine']},
-          ],
+      test('coverageRangeChanged should be called', done => {
+        element.reload();
+        flush(() => {
+          assert.equal(notifyStub.callCount, 2);
+          done();
         });
       });
     });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
index 8251e53..47cf771 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -41,7 +42,7 @@
         has-tooltip
         class$="[[_computeSelectedClass(mode, _VIEW_MODES.SIDE_BY_SIDE)]]"
         title="Side-by-side diff"
-        on-tap="_handleSideBySideTap">
+        on-click="_handleSideBySideTap">
       <iron-icon icon="gr-icons:side-by-side"></iron-icon>
     </gr-button>
     <gr-button
@@ -50,7 +51,7 @@
         has-tooltip
         title="Unified diff"
         class$="[[_computeSelectedClass(mode, _VIEW_MODES.UNIFIED)]]"
-        on-tap="_handleUnifiedTap">
+        on-click="_handleUnifiedTap">
       <iron-icon icon="gr-icons:unified"></iron-icon>
     </gr-button>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
index e2d6a28..88dd91a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-diff-mode-selector',
-    _legacyUndefinedCheck: true,
 
     properties: {
       mode: {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
index c011106..adeaa15 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-mode-selector</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="gr-diff-mode-selector.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
index b850f2c..b0167b3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html">
@@ -26,7 +27,7 @@
     <style include="shared-styles">
       .diffHeader,
       .diffActions {
-        padding: 1em 1.5em;
+        padding: var(--spacing-l) var(--spacing-xl);
       }
       .diffHeader,
       .diffActions {
@@ -42,7 +43,7 @@
         justify-content: flex-end;
       }
       .diffPrefsOverlay gr-button {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
       }
       div.edited:after {
         color: var(--deemphasized-text-color);
@@ -50,7 +51,7 @@
       }
       #diffPreferences {
         display: flex;
-        padding: .35em 1.5em;
+        padding: var(--spacing-s) var(--spacing-xl);
       }
     </style>
     <gr-overlay id="diffPrefsOverlay" with-backdrop>
@@ -63,13 +64,13 @@
         <gr-button
             id="cancelButton"
             link
-            on-tap="_handleCancelDiff">
+            on-click="_handleCancelDiff">
             Cancel
         </gr-button>
         <gr-button
             id="saveButton"
             link primary
-            on-tap="_handleSaveDiffPreferences"
+            on-click="_handleSaveDiffPreferences"
             disabled$="[[!_diffPrefsChanged]]">
             Save
         </gr-button>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
index 7f7cd73..9b723bd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-diff-preferences-dialog',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
@@ -39,15 +38,19 @@
       _diffPrefsChanged: Boolean,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     getFocusStops() {
       return {
-        start: this.$.contextSelect,
+        start: this.$.diffPreferences.$.contextSelect,
         end: this.$.saveButton,
       };
     },
 
     resetFocus() {
-      this.$.contextSelect.focus();
+      this.$.diffPreferences.$.contextSelect.focus();
     },
 
     _computeHeaderClass(changed) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html
index 663cf25..922ac87 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-diff-processor">
   <script src="../gr-diff/gr-diff-line.js"></script>
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 2cc69e6..e052a8f 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
@@ -24,12 +24,6 @@
     RIGHT: 'right',
   };
 
-  const DiffGroupType = {
-    ADDED: 'b',
-    BOTH: 'ab',
-    REMOVED: 'a',
-  };
-
   const DiffHighlights = {
     ADDED: 'edit_b',
     REMOVED: 'edit_a',
@@ -48,19 +42,30 @@
   /**
    * Converts the API's `DiffContent`s  to `GrDiffGroup`s for rendering.
    *
-   * This includes a number of tasks:
+   * Glossary:
+   * - "chunk": A single `DiffContent` as returned by the API.
+   * - "group": A single `GrDiffGroup` as used for rendering.
+   * - "common" chunk/group: A chunk/group that should be considered unchanged
+   *   for diffing purposes. This can mean its either actually unchanged, or it
+   *   has only whitespace changes.
+   * - "key location": A line number and side of the diff that should not be
+   *   collapsed e.g. because a comment is attached to it, or because it was
+   *   provided in the URL and thus should be visible
+   * - "uncollapsible" chunk/group: A chunk/group that is either not "common",
+   *   or cannot be collapsed because it contains a key location
+   *
+   * Here a a number of tasks this processor performs:
+   *  - splitting large chunks to allow more granular async rendering
    *  - adding a group for the "File" pseudo line that file-level comments can
    *    be attached to
-   *  - replacing unchanged parts of the diff that are outside the user's
+   *  - replacing common parts of the diff that are outside the user's
    *    context setting and do not have comments with a group representing the
-   *    "expand context" widget. This may require splitting a `DiffContent` so
+   *    "expand context" widget. This may require splitting a chunk/group so
    *    that the part that is within the context or has comments is shown, while
    *    the rest is not.
-   *  - splitting large `DiffContent`s to allow more granular async rendering
    */
   Polymer({
     is: 'gr-diff-processor',
-    _legacyUndefinedCheck: true,
 
     properties: {
 
@@ -127,13 +132,16 @@
     },
 
     /**
-     * Asynchronously process the diff object into groups. As it processes, it
+     * Asynchronously process the diff chunks into groups. As it processes, it
      * will splice groups into the `groups` property of the component.
      *
-     * @return {Promise} A promise that resolves when the diff is completely
-     *     processed.
+     * @param {!Array<!Gerrit.DiffChunk>} chunks
+     * @param {boolean} isBinary
+     *
+     * @return {!Promise<!Array<!Object>>} A promise that resolves with an
+     *     array of GrDiffGroups when the diff is completely processed.
      */
-    process(content, isBinary) {
+    process(chunks, isBinary) {
       // Cancel any still running process() calls, because they append to the
       // same groups field.
       this.cancel();
@@ -150,11 +158,11 @@
           new Promise(resolve => {
             const state = {
               lineNums: {left: 0, right: 0},
-              sectionIndex: 0,
+              chunkIndex: 0,
             };
 
-            content = this._splitLargeChunks(content);
-            content = this._splitUnchangedChunksWithComments(content);
+            chunks = this._splitLargeChunks(chunks);
+            chunks = this._splitCommonChunksWithKeyLocations(chunks);
 
             let currentBatch = 0;
             const nextStep = () => {
@@ -163,23 +171,23 @@
                 return;
               }
               // If we are done, resolve the promise.
-              if (state.sectionIndex >= content.length) {
-                resolve(this.groups);
+              if (state.chunkIndex >= chunks.length) {
+                resolve();
                 this._nextStepHandle = null;
                 return;
               }
 
-              // Process the next section and incorporate the result.
-              const result = this._processNext(state, content);
-              for (const group of result.groups) {
+              // Process the next chunk and incorporate the result.
+              const stateUpdate = this._processNext(state, chunks);
+              for (const group of stateUpdate.groups) {
                 this.push('groups', group);
                 currentBatch += group.lines.length;
               }
-              state.lineNums.left += result.lineDelta.left;
-              state.lineNums.right += result.lineDelta.right;
+              state.lineNums.left += stateUpdate.lineDelta.left;
+              state.lineNums.right += stateUpdate.lineDelta.right;
 
               // Increment the index and recurse.
-              state.sectionIndex++;
+              state.chunkIndex = stateUpdate.newChunkIndex;
               if (currentBatch >= this._asyncThreshold) {
                 currentBatch = 0;
                 this._nextStepHandle = this.async(nextStep, 1);
@@ -208,180 +216,199 @@
     },
 
     /**
-     * Process the next section of the diff.
+     * Process the next uncollapsible chunk, or the next collapsible chunks.
+     *
+     * @param {!Object} state
+     * @param {!Array<!Object>} chunks
+     * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
      */
-    _processNext(state, content) {
-      const section = content[state.sectionIndex];
-
-      const rows = {
-        both: section[DiffGroupType.BOTH] || null,
-        added: section[DiffGroupType.ADDED] || null,
-        removed: section[DiffGroupType.REMOVED] || null,
-      };
-
-      const highlights = {
-        added: section[DiffHighlights.ADDED] || null,
-        removed: section[DiffHighlights.REMOVED] || null,
-      };
-
-      if (rows.both) { // If it's a shared section.
-        let sectionEnd = null;
-        if (state.sectionIndex === 0) {
-          sectionEnd = 'first';
-        } else if (state.sectionIndex === content.length - 1) {
-          sectionEnd = 'last';
-        }
-
-        const sharedGroups = this._sharedGroupsFromRows(
-            rows.both,
-            content.length > 1 ? this.context : WHOLE_FILE,
-            state.lineNums.left,
-            state.lineNums.right,
-            sectionEnd);
-
+    _processNext(state, chunks) {
+      const firstUncollapsibleChunkIndex =
+          this._firstUncollapsibleChunkIndex(chunks, state.chunkIndex);
+      if (firstUncollapsibleChunkIndex === state.chunkIndex) {
+        const chunk = chunks[state.chunkIndex];
         return {
           lineDelta: {
-            left: rows.both.length,
-            right: rows.both.length,
+            left: this._linesLeft(chunk).length,
+            right: this._linesRight(chunk).length,
           },
-          groups: sharedGroups,
-        };
-      } else { // Otherwise it's a delta section.
-        const deltaGroup = this._deltaGroupFromRows(
-            rows.added,
-            rows.removed,
-            state.lineNums.left,
-            state.lineNums.right,
-            highlights);
-        deltaGroup.dueToRebase = section.due_to_rebase;
-
-        return {
-          lineDelta: {
-            left: rows.removed ? rows.removed.length : 0,
-            right: rows.added ? rows.added.length : 0,
-          },
-          groups: [deltaGroup],
+          groups: [this._chunkToGroup(
+              chunk, state.lineNums.left + 1, state.lineNums.right + 1)],
+          newChunkIndex: state.chunkIndex + 1,
         };
       }
+
+      return this._processCollapsibleChunks(
+          state, chunks, firstUncollapsibleChunkIndex);
+    },
+
+    _linesLeft(chunk) {
+      return chunk.ab || chunk.a || [];
+    },
+
+    _linesRight(chunk) {
+      return chunk.ab || chunk.b || [];
+    },
+
+    _firstUncollapsibleChunkIndex(chunks, offset) {
+      let chunkIndex = offset;
+      while (chunkIndex < chunks.length &&
+          this._isCollapsibleChunk(chunks[chunkIndex])) {
+        chunkIndex++;
+      }
+      return chunkIndex;
+    },
+
+    _isCollapsibleChunk(chunk) {
+      return (chunk.ab || chunk.common) && !chunk.keyLocation;
     },
 
     /**
-     * Take rows of a shared diff section and produce an array of corresponding
-     * (potentially collapsed) groups.
+     * Process a stretch of collapsible chunks.
      *
-     * @param {!Array<string>} rows
-     * @param {number} context
-     * @param {number} startLineNumLeft
-     * @param {number} startLineNumRight
-     * @param {?string=} opt_sectionEnd String representing whether this is the
-     *     first section or the last section or neither. Use the values 'first',
-     *     'last' and null respectively.
-     * @return {!Array<!Object>} Array of GrDiffGroup
+     * Outputs up to three groups:
+     *  1) Visible context before the hidden common code, unless it's the
+     *     very beginning of the file.
+     *  2) Context hidden behind a context bar, unless empty.
+     *  3) Visible context after the hidden common code, unless it's the very
+     *     end of the file.
+     *
+     * @param {!Object} state
+     * @param {!Array<Object>} chunks
+     * @param {number} firstUncollapsibleChunkIndex
+     * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
      */
-    _sharedGroupsFromRows(rows, context, startLineNumLeft,
-        startLineNumRight, opt_sectionEnd) {
-      const result = [];
-      const lines = [];
-      let line;
+    _processCollapsibleChunks(
+        state, chunks, firstUncollapsibleChunkIndex) {
+      const collapsibleChunks = chunks.slice(
+          state.chunkIndex, firstUncollapsibleChunkIndex);
+      const lineCount = collapsibleChunks.reduce(
+          (sum, chunk) => sum + this._commonChunkLength(chunk), 0);
 
-      // Map each row to a GrDiffLine.
-      for (let i = 0; i < rows.length; i++) {
-        line = new GrDiffLine(GrDiffLine.Type.BOTH);
-        line.text = rows[i];
-        line.beforeNumber = ++startLineNumLeft;
-        line.afterNumber = ++startLineNumRight;
-        lines.push(line);
+      let groups = this._chunksToGroups(
+          collapsibleChunks,
+          state.lineNums.left + 1,
+          state.lineNums.right + 1);
+
+      if (this.context !== WHOLE_FILE) {
+        const hiddenStart = state.chunkIndex === 0 ? 0 : this.context;
+        const hiddenEnd = lineCount - (
+          firstUncollapsibleChunkIndex === chunks.length ?
+            0 : this.context);
+        groups = GrDiffGroup.hideInContextControl(
+            groups, hiddenStart, hiddenEnd);
       }
 
-      // Find the hidden range based on the user's context preference. If this
-      // is the first or the last section of the diff, make sure the collapsed
-      // part of the section extends to the edge of the file.
-      const hiddenRange = [context, rows.length - context];
-      if (opt_sectionEnd === 'first') {
-        hiddenRange[0] = 0;
-      } else if (opt_sectionEnd === 'last') {
-        hiddenRange[1] = rows.length;
-      }
+      return {
+        lineDelta: {
+          left: lineCount,
+          right: lineCount,
+        },
+        groups,
+        newChunkIndex: firstUncollapsibleChunkIndex,
+      };
+    },
 
-      // If there is a range to hide.
-      if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 1) {
-        const linesBeforeCtx = lines.slice(0, hiddenRange[0]);
-        const hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
-        const linesAfterCtx = lines.slice(hiddenRange[1]);
-
-        if (linesBeforeCtx.length > 0) {
-          result.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
-        }
-
-        const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
-        ctxLine.contextGroup =
-            new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines);
-        result.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
-            [ctxLine]));
-
-        if (linesAfterCtx.length > 0) {
-          result.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx));
-        }
-      } else {
-        result.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, lines));
-      }
-
-      return result;
+    _commonChunkLength(chunk) {
+      console.assert(chunk.ab || chunk.common);
+      console.assert(
+          !chunk.a || (chunk.b && chunk.a.length === chunk.b.length),
+          `common chunk needs same number of a and b lines: `, chunk);
+      return this._linesLeft(chunk).length;
     },
 
     /**
-     * Take the rows of a delta diff section and produce the corresponding
-     * group.
-     *
-     * @param {!Array<string>} rowsAdded
-     * @param {!Array<string>} rowsRemoved
-     * @param {number} startLineNumLeft
-     * @param {number} startLineNumRight
-     * @return {!Object} (Gr-Diff-Group)
+     * @param {!Array<!Object>} chunks
+     * @param {number} offsetLeft
+     * @param {number} offsetRight
+     * @return {!Array<!Object>} (GrDiffGroup)
      */
-    _deltaGroupFromRows(rowsAdded, rowsRemoved, startLineNumLeft,
-        startLineNumRight, highlights) {
+    _chunksToGroups(chunks, offsetLeft, offsetRight) {
+      return chunks.map(chunk => {
+        const group = this._chunkToGroup(chunk, offsetLeft, offsetRight);
+        const chunkLength = this._commonChunkLength(chunk);
+        offsetLeft += chunkLength;
+        offsetRight += chunkLength;
+        return group;
+      });
+    },
+
+    /**
+     * @param {!Object} chunk
+     * @param {number} offsetLeft
+     * @param {number} offsetRight
+     * @return {!Object} (GrDiffGroup)
+     */
+    _chunkToGroup(chunk, offsetLeft, offsetRight) {
+      const type = chunk.ab ? GrDiffGroup.Type.BOTH : GrDiffGroup.Type.DELTA;
+      const lines = this._linesFromChunk(chunk, offsetLeft, offsetRight);
+      const group = new GrDiffGroup(type, lines);
+      group.keyLocation = chunk.keyLocation;
+      group.dueToRebase = chunk.due_to_rebase;
+      group.ignoredWhitespaceOnly = chunk.common;
+      return group;
+    },
+
+    _linesFromChunk(chunk, offsetLeft, offsetRight) {
+      if (chunk.ab) {
+        return chunk.ab.map((row, i) => this._lineFromRow(
+            GrDiffLine.Type.BOTH, offsetLeft, offsetRight, row, i));
+      }
       let lines = [];
-      if (rowsRemoved) {
-        lines = lines.concat(this._deltaLinesFromRows(GrDiffLine.Type.REMOVE,
-            rowsRemoved, startLineNumLeft, highlights.removed));
+      if (chunk.a) {
+        // Avoiding a.push(...b) because that causes callstack overflows for
+        // large b, which can occur when large files are added removed.
+        lines = lines.concat(this._linesFromRows(
+            GrDiffLine.Type.REMOVE, chunk.a, offsetLeft,
+            chunk[DiffHighlights.REMOVED]));
       }
-      if (rowsAdded) {
-        lines = lines.concat(this._deltaLinesFromRows(GrDiffLine.Type.ADD,
-            rowsAdded, startLineNumRight, highlights.added));
-      }
-      return new GrDiffGroup(GrDiffGroup.Type.DELTA, lines);
-    },
-
-    /**
-     * @return {!Array<!Object>} Array of GrDiffLines
-     */
-    _deltaLinesFromRows(lineType, rows, startLineNum,
-        opt_highlights) {
-      // Normalize highlights if they have been passed.
-      if (opt_highlights) {
-        opt_highlights = this._normalizeIntralineHighlights(rows,
-            opt_highlights);
-      }
-
-      const lines = [];
-      let line;
-      for (let i = 0; i < rows.length; i++) {
-        line = new GrDiffLine(lineType);
-        line.text = rows[i];
-        if (lineType === GrDiffLine.Type.ADD) {
-          line.afterNumber = ++startLineNum;
-        } else {
-          line.beforeNumber = ++startLineNum;
-        }
-        if (opt_highlights) {
-          line.highlights = opt_highlights.filter(hl => hl.contentIndex === i);
-        }
-        lines.push(line);
+      if (chunk.b) {
+        // Avoiding a.push(...b) because that causes callstack overflows for
+        // large b, which can occur when large files are added removed.
+        lines = lines.concat(this._linesFromRows(
+            GrDiffLine.Type.ADD, chunk.b, offsetRight,
+            chunk[DiffHighlights.ADDED]));
       }
       return lines;
     },
 
+    /**
+     * @param {string} lineType (GrDiffLine.Type)
+     * @param {!Array<string>} rows
+     * @param {number} offset
+     * @param {?Array<!Gerrit.IntralineInfo>=} opt_intralineInfos
+     * @return {!Array<!Object>} (GrDiffLine)
+     */
+    _linesFromRows(lineType, rows, offset, opt_intralineInfos) {
+      const grDiffHighlights = opt_intralineInfos ?
+        this._convertIntralineInfos(rows, opt_intralineInfos) : undefined;
+      return rows.map((row, i) => this._lineFromRow(
+          lineType, offset, offset, row, i, grDiffHighlights));
+    },
+
+    /**
+     * @param {string} type (GrDiffLine.Type)
+     * @param {number} offsetLeft
+     * @param {number} offsetRight
+     * @param {string} row
+     * @param {number} i
+     * @param {!Array<!Object>=} opt_highlights
+     * @return {!Object} (GrDiffLine)
+     */
+    _lineFromRow(type, offsetLeft, offsetRight, row, i, opt_highlights) {
+      const line = new GrDiffLine(type);
+      line.text = row;
+      if (type !== GrDiffLine.Type.ADD) line.beforeNumber = offsetLeft + i;
+      if (type !== GrDiffLine.Type.REMOVE) line.afterNumber = offsetRight + i;
+      if (opt_highlights) {
+        line.hasIntralineInfo = true;
+        line.highlights = opt_highlights.filter(hl => hl.contentIndex === i);
+      } else {
+        line.hasIntralineInfo = false;
+      }
+      return line;
+    },
+
     _makeFileComments() {
       const line = new GrDiffLine(GrDiffLine.Type.BOTH);
       line.beforeNumber = GrDiffLine.FILE;
@@ -402,16 +429,16 @@
      * into 2 chunks, one max sized one and the rest (for reasons that are
      * unclear to me).
      *
-     * @param {!Array<!Object>} chunks Chunks as returned from the server
-     * @return {!Array<!Object>} Finer grained chunks.
+     * @param {!Array<!Gerrit.DiffChunk>} chunks Chunks as returned from the server
+     * @return {!Array<!Gerrit.DiffChunk>} Finer grained chunks.
      */
     _splitLargeChunks(chunks) {
       const newChunks = [];
 
       for (const chunk of chunks) {
         if (!chunk.ab) {
-          for (const group of this._breakdownGroup(chunk)) {
-            newChunks.push(group);
+          for (const subChunk of this._breakdownChunk(chunk)) {
+            newChunks.push(subChunk);
           }
           continue;
         }
@@ -421,7 +448,7 @@
         // enabled for any other context preference because manipulating the
         // chunks in this way violates assumptions by the context grouper logic.
         if (this.context === -1 && chunk.ab.length > MAX_GROUP_SIZE * 2) {
-          // Split large shared groups in two, where the first is the maximum
+          // Split large shared chunks in two, where the first is the maximum
           // group size.
           newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)});
           newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)});
@@ -433,21 +460,21 @@
     },
 
     /**
-     * In order to show comments out of the bounds of the selected context,
-     * treat them as separate chunks within the model so that the content (and
-     * context surrounding it) renders correctly.
+     * In order to show key locations, such as comments, out of the bounds of
+     * the selected context, treat them as separate chunks within the model so
+     * that the content (and context surrounding it) renders correctly.
      *
      * @param {!Array<!Object>} chunks DiffContents as returned from server.
      * @return {!Array<!Object>} Finer grained DiffContents.
      */
-    _splitUnchangedChunksWithComments(chunks) {
+    _splitCommonChunksWithKeyLocations(chunks) {
       const result = [];
-      let leftLineNum = 0;
-      let rightLineNum = 0;
+      let leftLineNum = 1;
+      let rightLineNum = 1;
 
       for (const chunk of chunks) {
         // If it isn't a common chunk, append it as-is and update line numbers.
-        if (!chunk.ab) {
+        if (!chunk.ab && !chunk.common) {
           if (chunk.a) {
             leftLineNum += chunk.a.length;
           }
@@ -458,34 +485,26 @@
           continue;
         }
 
-        let currentChunk = {ab: []};
-
-        // For each line in the common group.
-        for (const line of chunk.ab) {
-          leftLineNum++;
-          rightLineNum++;
-
-          // If this line should not be collapsed.
-          if (this.keyLocations[DiffSide.LEFT][leftLineNum] ||
-              this.keyLocations[DiffSide.RIGHT][rightLineNum]) {
-            // If any lines have been accumulated into the chunk leading up to
-            // this non-collapse line, then add them as a chunk and start a new
-            // one.
-            if (currentChunk.ab && currentChunk.ab.length > 0) {
-              result.push(currentChunk);
-              currentChunk = {ab: []};
-            }
-
-            // Add the non-collapse line as its own chunk.
-            result.push({ab: [line]});
-          } else {
-            // Append the current line to the current chunk.
-            currentChunk.ab.push(line);
-          }
+        if (chunk.common && chunk.a.length != chunk.b.length) {
+          throw new Error(
+              'DiffContent with common=true must always have equal length');
         }
+        const numLines = this._commonChunkLength(chunk);
+        const chunkEnds = this._findChunkEndsAtKeyLocations(
+            numLines, leftLineNum, rightLineNum);
+        leftLineNum += numLines;
+        rightLineNum += numLines;
 
-        if (currentChunk.ab && currentChunk.ab.length > 0) {
-          result.push(currentChunk);
+        if (chunk.ab) {
+          result.push(...this._splitAtChunkEnds(chunk.ab, chunkEnds)
+              .map(({lines, keyLocation}) =>
+                Object.assign({}, 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})));
         }
       }
 
@@ -493,52 +512,84 @@
     },
 
     /**
-     * The `highlights` array consists of a list of <skip length, mark length>
-     * pairs, where the skip length is the number of characters between the
-     * end of the previous edit and the start of this edit, and the mark
-     * length is the number of edited characters following the skip. The start
-     * of the edits is from the beginning of the related diff content lines.
-     *
-     * Note that the implied newline character at the end of each line is
-     * included in the length calculation, and thus it is possible for the
-     * edits to span newlines.
-     *
-     * A line highlight object consists of three fields:
-     * - contentIndex: The index of the diffChunk `content` field (the line
-     *   being referred to).
-     * - startIndex: Where the highlight should begin.
-     * - endIndex: (optional) Where the highlight should end. If omitted, the
-     *   highlight is meant to be a continuation onto the next line.
+     * @return {!Array<{offset: number, keyLocation: boolean}>} Offsets of the
+     *   new chunk ends, including whether it's a key location.
      */
-    _normalizeIntralineHighlights(content, highlights) {
-      let contentIndex = 0;
+    _findChunkEndsAtKeyLocations(numLines, leftOffset, rightOffset) {
+      const result = [];
+      let lastChunkEnd = 0;
+      for (let i=0; i<numLines; i++) {
+        // If this line should not be collapsed.
+        if (this.keyLocations[DiffSide.LEFT][leftOffset + i] ||
+            this.keyLocations[DiffSide.RIGHT][rightOffset + i]) {
+          // If any lines have been accumulated into the chunk leading up to
+          // this non-collapse line, then add them as a chunk and start a new
+          // one.
+          if (i > lastChunkEnd) {
+            result.push({offset: i, keyLocation: false});
+            lastChunkEnd = i;
+          }
+
+          // Add the non-collapse line as its own chunk.
+          result.push({offset: i + 1, keyLocation: true});
+        }
+      }
+
+      if (numLines > lastChunkEnd) {
+        result.push({offset: numLines, keyLocation: false});
+      }
+
+      return result;
+    },
+
+    _splitAtChunkEnds(lines, chunkEnds) {
+      const result = [];
+      let lastChunkEndOffset = 0;
+      for (const {offset, keyLocation} of chunkEnds) {
+        result.push(
+            {lines: lines.slice(lastChunkEndOffset, offset), keyLocation});
+        lastChunkEndOffset = offset;
+      }
+      return result;
+    },
+
+    /**
+     * Converts `IntralineInfo`s return by the API to `GrLineHighlights` used
+     * for rendering.
+     *
+     * @param {!Array<string>} rows
+     * @param {!Array<!Gerrit.IntralineInfo>} intralineInfos
+     * @return {!Array<!Object>} (GrDiffLine.Highlight)
+     */
+    _convertIntralineInfos(rows, intralineInfos) {
+      let rowIndex = 0;
       let idx = 0;
       const normalized = [];
-      for (const hl of highlights) {
-        let line = content[contentIndex] + '\n';
+      for (const [skipLength, markLength] of intralineInfos) {
+        let line = rows[rowIndex] + '\n';
         let j = 0;
-        while (j < hl[0]) {
+        while (j < skipLength) {
           if (idx === line.length) {
             idx = 0;
-            line = content[++contentIndex] + '\n';
+            line = rows[++rowIndex] + '\n';
             continue;
           }
           idx++;
           j++;
         }
         let lineHighlight = {
-          contentIndex,
+          contentIndex: rowIndex,
           startIndex: idx,
         };
 
         j = 0;
-        while (line && j < hl[1]) {
+        while (line && j < markLength) {
           if (idx === line.length) {
             idx = 0;
-            line = content[++contentIndex] + '\n';
+            line = rows[++rowIndex] + '\n';
             normalized.push(lineHighlight);
             lineHighlight = {
-              contentIndex,
+              contentIndex: rowIndex,
               startIndex: idx,
             };
             continue;
@@ -554,32 +605,32 @@
 
     /**
      * If a group is an addition or a removal, break it down into smaller groups
-     * of that type using the MAX_GROUP_SIZE. If the group is a shared section
+     * of that type using the MAX_GROUP_SIZE. If the group is a shared chunk
      * or a delta it is returned as the single element of the result array.
      *
-     * @param {!Object} group A raw chunk from a diff response.
+     * @param {!Gerrit.DiffChunk} chunk A raw chunk from a diff response.
      * @return {!Array<!Array<!Object>>}
      */
-    _breakdownGroup(group) {
+    _breakdownChunk(chunk) {
       let key = null;
-      if (group.a && !group.b) {
+      if (chunk.a && !chunk.b) {
         key = 'a';
-      } else if (group.b && !group.a) {
+      } else if (chunk.b && !chunk.a) {
         key = 'b';
-      } else if (group.ab) {
+      } else if (chunk.ab) {
         key = 'ab';
       }
 
-      if (!key) { return [group]; }
+      if (!key) { return [chunk]; }
 
-      return this._breakdown(group[key], MAX_GROUP_SIZE)
-          .map(subgroupLines => {
-            const subGroup = {};
-            subGroup[key] = subgroupLines;
-            if (group.due_to_rebase) {
-              subGroup.due_to_rebase = true;
+      return this._breakdown(chunk[key], MAX_GROUP_SIZE)
+          .map(subChunkLines => {
+            const subChunk = {};
+            subChunk[key] = subChunkLines;
+            if (chunk.due_to_rebase) {
+              subChunk.due_to_rebase = true;
             }
-            return subGroup;
+            return subChunk;
           });
     },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 186a49e..c04b066 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-processor test</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-diff-processor.html">
 
@@ -60,7 +62,7 @@
         element.context = 4;
       });
 
-      test('process loaded content', done => {
+      test('process loaded content', () => {
         const content = [
           {
             ab: [
@@ -86,7 +88,7 @@
           },
         ];
 
-        element.process(content).then(() => {
+        return element.process(content).then(() => {
           const groups = element.groups;
 
           assert.equal(groups.length, 4);
@@ -139,31 +141,15 @@
             'everyone pretend to shower.',
             'Fry: Same as every day. Got it.',
           ]);
-
-          done();
         });
       });
 
-      test('insert context groups', done => {
+      test('first group is for file', () => {
         const content = [
-          {ab: []},
-          {a: ['all work and no play make andybons a dull boy']},
-          {ab: []},
-          {b: ['elgoog elgoog elgoog']},
-          {ab: []},
+          {b: ['foo']},
         ];
-        for (let i = 0; i < 100; i++) {
-          content[0].ab.push('all work and no play make jack a dull boy');
-          content[4].ab.push('all work and no play make jill a dull girl');
-        }
-        for (let i = 0; i < 5; i++) {
-          content[2].ab.push('no tv and no beer make homer go crazy');
-        }
 
-        const context = 10;
-        element.context = context;
-
-        element.process(content).then(() => {
+        return element.process(content).then(() => {
           const groups = element.groups;
 
           assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
@@ -171,107 +157,278 @@
           assert.equal(groups[0].lines[0].text, '');
           assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
           assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
-
-          assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-          assert.instanceOf(groups[1].lines[0].contextGroup, GrDiffGroup);
-          assert.equal(groups[1].lines[0].contextGroup.lines.length, 90);
-          for (const l of groups[1].lines[0].contextGroup.lines) {
-            assert.equal(l.text, content[0].ab[0]);
-          }
-
-          assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
-          assert.equal(groups[2].lines.length, context);
-          for (const l of groups[2].lines) {
-            assert.equal(l.text, content[0].ab[0]);
-          }
-
-          assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
-          assert.equal(groups[3].lines.length, 1);
-          assert.equal(groups[3].removes.length, 1);
-          assert.equal(groups[3].removes[0].text,
-              'all work and no play make andybons a dull boy');
-
-          assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
-          assert.equal(groups[4].lines.length, 5);
-          for (const l of groups[4].lines) {
-            assert.equal(l.text, content[2].ab[0]);
-          }
-
-          assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
-          assert.equal(groups[5].lines.length, 1);
-          assert.equal(groups[5].adds.length, 1);
-          assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
-
-          assert.equal(groups[6].type, GrDiffGroup.Type.BOTH);
-          assert.equal(groups[6].lines.length, context);
-          for (const l of groups[6].lines) {
-            assert.equal(l.text, content[4].ab[0]);
-          }
-
-          assert.equal(groups[7].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-          assert.instanceOf(groups[7].lines[0].contextGroup, GrDiffGroup);
-          assert.equal(groups[7].lines[0].contextGroup.lines.length, 90);
-          for (const l of groups[7].lines[0].contextGroup.lines) {
-            assert.equal(l.text, content[4].ab[0]);
-          }
-
-          done();
         });
       });
 
-      test('insert context groups', done => {
-        const content = [
-          {a: ['all work and no play make andybons a dull boy']},
-          {ab: []},
-          {b: ['elgoog elgoog elgoog']},
-        ];
-        for (let i = 0; i < 50; i++) {
-          content[1].ab.push('no tv and no beer make homer go crazy');
-        }
+      suite('context groups', () => {
+        test('at the beginning, larger than context', () => {
+          element.context = 10;
+          const content = [
+            {ab: new Array(100)
+                .fill('all work and no play make jack a dull boy')},
+            {a: ['all work and no play make andybons a dull boy']},
+          ];
 
-        const context = 10;
-        element.context = context;
+          return element.process(content).then(() => {
+            const groups = element.groups;
 
-        element.process(content).then(() => {
-          const groups = element.groups;
+            // group[0] is the file group
 
-          assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
-          assert.equal(groups[0].lines.length, 1);
-          assert.equal(groups[0].lines[0].text, '');
-          assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
-          assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
+            assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+            assert.instanceOf(groups[1].lines[0].contextGroups[0], GrDiffGroup);
+            assert.equal(groups[1].lines[0].contextGroups[0].lines.length, 90);
+            for (const l of groups[1].lines[0].contextGroups[0].lines) {
+              assert.equal(l.text, 'all work and no play make jack a dull boy');
+            }
 
-          assert.equal(groups[1].type, GrDiffGroup.Type.DELTA);
-          assert.equal(groups[1].lines.length, 1);
-          assert.equal(groups[1].removes.length, 1);
-          assert.equal(groups[1].removes[0].text,
-              'all work and no play make andybons a dull boy');
+            assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+            assert.equal(groups[2].lines.length, 10);
+            for (const l of groups[2].lines) {
+              assert.equal(l.text, 'all work and no play make jack a dull boy');
+            }
+          });
+        });
 
-          assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
-          assert.equal(groups[2].lines.length, context);
-          for (const l of groups[2].lines) {
-            assert.equal(l.text, content[1].ab[0]);
-          }
+        test('at the beginning, smaller than context', () => {
+          element.context = 10;
+          const content = [
+            {ab: new Array(5)
+                .fill('all work and no play make jack a dull boy')},
+            {a: ['all work and no play make andybons a dull boy']},
+          ];
 
-          assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-          assert.instanceOf(groups[3].lines[0].contextGroup, GrDiffGroup);
-          assert.equal(groups[3].lines[0].contextGroup.lines.length, 30);
-          for (const l of groups[3].lines[0].contextGroup.lines) {
-            assert.equal(l.text, content[1].ab[0]);
-          }
+          return element.process(content).then(() => {
+            const groups = element.groups;
 
-          assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
-          assert.equal(groups[4].lines.length, context);
-          for (const l of groups[4].lines) {
-            assert.equal(l.text, content[1].ab[0]);
-          }
+            // group[0] is the file group
 
-          assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
-          assert.equal(groups[5].lines.length, 1);
-          assert.equal(groups[5].adds.length, 1);
-          assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
+            assert.equal(groups[1].type, GrDiffGroup.Type.BOTH);
+            assert.equal(groups[1].lines.length, 5);
+            for (const l of groups[1].lines) {
+              assert.equal(l.text, 'all work and no play make jack a dull boy');
+            }
+          });
+        });
 
-          done();
+        test('at the end, larger than context', () => {
+          element.context = 10;
+          const content = [
+            {a: ['all work and no play make andybons a dull boy']},
+            {ab: new Array(100)
+                .fill('all work and no play make jill a dull girl')},
+          ];
+
+          return element.process(content).then(() => {
+            const groups = element.groups;
+
+            // group[0] is the file group
+            // group[1] is the "a" group
+
+            assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+            assert.equal(groups[2].lines.length, 10);
+            for (const l of groups[2].lines) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+
+            assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+            assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup);
+            assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 90);
+            for (const l of groups[3].lines[0].contextGroups[0].lines) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+          });
+        });
+
+        test('at the end, smaller than context', () => {
+          element.context = 10;
+          const content = [
+            {a: ['all work and no play make andybons a dull boy']},
+            {ab: new Array(5)
+                .fill('all work and no play make jill a dull girl')},
+          ];
+
+          return element.process(content).then(() => {
+            const groups = element.groups;
+
+            // group[0] is the file group
+            // group[1] is the "a" group
+
+            assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+            assert.equal(groups[2].lines.length, 5);
+            for (const l of groups[2].lines) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+          });
+        });
+
+        test('for interleaved ab and common: true chunks', () => {
+          element.context = 10;
+          const content = [
+            {a: ['all work and no play make andybons a dull boy']},
+            {ab: new Array(3)
+                .fill('all work and no play make jill a dull girl')},
+            {
+              a: new Array(3).fill(
+                  'all work and no play make jill a dull girl'),
+              b: new Array(3).fill(
+                  '  all work and no play make jill a dull girl'),
+              common: true,
+            },
+            {ab: new Array(3)
+                .fill('all work and no play make jill a dull girl')},
+            {
+              a: new Array(3).fill(
+                  'all work and no play make jill a dull girl'),
+              b: new Array(3).fill(
+                  '  all work and no play make jill a dull girl'),
+              common: true,
+            },
+            {ab: new Array(3)
+                .fill('all work and no play make jill a dull girl')},
+          ];
+
+          return element.process(content).then(() => {
+            const groups = element.groups;
+
+            // group[0] is the file group
+            // group[1] is the "a" group
+
+            // The first three interleaved chunks are completely shown because
+            // they are part of the context (3 * 3 <= 10)
+
+            assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+            assert.equal(groups[2].lines.length, 3);
+            for (const l of groups[2].lines) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+
+            assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
+            assert.equal(groups[3].lines.length, 6);
+            assert.equal(groups[3].adds.length, 3);
+            assert.equal(groups[3].removes.length, 3);
+            for (const l of groups[3].removes) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+            for (const l of groups[3].adds) {
+              assert.equal(
+                  l.text, '  all work and no play make jill a dull girl');
+            }
+
+            assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
+            assert.equal(groups[4].lines.length, 3);
+            for (const l of groups[4].lines) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+
+            // The next chunk is partially shown, so it results in two groups
+
+            assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
+            assert.equal(groups[5].lines.length, 2);
+            assert.equal(groups[5].adds.length, 1);
+            assert.equal(groups[5].removes.length, 1);
+            for (const l of groups[5].removes) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+            for (const l of groups[5].adds) {
+              assert.equal(
+                  l.text, '  all work and no play make jill a dull girl');
+            }
+
+            assert.equal(groups[6].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+            assert.equal(groups[6].lines[0].contextGroups.length, 2);
+
+            assert.equal(groups[6].lines[0].contextGroups[0].lines.length, 4);
+            assert.equal(groups[6].lines[0].contextGroups[0].removes.length, 2);
+            assert.equal(groups[6].lines[0].contextGroups[0].adds.length, 2);
+            for (const l of groups[6].lines[0].contextGroups[0].removes) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+            for (const l of groups[6].lines[0].contextGroups[0].adds) {
+              assert.equal(
+                  l.text, '  all work and no play make jill a dull girl');
+            }
+
+            // The final chunk is completely hidden
+            assert.equal(
+                groups[6].lines[0].contextGroups[1].type,
+                GrDiffGroup.Type.BOTH);
+            assert.equal(groups[6].lines[0].contextGroups[1].lines.length, 3);
+            for (const l of groups[6].lines[0].contextGroups[1].lines) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+          });
+        });
+
+        test('in the middle, larger than context', () => {
+          element.context = 10;
+          const content = [
+            {a: ['all work and no play make andybons a dull boy']},
+            {ab: new Array(100)
+                .fill('all work and no play make jill a dull girl')},
+            {a: ['all work and no play make andybons a dull boy']},
+          ];
+
+          return element.process(content).then(() => {
+            const groups = element.groups;
+
+            // group[0] is the file group
+            // group[1] is the "a" group
+
+            assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+            assert.equal(groups[2].lines.length, 10);
+            for (const l of groups[2].lines) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+
+            assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+            assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup);
+            assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 80);
+            for (const l of groups[3].lines[0].contextGroups[0].lines) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+
+            assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
+            assert.equal(groups[4].lines.length, 10);
+            for (const l of groups[4].lines) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+          });
+        });
+
+        test('in the middle, smaller than context', () => {
+          element.context = 10;
+          const content = [
+            {a: ['all work and no play make andybons a dull boy']},
+            {ab: new Array(5)
+                .fill('all work and no play make jill a dull girl')},
+            {a: ['all work and no play make andybons a dull boy']},
+          ];
+
+          return element.process(content).then(() => {
+            const groups = element.groups;
+
+            // group[0] is the file group
+            // group[1] is the "a" group
+
+            assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+            assert.equal(groups[2].lines.length, 5);
+            for (const l of groups[2].lines) {
+              assert.equal(
+                  l.text, 'all work and no play make jill a dull girl');
+            }
+          });
         });
       });
 
@@ -303,10 +460,11 @@
           },
         ];
         const result =
-            element._splitUnchangedChunksWithComments(content);
+            element._splitCommonChunksWithKeyLocations(content);
         assert.deepEqual(result, [
           {
             ab: ['Copyright (C) 2015 The Android Open Source Project'],
+            keyLocation: true,
           },
           {
             ab: [
@@ -320,10 +478,12 @@
               '',
               'Unless required by applicable law or agreed to in writing, ',
             ],
+            keyLocation: false,
           },
           {
             ab: [
               'software distributed under the License is distributed on an '],
+            keyLocation: true,
           },
           {
             ab: [
@@ -332,6 +492,7 @@
               'language governing permissions and limitations under the ' +
                   'License.',
             ],
+            keyLocation: false,
           },
         ]);
       });
@@ -348,14 +509,16 @@
         assert.deepEqual(result[1].ab, content[0].ab.slice(120));
       });
 
-      test('does not break-down shared chunks w/ context', () => {
+      test('does not break-down common chunks w/ context', () => {
         const content = [{
           ab: _.times(75, () => { return `${Math.random()}`; }),
         }];
         element.context = 4;
         const result =
-            element._splitUnchangedChunksWithComments(content);
-        assert.deepEqual(result, content);
+            element._splitCommonChunksWithKeyLocations(content);
+        assert.equal(result.length, 1);
+        assert.deepEqual(result[0].ab, content[0].ab);
+        assert.isFalse(result[0].keyLocation);
       });
 
       test('intraline normalization', () => {
@@ -371,7 +534,7 @@
           [31, 34], [42, 26],
         ];
 
-        let results = element._normalizeIntralineHighlights(content,
+        let results = element._convertIntralineInfos(content,
             highlights);
         assert.deepEqual(results, [
           {
@@ -412,7 +575,7 @@
           [12, 67],
           [14, 29],
         ];
-        results = element._normalizeIntralineHighlights(content, highlights);
+        results = element._convertIntralineInfos(content, highlights);
         assert.deepEqual(results, [
           {
             contentIndex: 0,
@@ -453,10 +616,13 @@
         sandbox.stub(element, 'async');
         element._isScrolling = true;
         element.process(content);
+        // Just the files group - no more processing during scrolling.
         assert.equal(element.groups.length, 1);
+
         element._isScrolling = false;
         element.process(content);
-        assert.equal(element.groups.length, 33);
+        // More groups have been processed. How many does not matter here.
+        assert.isAtLeast(element.groups.length, 2);
       });
 
       test('image diffs', () => {
@@ -475,6 +641,198 @@
         assert.equal(element.groups[0].lines.length, 1);
       });
 
+      suite('_processNext', () => {
+        let rows;
+
+        setup(() => {
+          rows = loremIpsum.split(' ');
+        });
+
+        test('WHOLE_FILE', () => {
+          element.context = WHOLE_FILE;
+          const state = {
+            lineNums: {left: 10, right: 100},
+            chunkIndex: 1,
+          };
+          const chunks = [
+            {a: ['foo']},
+            {ab: rows},
+            {a: ['bar']},
+          ];
+          const result = element._processNext(state, chunks);
+
+          // Results in one, uncollapsed group with all rows.
+          assert.equal(result.groups.length, 1);
+          assert.equal(result.groups[0].type, GrDiffGroup.Type.BOTH);
+          assert.equal(result.groups[0].lines.length, rows.length);
+
+          // Line numbers are set correctly.
+          assert.equal(
+              result.groups[0].lines[0].beforeNumber,
+              state.lineNums.left + 1);
+          assert.equal(
+              result.groups[0].lines[0].afterNumber,
+              state.lineNums.right + 1);
+
+          assert.equal(result.groups[0].lines[rows.length - 1].beforeNumber,
+              state.lineNums.left + rows.length);
+          assert.equal(result.groups[0].lines[rows.length - 1].afterNumber,
+              state.lineNums.right + rows.length);
+        });
+
+        test('with context', () => {
+          element.context = 10;
+          const state = {
+            lineNums: {left: 10, right: 100},
+            chunkIndex: 1,
+          };
+          const chunks = [
+            {a: ['foo']},
+            {ab: rows},
+            {a: ['bar']},
+          ];
+          const result = element._processNext(state, chunks);
+          const expectedCollapseSize = rows.length - 2 * element.context;
+
+          assert.equal(result.groups.length, 3, 'Results in three groups');
+
+          // The first and last are uncollapsed context, whereas the middle has
+          // a single context-control line.
+          assert.equal(result.groups[0].lines.length, element.context);
+          assert.equal(result.groups[1].lines.length, 1);
+          assert.equal(result.groups[2].lines.length, element.context);
+
+          // The collapsed group has the hidden lines as its context group.
+          assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length,
+              expectedCollapseSize);
+        });
+
+        test('first', () => {
+          element.context = 10;
+          const state = {
+            lineNums: {left: 10, right: 100},
+            chunkIndex: 0,
+          };
+          const chunks = [
+            {ab: rows},
+            {a: ['foo']},
+            {a: ['bar']},
+          ];
+          const result = element._processNext(state, chunks);
+          const expectedCollapseSize = rows.length - element.context;
+
+          assert.equal(result.groups.length, 2, 'Results in two groups');
+
+          // Only the first group is collapsed.
+          assert.equal(result.groups[0].lines.length, 1);
+          assert.equal(result.groups[1].lines.length, element.context);
+
+          // The collapsed group has the hidden lines as its context group.
+          assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length,
+              expectedCollapseSize);
+        });
+
+        test('few-rows', () => {
+          // Only ten rows.
+          rows = rows.slice(0, 10);
+          element.context = 10;
+          const state = {
+            lineNums: {left: 10, right: 100},
+            chunkIndex: 0,
+          };
+          const chunks = [
+            {ab: rows},
+            {a: ['foo']},
+            {a: ['bar']},
+          ];
+          const result = element._processNext(state, chunks);
+
+          // Results in one uncollapsed group with all rows.
+          assert.equal(result.groups.length, 1, 'Results in one group');
+          assert.equal(result.groups[0].lines.length, rows.length);
+        });
+
+        test('no single line collapse', () => {
+          rows = rows.slice(0, 7);
+          element.context = 3;
+          const state = {
+            lineNums: {left: 10, right: 100},
+            chunkIndex: 1,
+          };
+          const chunks = [
+            {a: ['foo']},
+            {ab: rows},
+            {a: ['bar']},
+          ];
+          const result = element._processNext(state, chunks);
+
+          // Results in one uncollapsed group with all rows.
+          assert.equal(result.groups.length, 1, 'Results in one group');
+          assert.equal(result.groups[0].lines.length, rows.length);
+        });
+
+        suite('with key location', () => {
+          let state;
+          let chunks;
+
+          setup(() => {
+            state = {
+              lineNums: {left: 10, right: 100},
+            };
+            element.context = 10;
+            chunks = [
+              {ab: rows},
+              {ab: ['foo'], keyLocation: true},
+              {ab: rows},
+            ];
+          });
+
+          test('context before', () => {
+            state.chunkIndex = 0;
+            const result = element._processNext(state, chunks);
+
+            // The first chunk is split into two groups:
+            // 1) A context-control, hiding everything but the context before
+            //    the key location.
+            // 2) The context before the key location.
+            // The key location is not processed in this call to _processNext
+            assert.equal(result.groups.length, 2);
+            assert.equal(result.groups[0].lines.length, 1);
+            // The collapsed group has the hidden lines as its context group.
+            assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length,
+                rows.length - element.context);
+            assert.equal(result.groups[1].lines.length, element.context);
+          });
+
+          test('key location itself', () => {
+            state.chunkIndex = 1;
+            const result = element._processNext(state, chunks);
+
+            // The second chunk results in a single group, that is just the
+            // line with the key location
+            assert.equal(result.groups.length, 1);
+            assert.equal(result.groups[0].lines.length, 1);
+            assert.equal(result.lineDelta.left, 1);
+            assert.equal(result.lineDelta.right, 1);
+          });
+
+          test('context after', () => {
+            state.chunkIndex = 2;
+            const result = element._processNext(state, chunks);
+
+            // The last chunk is split into two groups:
+            // 1) The context after the key location.
+            // 1) A context-control, hiding everything but the context after the
+            //    key location.
+            assert.equal(result.groups.length, 2);
+            assert.equal(result.groups[0].lines.length, element.context);
+            assert.equal(result.groups[1].lines.length, 1);
+            // The collapsed group has the hidden lines as its context group.
+            assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length,
+                rows.length - element.context);
+          });
+        });
+      });
 
       suite('gr-diff-processor helpers', () => {
         let rows;
@@ -483,90 +841,10 @@
           rows = loremIpsum.split(' ');
         });
 
-        test('_sharedGroupsFromRows WHOLE_FILE', () => {
-          const context = WHOLE_FILE;
-          const lineNumbers = {left: 10, right: 100};
-          const result = element._sharedGroupsFromRows(
-              rows, context, lineNumbers.left, lineNumbers.right, null);
-
-          // Results in one, uncollapsed group with all rows.
-          assert.equal(result.length, 1);
-          assert.equal(result[0].type, GrDiffGroup.Type.BOTH);
-          assert.equal(result[0].lines.length, rows.length);
-
-          // Line numbers are set correctly.
-          assert.equal(result[0].lines[0].beforeNumber, lineNumbers.left + 1);
-          assert.equal(result[0].lines[0].afterNumber, lineNumbers.right + 1);
-
-          assert.equal(result[0].lines[rows.length - 1].beforeNumber,
-              lineNumbers.left + rows.length);
-          assert.equal(result[0].lines[rows.length - 1].afterNumber,
-              lineNumbers.right + rows.length);
-        });
-
-        test('_sharedGroupsFromRows context', () => {
-          const context = 10;
-          const result = element._sharedGroupsFromRows(
-              rows, context, 10, 100, null);
-          const expectedCollapseSize = rows.length - 2 * context;
-
-          assert.equal(result.length, 3, 'Results in three groups');
-
-          // The first and last are uncollapsed context, whereas the middle has
-          // a single context-control line.
-          assert.equal(result[0].lines.length, context);
-          assert.equal(result[1].lines.length, 1);
-          assert.equal(result[2].lines.length, context);
-
-          // The collapsed group has the hidden lines as its context group.
-          assert.equal(result[1].lines[0].contextGroup.lines.length,
-              expectedCollapseSize);
-        });
-
-        test('_sharedGroupsFromRows first', () => {
-          const context = 10;
-          const result = element._sharedGroupsFromRows(
-              rows, context, 10, 100, 'first');
-          const expectedCollapseSize = rows.length - context;
-
-          assert.equal(result.length, 2, 'Results in two groups');
-
-          // Only the first group is collapsed.
-          assert.equal(result[0].lines.length, 1);
-          assert.equal(result[1].lines.length, context);
-
-          // The collapsed group has the hidden lines as its context group.
-          assert.equal(result[0].lines[0].contextGroup.lines.length,
-              expectedCollapseSize);
-        });
-
-        test('_sharedGroupsFromRows few-rows', () => {
-          // Only ten rows.
-          rows = rows.slice(0, 10);
-          const context = 10;
-          const result = element._sharedGroupsFromRows(
-              rows, context, 10, 100, 'first');
-
-          // Results in one uncollapsed group with all rows.
-          assert.equal(result.length, 1, 'Results in one group');
-          assert.equal(result[0].lines.length, rows.length);
-        });
-
-        test('_sharedGroupsFromRows no single line collapse', () => {
-          rows = rows.slice(0, 7);
-          const context = 3;
-          const result = element._sharedGroupsFromRows(
-              rows, context, 10, 100);
-
-          // Results in one uncollapsed group with all rows.
-          assert.equal(result.length, 1, 'Results in one group');
-          assert.equal(result[0].lines.length, rows.length);
-        });
-
-        test('_deltaLinesFromRows', () => {
+        test('_linesFromRows', () => {
           const startLineNum = 10;
-          let result = element._deltaLinesFromRows(GrDiffLine.Type.ADD, rows,
-              startLineNum);
+          let result = element._linesFromRows(GrDiffLine.Type.ADD, rows,
+              startLineNum + 1);
 
           assert.equal(result.length, rows.length);
           assert.equal(result[0].type, GrDiffLine.Type.ADD);
@@ -576,8 +854,8 @@
               startLineNum + rows.length);
           assert.notOk(result[result.length - 1].beforeNumber);
 
-          result = element._deltaLinesFromRows(GrDiffLine.Type.REMOVE, rows,
-              startLineNum);
+          result = element._linesFromRows(GrDiffLine.Type.REMOVE, rows,
+              startLineNum + 1);
 
           assert.equal(result.length, rows.length);
           assert.equal(result[0].type, GrDiffLine.Type.REMOVE);
@@ -590,19 +868,19 @@
       });
 
       suite('_breakdown*', () => {
-        test('_breakdownGroup breaks down additions', () => {
+        test('_breakdownChunk breaks down additions', () => {
           sandbox.spy(element, '_breakdown');
           const chunk = {b: ['blah', 'blah', 'blah']};
-          const result = element._breakdownGroup(chunk);
+          const result = element._breakdownChunk(chunk);
           assert.deepEqual(result, [chunk]);
           assert.isTrue(element._breakdown.called);
         });
 
-        test('_breakdownGroup keeps due_to_rebase for broken down additions',
+        test('_breakdownChunk keeps due_to_rebase for broken down additions',
             () => {
               sandbox.spy(element, '_breakdown');
               const chunk = {b: ['blah', 'blah', 'blah'], due_to_rebase: true};
-              const result = element._breakdownGroup(chunk);
+              const result = element._breakdownChunk(chunk);
               for (const subResult of result) {
                 assert.isTrue(subResult.due_to_rebase);
               }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
index f9822f2..cfa46a0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
@@ -14,36 +14,13 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/dom-util-behavior/dom-util-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<script src="../../../scripts/util.js"></script>
 
 <dom-module id="gr-diff-selection">
   <template>
-    <style include="shared-styles">
-      .contentWrapper ::content .content,
-      .contentWrapper ::content .contextControl,
-      .contentWrapper ::content .blame {
-        -webkit-user-select: none;
-        -moz-user-select: none;
-        -ms-user-select: none;
-        user-select: none;
-      }
-
-      :host-context(.selected-left:not(.selected-comment)) .contentWrapper ::content .side-by-side .left + .content .contentText,
-      :host-context(.selected-right:not(.selected-comment)) .contentWrapper ::content .side-by-side .right + .content .contentText,
-      :host-context(.selected-left:not(.selected-comment)) .contentWrapper ::content .unified .left.lineNum ~ .content:not(.both) .contentText,
-      :host-context(.selected-right:not(.selected-comment)) .contentWrapper ::content .unified .right.lineNum ~ .content .contentText,
-      :host-context(.selected-left.selected-comment) .contentWrapper ::content .side-by-side .left + .content .message,
-      :host-context(.selected-right.selected-comment) .contentWrapper ::content .side-by-side .right + .content .message :not(.collapsedContent),
-      :host-context(.selected-comment) .contentWrapper ::content .unified .message :not(.collapsedContent),
-      :host-context(.selected-blame) .contentWrapper ::content .blame {
-        -webkit-user-select: text;
-        -moz-user-select: text;
-        -ms-user-select: text;
-        user-select: text;
-      }
-    </style>
     <div class="contentWrapper">
       <slot></slot>
     </div>
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 3484693..5caac74 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
@@ -32,7 +32,6 @@
 
   Polymer({
     is: 'gr-diff-selection',
-    _legacyUndefinedCheck: true,
 
     properties: {
       diff: Object,
@@ -73,7 +72,26 @@
       this._linesCache = getNewCache();
     },
 
+    _handleDownOnRangeComment(node) {
+      if (node &&
+          node.nodeName &&
+          node.nodeName.toLowerCase() === 'gr-comment-thread') {
+        this._setClasses([
+          SelectionClass.COMMENT,
+          node.commentSide === 'left' ?
+            SelectionClass.LEFT :
+            SelectionClass.RIGHT,
+        ]);
+        return true;
+      }
+      return false;
+    },
+
     _handleDown(e) {
+      // Handle the down event on comment thread in Polymer 2
+      const handled = this._handleDownOnRangeComment(e.target);
+      if (handled) return;
+
       const lineEl = this.diffBuilder.getLineElByChild(e.target);
       const blameSelected = this._elementDescendedFromClass(e.target, 'blame');
       if (!lineEl && !blameSelected) { return; }
@@ -161,7 +179,18 @@
     },
 
     /**
-     * Get the text of the current window selection. If commentSelected is
+     * For Polymer 2, use shadowRoot.getSelection instead.
+     */
+    _getSelection() {
+      const diffHost = util.querySelector(document.body, 'gr-diff');
+      const selection = diffHost &&
+        diffHost.shadowRoot &&
+        diffHost.shadowRoot.getSelection();
+      return selection ? selection: window.getSelection();
+    },
+
+    /**
+     * Get the text of the current selection. If commentSelected is
      * true, it returns only the text of comments within the selection.
      * Otherwise it returns the text of the selected diff region.
      *
@@ -170,7 +199,7 @@
      * @return {string} The selected text.
      */
     _getSelectedText(side, commentSelected) {
-      const sel = window.getSelection();
+      const sel = this._getSelection();
       if (sel.rangeCount != 1) {
         return ''; // No multi-select support yet.
       }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index 469a894..0f5c6dd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-selection</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-diff-selection.html">
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 5ebe738..17b8b4c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -15,12 +15,14 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
@@ -70,7 +72,7 @@
         justify-content: space-between;
       }
       header {
-        padding: .75em var(--default-horizontal-margin);
+        padding: var(--spacing-s) var(--spacing-l);
       }
       .patchRangeLeft {
         align-items: center;
@@ -85,11 +87,11 @@
         white-space: nowrap;
       }
       .navLink {
-        padding: 0 .25em;
+        padding: 0 var(--spacing-xs);
       }
       .reviewed {
         display: inline-block;
-        margin: 0 .25em;
+        margin: 0 var(--spacing-xs);
         vertical-align: .15em;
       }
       .jumpToFileContainer {
@@ -99,22 +101,19 @@
         display: none;
       }
       gr-button {
-        padding: .3em 0;
+        padding: var(--spacing-s) 0;
         text-decoration: none;
       }
       .loading {
         color: var(--deemphasized-text-color);
-        font-size: 2rem;
+        font-size: var(--font-size-h1);
         height: 100%;
-        padding: 1em var(--default-horizontal-margin);
+        padding: var(--spacing-l);
         text-align: center;
       }
       .subHeader {
         flex-wrap: wrap;
-        margin: 0 var(--default-horizontal-margin) .75em;
-      }
-      .subHeader > div {
-        margin-top: .25em;
+        padding: 0 var(--spacing-l) var(--spacing-s);
       }
       .prefsButton {
         text-align: right;
@@ -145,7 +144,7 @@
       }
       .diffModeSelector span,
       .editButton span {
-        margin-right: .2rem;
+        margin-right: var(--spacing-xs);
       }
       .diffModeSelector.hide,
       .separator.hide {
@@ -161,7 +160,7 @@
       }
       @media screen and (max-width: 50em) {
         header {
-          padding: .5em var(--default-horizontal-margin);
+          padding: var(--spacing-s) var(--spacing-l);
         }
         .dash {
           display: none;
@@ -172,23 +171,23 @@
         .fileNav {
           align-items: flex-start;
           display: flex;
-          margin: 0 .25em;
+          margin: 0 var(--spacing-xs);
         }
         .fullFileName {
           display: block;
           font-style: italic;
           min-width: 50%;
-          padding: 0 .1em;
+          padding: 0 var(--spacing-xxs);
           text-align: center;
           width: 100%;
           word-wrap: break-word;
         }
         .reviewed {
-          vertical-align: -.1em;
+          vertical-align: -1px;
         }
         .mobileNavLink {
           color: var(--primary-text-color);
-          font-size: 1.5rem;
+          font-size: var(--font-size-h2);
           font-weight: var(--font-weight-bold);
           text-decoration: none;
         }
@@ -287,7 +286,8 @@
             <gr-button
                 link
                 disabled="[[_isBlameLoading]]"
-                on-tap="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button>
+                on-click="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button>
+            <span class="separator"></span>
           </span>
           <template is="dom-if" if="[[_computeCanEdit(_loggedIn, _change.*)]]">
             <span class="separator"></span>
@@ -314,13 +314,15 @@
                   class="prefsButton"
                   has-tooltip
                   title="Diff preferences"
-                  on-tap="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
+                  on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
             </span>
           </span>
           <gr-endpoint-decorator name="annotation-toggler">
             <span hidden id="annotation-span">
               <label for="annotation-checkbox" id="annotation-label"></label>
-              <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
+              <iron-input type="checkbox" disabled>
+                <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
+              </iron-input>
             </span>
           </gr-endpoint-decorator>
         </div>
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 d73042e..bd0486f 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
@@ -35,7 +35,6 @@
 
   Polymer({
     is: 'gr-diff-view',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -102,14 +101,24 @@
       // element for selected a file to view.
       _formattedFiles: {
         type: Array,
-        computed: '_formatFilesForDropdown(_fileList, _patchRange.patchNum, ' +
+        computed: '_formatFilesForDropdown(_files, _patchRange.patchNum, ' +
             '_changeComments)',
       },
       // An sorted array of files, as returned by the rest API.
       _fileList: {
         type: Array,
-        value() { return []; },
+        computed: '_getSortedFileList(_files)',
       },
+      /**
+       * Contains information about files as returned by the rest API.
+       *
+       * @type {{ sortedFileList: Array<string>, changeFilesByPath: Object }}
+       */
+      _files: {
+        type: Object,
+        value() { return {sortedFileList: [], changeFilesByPath: {}}; },
+      },
+
       _path: {
         type: String,
         observer: '_pathChanged',
@@ -181,6 +190,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.PathListBehavior,
@@ -189,7 +199,7 @@
 
     observers: [
       '_getProjectConfig(_change.project)',
-      '_getFiles(_changeNum, _patchRange.*)',
+      '_getFiles(_changeNum, _patchRange.*, _changeComments)',
       '_setReviewedObserver(_loggedIn, params.*, _prefs)',
     ],
 
@@ -262,11 +272,31 @@
       return this.$.restAPI.getChangeEdit(this._changeNum);
     },
 
-    _getFiles(changeNum, patchRangeRecord) {
+    _getSortedFileList(files) {
+      return files.sortedFileList;
+    },
+
+    _getFiles(changeNum, patchRangeRecord, changeComments) {
+      // Polymer 2: check for undefined
+      if ([changeNum, patchRangeRecord, patchRangeRecord.base, changeComments]
+          .some(arg => arg === undefined)) {
+        return Promise.resolve();
+      }
+
       const patchRange = patchRangeRecord.base;
-      return this.$.restAPI.getChangeFilePathsAsSpeciallySortedArray(
-          changeNum, patchRange).then(files => {
-        this._fileList = files;
+      return this.$.restAPI.getChangeFiles(
+          changeNum, patchRange).then(changeFiles => {
+        if (!changeFiles) return;
+        const commentedPaths = changeComments.getPaths(patchRange);
+        const files = Object.assign({}, changeFiles);
+        Object.keys(commentedPaths).forEach(commentedPath => {
+          if (files.hasOwnProperty(commentedPath)) { return; }
+          files[commentedPath] = {status: 'U'};
+        });
+        this._files = {
+          sortedFileList: Object.keys(files).sort(this.specialFilePathCompare),
+          changeFilesByPath: files,
+        };
       });
     },
 
@@ -562,7 +592,7 @@
      * @return {?Object}
      */
     _getNavLinkPath(path, fileList, direction, opt_noUp) {
-      if (!path || fileList.length === 0) { return null; }
+      if (!path || !fileList || fileList.length === 0) { return null; }
 
       let idx = fileList.indexOf(path);
       if (idx === -1) {
@@ -608,11 +638,11 @@
       this._initCursor(this.params);
 
       this._changeNum = value.changeNum;
+      this._path = value.path;
       this._patchRange = {
         patchNum: value.patchNum,
         basePatchNum: value.basePatchNum || PARENT,
       };
-      this._path = value.path;
 
       // 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
@@ -640,22 +670,24 @@
       promises.push(this._getChangeDetail(this._changeNum).then(change => {
         let commit;
         let baseCommit;
-        for (const commitSha in change.revisions) {
-          if (!change.revisions.hasOwnProperty(commitSha)) continue;
-          const revision = change.revisions[commitSha];
-          const patchNum = revision._number.toString();
-          if (patchNum === this._patchRange.patchNum) {
-            commit = commitSha;
-            const commitObj = revision.commit || {};
-            const parents = commitObj.parents || [];
-            if (this._patchRange.basePatchNum === PARENT && parents.length) {
-              baseCommit = parents[parents.length - 1].commit;
+        if (change) {
+          for (const commitSha in change.revisions) {
+            if (!change.revisions.hasOwnProperty(commitSha)) continue;
+            const revision = change.revisions[commitSha];
+            const patchNum = revision._number.toString();
+            if (patchNum === this._patchRange.patchNum) {
+              commit = commitSha;
+              const commitObj = revision.commit || {};
+              const parents = commitObj.parents || [];
+              if (this._patchRange.basePatchNum === PARENT && parents.length) {
+                baseCommit = parents[parents.length - 1].commit;
+              }
+            } else if (patchNum === this._patchRange.basePatchNum) {
+              baseCommit = commitSha;
             }
-          } else if (patchNum === this._patchRange.basePatchNum) {
-            baseCommit = commitSha;
           }
+          this._commitRange = {commit, baseCommit};
         }
-        this._commitRange = {commit, baseCommit};
       }));
 
       promises.push(this._loadComments());
@@ -674,8 +706,10 @@
         }
         this._loading = false;
         this.$.diffHost.comments = this._commentsForDiff;
-        return this.$.diffHost.reload();
+        return this.$.diffHost.reload(true);
       }).then(() => {
+        this.$.reporting.diffViewFullyLoaded();
+        // If diff view displayed has not ended yet, it ends here.
         this.$.reporting.diffViewDisplayed();
       });
     },
@@ -690,6 +724,11 @@
     },
 
     _setReviewedObserver(_loggedIn, paramsRecord, _prefs) {
+      // Polymer 2: check for undefined
+      if ([_loggedIn, paramsRecord, _prefs].some(arg => arg === undefined)) {
+        return;
+      }
+
       const params = paramsRecord.base || {};
       if (!_loggedIn) { return; }
 
@@ -741,6 +780,9 @@
     },
 
     _getDiffUrl(change, patchRange, path) {
+      if ([change, patchRange, path].some(arg => arg === undefined)) {
+        return '';
+      }
       return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum,
           patchRange.basePatchNum);
     },
@@ -779,6 +821,9 @@
     },
 
     _getChangePath(change, patchRange, revisions) {
+      if ([change, patchRange].some(arg => arg === undefined)) {
+        return '';
+      }
       const range = this._getChangeUrlRange(patchRange, revisions);
       return Gerrit.Nav.getUrlForChange(change, range.patchNum,
           range.basePatchNum);
@@ -793,22 +838,31 @@
       return this._getChangePath(change, patchRangeRecord.base, revisions);
     },
 
-    _formatFilesForDropdown(fileList, patchNum, changeComments) {
-      if (!fileList) { return; }
+    _formatFilesForDropdown(files, patchNum, changeComments) {
+      // Polymer 2: check for undefined
+      if ([
+        files,
+        patchNum,
+        changeComments,
+      ].some(arg => arg === undefined)) {
+        return;
+      }
+
+      if (!files) { return; }
       const dropdownContent = [];
-      for (const path of fileList) {
+      for (const path of files.sortedFileList) {
         dropdownContent.push({
           text: this.computeDisplayPath(path),
           mobileText: this.computeTruncatedPath(path),
           value: path,
           bottomText: this._computeCommentString(changeComments, patchNum,
-              path),
+              path, files.changeFilesByPath[path]),
         });
       }
       return dropdownContent;
     },
 
-    _computeCommentString(changeComments, patchNum, path) {
+    _computeCommentString(changeComments, patchNum, path, changeFileInfo) {
       const unresolvedCount = changeComments.computeUnresolvedNum(patchNum,
           path);
       const commentCount = changeComments.computeCommentCount(patchNum, path);
@@ -817,11 +871,13 @@
       const unresolvedString = GrCountStringFormatter.computeString(
           unresolvedCount, 'unresolved');
 
-      return commentString +
-          // Add a space if both comments and unresolved
-          (commentString && unresolvedString ? ', ' : '') +
-          // Add parentheses around unresolved if it exists.
-          (unresolvedString ? `${unresolvedString}` : '');
+      const unmodifiedString = changeFileInfo.status === 'U' ? 'no changes': '';
+
+      return [
+        unmodifiedString,
+        commentString,
+        unresolvedString]
+          .filter(v => v && v.length > 0).join(', ');
     },
 
     _computePrefsButtonHidden(prefs, prefsDisabled) {
@@ -990,6 +1046,15 @@
     },
 
     _computeCommentSkips(commentMap, fileList, path) {
+      // Polymer 2: check for undefined
+      if ([
+        commentMap,
+        fileList,
+        path,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       const skips = {previous: null, next: null};
       if (!fileList.length) { return skips; }
       const pathIndex = fileList.indexOf(path);
@@ -1070,6 +1135,11 @@
     },
 
     _computeFileNum(file, files) {
+      // Polymer 2: check for undefined
+      if ([file, files].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       return files.findIndex(({value}) => value === file) + 1;
     },
 
@@ -1109,13 +1179,12 @@
       this._getDiffPreferences();
     },
 
-    _computeIsLoggedIn(loggedIn) {
-      return loggedIn ? true : false;
-    },
-
     _computeCanEdit(loggedIn, changeChangeRecord) {
-      return this._computeIsLoggedIn(loggedIn) &&
-          this.changeIsOpen(changeChangeRecord.base.status);
+      if ([changeChangeRecord, changeChangeRecord.base]
+          .some(arg => arg === undefined)) {
+        return false;
+      }
+      return loggedIn && this.changeIsOpen(changeChangeRecord.base);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index b33c54c..9c59577 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="gr-diff-view.html">
@@ -74,6 +76,17 @@
 
     const PARENT = 'PARENT';
 
+    function getFilesFromFileList(fileList) {
+      const changeFilesByPath = fileList.reduce((files, path) => {
+        files[path] = {};
+        return files;
+      }, {});
+      return {
+        sortedFileList: fileList,
+        changeFilesByPath,
+      };
+    }
+
     setup(() => {
       sandbox = sinon.sandbox.create();
 
@@ -153,7 +166,8 @@
           a: {_number: 10, commit: {parents: []}},
         },
       };
-      element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+      element._files = getFilesFromFileList(
+          ['chell.go', 'glados.txt', 'wheatley.md']);
       element._path = 'glados.txt';
       element.changeViewState.selectedFileIndex = 1;
       element._loggedIn = true;
@@ -260,7 +274,8 @@
           b: {_number: 5, commit: {parents: []}},
         },
       };
-      element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+      element._files = getFilesFromFileList(
+          ['chell.go', 'glados.txt', 'wheatley.md']);
       element._path = 'glados.txt';
 
       const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
@@ -324,7 +339,8 @@
           b: {_number: 2, commit: {parents: []}},
         },
       };
-      element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+      element._files = getFilesFromFileList(
+          ['chell.go', 'glados.txt', 'wheatley.md']);
       element._path = 'glados.txt';
 
       const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
@@ -517,13 +533,22 @@
         unresolvedCountStub.withArgs(3, path).returns(2);
         unresolvedCountStub.withArgs(4, path).returns(0);
 
-        assert.equal(element._computeCommentString(comments, 1, path),
+        assert.equal(element._computeCommentString(comments, 1, path, {}),
             '1 unresolved');
-        assert.equal(element._computeCommentString(comments, 2, path),
+        assert.equal(
+            element._computeCommentString(comments, 2, path, {status: 'M'}),
             '1 comment');
-        assert.equal(element._computeCommentString(comments, 3, path),
+        assert.equal(
+            element._computeCommentString(comments, 2, path, {status: 'U'}),
+            'no changes, 1 comment');
+        assert.equal(
+            element._computeCommentString(comments, 3, path, {status: 'A'}),
             '2 comments, 2 unresolved');
-        assert.equal(element._computeCommentString(comments, 4, path), '');
+        assert.equal(
+            element._computeCommentString(comments, 4, path, {status: 'M'}), '');
+        assert.equal(
+            element._computeCommentString(comments, 4, path, {status: 'U'}),
+            'no changes');
         done();
       });
     });
@@ -545,8 +570,9 @@
           patchNum: '10',
         };
         element._change = {_number: 42};
-        element._fileList = ['chell.go', 'glados.txt', 'wheatley.md',
-          '/COMMIT_MSG', '/MERGE_LIST'];
+        element._files = getFilesFromFileList(
+            ['chell.go', 'glados.txt', 'wheatley.md',
+              '/COMMIT_MSG', '/MERGE_LIST']);
         element._path = 'glados.txt';
         const expectedFormattedFiles = [
           {
@@ -595,7 +621,8 @@
             a: {_number: 10, commit: {parents: []}},
           },
         };
-        element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+        element._files = getFilesFromFileList(
+            ['chell.go', 'glados.txt', 'wheatley.md']);
         element._path = 'glados.txt';
         flushAsynchronousOperations();
         const linkEls = Polymer.dom(element.root).querySelectorAll('.navLink');
@@ -637,7 +664,8 @@
             b: {_number: 10, commit: {parents: []}},
           },
         };
-        element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+        element._files = getFilesFromFileList(
+            ['chell.go', 'glados.txt', 'wheatley.md']);
         element._path = 'glados.txt';
         flushAsynchronousOperations();
         const linkEls = Polymer.dom(element.root).querySelectorAll('.navLink');
@@ -1064,9 +1092,9 @@
         setup(() => {
           navToChangeStub = sandbox.stub(element, '_navToChangeView');
           navToDiffStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
-          element._fileList = [
+          element._files = getFilesFromFileList([
             'path/one.jpg', 'path/two.m4v', 'path/three.wav',
-          ];
+          ]);
           element._patchRange = {patchNum: '2', basePatchNum: '1'};
         });
 
@@ -1217,7 +1245,7 @@
     });
 
     test('shift+m navigates to next unreviewed file', () => {
-      element._fileList = ['file1', 'file2', 'file3'];
+      element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
       element._reviewedFiles = new Set(['file1', 'file2']);
       element._path = 'file1';
       const reviewedStub = sandbox.stub(element, '_setReviewed');
@@ -1233,6 +1261,39 @@
       ]);
     });
 
+    test('File change should trigger navigateToDiff once', () => {
+      element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
+      sandbox.stub(element, '_getLineOfInterest');
+      sandbox.stub(element, '_initCursor');
+      sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+
+      // Load file1
+      element._paramsChanged({
+        view: Gerrit.Nav.View.DIFF,
+        patchNum: 1,
+        changeNum: 101,
+        project: 'test-project',
+        path: 'file1',
+      });
+      assert.isTrue(Gerrit.Nav.navigateToDiff.notCalled);
+
+      // Switch to file2
+      element.$.dropdown.value = 'file2';
+      assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce);
+
+      // This is to mock the param change triggered by above navigate
+      element._paramsChanged({
+        view: Gerrit.Nav.View.DIFF,
+        patchNum: 1,
+        changeNum: 101,
+        project: 'test-project',
+        path: 'file2',
+      });
+
+      // No extra call
+      assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce);
+    });
+
     test('_computeDownloadDropdownLinks', () => {
       const downloadLinks = [
         {
@@ -1328,4 +1389,54 @@
           '/changes/test~12/revisions/1/patch?zip&path=index.php');
     });
   });
+
+  suite('gr-diff-view tests unmodified files with comments', () => {
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      const changedFiles = {
+        'file1.txt': {},
+        'a/b/test.c': {},
+      };
+      stub('gr-rest-api-interface', {
+        getConfig() { return Promise.resolve({change: {}}); },
+        getLoggedIn() { return Promise.resolve(false); },
+        getProjectConfig() { return Promise.resolve({}); },
+        getDiffChangeDetail() { return Promise.resolve({}); },
+        getChangeFiles() { return Promise.resolve(changedFiles); },
+        saveFileReviewed() { return Promise.resolve(); },
+        getDiffComments() { return Promise.resolve({}); },
+        getDiffRobotComments() { return Promise.resolve({}); },
+        getDiffDrafts() { return Promise.resolve({}); },
+        getReviewedFiles() { return Promise.resolve([]); },
+      });
+      element = fixture('basic');
+      return element._loadComments();
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('_getFiles add files with comments without changes', () => {
+      const patchChangeRecord = {
+        base: {
+          basePatchNum: '5',
+          patchNum: '10',
+        },
+      };
+      const changeComments = {
+        getPaths: sandbox.stub().returns({'file2.txt': {}, 'file1.txt': {}}),
+      };
+      return element._getFiles(23, patchChangeRecord, changeComments).then(() => {
+        assert.deepEqual(element._files, {
+          sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'],
+          changeFilesByPath: {
+            'file1.txt': {},
+            'file2.txt': {status: 'U'},
+            'a/b/test.c': {},
+          },
+        });
+      });
+    });
+  });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
index dd69724..a7e391a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
@@ -22,20 +22,42 @@
 
   /**
    * A chunk of the diff that should be rendered together.
+   *
+   * @param {!GrDiffGroup.Type} type
+   * @param {!Array<!GrDiffLine>=} opt_lines
    */
   function GrDiffGroup(type, opt_lines) {
+    /** @type {!GrDiffGroup.Type} */
     this.type = type;
 
-    /** @type{!Array<!GrDiffLine>} */
+    /** @type {boolean} */
+    this.dueToRebase = false;
+
+    /**
+     * True means all changes in this line are whitespace changes that should
+     * not be highlighted as changed as per the user settings.
+     *
+     * @type{boolean}
+     */
+    this.ignoredWhitespaceOnly = false;
+
+    /**
+     * True means it should not be collapsed (because it was in the URL, or
+     * there is a comment on that line)
+     */
+    this.keyLocation = false;
+
+    /** @type {?HTMLElement} */
+    this.element = null;
+
+    /** @type {!Array<!GrDiffLine>} */
     this.lines = [];
-    /** @type{!Array<!GrDiffLine>} */
+    /** @type {!Array<!GrDiffLine>} */
     this.adds = [];
-    /** @type{!Array<!GrDiffLine>} */
+    /** @type {!Array<!GrDiffLine>} */
     this.removes = [];
 
-    /** @type{boolean|undefined} */
-    this.dueToRebase = undefined;
-
+    /** Both start and end line are inclusive. */
     this.lineRange = {
       left: {start: null, end: null},
       right: {start: null, end: null},
@@ -46,8 +68,7 @@
     }
   }
 
-  GrDiffGroup.prototype.element = null;
-
+  /** @enum {string} */
   GrDiffGroup.Type = {
     /** Unchanged context. */
     BOTH: 'both',
@@ -59,6 +80,139 @@
     DELTA: 'delta',
   };
 
+
+  /**
+   * Hides lines in the given range behind a context control group.
+   *
+   * Groups that would be partially visible are split into their visible and
+   * hidden parts, respectively.
+   * The groups need to be "common groups", meaning they have to have either
+   * originated from an `ab` chunk, or from an `a`+`b` chunk with
+   * `common: true`.
+   *
+   * If the hidden range is 1 line or less, nothing is hidden and no context
+   * control group is created.
+   *
+   * @param {!Array<!GrDiffGroup>} groups Common groups, ordered by their line
+   *     ranges.
+   * @param {number} hiddenStart The first element to be hidden, as a
+   *     non-negative line number offset relative to the first group's start
+   *     line, left and right respectively.
+   * @param {number} hiddenEnd The first visible element after the hidden range,
+   *     as a non-negative line number offset relative to the first group's
+   *     start line, left and right respectively.
+   * @return {!Array<!GrDiffGroup>}
+   */
+  GrDiffGroup.hideInContextControl = function(groups, hiddenStart, hiddenEnd) {
+    if (groups.length === 0) return [];
+    // Clamp hiddenStart and hiddenEnd - inspired by e.g. substring
+    hiddenStart = Math.max(hiddenStart, 0);
+    hiddenEnd = Math.max(hiddenEnd, hiddenStart);
+
+    let before = [];
+    let hidden = groups;
+    let after = [];
+
+    const numHidden = hiddenEnd - hiddenStart;
+
+    // Only collapse if there is more than 1 line to be hidden.
+    if (numHidden > 1) {
+      if (hiddenStart) {
+        [before, hidden] = GrDiffGroup._splitCommonGroups(hidden, hiddenStart);
+      }
+      if (hiddenEnd) {
+        [hidden, after] = GrDiffGroup._splitCommonGroups(
+            hidden, hiddenEnd - hiddenStart);
+      }
+    } else {
+      [hidden, after] = [[], hidden];
+    }
+
+    const result = [...before];
+    if (hidden.length) {
+      const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
+      ctxLine.contextGroups = hidden;
+      const ctxGroup = new GrDiffGroup(
+          GrDiffGroup.Type.CONTEXT_CONTROL, [ctxLine]);
+      result.push(ctxGroup);
+    }
+    result.push(...after);
+    return result;
+  };
+
+  /**
+   * Splits a list of common groups into two lists of groups.
+   *
+   * Groups where all lines are before or all lines are after the split will be
+   * retained as is and put into the first or second list respectively. Groups
+   * with some lines before and some lines after the split will be split into
+   * two groups, which will be put into the first and second list.
+   *
+   * @param {!Array<!GrDiffGroup>} groups
+   * @param {number} split A line number offset relative to the first group's
+   *     start line at which the groups should be split.
+   * @return {!Array<!Array<!GrDiffGroup>>} The outer array has 2 elements, the
+   *   list of groups before and the list of groups after the split.
+   */
+  GrDiffGroup._splitCommonGroups = function(groups, split) {
+    if (groups.length === 0) return [[], []];
+    const leftSplit = groups[0].lineRange.left.start + split;
+    const rightSplit = groups[0].lineRange.right.start + split;
+
+    const beforeGroups = [];
+    const afterGroups = [];
+    for (const group of groups) {
+      if (group.lineRange.left.end < leftSplit ||
+          group.lineRange.right.end < rightSplit) {
+        beforeGroups.push(group);
+        continue;
+      }
+      if (leftSplit <= group.lineRange.left.start ||
+          rightSplit <= group.lineRange.right.start) {
+        afterGroups.push(group);
+        continue;
+      }
+
+      const before = [];
+      const after = [];
+      for (const line of group.lines) {
+        if ((line.beforeNumber && line.beforeNumber < leftSplit) ||
+            (line.afterNumber && line.afterNumber < rightSplit)) {
+          before.push(line);
+        } else {
+          after.push(line);
+        }
+      }
+
+      if (before.length) {
+        beforeGroups.push(before.length === group.lines.length ?
+          group : group.cloneWithLines(before));
+      }
+      if (after.length) {
+        afterGroups.push(after.length === group.lines.length ?
+          group : group.cloneWithLines(after));
+      }
+    }
+    return [beforeGroups, afterGroups];
+  };
+
+  /**
+   * Creates a new group with the same properties but different lines.
+   *
+   * The element property is not copied, because the original element is still a
+   * rendering of the old lines, so that would not make sense.
+   *
+   * @param {!Array<!GrDiffLine>} lines
+   * @return {!GrDiffGroup}
+   */
+  GrDiffGroup.prototype.cloneWithLines = function(lines) {
+    const group = new GrDiffGroup(this.type, lines);
+    group.dueToRebase = this.dueToRebase;
+    group.ignoredWhitespaceOnly = this.ignoredWhitespaceOnly;
+    return group;
+  };
+
+  /** @param {!GrDiffLine} line */
   GrDiffGroup.prototype.addLine = function(line) {
     this.lines.push(line);
 
@@ -77,6 +231,7 @@
     this._updateRange(line);
   };
 
+  /** @return {!Array<{left: GrDiffLine, right: GrDiffLine}>} */
   GrDiffGroup.prototype.getSideBySidePairs = function() {
     if (this.type === GrDiffGroup.Type.BOTH ||
         this.type === GrDiffGroup.Type.CONTEXT_CONTROL) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
index 9dc5311..16e8036 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
@@ -18,8 +18,10 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-group</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="gr-diff-line.js"></script>
 <script src="gr-diff-group.js"></script>
@@ -28,12 +30,9 @@
   suite('gr-diff-group tests', () => {
     test('delta line pairs', () => {
       let group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
-      const l1 = new GrDiffLine(GrDiffLine.Type.ADD);
-      const l2 = new GrDiffLine(GrDiffLine.Type.ADD);
-      const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE);
-      l1.afterNumber = 128;
-      l2.afterNumber = 129;
-      l3.beforeNumber = 64;
+      const l1 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 128);
+      const l2 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 129);
+      const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE, 64, 0);
       group.addLine(l1);
       group.addLine(l2);
       group.addLine(l3);
@@ -64,17 +63,9 @@
     });
 
     test('group/header line pairs', () => {
-      const l1 = new GrDiffLine(GrDiffLine.Type.BOTH);
-      l1.beforeNumber = 64;
-      l1.afterNumber = 128;
-
-      const l2 = new GrDiffLine(GrDiffLine.Type.BOTH);
-      l2.beforeNumber = 65;
-      l2.afterNumber = 129;
-
-      const l3 = new GrDiffLine(GrDiffLine.Type.BOTH);
-      l3.beforeNumber = 66;
-      l3.afterNumber = 130;
+      const l1 = new GrDiffLine(GrDiffLine.Type.BOTH, 64, 128);
+      const l2 = new GrDiffLine(GrDiffLine.Type.BOTH, 65, 129);
+      const l3 = new GrDiffLine(GrDiffLine.Type.BOTH, 66, 130);
 
       let group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]);
 
@@ -122,6 +113,97 @@
       assert.throws(group.addLine.bind(group, l2));
       assert.doesNotThrow(group.addLine.bind(group, l3));
     });
+
+    suite('hideInContextControl', () => {
+      let groups;
+      setup(() => {
+        groups = [
+          new GrDiffGroup(GrDiffGroup.Type.BOTH, [
+            new GrDiffLine(GrDiffLine.Type.BOTH, 5, 7),
+            new GrDiffLine(GrDiffLine.Type.BOTH, 6, 8),
+            new GrDiffLine(GrDiffLine.Type.BOTH, 7, 9),
+          ]),
+          new GrDiffGroup(GrDiffGroup.Type.DELTA, [
+            new GrDiffLine(GrDiffLine.Type.REMOVE, 8),
+            new GrDiffLine(GrDiffLine.Type.ADD, 0, 10),
+            new GrDiffLine(GrDiffLine.Type.REMOVE, 9),
+            new GrDiffLine(GrDiffLine.Type.ADD, 0, 11),
+            new GrDiffLine(GrDiffLine.Type.REMOVE, 10),
+            new GrDiffLine(GrDiffLine.Type.ADD, 0, 12),
+          ]),
+          new GrDiffGroup(GrDiffGroup.Type.BOTH, [
+            new GrDiffLine(GrDiffLine.Type.BOTH, 11, 13),
+            new GrDiffLine(GrDiffLine.Type.BOTH, 12, 14),
+            new GrDiffLine(GrDiffLine.Type.BOTH, 13, 15),
+          ]),
+        ];
+      });
+
+      test('hides hidden groups in context control', () => {
+        const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 3, 6);
+        assert.equal(collapsedGroups.length, 3);
+
+        assert.equal(collapsedGroups[0], groups[0]);
+
+        assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+        assert.equal(collapsedGroups[1].lines.length, 1);
+        assert.equal(
+            collapsedGroups[1].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
+        assert.equal(
+            collapsedGroups[1].lines[0].contextGroups.length, 1);
+        assert.equal(
+            collapsedGroups[1].lines[0].contextGroups[0], groups[1]);
+
+        assert.equal(collapsedGroups[2], groups[2]);
+      });
+
+      test('splits partially hidden groups', () => {
+        const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 4, 7);
+        assert.equal(collapsedGroups.length, 4);
+        assert.equal(collapsedGroups[0], groups[0]);
+
+        assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.DELTA);
+        assert.deepEqual(collapsedGroups[1].adds, [groups[1].adds[0]]);
+        assert.deepEqual(collapsedGroups[1].removes, [groups[1].removes[0]]);
+
+        assert.equal(collapsedGroups[2].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+        assert.equal(collapsedGroups[2].lines.length, 1);
+        assert.equal(
+            collapsedGroups[2].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
+        assert.equal(
+            collapsedGroups[2].lines[0].contextGroups.length, 2);
+
+        assert.equal(
+            collapsedGroups[2].lines[0].contextGroups[0].type,
+            GrDiffGroup.Type.DELTA);
+        assert.deepEqual(
+            collapsedGroups[2].lines[0].contextGroups[0].adds,
+            groups[1].adds.slice(1));
+        assert.deepEqual(
+            collapsedGroups[2].lines[0].contextGroups[0].removes,
+            groups[1].removes.slice(1));
+
+        assert.equal(
+            collapsedGroups[2].lines[0].contextGroups[1].type,
+            GrDiffGroup.Type.BOTH);
+        assert.deepEqual(
+            collapsedGroups[2].lines[0].contextGroups[1].lines,
+            [groups[2].lines[0]]);
+
+        assert.equal(collapsedGroups[3].type, GrDiffGroup.Type.BOTH);
+        assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
+      });
+
+      test('groups unchanged if the hidden range is empty', () => {
+        assert.deepEqual(
+            GrDiffGroup.hideInContextControl(groups, 0, 0), groups);
+      });
+
+      test('groups unchanged if there is only 1 line to hide', () => {
+        assert.deepEqual(
+            GrDiffGroup.hideInContextControl(groups, 3, 4), groups);
+      });
+    });
   });
 
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
index 44bb52a..a295293 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
@@ -20,21 +20,32 @@
   // Prevent redefinition.
   if (window.GrDiffLine) { return; }
 
-  function GrDiffLine(type) {
+  /**
+   * @param {GrDiffLine.Type} type
+   * @param {number|string=} opt_beforeLine
+   * @param {number|string=} opt_afterLine
+   */
+  function GrDiffLine(type, opt_beforeLine, opt_afterLine) {
     this.type = type;
+
+    /** @type {number|string} */
+    this.beforeNumber = opt_beforeLine || 0;
+
+    /** @type {number|string} */
+    this.afterNumber = opt_afterLine || 0;
+
+    /** @type {boolean} */
+    this.hasIntralineInfo = false;
+
+    /** @type {Array<GrDiffLine.Highlights>} */
     this.highlights = [];
+
+    /** @type {?Array<Object>} ?Array<!GrDiffGroup> */
+    this.contextGroups = null;
+
+    this.text = '';
   }
 
-  /** @type {number|string} */
-  GrDiffLine.prototype.afterNumber = 0;
-
-  /** @type {number|string} */
-  GrDiffLine.prototype.beforeNumber = 0;
-
-  GrDiffLine.prototype.contextGroup = null;
-
-  GrDiffLine.prototype.text = '';
-
   GrDiffLine.Type = {
     ADD: 'add',
     BOTH: 'both',
@@ -43,6 +54,23 @@
     REMOVE: 'remove',
   };
 
+  /**
+   * A line highlight object consists of three fields:
+   * - contentIndex: The index of the chunk `content` field (the line
+   *   being referred to).
+   * - startIndex: Index of the character where the highlight should begin.
+   * - endIndex: (optional) Index of the character where the highlight should
+   *   end. If omitted, the highlight is meant to be a continuation onto the
+   *   next line.
+   *
+   * @typedef {{
+   *  contentIndex: number,
+   *  startIndex: number,
+   *  endIndex: number
+   * }}
+   */
+  GrDiffLine.Highlights;
+
   GrDiffLine.FILE = 'FILE';
 
   GrDiffLine.BLANK_LINE = new GrDiffLine(GrDiffLine.Type.BLANK);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 72fc1ee..f6abb14 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -23,19 +24,31 @@
 <link rel="import" href="../gr-diff-highlight/gr-diff-highlight.html">
 <link rel="import" href="../gr-diff-selection/gr-diff-selection.html">
 <link rel="import" href="../gr-syntax-themes/gr-syntax-theme.html">
+<link rel="import" href="../gr-ranged-comment-themes/gr-ranged-comment-theme.html">
 
 <script src="../../../scripts/hiddenscroll.js"></script>
 
 <dom-module id="gr-diff">
   <template>
     <style include="shared-styles">
-      :host(.no-left) .sideBySide ::content .left,
-      :host(.no-left) .sideBySide ::content .left + td,
-      :host(.no-left) .sideBySide ::content .right:not([data-value]),
-      :host(.no-left) .sideBySide ::content .right:not([data-value]) + td {
+      :host(.no-left) .sideBySide .left,
+      :host(.no-left) .sideBySide .left + td,
+      :host(.no-left) .sideBySide .right:not([data-value]),
+      :host(.no-left) .sideBySide .right:not([data-value]) + td {
         display: none;
       }
-      .thread-group, ::slotted(*) .thread-group {
+      ::slotted(*) .thread-group {
+        display: block;
+        max-width: var(--content-width, 80ch);
+        white-space: normal;
+      }
+      :host {
+        font-family: var(--monospace-font-family, ''), 'Roboto Mono';
+        font-size: var(--font-size, var(--font-size-code, 12px));
+        line-height: var(--line-height-code, 1.334);
+      }
+
+      .thread-group {
         display: block;
         max-width: var(--content-width, 80ch);
         white-space: normal;
@@ -46,7 +59,7 @@
         @apply --diff-container-styles;
       }
       .diffContainer.hiddenscroll {
-        margin-bottom: .8em;
+        margin-bottom: var(--spacing-m);
       }
       table {
         border-collapse: collapse;
@@ -80,10 +93,12 @@
         background-color: var(--diff-selection-background-color);
         color: var(--primary-text-color);
       }
-      .blank,
       .content {
         background-color: var(--view-background-color);
       }
+      .blank {
+        background-color: var(--diff-blank-background-color);
+      }
       .image-diff .content {
         background-color: var(--table-header-background-color);
       }
@@ -96,8 +111,6 @@
       }
       .lineNum,
       .content {
-        /* Set font size based the user's diff preference. */
-        font-size: var(--font-size, var(--font-size-normal));
         vertical-align: top;
         white-space: pre;
       }
@@ -109,7 +122,7 @@
         user-select: none;
 
         color: var(--deemphasized-text-color);
-        padding: 0 .5em;
+        padding: 0 var(--spacing-m);
         text-align: right;
       }
       .canComment .lineNum {
@@ -123,6 +136,8 @@
         width: var(--content-width, 80ch);
       }
       .content.add .intraline,
+      /* If there are no intraline info, consider everything changed */
+      .content.add.no-intraline-info,
       .delta.total .content.add {
         background-color: var(--dark-add-highlight-color);
       }
@@ -130,12 +145,16 @@
         background-color: var(--light-add-highlight-color);
       }
       .content.remove .intraline,
+      /* If there are no intraline info, consider everything changed */
+      .content.remove.no-intraline-info,
       .delta.total .content.remove {
         background-color: var(--dark-remove-highlight-color);
       }
       .content.remove {
         background-color: var(--light-remove-highlight-color);
       }
+
+      /* dueToRebase */
       .dueToRebase .content.add .intraline,
       .delta.total.dueToRebase .content.add {
         background-color: var(--dark-rebased-add-highlight-color);
@@ -150,20 +169,32 @@
       .dueToRebase .content.remove {
         background-color: var(--light-remove-add-highlight-color);
       }
+
+      /* ignoredWhitespaceOnly */
+      .ignoredWhitespaceOnly .content.add .intraline,
+      .delta.total.ignoredWhitespaceOnly .content.add,
+      .ignoredWhitespaceOnly .content.add,
+      .ignoredWhitespaceOnly .content.remove .intraline,
+      .delta.total.ignoredWhitespaceOnly .content.remove,
+      .ignoredWhitespaceOnly .content.remove {
+        background: none;
+      }
+
       .content .contentText:empty:after {
         /* Newline, to ensure empty lines are one line-height tall. */
         content: '\A';
       }
       .contextControl {
-        background-color: var(--diff-context-control-color);
+        background-color: var(--diff-context-control-background-color);
         border: 1px solid var(--diff-context-control-border-color);
+        color: var(--diff-context-control-color);
       }
       .contextControl gr-button {
         display: inline-block;
         text-decoration: none;
         --gr-button: {
-          color: var(--deemphasized-text-color);
-          padding: .2em;
+          color: var(--diff-context-control-color);
+          padding: var(--spacing-xs);
         }
       }
       .contextControl td:not(.lineNum) {
@@ -191,21 +222,19 @@
       .content .trailing-whitespace,
       .trailing-whitespace .intraline,
       .content .trailing-whitespace .intraline {
-        border-radius: .4em;
+        border-radius: var(--border-radius, 4px);
         background-color: var(--diff-trailing-whitespace-indicator);
       }
       #diffHeader {
         background-color: var(--table-header-background-color);
         border-bottom: 1px solid var(--border-color);
         color: var(--link-color);
-        font-family: var(--monospace-font-family);
-        font-size: var(--font-size, var(--font-size-normal));
-        padding: 0.5em 0 0.5em 4em;
+        padding: var(--spacing-m) 0 var(--spacing-m) 48px;
       }
       #loadingError,
       #sizeWarning {
         display: none;
-        margin: 1em auto;
+        margin: var(--spacing-l) auto;
         max-width: 60em;
         text-align: center;
       }
@@ -213,7 +242,7 @@
         color: var(--error-text-color);
       }
       #sizeWarning gr-button {
-        margin: 1em;
+        margin: var(--spacing-l);
       }
       #loadingError.showError,
       #sizeWarning.warn {
@@ -227,9 +256,7 @@
       }
       td.blame {
         display: none;
-        font-family: var(--font-family);
-        font-size: var(--font-size, var(--font-size-normal));
-        padding: 0 .5em;
+        padding: 0 var(--spacing-m);
         white-space: pre;
       }
       :host(.showBlame) col.blame {
@@ -251,11 +278,6 @@
         overflow: hidden;
         width: 200px;
       }
-      /** Since the line limit position is determined by charachter size, blank
-       lines also need to have the same font size as everything else */
-      .full-width .blank {
-        font-size: var(--font-size, var(--font-size-normal));
-      }
       /** Support the line length indicator **/
       .full-width td.content,
       .full-width td.blank {
@@ -280,8 +302,50 @@
       .lineNum.PARTIALLY_COVERED {
         background: linear-gradient(to right bottom, #FFD1A4 0%, #FFD1A4 50%, #E0F2F1 50%, #E0F2F1 100%);
       }
+
+      /** BEGIN: Select and copy for Polymer 2 */
+      /** Below was copied and modified from the original css in gr-diff-selection.html */
+      .content,
+      .contextControl,
+      .blame {
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+      }
+
+      .selected-left:not(.selected-comment) .side-by-side .left + .content .contentText,
+      .selected-right:not(.selected-comment) .side-by-side .right + .content .contentText,
+      .selected-left:not(.selected-comment) .unified .left.lineNum ~ .content:not(.both) .contentText,
+      .selected-right:not(.selected-comment) .unified .right.lineNum ~ .content .contentText,
+      .selected-left.selected-comment .side-by-side .left + .content .message,
+      .selected-right.selected-comment .side-by-side .right + .content .message :not(.collapsedContent),
+      .selected-comment .unified .message :not(.collapsedContent),
+      .selected-blame .blame {
+        -webkit-user-select: text;
+        -moz-user-select: text;
+        -ms-user-select: text;
+        user-select: text;
+      }
+
+      /** Make comments selectable when selected */
+      .selected-left.selected-comment ::slotted(gr-comment-thread[comment-side=left]),
+      .selected-right.selected-comment ::slotted(gr-comment-thread[comment-side=right]) {
+        -webkit-user-select: text;
+        -moz-user-select: text;
+        -ms-user-select: text;
+        user-select: text;
+      }
+      /** END: Select and copy for Polymer 2 */
+
+      .whitespace-change-only-message {
+        background-color: var(--diff-context-control-background-color);
+        border: 1px solid var(--diff-context-control-border-color);
+        text-align: center;
+      }
     </style>
     <style include="gr-syntax-theme"></style>
+    <style include="gr-ranged-comment-theme"></style>
     <div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]">
       <template
           is="dom-repeat"
@@ -302,18 +366,26 @@
               coverage-ranges="[[coverageRanges]]"
               project-name="[[projectName]]"
               diff="[[diff]]"
-              diff-path="[[path]]"
+              path="[[path]]"
               change-num="[[changeNum]]"
               patch-num="[[patchRange.patchNum]]"
               view-mode="[[viewMode]]"
               line-wrapping="[[lineWrapping]]"
               is-image-diff="[[isImageDiff]]"
               base-image="[[baseImage]]"
+              layers="[[layers]]"
               revision-image="[[revisionImage]]">
             <table
                 id="diffTable"
                 class$="[[_diffTableClass]]"
                 role="presentation"></table>
+
+            <template is="dom-if" if="[[showNoChangeMessage(loading, prefs, _diffLength)]]">
+              <div class="whitespace-change-only-message">
+                This file only contains whitespace changes.
+                Modify the whitespace setting to see the changes.
+              </div>
+            </template>
           </gr-diff-builder>
         </gr-diff-highlight>
       </gr-diff-selection>
@@ -329,15 +401,17 @@
         Prevented render because "Whole file" is enabled and this diff is very
         large (about [[_diffLength]] lines).
       </p>
-      <gr-button on-tap="_handleLimitedBypass">
+      <gr-button on-click="_handleLimitedBypass">
         Render with limited context
       </gr-button>
-      <gr-button on-tap="_handleFullBypass">
+      <gr-button on-click="_handleFullBypass">
         Render anyway (may be slow)
       </gr-button>
     </div>
   </template>
   <script src="gr-diff-line.js"></script>
   <script src="gr-diff-group.js"></script>
+  <!-- gr-diff.js contains an 'import' statement, which is allowed only in modules -->
+  <script src="../../../scripts/shadow.js"></script>
   <script src="gr-diff.js"></script>
 </dom-module>
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 4e023d4..c6b2e3a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -35,26 +35,10 @@
     RIGHT: 'right',
   };
 
-  const Defs = {};
-
-  /**
-   * Special line number which should not be collapsed into a shared region.
-   *
-   * @typedef {{
-   *  number: number,
-   *  leftSide: boolean
-   * }}
-   */
-  Defs.LineOfInterest;
-
   const LARGE_DIFF_THRESHOLD_LINES = 10000;
   const FULL_CONTEXT = -1;
   const LIMITED_CONTEXT = 10;
 
-  /** @typedef {{start_line: number, start_character: number,
-   *             end_line: number, end_character: number}} */
-  Gerrit.Range;
-
   /**
    * Compare two ranges. Either argument may be falsy, but will only return
    * true if both are falsy or if neither are falsy and have the same position
@@ -86,6 +70,9 @@
    * implements the same behavior as the template parsing for imperative slots.
    */
   Gerrit.slotToContent = function(slot) {
+    if (Polymer.Element) {
+      return slot;
+    }
     const content = document.createElement('content');
     content.name = slot.name;
     content.setAttribute('select', `[slot='${slot.name}']`);
@@ -106,7 +93,6 @@
 
   Polymer({
     is: 'gr-diff',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user selects a line.
@@ -184,7 +170,7 @@
         observer: '_viewModeObserver',
       },
 
-      /** @type {?Defs.LineOfInterest} */
+      /** @type {?Gerrit.LineOfInterest} */
       lineOfInterest: Object,
 
       loading: {
@@ -271,9 +257,11 @@
 
       /** Set by Polymer. */
       isAttached: Boolean,
+      layers: Array,
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.PatchSetBehavior,
     ],
 
@@ -295,12 +283,23 @@
       this._unobserveNodes();
     },
 
+    showNoChangeMessage(loading, prefs, diffLength) {
+      return !loading &&
+        prefs && prefs.ignore_whitespace !== 'IGNORE_NONE'
+        && diffLength === 0;
+    },
+
     _enableSelectionObserver(loggedIn, isAttached) {
+      // Polymer 2: check for undefined
+      if ([loggedIn, isAttached].some(arg => arg === undefined)) {
+        return;
+      }
+
       if (loggedIn && isAttached) {
-        this.listen(document, 'selectionchange', '_handleSelectionChange');
+        this.listen(document, '-shadow-selectionchange', '_handleSelectionChange');
         this.listen(document, 'mouseup', '_handleMouseUp');
       } else {
-        this.unlisten(document, 'selectionchange', '_handleSelectionChange');
+        this.unlisten(document, '-shadow-selectionchange', '_handleSelectionChange');
         this.unlisten(document, 'mouseup', '_handleMouseUp');
       }
     },
@@ -329,7 +328,8 @@
       // This takes the shadow DOM selection if one exists.
       return this.root.getSelection ?
         this.root.getSelection() :
-        document.getSelection();
+        // This is coming from shadow.js
+        getRange(this.root);
     },
 
     _observeNodes() {
@@ -361,7 +361,10 @@
         });
         this.splice('_commentRanges', i, 1);
       }
-      this.push('_commentRanges', ...addedCommentRanges);
+
+      if (addedCommentRanges && addedCommentRanges.length) {
+        this.push('_commentRanges', ...addedCommentRanges);
+      }
     },
 
     /**
@@ -384,7 +387,13 @@
         const commentSide = threadEl.getAttribute('comment-side');
         const lineNum = Number(threadEl.getAttribute('line-num')) ||
             GrDiffLine.FILE;
+        const commentRange = threadEl.range || {};
         keyLocations[commentSide][lineNum] = true;
+        // Add start_line as well if exists,
+        // the being and end of the range should not be collapsed.
+        if (commentRange.start_line) {
+          keyLocations[commentSide][commentRange.start_line] = true;
+        }
       }
       return keyLocations;
     },
@@ -393,12 +402,12 @@
     _redispatchHoverEvents(addedThreadEls) {
       for (const threadEl of addedThreadEls) {
         threadEl.addEventListener('mouseenter', () => {
-          threadEl.dispatchEvent(
-              new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
+          threadEl.dispatchEvent(new CustomEvent(
+              'comment-thread-mouseenter', {bubbles: true, composed: true}));
         });
         threadEl.addEventListener('mouseleave', () => {
-          threadEl.dispatchEvent(
-              new CustomEvent('comment-thread-mouseleave', {bubbles: true}));
+          threadEl.dispatchEvent(new CustomEvent(
+              'comment-thread-mouseleave', {bubbles: true, composed: true}));
         });
       }
     },
@@ -415,7 +424,6 @@
         return [];
       }
 
-      // Polymer2: querySelectorAll returns NodeList instead of Array.
       return Array.from(
           Polymer.dom(this.root).querySelectorAll('.diff-row'));
     },
@@ -554,6 +562,7 @@
           this._getIsParentCommentByLineAndContent(lineEl, contentEl);
       this.dispatchEvent(new CustomEvent('create-comment', {
         bubbles: true,
+        composed: true,
         detail: {
           lineNum,
           side,
@@ -713,7 +722,7 @@
 
     _diffChanged(newValue) {
       if (newValue) {
-        this._diffLength = this.$.diffBuilder.getDiffLength();
+        this._diffLength = this.getDiffLength(newValue);
         this._debounceRenderDiffTable();
       }
     },
@@ -736,14 +745,16 @@
     _renderDiffTable() {
       this._unobserveIncrementalNodes();
       if (!this.prefs) {
-        this.dispatchEvent(new CustomEvent('render', {bubbles: true}));
+        this.dispatchEvent(
+            new CustomEvent('render', {bubbles: true, composed: true}));
         return;
       }
       if (this.prefs.context === -1 &&
           this._diffLength >= LARGE_DIFF_THRESHOLD_LINES &&
           this._safetyBypass === null) {
         this._showWarning = true;
-        this.dispatchEvent(new CustomEvent('render', {bubbles: true}));
+        this.dispatchEvent(
+            new CustomEvent('render', {bubbles: true, composed: true}));
         return;
       }
 
@@ -753,7 +764,11 @@
       this.$.diffBuilder.render(keyLocations, this._getBypassPrefs())
           .then(() => {
             this.dispatchEvent(
-                new CustomEvent('render', {bubbles: true}));
+                new CustomEvent('render', {
+                  bubbles: true,
+                  composed: true,
+                  detail: {contentRendered: true},
+                }));
           });
     },
 
@@ -765,6 +780,7 @@
         // not hurt. It's probably a bigger performance cost to remove them than
         // to keep them around. Medium term we can even consider to add one slot
         // for each line from the start.
+        let lastEl;
         for (const threadEl of addedThreadEls) {
           const lineNumString = threadEl.getAttribute('line-num') || 'FILE';
           const commentSide = threadEl.getAttribute('comment-side');
@@ -783,6 +799,14 @@
           const slot = document.createElement('slot');
           slot.name = threadEl.getAttribute('slot');
           Polymer.dom(threadGroupEl).appendChild(Gerrit.slotToContent(slot));
+          lastEl = threadEl;
+        }
+
+        // Safari is not binding newly created comment-thread
+        // with the slot somehow, replace itself will rebind it
+        // @see Issue 11182
+        if (lastEl && lastEl.replaceWith) {
+          lastEl.replaceWith(lastEl);
         }
       });
     },
@@ -886,7 +910,8 @@
           !chunk.ab &&
 
           // The chunk doesn't have the given side.
-          ((leftSide && !chunk.a) || (!leftSide && !chunk.b)));
+          ((leftSide && (!chunk.a || !chunk.a.length)) ||
+           (!leftSide && (!chunk.b || !chunk.b.length))));
 
       // If we reached the beginning of the diff and failed to find a chunk
       // with the given side, return null.
@@ -944,5 +969,25 @@
       if (loading || !warning) { return 'newlineWarning hidden'; }
       return 'newlineWarning';
     },
+
+    /**
+     * Get the approximate length of the diff as the sum of the maximum
+     * length of the chunks.
+     *
+     * @param {Object} diff object
+     * @return {number}
+     */
+    getDiffLength(diff) {
+      if (!diff) return 0;
+      return diff.content.reduce((sum, sec) => {
+        if (sec.hasOwnProperty('ab')) {
+          return sum + sec.ab.length;
+        } else {
+          return sum + Math.max(
+              sec.hasOwnProperty('a') ? sec.a.length : 0,
+              sec.hasOwnProperty('b') ? sec.b.length : 0);
+        }
+      }, 0);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 594d60ed..b177bad 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -18,12 +18,15 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
 <link rel="import" href="gr-diff.html">
 
@@ -85,14 +88,14 @@
       element = fixture('basic');
       element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
       flushAsynchronousOperations();
-      assert.equal(element.customStyle['--line-limit'], '80ch');
+      assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
     });
 
     test('line limit without line_wrapping', () => {
       element = fixture('basic');
       element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
       flushAsynchronousOperations();
-      assert.isNotOk(element.customStyle['--line-limit']);
+      assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
     });
 
     suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
@@ -776,12 +779,12 @@
         element = fixture('basic');
         renderStub = sandbox.stub(element.$.diffBuilder, 'render',
             () => {
-              Promise.resolve();
               element.$.diffBuilder.dispatchEvent(
-                  new CustomEvent('render', {bubbles: true}));
+                  new CustomEvent('render', {bubbles: true, composed: true}));
+              return Promise.resolve({});
             });
         const mock = document.createElement('mock-diff-response');
-        sandbox.stub(element.$.diffBuilder, 'getDiffLength').returns(10000);
+        sandbox.stub(element, 'getDiffLength').returns(10000);
         element.diff = mock.diffResponse;
         element.noRenderOnPrefsChange = true;
       });
@@ -865,7 +868,7 @@
           assert.equal(element._lastChunkForSide(diff, true), diff.content[3]);
         });
 
-        test('addition', () => {
+        test('addition with a undefined', () => {
           const diff = {content: [
             {b: ['foo', 'bar', 'baz']},
           ]};
@@ -873,7 +876,16 @@
           assert.isNull(element._lastChunkForSide(diff, true));
         });
 
-        test('deletion', () => {
+        test('addition with a empty', () => {
+          const diff = {content: [
+            {a: [], b: ['foo', 'bar', 'baz']},
+          ]};
+          assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
+          assert.isNull(element._lastChunkForSide(diff, true));
+        });
+
+
+        test('deletion with b undefined', () => {
           const diff = {content: [
             {a: ['foo', 'bar', 'baz']},
           ]};
@@ -881,6 +893,14 @@
           assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
         });
 
+        test('deletion with b empty', () => {
+          const diff = {content: [
+            {a: ['foo', 'bar', 'baz'], b: []},
+          ]};
+          assert.isNull(element._lastChunkForSide(diff, false));
+          assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
+        });
+
         test('empty', () => {
           const diff = {content: []};
           assert.isNull(element._lastChunkForSide(diff, false));
@@ -1039,6 +1059,117 @@
         });
       });
     });
+
+    suite('whitespace changes only message', () => {
+      const setupDiff = function(ignore_whitespace, diffContent) {
+        element = fixture('basic');
+        element.prefs = {
+          ignore_whitespace,
+          auto_hide_diff_table_header: true,
+          context: 10,
+          cursor_blink_rate: 0,
+          font_size: 12,
+          intraline_difference: true,
+          line_length: 100,
+          line_wrapping: false,
+          show_line_endings: true,
+          show_tabs: true,
+          show_whitespace_errors: true,
+          syntax_highlighting: true,
+          tab_size: 8,
+          theme: 'DEFAULT',
+        };
+
+        element.diff = {
+          intraline_status: 'OK',
+          change_type: 'MODIFIED',
+          diff_header: [
+            'diff --git a/carrot.js b/carrot.js',
+            'index 2adc47d..f9c2f2c 100644',
+            '--- a/carrot.js',
+            '+++ b/carrot.jjs',
+            'file differ',
+          ],
+          content: diffContent,
+          binary: true,
+        };
+
+        element._renderDiffTable();
+        flushAsynchronousOperations();
+      };
+
+      test('show the message if ignore_whitespace is criteria matches', () => {
+        setupDiff('IGNORE_ALL', [{skip: 100}]);
+        assert.isTrue(element.showNoChangeMessage(
+            /* loading= */ false,
+            element.prefs,
+            element._diffLength
+        ));
+      });
+
+      test('do not show the message if still loading', () => {
+        setupDiff('IGNORE_ALL', [{skip: 100}]);
+        assert.isFalse(element.showNoChangeMessage(
+            /* loading= */ true,
+            element.prefs,
+            element._diffLength
+        ));
+      });
+
+      test('do not show the message if contains valid changes', () => {
+        const content = [{
+          a: ['all work and no play make andybons a dull boy'],
+          b: ['elgoog elgoog elgoog'],
+        }, {
+          ab: [
+            'Non eram nescius, Brute, cum, quae summis ingeniis ',
+            'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+          ],
+        }];
+        setupDiff('IGNORE_ALL', content);
+        assert.equal(element._diffLength, 3);
+        assert.isFalse(element.showNoChangeMessage(
+            /* loading= */ false,
+            element.prefs,
+            element._diffLength
+        ));
+      });
+
+      test('do not show message if ignore whitespace is disabled', () => {
+        const content = [{
+          a: ['all work and no play make andybons a dull boy'],
+          b: ['elgoog elgoog elgoog'],
+        }, {
+          ab: [
+            'Non eram nescius, Brute, cum, quae summis ingeniis ',
+            'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+          ],
+        }];
+        setupDiff('IGNORE_NONE', content);
+        assert.isFalse(element.showNoChangeMessage(
+            /* loading= */ false,
+            element.prefs,
+            element._diffLength
+        ));
+      });
+    });
+
+    test('getDiffLength', () => {
+      const diff = document.createElement('mock-diff-response').diffResponse;
+      assert.equal(element.getDiffLength(diff), 52);
+    });
+
+    test('`render` event has contentRendered field in detail', done => {
+      element = fixture('basic');
+      element.prefs = {};
+      renderStub = sandbox.stub(element.$.diffBuilder, 'render')
+          .returns(Promise.resolve());
+      element.addEventListener('render', event => {
+        assert.isTrue(event.detail.contentRendered);
+        done();
+      });
+      element._renderDiffTable();
+    });
   });
 
   a11ySuite('basic');
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
index 3de4284..ee1f536 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -34,7 +34,7 @@
       }
       .arrow {
         color: var(--deemphasized-text-color);
-        margin: 0 .5em;
+        margin: 0 var(--spacing-m);
       }
       gr-dropdown-list {
         --trigger-style: {
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 f913080..c7bf4ec3 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
@@ -31,7 +31,6 @@
 
   Polymer({
     is: 'gr-patch-range-select',
-    _legacyUndefinedCheck: true,
 
     properties: {
       availablePatches: Array,
@@ -68,6 +67,17 @@
 
     _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions,
         changeComments, revisionInfo) {
+      // Polymer 2: check for undefined
+      if ([
+        availablePatches,
+        patchNum,
+        _sortedRevisions,
+        changeComments,
+        revisionInfo,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       const parentCounts = revisionInfo.getParentCountMap();
       const currentParentCount = parentCounts.hasOwnProperty(patchNum) ?
         parentCounts[patchNum] : 1;
@@ -111,6 +121,16 @@
 
     _computePatchDropdownContent(availablePatches, basePatchNum,
         _sortedRevisions, changeComments) {
+      // Polymer 2: check for undefined
+      if ([
+        availablePatches,
+        basePatchNum,
+        _sortedRevisions,
+        changeComments,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       const dropdownContent = [];
       for (const patch of availablePatches) {
         const patchNum = patch.num;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index f2b35a5..ee893ee 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-patch-range-select</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
index c9e9f50..17a4866 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <dom-module id="gr-ranged-comment-layer">
   <template>
   </template>
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 2183e7d..5a03579 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
@@ -17,17 +17,14 @@
 (function() {
   'use strict';
 
-  const HOVER_PATH_PATTERN = /^commentRanges\.\#(\d+)\.hovering$/;
+  // Polymer 1 adds # before array's key, while Polymer 2 doesn't
+  const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/;
 
-  const RANGE_HIGHLIGHT = 'range';
-  const HOVER_HIGHLIGHT = 'rangeHighlight';
-
-  /** @typedef {{side: string, range: Gerrit.Range, hovering: boolean}} */
-  Gerrit.HoveredRange;
+  const RANGE_HIGHLIGHT = 'style-scope gr-diff range';
+  const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight';
 
   Polymer({
     is: 'gr-ranged-comment-layer',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the range in a range comment was malformed and had to be
@@ -55,6 +52,10 @@
       '_handleCommentRangesChange(commentRanges.*)',
     ],
 
+    get styleModuleName() {
+      return 'gr-ranged-comment-styles';
+    },
+
     /**
      * Layer method to add annotations to a line.
      *
@@ -130,8 +131,10 @@
       // If the change only changed the `hovering` property of a comment.
       const match = record.path.match(HOVER_PATH_PATTERN);
       if (match) {
-        const commentRangesIndex = match[1];
-        const {side, range, hovering} = this.commentRanges[commentRangesIndex];
+        // The #number indicates the key of that item in the array
+        // not the index, especially in polymer 1.
+        const {side, range, hovering} = this.get(match[1]);
+
         this._updateRangesMap(
             side, range, hovering, (forLine, start, end, hovering) => {
               const index = forLine.findIndex(lineRange =>
@@ -192,6 +195,7 @@
               range.end = line.text.length;
               this.dispatchEvent(new CustomEvent('normalize-range', {
                 bubbles: true,
+                composed: true,
                 detail: {lineNum, side},
               }));
             }
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
index 682c026..9d207a5 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-ranged-comment-layer</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../gr-diff/gr-diff-line.js"></script>
 
@@ -130,7 +132,7 @@
         assert.equal(lastCall.args[0], el);
         assert.equal(lastCall.args[1], expectedStart);
         assert.equal(lastCall.args[2], expectedLength);
-        assert.equal(lastCall.args[3], 'range');
+        assert.equal(lastCall.args[3], 'style-scope gr-diff range');
       });
 
       test('type=Remove has-comment hovering', () => {
@@ -148,7 +150,7 @@
         assert.equal(lastCall.args[0], el);
         assert.equal(lastCall.args[1], expectedStart);
         assert.equal(lastCall.args[2], expectedLength);
-        assert.equal(lastCall.args[3], 'rangeHighlight');
+        assert.equal(lastCall.args[3], 'style-scope gr-diff rangeHighlight');
       });
 
       test('type=Both has-comment', () => {
@@ -165,7 +167,7 @@
         assert.equal(lastCall.args[0], el);
         assert.equal(lastCall.args[1], expectedStart);
         assert.equal(lastCall.args[2], expectedLength);
-        assert.equal(lastCall.args[3], 'range');
+        assert.equal(lastCall.args[3], 'style-scope gr-diff range');
       });
 
       test('type=Both has-comment off side', () => {
@@ -193,7 +195,7 @@
         assert.equal(lastCall.args[0], el);
         assert.equal(lastCall.args[1], expectedStart);
         assert.equal(lastCall.args[2], expectedLength);
-        assert.equal(lastCall.args[3], 'range');
+        assert.equal(lastCall.args[3], 'style-scope gr-diff range');
       });
     });
 
@@ -207,6 +209,7 @@
     test('_handleCommentRangesChange hovering', () => {
       const notifyStub = sinon.stub();
       element.addListener(notifyStub);
+      const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap');
 
       element.set(['commentRanges', 1, 'hovering'], true);
 
@@ -215,6 +218,8 @@
       assert.equal(lastCall.args[0], 10);
       assert.equal(lastCall.args[1], 12);
       assert.equal(lastCall.args[2], 'right');
+
+      assert.isTrue(updateRangesMapSpy.called);
     });
 
     test('_handleCommentRangesChange splice out', () => {
@@ -251,6 +256,31 @@
       assert.equal(lastCall.args[2], 'left');
     });
 
+    test('_handleCommentRangesChange mixed actions', () => {
+      const notifyStub = sinon.stub();
+      element.addListener(notifyStub);
+      const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap');
+
+      element.set(['commentRanges', 1, 'hovering'], true);
+      assert.isTrue(updateRangesMapSpy.callCount === 1);
+      element.splice('commentRanges', 1, 1);
+      assert.isTrue(updateRangesMapSpy.callCount === 2);
+      element.splice('commentRanges', 1, 1);
+      assert.isTrue(updateRangesMapSpy.callCount === 3);
+      element.splice('commentRanges', 1, 0, {
+        side: 'left',
+        range: {
+          end_character: 15,
+          end_line: 275,
+          start_character: 5,
+          start_line: 250,
+        },
+      });
+      assert.isTrue(updateRangesMapSpy.callCount === 4);
+      element.set(['commentRanges', 2, 'hovering'], true);
+      assert.isTrue(updateRangesMapSpy.callCount === 5);
+    });
+
     test('_computeCommentMap creates maps correctly', () => {
       // There is only one ranged comment on the left, but it spans ll.36-39.
       const leftKeys = [];
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.html
new file mode 100644
index 0000000..cefd241
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.html
@@ -0,0 +1,30 @@
+<!--
+@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.
+-->
+<dom-module id="gr-ranged-comment-theme">
+  <template>
+    <style>
+      .range {
+        background-color: var(--diff-highlight-range-color);
+        display: inline;
+      }
+      .rangeHighlight {
+        background-color: var(--diff-highlight-range-hover-color);
+        display: inline;
+      }
+    </style>
+  </template>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
index 633530f..aa4d2e1 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-tooltip/gr-tooltip.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index 26bf738..b1b3e0f 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-selection-action-box',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the comment creation action was taken (hotkey, click).
@@ -49,6 +48,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
     ],
 
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
index dece366..b950e7b 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-selection-action-box</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-selection-action-box.html">
 
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
index 67c32bb..dd6bfec 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-lib-loader/gr-lib-loader.html">
 
 <dom-module id="gr-syntax-layer">
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index 944fa49..50cf6b4 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -134,7 +134,6 @@
 
   Polymer({
     is: 'gr-syntax-layer',
-    _legacyUndefinedCheck: true,
 
     properties: {
       diff: {
@@ -179,6 +178,10 @@
       this.push('_listeners', fn);
     },
 
+    removeListener(fn) {
+      this._listeners = this._listeners.filter(f => f != fn);
+    },
+
     /**
      * Annotation layer method to add syntax annotations to the given element
      * for the given line.
@@ -225,7 +228,7 @@
     },
 
     /**
-     * Start processing symtax for the loaded diff and notify layer listeners
+     * Start processing syntax for the loaded diff and notify layer listeners
      * as syntax info comes online.
      *
      * @return {Promise}
@@ -233,7 +236,7 @@
     process() {
       // Cancel any still running process() calls, because they append to the
       // same _baseRanges and _revisionRanges fields.
-      this.cancel();
+      this._cancel();
 
       // Discard existing ranges.
       this._baseRanges = [];
@@ -303,7 +306,7 @@
     /**
      * Cancel any asynchronous syntax processing jobs.
      */
-    cancel() {
+    _cancel() {
       if (this._processHandle != null) {
         this.cancelAsync(this._processHandle);
         this._processHandle = null;
@@ -314,7 +317,7 @@
     },
 
     _diffChanged() {
-      this.cancel();
+      this._cancel();
       this._baseRanges = [];
       this._revisionRanges = [];
     },
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index b63675a..472db21 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-syntax-layer</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
 <link rel="import" href="gr-syntax-layer.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
index 10710ae..e5ae06d 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
@@ -41,7 +41,6 @@
       .gr-syntax-keyword,
       .gr-syntax-name {
         color: var(--syntax-keyword-color);
-        line-height: 1;
       }
       .gr-syntax-number {
         color: var(--syntax-number-color);
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html
index 720f353..5072b9d 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html
@@ -14,11 +14,10 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
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 995326f..f850b9d 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-documentation-search',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /**
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
index 84addb0..84298e2 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-documentation-search</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-documentation-search.html">
 
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html
index 093e979..19a4e63 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-default-editor">
@@ -25,6 +25,8 @@
         border: none;
         box-sizing: border-box;
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-code);
+        line-height: var(--line-height-code);
         min-height: 60vh;
         resize: none;
         white-space: pre;
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
index 01cd9df..ed96bb2 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-default-editor',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the content of the editor changes.
@@ -32,8 +31,9 @@
     },
 
     _handleTextareaInput(e) {
-      this.dispatchEvent(new CustomEvent('content-change',
-          {detail: {value: e.target.value}, bubbles: true}));
+      this.dispatchEvent(new CustomEvent(
+          'content-change',
+          {detail: {value: e.target.value}, bubbles: true, composed: true}));
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
index b79cd9d..c986e7c 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
@@ -17,9 +17,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-default-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="gr-default-editor.html">
@@ -50,7 +52,7 @@
       element.addEventListener('content-change', contentChangedHandler);
       textarea.value = 'test';
       textarea.dispatchEvent(new CustomEvent('input',
-          {target: textarea, bubbles: true}));
+          {target: textarea, bubbles: true, composed: true}));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
index 81b3c07..52692a7 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -43,7 +43,7 @@
         display: none;
       }
       gr-button {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
         text-decoration: none;
       }
       gr-dialog {
@@ -52,22 +52,23 @@
       gr-dialog .main {
         width: 100%;
       }
+      gr-dialog .main > iron-input{
+        width: 100%;
+      }
       gr-autocomplete {
         --gr-autocomplete: {
           border: 1px solid var(--border-color);
-          border-radius: 2px;
-          font-size: var(--font-size-normal);
+          border-radius: var(--border-radius);
           height: 2em;
-          padding: 0 .15em;
+          padding: 0 var(--spacing-xs);
         }
       }
       input {
         border: 1px solid var(--border-color);
-        border-radius: 2px;
-        font-size: var(--font-size-normal);
+        border-radius: var(--border-radius);
         height: 2em;
-        margin: .5em 0;
-        padding: 0 .15em;
+        margin: var(--spacing-m) 0;
+        padding: 0 var(--spacing-xs);
         width: 100%;
       }
       @media screen and (max-width: 50em) {
@@ -81,7 +82,7 @@
           id$="[[action.id]]"
           class$="[[_computeIsInvisible(action.id, hiddenActions)]]"
           link
-          on-tap="_handleTap">[[action.label]]</gr-button>
+          on-click="_handleTap">[[action.label]]</gr-button>
     </template>
     <gr-overlay id="overlay" with-backdrop>
       <gr-dialog
@@ -132,11 +133,16 @@
               placeholder="Enter an existing full file path."
               query="[[_query]]"
               text="{{_path}}"></gr-autocomplete>
-          <input
-              class="newPathInput"
-              is="iron-input"
+          <iron-input
+              class="newPathIronInput"
               bind-value="{{_newPath}}"
-              placeholder="Enter the new path."/>
+              placeholder="Enter the new path.">
+            <input
+                class="newPathInput"
+                is="iron-input"
+                bind-value="{{_newPath}}"
+                placeholder="Enter the new path.">
+          </iron-input>
         </div>
       </gr-dialog>
       <gr-dialog
@@ -148,10 +154,14 @@
           on-cancel="_handleDialogCancel">
         <div class="header" slot="header">Restore this file?</div>
         <div class="main" slot="main">
-          <input
-              is="iron-input"
+          <iron-input
               disabled
-              bind-value="{{_path}}"/>
+              bind-value="{{_path}}">
+            <input
+                is="iron-input"
+                disabled
+                bind-value="{{_path}}">
+          </iron-input>
         </div>
       </gr-dialog>
     </gr-overlay>
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 3567d06..b7e12fe 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
@@ -19,7 +19,7 @@
 
   Polymer({
     is: 'gr-edit-controls',
-    _legacyUndefinedCheck: true,
+
     properties: {
       change: Object,
       patchNum: String,
@@ -170,7 +170,8 @@
         // just make two separate queries.
         dialog.querySelectorAll('gr-autocomplete')
             .forEach(input => { input.text = ''; });
-        dialog.querySelectorAll('input')
+
+        dialog.querySelectorAll('iron-input')
             .forEach(input => { input.bindValue = ''; });
       }
 
@@ -190,28 +191,33 @@
     },
 
     _handleDeleteConfirm(e) {
+      // Get the dialog before the api call as the event will change during bubbling
+      // which will make Polymer.dom(e).path an emtpy array in polymer 2
+      const dialog = this._getDialogFromEvent(e);
       this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path)
           .then(res => {
             if (!res.ok) { return; }
-            this._closeDialog(this._getDialogFromEvent(e), true);
+            this._closeDialog(dialog, true);
             Gerrit.Nav.navigateToChange(this.change);
           });
     },
 
     _handleRestoreConfirm(e) {
+      const dialog = this._getDialogFromEvent(e);
       this.$.restAPI.restoreFileInChangeEdit(this.change._number, this._path)
           .then(res => {
             if (!res.ok) { return; }
-            this._closeDialog(this._getDialogFromEvent(e), true);
+            this._closeDialog(dialog, true);
             Gerrit.Nav.navigateToChange(this.change);
           });
     },
 
     _handleRenameConfirm(e) {
+      const dialog = this._getDialogFromEvent(e);
       return this.$.restAPI.renameFileInChangeEdit(this.change._number,
           this._path, this._newPath).then(res => {
         if (!res.ok) { return; }
-        this._closeDialog(this._getDialogFromEvent(e), true);
+        this._closeDialog(dialog, true);
         Gerrit.Nav.navigateToChange(this.change);
       });
     },
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
index c67a2af..dd8cb74 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -17,9 +17,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-edit-controls</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="gr-edit-controls.html">
@@ -189,6 +191,9 @@
     let navStub;
     let renameStub;
     let renameAutocomplete;
+    const inputSelector = Polymer.Element ?
+      '.newPathIronInput' :
+      '.newPathInput';
 
     setup(() => {
       navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
@@ -208,7 +213,7 @@
         assert.isTrue(queryStub.called);
         assert.isTrue(element.$.renameDialog.disabled);
 
-        element.$.renameDialog.querySelector('.newPathInput').bindValue =
+        element.$.renameDialog.querySelector(inputSelector).bindValue =
             'src/test.newPath';
 
         assert.isFalse(element.$.renameDialog.disabled);
@@ -236,7 +241,7 @@
         assert.isTrue(queryStub.called);
         assert.isTrue(element.$.renameDialog.disabled);
 
-        element.$.renameDialog.querySelector('.newPathInput').bindValue =
+        element.$.renameDialog.querySelector(inputSelector).bindValue =
             'src/test.newPath';
 
         assert.isFalse(element.$.renameDialog.disabled);
@@ -258,7 +263,7 @@
         assert.isTrue(element.$.renameDialog.disabled);
         element.$.renameDialog.querySelector('gr-autocomplete').text =
             'src/test.cpp';
-        element.$.renameDialog.querySelector('.newPathInput').bindValue =
+        element.$.renameDialog.querySelector(inputSelector).bindValue =
             'src/test.newPath';
         assert.isFalse(element.$.renameDialog.disabled);
         MockInteractions.tap(element.$.renameDialog.$$('gr-button'));
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
index c57a147..f6c7803 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
@@ -32,7 +32,7 @@
         justify-content: flex-end;
       }
       #actions {
-        margin-right: 1em;
+        margin-right: var(--spacing-l);
       }
       gr-button,
       gr-dropdown {
@@ -45,7 +45,6 @@
           background-color: transparent;
           border: none;
           color: var(--link-color);
-          font-size: inherit;
           text-transform: uppercase;
         }
       }
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
index 9407f18..250816b 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-edit-file-controls',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when an action in the overflow menu is tapped.
@@ -46,8 +45,9 @@
     },
 
     _dispatchFileAction(action, path) {
-      this.dispatchEvent(new CustomEvent('file-action-tap',
-          {detail: {action, path}, bubbles: true}));
+      this.dispatchEvent(new CustomEvent(
+          'file-action-tap',
+          {detail: {action, path}, bubbles: true, composed: true}));
     },
 
     _computeFileActions(actions) {
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
index 12d9e0b..7979e57 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
@@ -17,9 +17,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-edit-file-controls</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="../gr-edit-constants.html">
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
index b107221..ce90c69 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
@@ -15,8 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
@@ -47,23 +48,23 @@
         align-items: center;
         display: flex;
         justify-content: space-between;
-        padding: .75em var(--default-horizontal-margin);
+        padding: var(--spacing-m) var(--spacing-l);
       }
       header gr-editable-label {
-        font-size: var(--font-size-large);
+        font-size: var(--font-size-h3);
         --label-style: {
           text-overflow: initial;
           white-space: initial;
           word-break: break-all;
         }
         --input-style: {
-          margin-top: 1em;
+          margin-top: var(--spacing-l);
         }
       }
       .textareaWrapper {
         border: 1px solid var(--border-color);
-        border-radius: 3px;
-        margin: var(--default-horizontal-margin);
+        border-radius: var(--border-radius);
+        margin: var(--spacing-l);
       }
       .textareaWrapper .editButtons {
         display: none;
@@ -71,7 +72,7 @@
       .controlGroup {
         align-items: center;
         display: flex;
-        font-size: var(--font-size-large);
+        font-size: var(--font-size-h3);
       }
       .rightControls {
         justify-content: flex-end;
@@ -101,13 +102,13 @@
           <gr-button
               id="close"
               link
-              on-tap="_handleCloseTap">Close</gr-button>
+              on-click="_handleCloseTap">Close</gr-button>
           <gr-button
               id="save"
               disabled$="[[_saveDisabled]]"
               primary
               link
-              on-tap="_saveEdit">Save</gr-button>
+              on-click="_saveEdit">Save</gr-button>
         </span>
       </header>
     </gr-fixed-panel>
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 3ed165e..a21975d 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
@@ -26,7 +26,6 @@
 
   Polymer({
     is: 'gr-editor-view',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -75,6 +74,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.PathListBehavior,
@@ -105,7 +105,9 @@
     },
 
     _paramsChanged(value) {
-      if (value.view !== Gerrit.Nav.View.EDIT) { return; }
+      if (value.view !== Gerrit.Nav.View.EDIT) {
+        return;
+      }
 
       this._changeNum = value.changeNum;
       this._path = value.path;
@@ -136,7 +138,9 @@
 
     _handlePathChanged(e) {
       const path = e.detail;
-      if (path === this._path) { return Promise.resolve(); }
+      if (path === this._path) {
+        return Promise.resolve();
+      }
       return this.$.restAPI.renameFileInChangeEdit(this._changeNum,
           this._path, path).then(res => {
         if (!res.ok) { return; }
@@ -160,8 +164,11 @@
           .then(res => {
             if (storedContent && storedContent.message &&
                 storedContent.message !== res.content) {
-              this.dispatchEvent(new CustomEvent('show-alert',
-                  {detail: {message: RESTORED_MESSAGE}, bubbles: true}));
+              this.dispatchEvent(new CustomEvent('show-alert', {
+                detail: {message: RESTORED_MESSAGE},
+                bubbles: true,
+                composed: true,
+              }));
 
               this._newContent = storedContent.message;
             } else {
@@ -199,11 +206,23 @@
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {message},
         bubbles: true,
+        composed: true,
       }));
     },
 
     _computeSaveDisabled(content, newContent, saving) {
-      if (saving) { return true; }
+      // Polymer 2: check for undefined
+      if ([
+        content,
+        newContent,
+        saving,
+      ].some(arg => arg === undefined)) {
+        return true;
+      }
+
+      if (saving) {
+        return true;
+      }
       return content === newContent;
     },
 
@@ -226,7 +245,9 @@
 
     _handleSaveShortcut(e) {
       e.preventDefault();
-      if (!this._saveDisabled) { this._saveEdit(); }
+      if (!this._saveDisabled) {
+        this._saveEdit();
+      }
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
index 010ff4a..226472f 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -17,9 +17,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-editor-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="gr-editor-view.html">
@@ -121,7 +123,7 @@
     const storeStub = sandbox.spy(element.$.storage, 'setEditableContentItem');
     element._newContent = 'test';
     element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', {
-      bubbles: true,
+      bubbles: true, composed: true,
       detail: {value: 'new content value'},
     }));
     element.flushDebouncer('store');
diff --git a/polygerrit-ui/app/elements/gr-app-element.html b/polygerrit-ui/app/elements/gr-app-element.html
new file mode 100644
index 0000000..046e5ff
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app-element.html
@@ -0,0 +1,238 @@
+<!--
+@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.
+-->
+<script src="/bower_components/moment/moment.js"></script>
+<script src="../scripts/util.js"></script>
+
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
+<link rel="import" href="../styles/shared-styles.html">
+<link rel="import" href="../styles/themes/app-theme.html">
+<link rel="import" href="./admin/gr-admin-view/gr-admin-view.html">
+<link rel="import" href="./documentation/gr-documentation-search/gr-documentation-search.html">
+<link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
+<link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
+<link rel="import" href="./change/gr-change-view/gr-change-view.html">
+<link rel="import" href="./core/gr-error-manager/gr-error-manager.html">
+<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
+<link rel="import" href="./core/gr-main-header/gr-main-header.html">
+<link rel="import" href="./core/gr-navigation/gr-navigation.html">
+<link rel="import" href="./core/gr-reporting/gr-reporting.html">
+<link rel="import" href="./core/gr-router/gr-router.html">
+<link rel="import" href="./core/gr-smart-search/gr-smart-search.html">
+<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
+<link rel="import" href="./edit/gr-editor-view/gr-editor-view.html">
+<link rel="import" href="./plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
+<link rel="import" href="./plugins/gr-endpoint-param/gr-endpoint-param.html">
+<link rel="import" href="./plugins/gr-external-style/gr-external-style.html">
+<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
+<link rel="import" href="./settings/gr-cla-view/gr-cla-view.html">
+<link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html">
+<link rel="import" href="./settings/gr-settings-view/gr-settings-view.html">
+<link rel="import" href="./shared/gr-fixed-panel/gr-fixed-panel.html">
+<link rel="import" href="./shared/gr-lib-loader/gr-lib-loader.html">
+<link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<dom-module id="gr-app-element">
+  <template>
+    <style include="shared-styles">
+      :host {
+        background-color: var(--view-background-color);
+        display: flex;
+        flex-direction: column;
+        min-height: 100%;
+      }
+      gr-fixed-panel {
+        /**
+         * This one should be greater that the z-index in gr-diff-view
+         * because gr-main-header contains overlay.
+         */
+        z-index: 10;
+      }
+      gr-main-header,
+      footer {
+        color: var(--primary-text-color);
+      }
+      gr-main-header {
+        background: var(--header-background, var(--header-background-color, #eee));
+        padding: var(--header-padding);
+        border-bottom: var(--header-border-bottom);
+        border-image: var(--header-border-image);
+        border-right: 0;
+        border-left: 0;
+        border-top: 0;
+        box-shadow: var(--header-box-shadow);
+      }
+      footer {
+        background: var(--footer-background, var(--footer-background-color, #eee));
+        border-top: var(--footer-border-top);
+        display: flex;
+        justify-content: space-between;
+        padding: var(--spacing-m) var(--spacing-l);
+        z-index: 100;
+      }
+      main {
+        flex: 1;
+        padding-bottom: var(--spacing-xxl);
+        position: relative;
+      }
+      .errorView {
+        align-items: center;
+        display: none;
+        flex-direction: column;
+        justify-content: center;
+        position: absolute;
+        top: 0;
+        right: 0;
+        bottom: 0;
+        left: 0;
+      }
+      .errorView.show {
+        display: flex;
+      }
+      .errorEmoji {
+        font-size: 2.6rem;
+      }
+      .errorText,
+      .errorMoreInfo {
+        margin-top: var(--spacing-m);
+      }
+      .errorText {
+        font-size: var(--font-size-h3);
+      }
+      .errorMoreInfo {
+        color: var(--deemphasized-text-color);
+      }
+      .feedback {
+        color: var(--error-text-color);
+      }
+    </style>
+    <gr-endpoint-decorator name="banner"></gr-endpoint-decorator>
+    <gr-fixed-panel id="header">
+      <gr-main-header
+          id="mainHeader"
+          search-query="{{params.query}}"
+          on-mobile-search="_mobileSearchToggle">
+      </gr-main-header>
+    </gr-fixed-panel>
+    <main>
+      <gr-smart-search
+          id="search"
+          search-query="{{params.query}}"
+          hidden="[[!mobileSearch]]">
+      </gr-smart-search>
+      <template is="dom-if" if="[[_showChangeListView]]" restamp="true">
+        <gr-change-list-view
+            params="[[params]]"
+            account="[[_account]]"
+            view-state="{{_viewState.changeListView}}"></gr-change-list-view>
+      </template>
+      <template is="dom-if" if="[[_showDashboardView]]" restamp="true">
+        <gr-dashboard-view
+            account="[[_account]]"
+            params="[[params]]"
+            view-state="{{_viewState.dashboardView}}"></gr-dashboard-view>
+      </template>
+      <template is="dom-if" if="[[_showChangeView]]" restamp="true">
+        <gr-change-view
+            params="[[params]]"
+            view-state="{{_viewState.changeView}}"
+            back-page="[[_lastSearchPage]]"></gr-change-view>
+      </template>
+      <template is="dom-if" if="[[_showEditorView]]" restamp="true">
+        <gr-editor-view
+            params="[[params]]"></gr-editor-view>
+      </template>
+      <template is="dom-if" if="[[_showDiffView]]" restamp="true">
+          <gr-diff-view
+              params="[[params]]"
+              change-view-state="{{_viewState.changeView}}"></gr-diff-view>
+        </template>
+      <template is="dom-if" if="[[_showSettingsView]]" restamp="true">
+        <gr-settings-view
+            params="[[params]]"
+            on-account-detail-update="_handleAccountDetailUpdate">
+        </gr-settings-view>
+      </template>
+      <template is="dom-if" if="[[_showAdminView]]" restamp="true">
+        <gr-admin-view path="[[_path]]"
+            params=[[params]]></gr-admin-view>
+      </template>
+      <template is="dom-if" if="[[_showPluginScreen]]" restamp="true">
+        <gr-endpoint-decorator name="[[_pluginScreenName]]">
+          <gr-endpoint-param name="token" value="[[params.screen]]"></gr-endpoint-param>
+        </gr-endpoint-decorator>
+      </template>
+      <template is="dom-if" if="[[_showCLAView]]" restamp="true">
+        <gr-cla-view></gr-cla-view>
+      </template>
+      <template is="dom-if" if="[[_showDocumentationSearch]]" restamp="true">
+        <gr-documentation-search
+            params="[[params]]">
+        </gr-documentation-search>
+      </template>
+      <div id="errorView" class="errorView">
+        <div class="errorEmoji">[[_lastError.emoji]]</div>
+        <div class="errorText">[[_lastError.text]]</div>
+        <div class="errorMoreInfo">[[_lastError.moreInfo]]</div>
+      </div>
+    </main>
+    <footer r="contentinfo">
+      <div>
+        Powered by <a href="https://www.gerritcodereview.com/" rel="noopener"
+        target="_blank">Gerrit Code Review</a>
+        ([[_version]])
+        <gr-endpoint-decorator name="footer-left"></gr-endpoint-decorator>
+      </div>
+      <div>
+        <template is="dom-if" if="[[_feedbackUrl]]">
+          <a class="feedback"
+              href$="[[_feedbackUrl]]"
+              rel="noopener"
+              target="_blank">Report bug</a> |
+        </template>
+        Press &ldquo;?&rdquo; for keyboard shortcuts
+        <gr-endpoint-decorator name="footer-right"></gr-endpoint-decorator>
+      </div>
+    </footer>
+    <gr-overlay id="keyboardShortcuts" with-backdrop>
+      <gr-keyboard-shortcuts-dialog
+          view="[[params.view]]"
+          on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
+    </gr-overlay>
+    <gr-overlay id="registrationOverlay" with-backdrop>
+      <gr-registration-dialog
+          id="registrationDialog"
+          settings-url="[[_settingsUrl]]"
+          on-account-detail-update="_handleAccountDetailUpdate"
+          on-close="_handleRegistrationDialogClose">
+      </gr-registration-dialog>
+    </gr-overlay>
+    <gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator>
+    <gr-error-manager id="errorManager"></gr-error-manager>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-reporting id="reporting"></gr-reporting>
+    <gr-router id="router"></gr-router>
+    <gr-plugin-host id="plugins"
+        config="[[_serverConfig]]">
+    </gr-plugin-host>
+    <gr-lib-loader id="libLoader"></gr-lib-loader>
+    <gr-external-style id="externalStyleForAll" name="app-theme"></gr-external-style>
+    <gr-external-style id="externalStyleForTheme" name="[[getThemeEndpoint()]]"></gr-external-style>
+  </template>
+  <script src="gr-app-element.js" crossorigin="anonymous"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
new file mode 100644
index 0000000..ce6b98b
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -0,0 +1,468 @@
+/**
+ * @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.
+ */
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-app-element',
+
+    /**
+     * Fired when the URL location changes.
+     *
+     * @event location-change
+     */
+
+    properties: {
+      /**
+       * @type {{ query: string, view: string, screen: string }}
+       */
+      params: Object,
+      keyEventTarget: {
+        type: Object,
+        value() { return document.body; },
+      },
+
+      _account: {
+        type: Object,
+        observer: '_accountChanged',
+      },
+
+      /**
+       * The last time the g key was pressed in milliseconds (or a keydown event
+       * was handled if the key is held down).
+       *
+       * @type {number|null}
+       */
+      _lastGKeyPressTimestamp: {
+        type: Number,
+        value: null,
+      },
+
+      /**
+       * @type {{ plugin: Object }}
+       */
+      _serverConfig: Object,
+      _version: String,
+      _showChangeListView: Boolean,
+      _showDashboardView: Boolean,
+      _showChangeView: Boolean,
+      _showDiffView: Boolean,
+      _showSettingsView: Boolean,
+      _showAdminView: Boolean,
+      _showCLAView: Boolean,
+      _showEditorView: Boolean,
+      _showPluginScreen: Boolean,
+      _showDocumentationSearch: Boolean,
+      /** @type {?} */
+      _viewState: Object,
+      /** @type {?} */
+      _lastError: Object,
+      _lastSearchPage: String,
+      _path: String,
+      _pluginScreenName: {
+        type: String,
+        computed: '_computePluginScreenName(params)',
+      },
+      _settingsUrl: String,
+      _feedbackUrl: String,
+      // Used to allow searching on mobile
+      mobileSearch: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    listeners: {
+      'page-error': '_handlePageError',
+      'title-change': '_handleTitleChange',
+      'location-change': '_handleLocationChange',
+      'rpc-log': '_handleRpcLog',
+    },
+
+    observers: [
+      '_viewChanged(params.view)',
+      '_paramsChanged(params.*)',
+    ],
+
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+      Gerrit.KeyboardShortcutBehavior,
+    ],
+
+    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',
+      };
+    },
+
+    created() {
+      this._bindKeyboardShortcuts();
+    },
+
+    ready() {
+      this.$.reporting.appStarted(document.visibilityState === 'hidden');
+      this.$.router.start();
+
+      this.$.restAPI.getAccount().then(account => {
+        this._account = account;
+      });
+      this.$.restAPI.getConfig().then(config => {
+        this._serverConfig = config;
+
+        if (config && config.gerrit && config.gerrit.report_bug_url) {
+          this._feedbackUrl = config.gerrit.report_bug_url;
+        }
+      });
+      this.$.restAPI.getVersion().then(version => {
+        this._version = version;
+        this._logWelcome();
+      });
+
+      if (window.localStorage.getItem('dark-theme')) {
+        // No need to add the style module to element again as it's imported
+        // by importHref already
+        this.$.libLoader.getDarkTheme();
+      }
+
+      // Note: this is evaluated here to ensure that it only happens after the
+      // router has been initialized. @see Issue 7837
+      this._settingsUrl = Gerrit.Nav.getUrlForSettings();
+
+      this._viewState = {
+        changeView: {
+          changeNum: null,
+          patchRange: null,
+          selectedFileIndex: 0,
+          showReplyDialog: false,
+          diffMode: null,
+          numFilesShown: null,
+          scrollTop: 0,
+        },
+        changeListView: {
+          query: null,
+          offset: 0,
+          selectedChangeIndex: 0,
+        },
+        dashboardView: {
+          selectedChangeIndex: 0,
+        },
+      };
+    },
+
+    _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(
+          this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
+      this.bindShortcut(
+          this.Shortcut.GO_TO_USER_DASHBOARD, this.GO_KEY, 'i');
+      this.bindShortcut(
+          this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
+      this.bindShortcut(
+          this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm');
+      this.bindShortcut(
+          this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a');
+      this.bindShortcut(
+          this.Shortcut.GO_TO_WATCHED_CHANGES, this.GO_KEY, 'w');
+
+      this.bindShortcut(
+          this.Shortcut.CURSOR_NEXT_CHANGE, 'j');
+      this.bindShortcut(
+          this.Shortcut.CURSOR_PREV_CHANGE, 'k');
+      this.bindShortcut(
+          this.Shortcut.OPEN_CHANGE, 'o');
+      this.bindShortcut(
+          this.Shortcut.NEXT_PAGE, 'n', ']');
+      this.bindShortcut(
+          this.Shortcut.PREV_PAGE, 'p', '[');
+      this.bindShortcut(
+          this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup');
+      this.bindShortcut(
+          this.Shortcut.TOGGLE_CHANGE_STAR, 's:keyup');
+      this.bindShortcut(
+          this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
+      this.bindShortcut(
+          this.Shortcut.EDIT_TOPIC, 't');
+
+      this.bindShortcut(
+          this.Shortcut.OPEN_REPLY_DIALOG, 'a');
+      this.bindShortcut(
+          this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
+      this.bindShortcut(
+          this.Shortcut.EXPAND_ALL_MESSAGES, 'x');
+      this.bindShortcut(
+          this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
+      this.bindShortcut(
+          this.Shortcut.REFRESH_CHANGE, 'shift+r:keyup');
+      this.bindShortcut(
+          this.Shortcut.UP_TO_DASHBOARD, 'u');
+      this.bindShortcut(
+          this.Shortcut.UP_TO_CHANGE, 'u');
+      this.bindShortcut(
+          this.Shortcut.TOGGLE_DIFF_MODE, 'm:keyup');
+
+      this.bindShortcut(
+          this.Shortcut.NEXT_LINE, 'j', 'down');
+      this.bindShortcut(
+          this.Shortcut.PREV_LINE, 'k', 'up');
+      this.bindShortcut(
+          this.Shortcut.NEXT_CHUNK, 'n');
+      this.bindShortcut(
+          this.Shortcut.PREV_CHUNK, 'p');
+      this.bindShortcut(
+          this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
+      this.bindShortcut(
+          this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
+      this.bindShortcut(
+          this.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
+      this.bindShortcut(
+          this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
+      this.bindShortcut(
+          this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
+          this.DOC_ONLY, 'shift+e');
+      this.bindShortcut(
+          this.Shortcut.LEFT_PANE, 'shift+left');
+      this.bindShortcut(
+          this.Shortcut.RIGHT_PANE, 'shift+right');
+      this.bindShortcut(
+          this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+      this.bindShortcut(
+          this.Shortcut.NEW_COMMENT, 'c');
+      this.bindShortcut(
+          this.Shortcut.SAVE_COMMENT,
+          'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
+      this.bindShortcut(
+          this.Shortcut.OPEN_DIFF_PREFS, ',');
+      this.bindShortcut(
+          this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup');
+
+      this.bindShortcut(
+          this.Shortcut.NEXT_FILE, ']');
+      this.bindShortcut(
+          this.Shortcut.PREV_FILE, '[');
+      this.bindShortcut(
+          this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
+      this.bindShortcut(
+          this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
+      this.bindShortcut(
+          this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+      this.bindShortcut(
+          this.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
+      this.bindShortcut(
+          this.Shortcut.OPEN_FILE, 'o', 'enter');
+      this.bindShortcut(
+          this.Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup');
+      this.bindShortcut(
+          this.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
+      this.bindShortcut(
+          this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
+      this.bindShortcut(
+          this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
+
+      this.bindShortcut(
+          this.Shortcut.OPEN_FIRST_FILE, ']');
+      this.bindShortcut(
+          this.Shortcut.OPEN_LAST_FILE, '[');
+
+      this.bindShortcut(
+          this.Shortcut.SEARCH, '/');
+    },
+
+    _accountChanged(account) {
+      if (!account) { return; }
+
+      // Preferences are cached when a user is logged in; warm them.
+      this.$.restAPI.getPreferences();
+      this.$.restAPI.getDiffPreferences();
+      this.$.restAPI.getEditPreferences();
+      this.$.errorManager.knownAccountId =
+          this._account && this._account._account_id || null;
+    },
+
+    _viewChanged(view) {
+      this.$.errorView.classList.remove('show');
+      this.set('_showChangeListView', view === Gerrit.Nav.View.SEARCH);
+      this.set('_showDashboardView', view === Gerrit.Nav.View.DASHBOARD);
+      this.set('_showChangeView', view === Gerrit.Nav.View.CHANGE);
+      this.set('_showDiffView', view === Gerrit.Nav.View.DIFF);
+      this.set('_showSettingsView', view === Gerrit.Nav.View.SETTINGS);
+      this.set('_showAdminView', view === Gerrit.Nav.View.ADMIN ||
+          view === Gerrit.Nav.View.GROUP || view === Gerrit.Nav.View.REPO);
+      this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS);
+      this.set('_showEditorView', view === Gerrit.Nav.View.EDIT);
+      const isPluginScreen = view === Gerrit.Nav.View.PLUGIN_SCREEN;
+      this.set('_showPluginScreen', false);
+      // Navigation within plugin screens does not restamp gr-endpoint-decorator
+      // because _showPluginScreen value does not change. To force restamp,
+      // change _showPluginScreen value between true and false.
+      if (isPluginScreen) {
+        this.async(() => this.set('_showPluginScreen', true), 1);
+      }
+      this.set('_showDocumentationSearch',
+          view === Gerrit.Nav.View.DOCUMENTATION_SEARCH);
+      if (this.params.justRegistered) {
+        this.$.registrationOverlay.open();
+        this.$.registrationDialog.loadData().then(() => {
+          this.$.registrationOverlay.refit();
+        });
+      }
+      this.$.header.unfloat();
+    },
+
+    _handlePageError(e) {
+      const props = [
+        '_showChangeListView',
+        '_showDashboardView',
+        '_showChangeView',
+        '_showDiffView',
+        '_showSettingsView',
+        '_showAdminView',
+      ];
+      for (const showProp of props) {
+        this.set(showProp, false);
+      }
+
+      this.$.errorView.classList.add('show');
+      const response = e.detail.response;
+      const err = {text: [response.status, response.statusText].join(' ')};
+      if (response.status === 404) {
+        err.emoji = '¯\\_(ツ)_/¯';
+        this._lastError = err;
+      } else {
+        err.emoji = 'o_O';
+        response.text().then(text => {
+          err.moreInfo = text;
+          this._lastError = err;
+        });
+      }
+    },
+
+    _handleLocationChange(e) {
+      const hash = e.detail.hash.substring(1);
+      let pathname = e.detail.pathname;
+      if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) {
+        pathname += '@' + hash;
+      }
+      this.set('_path', pathname);
+    },
+
+    _paramsChanged(paramsRecord) {
+      const params = paramsRecord.base;
+      const viewsToCheck = [Gerrit.Nav.View.SEARCH, Gerrit.Nav.View.DASHBOARD];
+      if (viewsToCheck.includes(params.view)) {
+        this.set('_lastSearchPage', location.pathname);
+      }
+    },
+
+    _handleTitleChange(e) {
+      if (e.detail.title) {
+        document.title = e.detail.title + ' · Gerrit Code Review';
+      } else {
+        document.title = '';
+      }
+    },
+
+    _showKeyboardShortcuts(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+      this.$.keyboardShortcuts.open();
+    },
+
+    _handleKeyboardShortcutDialogClose() {
+      this.$.keyboardShortcuts.close();
+    },
+
+    _handleAccountDetailUpdate(e) {
+      this.$.mainHeader.reload();
+      if (this.params.view === Gerrit.Nav.View.SETTINGS) {
+        this.$$('gr-settings-view').reloadAccountDetail();
+      }
+    },
+
+    _handleRegistrationDialogClose(e) {
+      this.params.justRegistered = false;
+      this.$.registrationOverlay.close();
+    },
+
+    _goToOpenedChanges() {
+      Gerrit.Nav.navigateToStatusSearch('open');
+    },
+
+    _goToUserDashboard() {
+      Gerrit.Nav.navigateToUserDashboard();
+    },
+
+    _goToMergedChanges() {
+      Gerrit.Nav.navigateToStatusSearch('merged');
+    },
+
+    _goToAbandonedChanges() {
+      Gerrit.Nav.navigateToStatusSearch('abandoned');
+    },
+
+    _goToWatchedChanges() {
+      // The query is hardcoded, and doesn't respect custom menu entries
+      Gerrit.Nav.navigateToSearchQuery('is:watched is:open');
+    },
+
+    _computePluginScreenName({plugin, screen}) {
+      if (!plugin || !screen) return '';
+      return `${plugin}-screen-${screen}`;
+    },
+
+    _logWelcome() {
+      console.group('Runtime Info');
+      console.log('Gerrit UI (PolyGerrit)');
+      console.log(`Gerrit Server Version: ${this._version}`);
+      if (window.VERSION_INFO) {
+        console.log(`UI Version Info: ${window.VERSION_INFO}`);
+      }
+      if (this._feedbackUrl) {
+        console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
+      }
+      console.groupEnd();
+    },
+
+    /**
+     * Intercept RPC log events emitted by REST API interfaces.
+     * Note: the REST API interface cannot use gr-reporting directly because
+     * that would create a cyclic dependency.
+     */
+    _handleRpcLog(e) {
+      this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl,
+          e.detail.elapsed);
+    },
+
+    _mobileSearchToggle(e) {
+      this.mobileSearch = !this.mobileSearch;
+    },
+
+    getThemeEndpoint() {
+      // For now, we only have dark mode and light mode
+      return window.localStorage.getItem('dark-theme') ?
+        'app-theme-dark' :
+        'app-theme-light';
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 8c50358..f49d8aa 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -15,28 +15,18 @@
 limitations under the License.
 -->
 <script>
-  if (!window.POLYMER2) {
-    // This must be set prior to loading Polymer for the first time.
-    if (localStorage.getItem('USE_SHADOW_DOM') === 'true') {
-      window.Polymer = {
-        dom: 'shadow',
-        passiveTouchGestures: true,
-      };
-    } else if (!window.Polymer) {
-      window.Polymer = {
-        passiveTouchGestures: true,
-      };
-    }
+  if (!window.Polymer) {
+    window.Polymer = {
+      passiveTouchGestures: true,
+      lazyRegister: true,
+    };
   }
-  // Needed for JSCompiler to understand it's global.
-  // eslint-disable-next-line no-unused-vars, prefer-const
-  let Gerrit = window.Gerrit || {};
-  window.Gerrit = Gerrit;
+  window.Gerrit = window.Gerrit || {};
 </script>
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/polymer-resin/standalone/polymer-resin.html">
-<link rel="import" href="../bower_components/polymer/lib/legacy/legacy-data-mixin.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer-resin/standalone/polymer-resin.html">
+<!-- TODO(taoalpha): Remove once all legacyUndefinedCheck removed. -->
 <link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html">
 <script>
   security.polymer_resin.install({
@@ -45,228 +35,11 @@
     safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
   });
 </script>
-<script src="../bower_components/moment/moment.js"></script>
 
-<link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../styles/shared-styles.html">
-<link rel="import" href="../styles/themes/app-theme.html">
-<link rel="import" href="./admin/gr-admin-view/gr-admin-view.html">
-<link rel="import" href="./documentation/gr-documentation-search/gr-documentation-search.html">
-<link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
-<link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
-<link rel="import" href="./change/gr-change-view/gr-change-view.html">
-<link rel="import" href="./core/gr-error-manager/gr-error-manager.html">
-<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
-<link rel="import" href="./core/gr-main-header/gr-main-header.html">
-<link rel="import" href="./core/gr-navigation/gr-navigation.html">
-<link rel="import" href="./core/gr-reporting/gr-reporting.html">
-<link rel="import" href="./core/gr-router/gr-router.html">
-<link rel="import" href="./core/gr-smart-search/gr-smart-search.html">
-<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
-<link rel="import" href="./edit/gr-editor-view/gr-editor-view.html">
-<link rel="import" href="./plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="./plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="./plugins/gr-external-style/gr-external-style.html">
-<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
-<link rel="import" href="./settings/gr-cla-view/gr-cla-view.html">
-<link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html">
-<link rel="import" href="./settings/gr-settings-view/gr-settings-view.html">
-<link rel="import" href="./shared/gr-fixed-panel/gr-fixed-panel.html">
-<link rel="import" href="./shared/gr-lib-loader/gr-lib-loader.html">
-<link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<script src="../scripts/util.js"></script>
-
+<link rel="import" href="./gr-app-element.html">
 <dom-module id="gr-app">
   <template>
-    <style include="shared-styles">
-      :host {
-        background-color: var(--view-background-color);
-        display: flex;
-        flex-direction: column;
-        min-height: 100%;
-      }
-      gr-fixed-panel {
-        /**
-         * This one should be greater that the z-index in gr-diff-view
-         * because gr-main-header contains overlay.
-         */
-        z-index: 10;
-      }
-      gr-main-header,
-      footer {
-        color: var(--primary-text-color);
-      }
-      gr-main-header {
-        background: var(--header-background, var(--header-background-color, #eee));
-        padding: 0 var(--default-horizontal-margin);
-        border-bottom: var(--header-border-bottom);
-        border-image: var(--header-border-image);
-        border-right: 0;
-        border-left: 0;
-        border-top: 0;
-      }
-      gr-main-header.shadow {
-        /* Make it obvious for shadow dom testing */
-        border-bottom: 1px solid pink;
-      }
-      footer {
-        background: var(--footer-background, var(--footer-background-color, #eee));
-        border-top: var(--footer-border-top);
-        display: flex;
-        justify-content: space-between;
-        padding: .5rem var(--default-horizontal-margin);
-        z-index: 100;
-      }
-      main {
-        flex: 1;
-        padding-bottom: 2em;
-        position: relative;
-      }
-      .errorView {
-        align-items: center;
-        display: none;
-        flex-direction: column;
-        justify-content: center;
-        position: absolute;
-        top: 0;
-        right: 0;
-        bottom: 0;
-        left: 0;
-      }
-      .errorView.show {
-        display: flex;
-      }
-      .errorEmoji {
-        font-size: 2.6rem;
-      }
-      .errorText,
-      .errorMoreInfo {
-        margin-top: .75em;
-      }
-      .errorText {
-        font-size: 1.2rem;
-      }
-      .errorMoreInfo {
-        color: var(--deemphasized-text-color);
-      }
-      .feedback {
-        color: var(--error-text-color);
-      }
-    </style>
-    <gr-endpoint-decorator name="banner"></gr-endpoint-decorator>
-    <gr-fixed-panel id="header">
-      <gr-main-header
-          id="mainHeader"
-          search-query="{{params.query}}"
-          class$="[[_computeShadowClass(_isShadowDom)]]"
-          on-mobile-search="_mobileSearchToggle">
-      </gr-main-header>
-    </gr-fixed-panel>
-    <main>
-      <gr-smart-search
-          id="search"
-          search-query="{{params.query}}"
-          hidden="[[!mobileSearch]]">
-      </gr-smart-search>
-      <template is="dom-if" if="[[_showChangeListView]]" restamp="true">
-        <gr-change-list-view
-            params="[[params]]"
-            account="[[_account]]"
-            view-state="{{_viewState.changeListView}}"></gr-change-list-view>
-      </template>
-      <template is="dom-if" if="[[_showDashboardView]]" restamp="true">
-        <gr-dashboard-view
-            account="[[_account]]"
-            params="[[params]]"
-            view-state="{{_viewState.dashboardView}}"></gr-dashboard-view>
-      </template>
-      <template is="dom-if" if="[[_showChangeView]]" restamp="true">
-        <gr-change-view
-            params="[[params]]"
-            view-state="{{_viewState.changeView}}"
-            back-page="[[_lastSearchPage]]"></gr-change-view>
-      </template>
-      <template is="dom-if" if="[[_showEditorView]]" restamp="true">
-        <gr-editor-view
-            params="[[params]]"></gr-editor-view>
-      </template>
-      <template is="dom-if" if="[[_showDiffView]]" restamp="true">
-          <gr-diff-view
-              params="[[params]]"
-              change-view-state="{{_viewState.changeView}}"></gr-diff-view>
-        </template>
-      <template is="dom-if" if="[[_showSettingsView]]" restamp="true">
-        <gr-settings-view
-            params="[[params]]"
-            on-account-detail-update="_handleAccountDetailUpdate">
-        </gr-settings-view>
-      </template>
-      <template is="dom-if" if="[[_showAdminView]]" restamp="true">
-        <gr-admin-view path="[[_path]]"
-            params=[[params]]></gr-admin-view>
-      </template>
-      <template is="dom-if" if="[[_showPluginScreen]]" restamp="true">
-        <gr-endpoint-decorator name="[[_pluginScreenName]]">
-          <gr-endpoint-param name="token" value="[[params.screen]]"></gr-endpoint-param>
-        </gr-endpoint-decorator>
-      </template>
-      <template is="dom-if" if="[[_showCLAView]]" restamp="true">
-        <gr-cla-view></gr-cla-view>
-      </template>
-      <template is="dom-if" if="[[_showDocumentationSearch]]" restamp="true">
-        <gr-documentation-search
-            params="[[params]]">
-        </gr-documentation-search>
-      </template>
-      <div id="errorView" class="errorView">
-        <div class="errorEmoji">[[_lastError.emoji]]</div>
-        <div class="errorText">[[_lastError.text]]</div>
-        <div class="errorMoreInfo">[[_lastError.moreInfo]]</div>
-      </div>
-    </main>
-    <footer r="contentinfo" class$="[[_computeShadowClass(_isShadowDom)]]">
-      <div>
-        Powered by <a href="https://www.gerritcodereview.com/" rel="noopener"
-        target="_blank">Gerrit Code Review</a>
-        ([[_version]])
-        <gr-endpoint-decorator name="footer-left"></gr-endpoint-decorator>
-      </div>
-      <div>
-        <template is="dom-if" if="[[_feedbackUrl]]">
-          <a class="feedback"
-              href$="[[_feedbackUrl]]"
-              rel="noopener"
-              target="_blank">Send feedback</a> |
-        </template>
-        Press &ldquo;?&rdquo; for keyboard shortcuts
-        <gr-endpoint-decorator name="footer-right"></gr-endpoint-decorator>
-      </div>
-    </footer>
-    <gr-overlay id="keyboardShortcuts" with-backdrop>
-      <gr-keyboard-shortcuts-dialog
-          view="[[params.view]]"
-          on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
-    </gr-overlay>
-    <gr-overlay id="registrationOverlay" with-backdrop>
-      <gr-registration-dialog
-          id="registrationDialog"
-          settings-url="[[_settingsUrl]]"
-          on-account-detail-update="_handleAccountDetailUpdate"
-          on-close="_handleRegistrationDialogClose">
-      </gr-registration-dialog>
-    </gr-overlay>
-    <gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator>
-    <gr-error-manager id="errorManager"></gr-error-manager>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-    <gr-reporting id="reporting"></gr-reporting>
-    <gr-router id="router"></gr-router>
-    <gr-plugin-host id="plugins"
-        config="[[_serverConfig]]">
-    </gr-plugin-host>
-    <gr-lib-loader id="libLoader"></gr-lib-loader>
-    <gr-external-style id="externalStyle" name="app-theme"></gr-external-style>
+    <gr-app-element id="app-element"></gr-app-element>
   </template>
   <script src="gr-app.js" crossorigin="anonymous"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index b89d90e..ac8ea1a 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -17,461 +17,7 @@
 (function() {
   'use strict';
 
-  // Eagerly render Polymer components when backgrounded. (Skips
-  // requestAnimationFrame.)
-  // @see https://github.com/Polymer/polymer/issues/3851
-  // @see Issue 4699
-  if (!window.POLYMER2) {
-    Polymer.RenderStatus._makeReady();
-  }
-
   Polymer({
     is: 'gr-app',
-    _legacyUndefinedCheck: true,
-
-    /**
-     * Fired when the URL location changes.
-     *
-     * @event location-change
-     */
-
-    properties: {
-      /**
-       * @type {{ query: string, view: string, screen: string }}
-       */
-      params: Object,
-      keyEventTarget: {
-        type: Object,
-        value() { return document.body; },
-      },
-
-      _account: {
-        type: Object,
-        observer: '_accountChanged',
-      },
-
-      /**
-       * The last time the g key was pressed in milliseconds (or a keydown event
-       * was handled if the key is held down).
-       *
-       * @type {number|null}
-       */
-      _lastGKeyPressTimestamp: {
-        type: Number,
-        value: null,
-      },
-
-      /**
-       * @type {{ plugin: Object }}
-       */
-      _serverConfig: Object,
-      _version: String,
-      _showChangeListView: Boolean,
-      _showDashboardView: Boolean,
-      _showChangeView: Boolean,
-      _showDiffView: Boolean,
-      _showSettingsView: Boolean,
-      _showAdminView: Boolean,
-      _showCLAView: Boolean,
-      _showEditorView: Boolean,
-      _showPluginScreen: Boolean,
-      _showDocumentationSearch: Boolean,
-      /** @type {?} */
-      _viewState: Object,
-      /** @type {?} */
-      _lastError: Object,
-      _lastSearchPage: String,
-      _path: String,
-      _isShadowDom: Boolean,
-      _pluginScreenName: {
-        type: String,
-        computed: '_computePluginScreenName(params)',
-      },
-      _settingsUrl: String,
-      _feedbackUrl: String,
-      // Used to allow searching on mobile
-      mobileSearch: {
-        type: Boolean,
-        value: false,
-      },
-    },
-
-    listeners: {
-      'page-error': '_handlePageError',
-      'title-change': '_handleTitleChange',
-      'location-change': '_handleLocationChange',
-      'rpc-log': '_handleRpcLog',
-    },
-
-    observers: [
-      '_viewChanged(params.view)',
-      '_paramsChanged(params.*)',
-    ],
-
-    behaviors: [
-      Gerrit.BaseUrlBehavior,
-      Gerrit.KeyboardShortcutBehavior,
-    ],
-
-    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',
-      };
-    },
-
-    created() {
-      this._bindKeyboardShortcuts();
-    },
-
-    ready() {
-      this._isShadowDom = Polymer.Settings.useShadow;
-      this.$.router.start();
-
-      this.$.restAPI.getAccount().then(account => {
-        this._account = account;
-      });
-      this.$.restAPI.getConfig().then(config => {
-        this._serverConfig = config;
-
-        if (config && config.gerrit && config.gerrit.report_bug_url) {
-          this._feedbackUrl = config.gerrit.report_bug_url;
-        }
-      });
-      this.$.restAPI.getVersion().then(version => {
-        this._version = version;
-        this._logWelcome();
-      });
-
-      if (window.localStorage.getItem('dark-theme')) {
-        this.$.libLoader.getDarkTheme().then(module => {
-          Polymer.dom(this.root).appendChild(module);
-        });
-      }
-
-      // Note: this is evaluated here to ensure that it only happens after the
-      // router has been initialized. @see Issue 7837
-      this._settingsUrl = Gerrit.Nav.getUrlForSettings();
-
-      this.$.reporting.appStarted(document.visibilityState === 'hidden');
-
-      this._viewState = {
-        changeView: {
-          changeNum: null,
-          patchRange: null,
-          selectedFileIndex: 0,
-          showReplyDialog: false,
-          diffMode: null,
-          numFilesShown: null,
-          scrollTop: 0,
-        },
-        changeListView: {
-          query: null,
-          offset: 0,
-          selectedChangeIndex: 0,
-        },
-        dashboardView: {
-          selectedChangeIndex: 0,
-        },
-      };
-    },
-
-    _bindKeyboardShortcuts() {
-      this.bindShortcut(this.Shortcut.SEND_REPLY,
-          this.DOC_ONLY, 'ctrl+enter', 'meta+enter');
-
-      this.bindShortcut(
-          this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
-      this.bindShortcut(
-          this.Shortcut.GO_TO_USER_DASHBOARD, this.GO_KEY, 'i');
-      this.bindShortcut(
-          this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
-      this.bindShortcut(
-          this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm');
-      this.bindShortcut(
-          this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a');
-      this.bindShortcut(
-          this.Shortcut.GO_TO_WATCHED_CHANGES, this.GO_KEY, 'w');
-
-      this.bindShortcut(
-          this.Shortcut.CURSOR_NEXT_CHANGE, 'j');
-      this.bindShortcut(
-          this.Shortcut.CURSOR_PREV_CHANGE, 'k');
-      this.bindShortcut(
-          this.Shortcut.OPEN_CHANGE, 'o');
-      this.bindShortcut(
-          this.Shortcut.NEXT_PAGE, 'n', ']');
-      this.bindShortcut(
-          this.Shortcut.PREV_PAGE, 'p', '[');
-      this.bindShortcut(
-          this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
-      this.bindShortcut(
-          this.Shortcut.TOGGLE_CHANGE_STAR, 's');
-      this.bindShortcut(
-          this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
-      this.bindShortcut(
-          this.Shortcut.EDIT_TOPIC, 't');
-
-      this.bindShortcut(
-          this.Shortcut.OPEN_REPLY_DIALOG, 'a');
-      this.bindShortcut(
-          this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
-      this.bindShortcut(
-          this.Shortcut.EXPAND_ALL_MESSAGES, 'x');
-      this.bindShortcut(
-          this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
-      this.bindShortcut(
-          this.Shortcut.REFRESH_CHANGE, 'shift+r');
-      this.bindShortcut(
-          this.Shortcut.UP_TO_DASHBOARD, 'u');
-      this.bindShortcut(
-          this.Shortcut.UP_TO_CHANGE, 'u');
-      this.bindShortcut(
-          this.Shortcut.TOGGLE_DIFF_MODE, 'm');
-
-      this.bindShortcut(
-          this.Shortcut.NEXT_LINE, 'j', 'down');
-      this.bindShortcut(
-          this.Shortcut.PREV_LINE, 'k', 'up');
-      this.bindShortcut(
-          this.Shortcut.NEXT_CHUNK, 'n');
-      this.bindShortcut(
-          this.Shortcut.PREV_CHUNK, 'p');
-      this.bindShortcut(
-          this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
-      this.bindShortcut(
-          this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
-      this.bindShortcut(
-          this.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
-      this.bindShortcut(
-          this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
-      this.bindShortcut(
-          this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
-          this.DOC_ONLY, 'shift+e');
-      this.bindShortcut(
-          this.Shortcut.LEFT_PANE, 'shift+left');
-      this.bindShortcut(
-          this.Shortcut.RIGHT_PANE, 'shift+right');
-      this.bindShortcut(
-          this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
-      this.bindShortcut(
-          this.Shortcut.NEW_COMMENT, 'c');
-      this.bindShortcut(
-          this.Shortcut.SAVE_COMMENT,
-          'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
-      this.bindShortcut(
-          this.Shortcut.OPEN_DIFF_PREFS, ',');
-      this.bindShortcut(
-          this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r');
-
-      this.bindShortcut(
-          this.Shortcut.NEXT_FILE, ']');
-      this.bindShortcut(
-          this.Shortcut.PREV_FILE, '[');
-      this.bindShortcut(
-          this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
-      this.bindShortcut(
-          this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
-      this.bindShortcut(
-          this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
-      this.bindShortcut(
-          this.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
-      this.bindShortcut(
-          this.Shortcut.OPEN_FILE, 'o', 'enter');
-      this.bindShortcut(
-          this.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
-      this.bindShortcut(
-          this.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
-      this.bindShortcut(
-          this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
-      this.bindShortcut(
-          this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
-
-      this.bindShortcut(
-          this.Shortcut.OPEN_FIRST_FILE, ']');
-      this.bindShortcut(
-          this.Shortcut.OPEN_LAST_FILE, '[');
-
-      this.bindShortcut(
-          this.Shortcut.SEARCH, '/');
-    },
-
-    _accountChanged(account) {
-      if (!account) { return; }
-
-      // Preferences are cached when a user is logged in; warm them.
-      this.$.restAPI.getPreferences();
-      this.$.restAPI.getDiffPreferences();
-      this.$.restAPI.getEditPreferences();
-      this.$.errorManager.knownAccountId =
-          this._account && this._account._account_id || null;
-    },
-
-    _viewChanged(view) {
-      this.$.errorView.classList.remove('show');
-      this.set('_showChangeListView', view === Gerrit.Nav.View.SEARCH);
-      this.set('_showDashboardView', view === Gerrit.Nav.View.DASHBOARD);
-      this.set('_showChangeView', view === Gerrit.Nav.View.CHANGE);
-      this.set('_showDiffView', view === Gerrit.Nav.View.DIFF);
-      this.set('_showSettingsView', view === Gerrit.Nav.View.SETTINGS);
-      this.set('_showAdminView', view === Gerrit.Nav.View.ADMIN ||
-          view === Gerrit.Nav.View.GROUP || view === Gerrit.Nav.View.REPO);
-      this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS);
-      this.set('_showEditorView', view === Gerrit.Nav.View.EDIT);
-      const isPluginScreen = view === Gerrit.Nav.View.PLUGIN_SCREEN;
-      this.set('_showPluginScreen', false);
-      // Navigation within plugin screens does not restamp gr-endpoint-decorator
-      // because _showPluginScreen value does not change. To force restamp,
-      // change _showPluginScreen value between true and false.
-      if (isPluginScreen) {
-        this.async(() => this.set('_showPluginScreen', true), 1);
-      }
-      this.set('_showDocumentationSearch',
-          view === Gerrit.Nav.View.DOCUMENTATION_SEARCH);
-      if (this.params.justRegistered) {
-        this.$.registrationOverlay.open();
-        this.$.registrationDialog.loadData().then(() => {
-          this.$.registrationOverlay.refit();
-        });
-      }
-      this.$.header.unfloat();
-    },
-
-    _handlePageError(e) {
-      const props = [
-        '_showChangeListView',
-        '_showDashboardView',
-        '_showChangeView',
-        '_showDiffView',
-        '_showSettingsView',
-        '_showAdminView',
-      ];
-      for (const showProp of props) {
-        this.set(showProp, false);
-      }
-
-      this.$.errorView.classList.add('show');
-      const response = e.detail.response;
-      const err = {text: [response.status, response.statusText].join(' ')};
-      if (response.status === 404) {
-        err.emoji = '¯\\_(ツ)_/¯';
-        this._lastError = err;
-      } else {
-        err.emoji = 'o_O';
-        response.text().then(text => {
-          err.moreInfo = text;
-          this._lastError = err;
-        });
-      }
-    },
-
-    _handleLocationChange(e) {
-      const hash = e.detail.hash.substring(1);
-      let pathname = e.detail.pathname;
-      if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) {
-        pathname += '@' + hash;
-      }
-      this.set('_path', pathname);
-    },
-
-    _paramsChanged(paramsRecord) {
-      const params = paramsRecord.base;
-      const viewsToCheck = [Gerrit.Nav.View.SEARCH, Gerrit.Nav.View.DASHBOARD];
-      if (viewsToCheck.includes(params.view)) {
-        this.set('_lastSearchPage', location.pathname);
-      }
-    },
-
-    _handleTitleChange(e) {
-      if (e.detail.title) {
-        document.title = e.detail.title + ' · Gerrit Code Review';
-      } else {
-        document.title = '';
-      }
-    },
-
-    _showKeyboardShortcuts(e) {
-      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-      this.$.keyboardShortcuts.open();
-    },
-
-    _handleKeyboardShortcutDialogClose() {
-      this.$.keyboardShortcuts.close();
-    },
-
-    _handleAccountDetailUpdate(e) {
-      this.$.mainHeader.reload();
-      if (this.params.view === Gerrit.Nav.View.SETTINGS) {
-        this.$$('gr-settings-view').reloadAccountDetail();
-      }
-    },
-
-    _handleRegistrationDialogClose(e) {
-      this.params.justRegistered = false;
-      this.$.registrationOverlay.close();
-    },
-
-    _computeShadowClass(isShadowDom) {
-      return isShadowDom ? 'shadow' : '';
-    },
-
-    _goToUserDashboard() {
-      Gerrit.Nav.navigateToUserDashboard();
-    },
-
-    _goToOpenedChanges() {
-      Gerrit.Nav.navigateToStatusSearch('open');
-    },
-
-    _goToMergedChanges() {
-      Gerrit.Nav.navigateToStatusSearch('merged');
-    },
-
-    _goToAbandonedChanges() {
-      Gerrit.Nav.navigateToStatusSearch('abandoned');
-    },
-
-    _goToWatchedChanges() {
-      // The query is hardcoded, and doesn't respect custom menu entries
-      Gerrit.Nav.navigateToSearchQuery('is:watched is:open');
-    },
-
-    _computePluginScreenName({plugin, screen}) {
-      return Gerrit._getPluginScreenName(plugin, screen);
-    },
-
-    _logWelcome() {
-      console.group('Runtime Info');
-      console.log('Gerrit UI (PolyGerrit)');
-      console.log(`Gerrit Server Version: ${this._version}`);
-      if (window.VERSION_INFO) {
-        console.log(`UI Version Info: ${window.VERSION_INFO}`);
-      }
-      const renderTime = new Date(window.performance.timing.loadEventStart);
-      console.log(`Document loaded at: ${renderTime}`);
-      if (this._feedbackUrl) {
-        console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
-      }
-      console.groupEnd();
-    },
-
-    /**
-     * Intercept RPC log events emitted by REST API interfaces.
-     * Note: the REST API interface cannot use gr-reporting directly because
-     * that would create a cyclic dependency.
-     */
-    _handleRpcLog(e) {
-      this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl,
-          e.detail.elapsed);
-    },
-
-    _mobileSearchToggle(e) {
-      this.mobileSearch = !this.mobileSearch;
-    },
-
   });
 })();
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 71ceab4..9f1b7f8 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -18,11 +18,19 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-app</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../test/common-test-setup.html"/>
-<link rel="import" href="gr-app.html">
+
+<script>
+  const link = document.createElement('link');
+  link.setAttribute('rel', 'import');
+  link.setAttribute('href', 'gr-app.html');
+  document.head.appendChild(link);
+</script>
 
 <script>void(0);</script>
 
@@ -45,12 +53,18 @@
       stub('gr-account-dropdown', {
         _getTopContent: sinon.stub(),
       });
+      stub('gr-router', {
+        start: sandbox.stub(),
+      });
       stub('gr-rest-api-interface', {
         getAccount() { return Promise.resolve({}); },
         getAccountCapabilities() { return Promise.resolve({}); },
         getConfig() {
           return Promise.resolve({
             plugin: {},
+            auth: {
+              auth_type: undefined,
+            },
           });
         },
         getPreferences() { return Promise.resolve({my: []}); },
@@ -68,21 +82,33 @@
       sandbox.restore();
     });
 
+    appElement = () => {
+      return element.$['app-element'];
+    };
+
     test('reporting', () => {
-      assert.isTrue(element.$.reporting.appStarted.calledOnce);
+      assert.isTrue(appElement().$.reporting.appStarted.calledOnce);
+    });
+
+    test('reporting called before router start', () => {
+      const element = appElement();
+      const appStartedStub = element.$.reporting.appStarted;
+      const routerStartStub = element.$.router.start;
+      sinon.assert.callOrder(appStartedStub, routerStartStub);
     });
 
     test('passes config to gr-plugin-host', () => {
-      return element.$.restAPI.getConfig.lastCall.returnValue.then(config => {
-        assert.deepEqual(element.$.plugins.config, config);
+      const config = appElement().$.restAPI.getConfig;
+      return config.lastCall.returnValue.then(config => {
+        assert.deepEqual(appElement().$.plugins.config, config);
       });
     });
 
     test('_paramsChanged sets search page', () => {
-      element._paramsChanged({base: {view: Gerrit.Nav.View.CHANGE}});
-      assert.notOk(element._lastSearchPage);
-      element._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}});
-      assert.ok(element._lastSearchPage);
+      appElement()._paramsChanged({base: {view: Gerrit.Nav.View.CHANGE}});
+      assert.notOk(appElement()._lastSearchPage);
+      appElement()._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}});
+      assert.ok(appElement()._lastSearchPage);
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
index 159f50a..2537a37 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-admin-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-admin-api.html">
@@ -37,7 +39,7 @@
       let plugin;
       Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
-      sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+      Gerrit._loadPlugins([]);
       adminApi = plugin.admin();
     });
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
index 208f1e8..ece8677 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-attribute-helper">
   <script src="gr-attribute-helper.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
index 86238cf..0c4149c 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-attribute-helper</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-attribute-helper.html"/>
 
@@ -30,7 +32,6 @@
   <script>
     Polymer({
       is: 'some-element',
-      _legacyUndefinedCheck: true,
       properties: {
         fooBar: {
           type: Object,
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
index eddb52b..dd532e1 100644
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-change-metadata-api">
   <script src="gr-change-metadata-api.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html
index 252e812..8b9000f 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html
@@ -15,8 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-dom-hooks">
   <script src="gr-dom-hooks.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index c6d3e0f..c0f111a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -59,7 +59,6 @@
   GrDomHook.prototype._createPlaceholder = function(hookName) {
     Polymer({
       is: hookName,
-      _legacyUndefinedCheck: true,
       properties: {
         plugin: Object,
         content: Object,
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
index 3dde458..9e657fa 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
@@ -18,11 +18,14 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-dom-hooks</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-dom-hooks.html"/>
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <script>void(0);</script>
 
@@ -116,7 +119,7 @@
         hookInternal.handleInstanceAttached(el1);
         hookInternal.handleInstanceAttached(el2);
         assert.deepEqual([el1, el2], hook.getAllAttached());
-        hookI.handleInstanceDetached(el1);
+        hookInternal.handleInstanceDetached(el1);
         assert.deepEqual([el2], hook.getAllAttached());
       });
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
index 50b80d5..ab892ac 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-endpoint-decorator">
@@ -23,4 +23,4 @@
     <slot></slot>
   </template>
   <script src="gr-endpoint-decorator.js"></script>
-</dom-module>
+</dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index f3d6eb4..b38107e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -21,7 +21,6 @@
 
   Polymer({
     is: 'gr-endpoint-decorator',
-    _legacyUndefinedCheck: true,
 
     properties: {
       name: String,
@@ -50,9 +49,12 @@
       Gerrit._endpoints.onDetachedEndpoint(this.name, this._endpointCallBack);
     },
 
+    /**
+     * @suppress {checkTypes}
+     */
     _import(url) {
       return new Promise((resolve, reject) => {
-        this.importHref(url, resolve, reject);
+        (this.importHref || Polymer.importHref)(url, resolve, reject);
       });
     },
 
@@ -74,7 +76,6 @@
     },
 
     _getEndpointParams() {
-      // Polymer2: querySelectorAll returns NodeList instead of Array.
       return Array.from(
           Polymer.dom(this).querySelectorAll('gr-endpoint-param'));
     },
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index 2e9b266..b0ad585 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-endpoint-decorator</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-endpoint-decorator.html">
 <link rel="import" href="../gr-endpoint-param/gr-endpoint-param.html">
@@ -56,7 +58,7 @@
       stub('gr-endpoint-decorator', {
         _import: sandbox.stub().returns(Promise.resolve()),
       });
-      Gerrit._resetPlugins();
+      Gerrit._testOnly_resetPlugins();
       container = fixture('basic');
       Gerrit.install(p => plugin = p, '0.1', 'http://some/plugin/url.html');
       // Decoration
@@ -65,7 +67,7 @@
       replacementHook = plugin.registerCustomComponent(
           'second', 'other-module', {replace: true});
       // Mimic all plugins loaded.
-      Gerrit._setPluginsPending([]);
+      Gerrit._loadPlugins([]);
       flush(done);
     });
 
@@ -86,7 +88,7 @@
     test('decoration', () => {
       const element =
           container.querySelector('gr-endpoint-decorator[name="first"]');
-      const modules = Polymer.dom(element.root).children.filter(
+      const modules = Array.from(Polymer.dom(element.root).children).filter(
           element => element.nodeName === 'SOME-MODULE');
       assert.equal(modules.length, 1);
       const [module] = modules;
@@ -103,7 +105,7 @@
     test('replacement', () => {
       const element =
           container.querySelector('gr-endpoint-decorator[name="second"]');
-      const module = Polymer.dom(element.root).children.find(
+      const module = Array.from(Polymer.dom(element.root).children).find(
           element => element.nodeName === 'OTHER-MODULE');
       assert.isOk(module);
       assert.equal(module['someparam'], 'foofoo');
@@ -120,7 +122,7 @@
       flush(() => {
         const element =
             container.querySelector('gr-endpoint-decorator[name="banana"]');
-        const module = Polymer.dom(element.root).children.find(
+        const module = Array.from(Polymer.dom(element.root).children).find(
             element => element.nodeName === 'NOOB-NOOB');
         assert.isOk(module);
         done();
@@ -133,10 +135,10 @@
       flush(() => {
         const element =
             container.querySelector('gr-endpoint-decorator[name="banana"]');
-        const module1 = Polymer.dom(element.root).children.find(
+        const module1 = Array.from(Polymer.dom(element.root).children).find(
             element => element.nodeName === 'MOD-ONE');
         assert.isOk(module1);
-        const module2 = Polymer.dom(element.root).children.find(
+        const module2 = Array.from(Polymer.dom(element.root).children).find(
             element => element.nodeName === 'MOD-TWO');
         assert.isOk(module2);
         done();
@@ -150,14 +152,14 @@
       param['value'] = undefined;
       plugin.registerCustomComponent('banana', 'noob-noob');
       flush(() => {
-        let module = Polymer.dom(element.root).children.find(
+        let module = Array.from(Polymer.dom(element.root).children).find(
             element => element.nodeName === 'NOOB-NOOB');
         // Module waits for param to be defined.
         assert.isNotOk(module);
         const value = {abc: 'def'};
         param.value = value;
         flush(() => {
-          module = Polymer.dom(element.root).children.find(
+          module = Array.from(Polymer.dom(element.root).children).find(
               element => element.nodeName === 'NOOB-NOOB');
           assert.isOk(module);
           assert.strictEqual(module['someParam'], value);
@@ -175,7 +177,7 @@
       param.value = value1;
       plugin.registerCustomComponent('banana', 'noob-noob');
       flush(() => {
-        const module = Polymer.dom(element.root).children.find(
+        const module = Array.from(Polymer.dom(element.root).children).find(
             element => element.nodeName === 'NOOB-NOOB');
         assert.strictEqual(module['someParam'], value1);
         param.value = value2;
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html
index 9d28ac3..6a5b558 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-endpoint-param">
   <script src="gr-endpoint-param.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
index e21fc72..c7a2d9a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
@@ -19,13 +19,29 @@
 
   Polymer({
     is: 'gr-endpoint-param',
-    _legacyUndefinedCheck: true,
+
     properties: {
       name: String,
       value: {
         type: Object,
         notify: true,
+        observer: '_valueChanged',
       },
     },
+
+    _valueChanged(newValue, oldValue) {
+      /* In polymer 2 the following change was made:
+      "Property change notifications (property-changed events) aren't fired when
+      the value changes as a result of a binding from the host"
+      (see https://polymer-library.polymer-project.org/2.0/docs/about_20).
+      To workaround this problem, we fire the event from the observer.
+      In some cases this fire the event twice, but our code is
+      ready for it.
+      */
+      const detail = {
+        value: newValue,
+      };
+      this.dispatchEvent(new CustomEvent('value-changed', {detail}));
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
index d34bdef..15db861 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 
 <dom-module id="gr-event-helper">
   <script src="gr-event-helper.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
index 81b59b6..845c1e1 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
@@ -35,17 +35,35 @@
   };
 
   /**
+   * Alias of onClick
+   *
+   * @see onClick
+   */
+  GrEventHelper.prototype.onTap = function(callback) {
+    return this._listen(this.element, callback);
+  };
+
+  /**
    * Add a callback to element click or touch.
    * The callback may return false to prevent event bubbling.
    *
    * @param {function(Event):boolean} callback
    * @return {function()} Unsubscribe function.
    */
-  GrEventHelper.prototype.onTap = function(callback) {
+  GrEventHelper.prototype.onClick = function(callback) {
     return this._listen(this.element, callback);
   };
 
   /**
+   * Alias of captureClick
+   *
+   * @see captureClick
+   */
+  GrEventHelper.prototype.captureTap = function(callback) {
+    return this._listen(this.element.parentElement, callback, {capture: true});
+  };
+
+  /**
    * Add a callback to element click or touch ahead of normal flow.
    * Callback is installed on parent during capture phase.
    * https://www.w3.org/TR/DOM-Level-3-Events/#event-flow
@@ -54,13 +72,13 @@
    * @param {function(Event):boolean} callback
    * @return {function()} Unsubscribe function.
    */
-  GrEventHelper.prototype.captureTap = function(callback) {
+  GrEventHelper.prototype.captureClick = function(callback) {
     return this._listen(this.element.parentElement, callback, {capture: true});
   };
 
   GrEventHelper.prototype._listen = function(container, callback, opt_options) {
     const capture = opt_options && opt_options.capture;
-    const event = opt_options && opt_options.event || 'tap';
+    const event = opt_options && opt_options.event || 'click';
     const handler = e => {
       if (e.path.indexOf(this.element) !== -1) {
         let mayContinue = true;
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
index 47274f6..bd76bd4 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-event-helper</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-event-helper.html"/>
 
@@ -30,13 +32,17 @@
   <script>
     Polymer({
       is: 'some-element',
-      _legacyUndefinedCheck: true,
+
       properties: {
         fooBar: {
           type: Object,
           notify: true,
         },
       },
+
+      behaviors: [
+        Gerrit.FireBehavior,
+      ],
     });
   </script>
 </dom-element>
@@ -67,14 +73,23 @@
       instance.onTap(() => {
         done();
       });
-      element.fire('tap');
+      MockInteractions.tap(element);
     });
 
     test('onTap() cancel', () => {
       const tapStub = sandbox.stub();
-      element.parentElement.addEventListener('tap', tapStub);
+      Polymer.Gestures.addListener(element.parentElement, 'tap', tapStub);
       instance.onTap(() => false);
-      element.fire('tap');
+      MockInteractions.tap(element);
+      flushAsynchronousOperations();
+      assert.isFalse(tapStub.called);
+    });
+
+    test('onClick() cancel', () => {
+      const tapStub = sandbox.stub();
+      element.parentElement.addEventListener('click', tapStub);
+      instance.onTap(() => false);
+      MockInteractions.tap(element);
       flushAsynchronousOperations();
       assert.isFalse(tapStub.called);
     });
@@ -83,14 +98,30 @@
       instance.captureTap(() => {
         done();
       });
-      element.fire('tap');
+      MockInteractions.tap(element);
+    });
+
+    test('captureClick()', done => {
+      instance.captureClick(() => {
+        done();
+      });
+      MockInteractions.tap(element);
     });
 
     test('captureTap() cancels tap()', () => {
       const tapStub = sandbox.stub();
-      element.addEventListener('tap', tapStub);
+      Polymer.Gestures.addListener(element.parentElement, 'tap', tapStub);
       instance.captureTap(() => false);
-      element.fire('tap');
+      MockInteractions.tap(element);
+      flushAsynchronousOperations();
+      assert.isFalse(tapStub.called);
+    });
+
+    test('captureClick() cancels click()', () => {
+      const tapStub = sandbox.stub();
+      element.addEventListener('click', tapStub);
+      instance.captureTap(() => false);
+      MockInteractions.tap(element);
       flushAsynchronousOperations();
       assert.isFalse(tapStub.called);
     });
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html
index a83b2ab..6a55349 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-external-style">
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index 7924e27..e90ff30 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-external-style',
-    _legacyUndefinedCheck: true,
 
     properties: {
       name: String,
@@ -33,20 +32,31 @@
       },
     },
 
+    /**
+     * @suppress {checkTypes}
+     */
     _import(url) {
       if (this._urlsImported.includes(url)) { return Promise.resolve(); }
       this._urlsImported.push(url);
       return new Promise((resolve, reject) => {
-        this.importHref(url, resolve, reject);
+        (this.importHref || Polymer.importHref)(url, resolve, reject);
       });
     },
 
     _applyStyle(name) {
       if (this._stylesApplied.includes(name)) { return; }
       this._stylesApplied.push(name);
+      // Hybrid custom-style syntax:
+      // https://polymer-library.polymer-project.org/2.0/docs/devguide/style-shadow-dom
       const s = document.createElement('style', 'custom-style');
       s.setAttribute('include', name);
-      Polymer.dom(this.root).appendChild(s);
+      const cs = document.createElement('custom-style');
+      cs.appendChild(s);
+      // When using Shadow DOM <custom-style> must be added to the <body>.
+      // Within <gr-external-style> itself the styles would have no effect.
+      const topEl = document.getElementsByTagName('body')[0];
+      topEl.insertBefore(cs, topEl.firstChild);
+      Polymer.updateStyles();
     },
 
     _importAndApply() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
index ec2888d..9566067 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-external-style</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-external-style.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
index 8e106cc..f277899 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index 65f2207..6c66fbf 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-plugin-host',
-    _legacyUndefinedCheck: true,
 
     properties: {
       config: {
@@ -28,33 +27,28 @@
       },
     },
 
-    behaviors: [
-      Gerrit.BaseUrlBehavior,
-    ],
-
     _configChanged(config) {
       const plugins = config.plugin;
-      const htmlPlugins = (plugins.html_resource_paths || [])
-          .map(p => this._urlFor(p))
-          .filter(p => !Gerrit._isPluginPreloaded(p));
+      const htmlPlugins = (plugins.html_resource_paths || []);
       const jsPlugins =
-          this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins)
-              .map(p => this._urlFor(p))
-              .filter(p => !Gerrit._isPluginPreloaded(p));
+          this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins);
       const shouldLoadTheme = config.default_theme &&
             !Gerrit._isPluginPreloaded('preloaded:gerrit-theme');
-      const defaultTheme =
-            shouldLoadTheme ? this._urlFor(config.default_theme) : null;
+      const themeToLoad =
+            shouldLoadTheme ? [config.default_theme] : [];
+
+      // Theme should be loaded first if has one to have better UX
       const pluginsPending =
-          [].concat(jsPlugins, htmlPlugins, defaultTheme || []);
-      Gerrit._setPluginsPending(pluginsPending);
-      if (defaultTheme) {
-        // Make theme first to be first to load.
-        // Load sync to work around rare theme loading race condition.
-        this._importHtmlPlugins([defaultTheme], true);
+          themeToLoad.concat(jsPlugins, htmlPlugins);
+
+      const pluginOpts = {};
+
+      if (shouldLoadTheme) {
+        // Theme needs to be loaded synchronous.
+        pluginOpts[config.default_theme] = {sync: true};
       }
-      this._loadJsPlugins(jsPlugins);
-      this._importHtmlPlugins(htmlPlugins);
+
+      Gerrit._loadPlugins(pluginsPending, pluginOpts);
     },
 
     /**
@@ -67,53 +61,5 @@
         return !htmlPlugins.includes(counterpart);
       });
     },
-
-    /**
-     * @suppress {checkTypes}
-     * States that it expects no more than 3 parameters, but that's not true.
-     * @todo (beckysiegel) check Polymer annotations and submit change.
-     * @param {Array} plugins
-     * @param {boolean=} opt_sync
-     */
-    _importHtmlPlugins(plugins, opt_sync) {
-      const async = !opt_sync;
-      for (const url of plugins) {
-        // onload (second param) needs to be a function. When null or undefined
-        // were passed, plugins were not loaded correctly.
-        this.importHref(
-            this._urlFor(url), () => {},
-            Gerrit._pluginInstallError.bind(null, `${url} import error`),
-            async);
-      }
-    },
-
-    _loadJsPlugins(plugins) {
-      for (const url of plugins) {
-        this._createScriptTag(this._urlFor(url));
-      }
-    },
-
-    _createScriptTag(url) {
-      const el = document.createElement('script');
-      el.defer = true;
-      el.src = url;
-      el.onerror = Gerrit._pluginInstallError.bind(null, `${url} load error`);
-      return document.body.appendChild(el);
-    },
-
-    _urlFor(pathOrUrl) {
-      if (!pathOrUrl) {
-        return pathOrUrl;
-      }
-      if (pathOrUrl.startsWith('preloaded:') ||
-          pathOrUrl.startsWith('http')) {
-        // Plugins are loaded from another domain or preloaded.
-        return pathOrUrl;
-      }
-      if (!pathOrUrl.startsWith('/')) {
-        pathOrUrl = '/' + pathOrUrl;
-      }
-      return window.location.origin + this.getBaseUrl() + pathOrUrl;
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index 9901d9f..3a8e4d8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-host</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-plugin-host.html">
 
@@ -36,195 +38,57 @@
   suite('gr-plugin-host tests', () => {
     let element;
     let sandbox;
-    let url;
 
     setup(() => {
       element = fixture('basic');
       sandbox = sinon.sandbox.create();
       sandbox.stub(document.body, 'appendChild');
       sandbox.stub(element, 'importHref');
-      url = window.location.origin;
     });
 
     teardown(() => {
       sandbox.restore();
     });
 
-    test('counts plugins', () => {
-      sandbox.stub(Gerrit, '_setPluginsCount');
+    test('load plugins should be called', () => {
+      sandbox.stub(Gerrit, '_loadPlugins');
       element.config = {
         plugin: {
           html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
           js_resource_paths: ['plugins/42'],
         },
       };
-      assert.isTrue(Gerrit._setPluginsCount.calledWith(3));
+      assert.isTrue(Gerrit._loadPlugins.calledOnce);
+      assert.isTrue(Gerrit._loadPlugins.calledWith([
+        'plugins/42', 'plugins/foo/bar', 'plugins/baz',
+      ], {}));
     });
 
-    test('imports relative html plugins from config', () => {
-      sandbox.stub(Gerrit, '_pluginInstallError');
+    test('theme plugins should be loaded if enabled', () => {
+      sandbox.stub(Gerrit, '_loadPlugins');
       element.config = {
-        plugin: {html_resource_paths: ['foo/bar', 'baz']},
-      };
-      assert.equal(element.importHref.firstCall.args[0], url + '/foo/bar');
-      assert.isTrue(element.importHref.firstCall.args[3]);
-
-      assert.equal(element.importHref.secondCall.args[0], url + '/baz');
-      assert.isTrue(element.importHref.secondCall.args[3]);
-
-      assert.equal(Gerrit._pluginInstallError.callCount, 0);
-      element.importHref.firstCall.args[2]();
-      assert.equal(Gerrit._pluginInstallError.callCount, 1);
-      element.importHref.secondCall.args[2]();
-      assert.equal(Gerrit._pluginInstallError.callCount, 2);
-    });
-
-    test('imports relative html plugins from config with a base url', () => {
-      sandbox.stub(Gerrit, '_pluginInstallError');
-      sandbox.stub(element, 'getBaseUrl').returns('/the-base');
-      element.config = {
-        plugin: {html_resource_paths: ['foo/bar', 'baz']}};
-      assert.equal(element.importHref.firstCall.args[0],
-          url + '/the-base/foo/bar');
-      assert.isTrue(element.importHref.firstCall.args[3]);
-
-      assert.equal(element.importHref.secondCall.args[0],
-          url + '/the-base/baz');
-      assert.isTrue(element.importHref.secondCall.args[3]);
-      assert.equal(Gerrit._pluginInstallError.callCount, 0);
-      element.importHref.firstCall.args[2]();
-      assert.equal(Gerrit._pluginInstallError.callCount, 1);
-      element.importHref.secondCall.args[2]();
-      assert.equal(Gerrit._pluginInstallError.callCount, 2);
-    });
-
-    test('importHref is not called with null callback functions', () => {
-      const plugins = ['path/to/plugin'];
-      element._importHtmlPlugins(plugins);
-      assert.isTrue(element.importHref.calledOnce);
-      assert.isFunction(element.importHref.lastCall.args[1]);
-      assert.isFunction(element.importHref.lastCall.args[2]);
-    });
-
-    test('imports absolute html plugins from config', () => {
-      sandbox.stub(Gerrit, '_pluginInstallError');
-      element.config = {
+        default_theme: 'gerrit-theme.html',
         plugin: {
-          html_resource_paths: [
-            'http://example.com/foo/bar',
-            'https://example.com/baz',
-          ],
+          html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
+          js_resource_paths: ['plugins/42'],
         },
       };
-      assert.equal(element.importHref.firstCall.args[0],
-          'http://example.com/foo/bar');
-      assert.isTrue(element.importHref.firstCall.args[3]);
-
-      assert.equal(element.importHref.secondCall.args[0],
-          'https://example.com/baz');
-      assert.isTrue(element.importHref.secondCall.args[3]);
-      assert.equal(Gerrit._pluginInstallError.callCount, 0);
-      element.importHref.firstCall.args[2]();
-      assert.equal(Gerrit._pluginInstallError.callCount, 1);
-      element.importHref.secondCall.args[2]();
-      assert.equal(Gerrit._pluginInstallError.callCount, 2);
+      assert.isTrue(Gerrit._loadPlugins.calledOnce);
+      assert.isTrue(Gerrit._loadPlugins.calledWith([
+        'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
+      ], {'gerrit-theme.html': {sync: true}}));
     });
 
-    test('adds js plugins from config to the body', () => {
-      element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
-      assert.isTrue(document.body.appendChild.calledTwice);
-    });
-
-    test('imports relative js plugins from config', () => {
-      sandbox.stub(element, '_createScriptTag');
-      element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
-      assert.isTrue(element._createScriptTag.calledWith(url + '/foo/bar'));
-      assert.isTrue(element._createScriptTag.calledWith(url + '/baz'));
-    });
-
-    test('imports relative html plugins from config with a base url', () => {
-      sandbox.stub(element, '_createScriptTag');
-      sandbox.stub(element, 'getBaseUrl').returns('/the-base');
-      element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
-      assert.isTrue(element._createScriptTag.calledWith(
-          url + '/the-base/foo/bar'));
-      assert.isTrue(element._createScriptTag.calledWith(
-          url + '/the-base/baz'));
-    });
-
-    test('imports absolute html plugins from config', () => {
-      sandbox.stub(element, '_createScriptTag');
-      element.config = {
-        plugin: {
-          js_resource_paths: [
-            'http://example.com/foo/bar',
-            'https://example.com/baz',
-          ],
-        },
-      };
-      assert.isTrue(element._createScriptTag.calledWith(
-          'http://example.com/foo/bar'));
-      assert.isTrue(element._createScriptTag.calledWith(
-          'https://example.com/baz'));
-    });
-
-    test('default theme is loaded with html plugins', () => {
-      sandbox.stub(Gerrit, '_pluginInstallError');
-      element.config = {
-        default_theme: '/oof',
-        plugin: {
-          html_resource_paths: ['some'],
-        },
-      };
-      assert.equal(element.importHref.firstCall.args[0], url + '/oof');
-      assert.isFalse(element.importHref.firstCall.args[3]);
-
-      assert.equal(element.importHref.secondCall.args[0], url + '/some');
-      assert.isTrue(element.importHref.secondCall.args[3]);
-      assert.equal(Gerrit._pluginInstallError.callCount, 0);
-      element.importHref.firstCall.args[2]();
-      assert.equal(Gerrit._pluginInstallError.callCount, 1);
-      element.importHref.secondCall.args[2]();
-      assert.equal(Gerrit._pluginInstallError.callCount, 2);
-    });
-
-    test('default theme is loaded with html plugins', () => {
-      sandbox.stub(Gerrit, '_setPluginsPending');
-      element.config = {
-        default_theme: '/oof',
-        plugin: {},
-      };
-      assert.isTrue(Gerrit._setPluginsPending.calledWith([url + '/oof']));
-    });
-
-    test('skips default theme loading if preloaded', () => {
+    test('skip theme if preloaded', () => {
       sandbox.stub(Gerrit, '_isPluginPreloaded')
           .withArgs('preloaded:gerrit-theme').returns(true);
-      sandbox.stub(Gerrit, '_setPluginsPending');
+      sandbox.stub(Gerrit, '_loadPlugins');
       element.config = {
         default_theme: '/oof',
         plugin: {},
       };
-      assert.isFalse(element.importHref.calledWith(url + '/oof'));
-    });
-
-    test('skips preloaded plugins', () => {
-      sandbox.stub(Gerrit, '_isPluginPreloaded')
-          .withArgs(url + '/plugins/foo/bar').returns(true)
-          .withArgs(url + '/plugins/42').returns(true);
-      sandbox.stub(Gerrit, '_setPluginsCount');
-      sandbox.stub(Gerrit, '_setPluginsPending');
-      sandbox.stub(element, '_createScriptTag');
-      element.config = {
-        plugin: {
-          html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
-          js_resource_paths: ['plugins/42'],
-        },
-      };
-      assert.isTrue(
-          Gerrit._setPluginsPending.calledWith([url + '/plugins/baz']));
-      assert.equal(element._createScriptTag.callCount, 0);
-      assert.isTrue(element.importHref.calledWith(url + '/plugins/baz'));
+      assert.isTrue(Gerrit._loadPlugins.calledOnce);
+      assert.isTrue(Gerrit._loadPlugins.calledWith([], {}));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
index ce0bf1b..402d988 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 
 <dom-module id="gr-plugin-popup">
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
index 3ef93e4..2e7a2b7 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -16,15 +16,18 @@
  */
 (function(window) {
   'use strict';
+
   Polymer({
     is: 'gr-plugin-popup',
-    _legacyUndefinedCheck: true,
+
     get opened() {
       return this.$.overlay.opened;
     },
+
     open() {
       return this.$.overlay.open();
     },
+
     close() {
       this.$.overlay.close();
     },
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
index 91386b9..1f1e81e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-popup</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-plugin-popup.html"/>
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
index 2fdf28c..26ece30 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-plugin-popup.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
index 983c795..53370e2 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-popup-interface</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-popup-interface.html"/>
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
index c9486ae..593c1e0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../admin/gr-repo-command/gr-repo-command.html">
 
 <dom-module id="gr-plugin-repo-command">
@@ -25,7 +25,6 @@
   <script>
     Polymer({
       is: 'gr-plugin-repo-command',
-      _legacyUndefinedCheck: true,
       properties: {
         title: String,
         repoName: String,
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
index 34c9797..8e6c053 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-plugin-repo-command.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
index bb9ae87..0b32f8a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="gr-repo-api.html">
@@ -44,7 +46,7 @@
       let plugin;
       Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
-      sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+      Gerrit._loadPlugins([]);
       repoApi = plugin.project();
     });
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
index 7c916dc..20cc71b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../settings/gr-settings-view/gr-settings-item.html">
 <link rel="import" href="../../settings/gr-settings-view/gr-settings-menu-item.html">
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
index cabd26b..cbc2de6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="gr-settings-api.html">
@@ -46,7 +48,7 @@
       let plugin;
       Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
-      sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+      Gerrit._loadPlugins([]);
       settingsApi = plugin.settings();
     });
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html
new file mode 100644
index 0000000..74b87c8
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html
@@ -0,0 +1,18 @@
+<!--
+@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.
+-->
+
+<script src="gr-styles-api.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
new file mode 100644
index 0000000..d5647ea
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
@@ -0,0 +1,83 @@
+/**
+ * @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.
+ */
+(function(window) {
+  'use strict';
+
+  // Prevent redefinition.
+  if (window.GrStylesApi) { return; }
+
+  let styleObjectCount = 0;
+
+  function GrStyleObject(rulesStr) {
+    this._rulesStr = rulesStr;
+    this._className = `__pg_js_api_class_${styleObjectCount}`;
+    styleObjectCount++;
+  }
+
+  /**
+   * Creates a new unique CSS class and injects it in a root node of the element
+   * if it hasn't been added yet. A root node is an document or is the
+   * associated shadowRoot. This class can be added to any element with the same
+   * root node.
+   *
+   * @param {HTMLElement} element The element to get class name for.
+   * @return {string} Appropriate class name for the element is returned
+   */
+  GrStyleObject.prototype.getClassName = function(element) {
+    let rootNode = Polymer.Settings.useShadow
+      ? element.getRootNode() : document.body;
+    if (rootNode === document) {
+      rootNode = document.head;
+    }
+    if (!rootNode.__pg_js_api_style_tags) {
+      rootNode.__pg_js_api_style_tags = {};
+    }
+    if (!rootNode.__pg_js_api_style_tags[this._className]) {
+      const styleTag = document.createElement('style');
+      styleTag.innerHTML = `.${this._className} { ${this._rulesStr} }`;
+      rootNode.appendChild(styleTag);
+      rootNode.__pg_js_api_style_tags[this._className] = true;
+    }
+    return this._className;
+  };
+
+  /**
+   * Apply shared style to the element.
+   *
+   * @param {HTMLElement} element The element to apply style for
+   */
+  GrStyleObject.prototype.apply = function(element) {
+    element.classList.add(this.getClassName(element));
+  };
+
+
+  function GrStylesApi() {
+  }
+
+  /**
+   * Creates a new GrStyleObject with specified style properties.
+   *
+   * @param {string} String with style properties.
+   * @return {GrStyleObject}
+   */
+  GrStylesApi.prototype.css = function(ruleStr) {
+    return new GrStyleObject(ruleStr);
+  };
+
+
+  window.GrStylesApi = GrStylesApi;
+})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
new file mode 100644
index 0000000..46bda6d
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-admin-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
+<link rel="import" href="gr-styles-api.html">
+
+<script>void(0);</script>
+
+<dom-module id="gr-style-test-element">
+  <template>
+    <div id="wrapper"></div>
+  </template>
+  <script>Polymer({is: 'gr-style-test-element'});</script>
+</dom-module>
+
+<script>
+  suite('gr-styles-api tests', () => {
+    let sandbox;
+    let stylesApi;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      let plugin;
+      Gerrit.install(p => { plugin = p; }, '0.1',
+          'http://test.com/plugins/testplugin/static/test.js');
+      Gerrit._loadPlugins([]);
+      stylesApi = plugin.styles();
+    });
+
+    teardown(() => {
+      stylesApi = null;
+      sandbox.restore();
+    });
+
+    test('exists', () => {
+      assert.isOk(stylesApi);
+    });
+
+    test('css', () => {
+      const styleObject = stylesApi.css('background: red');
+      assert.isDefined(styleObject);
+    });
+  });
+
+  suite('GrStyleObject tests', () => {
+    let sandbox;
+    let stylesApi;
+    let displayInlineStyle;
+    let displayNoneStyle;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      let plugin;
+      Gerrit.install(p => { plugin = p; }, '0.1',
+          'http://test.com/plugins/testplugin/static/test.js');
+      Gerrit._loadPlugins([]);
+      stylesApi = plugin.styles();
+      displayInlineStyle = stylesApi.css('display: inline');
+      displayNoneStyle = stylesApi.css('display: none');
+    });
+
+    teardown(() => {
+      displayInlineStyle = null;
+      displayNoneStyle = null;
+      stylesApi = null;
+      sandbox.restore();
+    });
+
+    function createNestedElements(parentElement) {
+      /* parentElement
+      *  |--- element1
+      *  |--- element2
+      *       |--- element3
+      **/
+      const element1 = document.createElement('div');
+      const element2 = document.createElement('div');
+      const element3 = document.createElement('div');
+      Polymer.dom(parentElement).appendChild(element1);
+      Polymer.dom(parentElement).appendChild(element2);
+      Polymer.dom(element2).appendChild(element3);
+
+      return [element1, element2, element3];
+    }
+
+
+    test('getClassName  - body level elements', () => {
+      const bodyLevelElements = createNestedElements(document.body);
+
+      testGetClassName(bodyLevelElements);
+    });
+
+    test('getClassName  - elements inside polymer element', () => {
+      const polymerElement = document.createElement('gr-style-test-element');
+      Polymer.dom(document.body).appendChild(polymerElement);
+      const contentElements = createNestedElements(polymerElement.$.wrapper);
+
+      testGetClassName(contentElements);
+    });
+
+    function testGetClassName(elements) {
+      assertAllElementsHaveDefaultStyle(elements);
+
+      const className1 = displayInlineStyle.getClassName(elements[0]);
+      const className2 = displayNoneStyle.getClassName(elements[1]);
+      const className3 = displayInlineStyle.getClassName(elements[2]);
+
+      assert.notEqual(className2, className1);
+      assert.equal(className3, className1);
+
+      assertAllElementsHaveDefaultStyle(elements);
+
+      elements[0].classList.add(className1);
+      elements[1].classList.add(className2);
+      elements[2].classList.add(className1);
+
+      assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
+    }
+
+    test('apply - body level elements', () => {
+      const bodyLevelElements = createNestedElements(document.body);
+
+      testApply(bodyLevelElements);
+    });
+
+    test('apply - elements inside polymer element', () => {
+      const polymerElement = document.createElement('gr-style-test-element');
+      Polymer.dom(document.body).appendChild(polymerElement);
+      const contentElements = createNestedElements(polymerElement.$.wrapper);
+
+      testApply(contentElements);
+    });
+
+    function testApply(elements) {
+      assertAllElementsHaveDefaultStyle(elements);
+      displayInlineStyle.apply(elements[0]);
+      displayNoneStyle.apply(elements[1]);
+      displayInlineStyle.apply(elements[2]);
+      assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
+    }
+
+
+    function assertAllElementsHaveDefaultStyle(elements) {
+      for (const element of elements) {
+        assert.equal(getComputedStyle(element).getPropertyValue('display'),
+            'block');
+      }
+    }
+
+    function assertDisplayPropertyValues(elements, expectedDisplayValues) {
+      for (const key in elements) {
+        if (elements.hasOwnProperty(key)) {
+          assert.equal(
+              getComputedStyle(elements[key]).getPropertyValue('display'),
+              expectedDisplayValues[key]);
+        }
+      }
+    }
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
index 496d0e7..f0eacd2 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-custom-plugin-header">
   <template>
@@ -26,7 +26,7 @@
         vertical-align: middle;
       }
       .title {
-        margin-left: .25em;
+        margin-left: var(--spacing-xs);
       }
     </style>
     <span>
@@ -37,7 +37,6 @@
   <script>
     Polymer({
       is: 'gr-custom-plugin-header',
-      _legacyUndefinedCheck: true,
       properties: {
         logoUrl: String,
         title: String,
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
index b84f5b9..d6e67fe 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-custom-plugin-header.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
index 8d23ea2..6332b91 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-theme-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="gr-theme-api.html">
@@ -65,7 +67,7 @@
         stub('gr-custom-plugin-header', {
           ready() { customHeader = this; },
         });
-        Gerrit._setPluginsPending([]);
+        Gerrit._loadPlugins([]);
       });
 
       test('sets logo and title', done => {
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
index f534771..662c6f1 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
@@ -15,8 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -31,10 +33,10 @@
       gr-avatar {
         height: 120px;
         width: 120px;
-        margin-right: .15em;
+        margin-right: var(--spacing-xs);
         vertical-align: -.25em;
       }
-      .hide {
+      div section.hide {
         display: none;
       }
     </style>
@@ -79,12 +81,17 @@
         <span
             hidden$="[[!usernameMutable]]"
             class="value">
-          <input
-              is="iron-input"
-              id="usernameInput"
+          <iron-input
               disabled="[[_saving]]"
               on-keydown="_handleKeydown"
               bind-value="{{_username}}">
+            <input
+                is="iron-input"
+                id="usernameInput"
+                disabled="[[_saving]]"
+                on-keydown="_handleKeydown"
+                bind-value="{{_username}}">
+          </iron-input>
         </span>
       </section>
       <section id="nameSection">
@@ -95,24 +102,34 @@
         <span
             hidden$="[[!nameMutable]]"
             class="value">
-          <input
-              is="iron-input"
-              id="nameInput"
+          <iron-input
               disabled="[[_saving]]"
               on-keydown="_handleKeydown"
               bind-value="{{_account.name}}">
+            <input
+                is="iron-input"
+                id="nameInput"
+                disabled="[[_saving]]"
+                on-keydown="_handleKeydown"
+                bind-value="{{_account.name}}">
+          </iron-input>
         </span>
       </section>
       <section>
         <span class="title">Status (e.g. "Vacation")</span>
         <span class="value">
-          <input
-              is="iron-input"
-              id="statusInput"
+          <iron-input
               disabled="[[_saving]]"
               on-keydown="_handleKeydown"
               bind-value="{{_account.status}}">
-          </span>
+            <input
+                is="iron-input"
+                id="statusInput"
+                disabled="[[_saving]]"
+                on-keydown="_handleKeydown"
+                bind-value="{{_account.status}}">
+          </iron-input>
+        </span>
       </section>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 51a22fc..3ba3a80 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-account-info',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when account details are changed.
@@ -69,6 +68,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     observers: [
       '_nameChanged(_account.name)',
       '_statusChanged(_account.status)',
@@ -145,6 +148,14 @@
     },
 
     _computeUsernameMutable(config, username) {
+      // Polymer 2: check for undefined
+      if ([
+        config,
+        username,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       // Username may not be changed once it is set.
       return config.auth.editable_account_fields.includes('USER_NAME') &&
           !username;
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index 75b9910..a35c1f0 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-info</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-account-info.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
index 72ea503..852161c 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
@@ -16,7 +16,7 @@
 -->
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
index fe36a86..41595a98 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-agreements-list',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _agreements: Array,
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
index 56122a9..14cf97c 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-agreements-list.html">
 
@@ -45,7 +47,7 @@
       }];
 
       stub('gr-rest-api-interface', {
-        getAccountGroups() { return Promise.resolve(agreements); },
+        getAccountAgreements() { return Promise.resolve(agreements); },
       });
 
       element = fixture('basic');
@@ -56,10 +58,10 @@
     test('renders', () => {
       const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
 
-      assert.equal(rows.length, 3);
+      assert.equal(rows.length, 1);
 
-      const nameCells = rows.map(row =>
-        row.querySelectorAll('td')[0].textContent
+      const nameCells = Array.from(rows).map(row =>
+        row.querySelectorAll('td')[0].textContent.trim()
       );
 
       assert.equal(nameCells[0], 'Agreements 1');
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
index 4f69513..88a53ee 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -15,8 +15,7 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -56,11 +55,11 @@
           <tr>
             <td>Number</td>
             <td class="checkboxContainer"
-                on-tap="_handleCheckboxContainerTap">
+                on-click="_handleCheckboxContainerClick">
               <input
                   type="checkbox"
                   name="number"
-                  on-tap="_handleNumberCheckboxTap"
+                  on-click="_handleNumberCheckboxClick"
                   checked$="[[showNumber]]">
             </td>
           </tr>
@@ -68,11 +67,11 @@
             <tr>
               <td>[[item]]</td>
               <td class="checkboxContainer"
-                  on-tap="_handleCheckboxContainerTap">
+                  on-click="_handleCheckboxContainerClick">
                 <input
                     type="checkbox"
                     name="[[item]]"
-                    on-tap="_handleTargetTap"
+                    on-click="_handleTargetClick"
                     checked$="[[!isColumnHidden(item, displayedColumns)]]">
               </td>
             </tr>
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 d660ee5..0fef3d62 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-change-table-editor',
-    _legacyUndefinedCheck: true,
 
     properties: {
       displayedColumns: {
@@ -43,7 +42,6 @@
      * @return {!Array<string>}
      */
     _getDisplayedColumns() {
-      // Polymer2: querySelectorAll returns NodeList instead of Array.
       return Array.from(Polymer.dom(this.root)
           .querySelectorAll('.checkboxContainer input:not([name=number])'))
           .filter(checkbox => checkbox.checked)
@@ -51,28 +49,28 @@
     },
 
     /**
-     * Handle a tap on a checkbox container and relay the tap to the checkbox it
+     * Handle a click on a checkbox container and relay the click to the checkbox it
      * contains.
      */
-    _handleCheckboxContainerTap(e) {
+    _handleCheckboxContainerClick(e) {
       const checkbox = Polymer.dom(e.target).querySelector('input');
       if (!checkbox) { return; }
       checkbox.click();
     },
 
     /**
-     * Handle a tap on the number checkbox and update the showNumber property
+     * Handle a click on the number checkbox and update the showNumber property
      * accordingly.
      */
-    _handleNumberCheckboxTap(e) {
+    _handleNumberCheckboxClick(e) {
       this.showNumber = Polymer.dom(e).rootTarget.checked;
     },
 
     /**
-     * Handle a tap on a displayed column checkboxes (excluding number) and
+     * Handle a click on a displayed column checkboxes (excluding number) and
      * update the displayedColumns property accordingly.
      */
-    _handleTargetTap(e) {
+    _handleTargetClick(e) {
       this.set('displayedColumns', this._getDisplayedColumns());
     },
   });
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index 32fab9d..29a7081 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-table-editor.html">
 
@@ -117,41 +119,41 @@
           columns.filter(c => c !== 'Assignee'));
     });
 
-    test('_handleCheckboxContainerTap relayes taps to checkboxes', () => {
-      sandbox.stub(element, '_handleNumberCheckboxTap');
-      sandbox.stub(element, '_handleTargetTap');
+    test('_handleCheckboxContainerClick relayes taps to checkboxes', () => {
+      sandbox.stub(element, '_handleNumberCheckboxClick');
+      sandbox.stub(element, '_handleTargetClick');
 
       MockInteractions.tap(
           element.$$('table tr:first-of-type .checkboxContainer'));
-      assert.isTrue(element._handleNumberCheckboxTap.calledOnce);
-      assert.isFalse(element._handleTargetTap.called);
+      assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
+      assert.isFalse(element._handleTargetClick.called);
 
       MockInteractions.tap(
           element.$$('table tr:last-of-type .checkboxContainer'));
-      assert.isTrue(element._handleNumberCheckboxTap.calledOnce);
-      assert.isTrue(element._handleTargetTap.calledOnce);
+      assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
+      assert.isTrue(element._handleTargetClick.calledOnce);
     });
 
-    test('_handleNumberCheckboxTap', () => {
-      sandbox.spy(element, '_handleNumberCheckboxTap');
+    test('_handleNumberCheckboxClick', () => {
+      sandbox.spy(element, '_handleNumberCheckboxClick');
 
       MockInteractions
           .tap(element.$$('.checkboxContainer input[name=number]'));
-      assert.isTrue(element._handleNumberCheckboxTap.calledOnce);
+      assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
       assert.isTrue(element.showNumber);
 
       MockInteractions
           .tap(element.$$('.checkboxContainer input[name=number]'));
-      assert.isTrue(element._handleNumberCheckboxTap.calledTwice);
+      assert.isTrue(element._handleNumberCheckboxClick.calledTwice);
       assert.isFalse(element.showNumber);
     });
 
-    test('_handleTargetTap', () => {
-      sandbox.spy(element, '_handleTargetTap');
+    test('_handleTargetClick', () => {
+      sandbox.spy(element, '_handleTargetClick');
       assert.include(element.displayedColumns, 'Assignee');
       MockInteractions
           .tap(element.$$('.checkboxContainer input[name=Assignee]'));
-      assert.isTrue(element._handleTargetTap.calledOnce);
+      assert.isTrue(element._handleTargetClick.calledOnce);
       assert.notInclude(element.displayedColumns, 'Assignee');
     });
   });
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
index 4a939e6..c29153e 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
@@ -16,8 +16,9 @@
 -->
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -27,17 +28,17 @@
   <template>
     <style include="shared-styles">
       h1 {
-        margin-bottom: .6em;
+        margin-bottom: var(--spacing-m);
       }
       h3 {
-        margin-bottom: .5em;
+        margin-bottom: var(--spacing-m);
       }
       .agreementsUrl {
-        border: 0.1em solid #b0bdcc;
-        margin-bottom: 1.25em;
-        margin-left: 1.25em;
-        margin-right: 1.25em;
-        padding: 0.3em;
+        border: 1px solid #b0bdcc;
+        margin-bottom: var(--spacing-xl);
+        margin-left: var(--spacing-xl);
+        margin-right: var(--spacing-xl);
+        padding: var(--spacing-s);
       }
       #claNewAgreementsLabel {
         font-weight: var(--font-weight-bold);
@@ -53,15 +54,15 @@
       }
       .alreadySubmittedText {
         color: var(--error-text-color);
-        margin: 0 2em;
-        padding: .5em;
+        margin: 0 var(--spacing-xxl);
+        padding: var(--spacing-m);
       }
       .alreadySubmittedText.hide,
       .hideAgreementsTextBox {
         display: none;
       }
       main {
-        margin: 2em auto;
+        margin: var(--spacing-xxl) auto;
         max-width: 50em;
       }
     </style>
@@ -76,7 +77,7 @@
               type="radio"
               data-name$="[[item.name]]"
               data-url$="[[item.url]]"
-              on-tap="_handleShowAgreement"
+              on-click="_handleShowAgreement"
               disabled$="[[_disableAgreements(item, _groups, _signedAgreements)]]">
           <label id="claNewAgreementsLabel">[[item.name]]</label>
         </span>
@@ -95,8 +96,14 @@
         </div>
         <div class$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]">
           <h3 class="smallHeading">Complete the agreement:</h3>
-          <input id="input-agreements" is="iron-input" bind-value="{{_agreementsText}}" placeholder="Enter 'I agree' here" />
-          <gr-button on-tap="_handleSaveAgreements" disabled="[[_disableAgreementsText(_agreementsText)]]">
+          <iron-input bind-value="{{_agreementsText}}"
+                      placeholder="Enter 'I agree' here">
+            <input id="input-agreements"
+                   is="iron-input"
+                   bind-value="{{_agreementsText}}"
+                   placeholder="Enter 'I agree' here">
+          </iron-input>
+          <gr-button on-click="_handleSaveAgreements" disabled="[[_disableAgreementsText(_agreementsText)]]">
             Submit
           </gr-button>
         </div>
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
index 2a93155..cfd7b21 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-cla-view',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _groups: Object,
@@ -37,6 +36,7 @@
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
+      Gerrit.FireBehavior,
     ],
 
     attached() {
@@ -66,7 +66,9 @@
 
     _getAgreementsUrl(configUrl) {
       let url;
-      if (!configUrl) { return ''; }
+      if (!configUrl) {
+        return '';
+      }
       if (configUrl.startsWith('http:') || configUrl.startsWith('https:')) {
         url = configUrl;
       } else {
@@ -100,8 +102,8 @@
     },
 
     _createToast(message) {
-      this.dispatchEvent(new CustomEvent('show-alert',
-          {detail: {message}, bubbles: true}));
+      this.dispatchEvent(new CustomEvent(
+          'show-alert', {detail: {message}, bubbles: true, composed: true}));
     },
 
     _computeShowAgreementsClass(agreements) {
@@ -135,9 +137,13 @@
     _computeHideAgreementClass(name, config) {
       if (!config) return '';
       for (const key in config) {
-        if (!config.hasOwnProperty(key)) { continue; }
+        if (!config.hasOwnProperty(key)) {
+          continue;
+        }
         for (const prop in config[key]) {
-          if (!config[key].hasOwnProperty(prop)) { continue; }
+          if (!config[key].hasOwnProperty(prop)) {
+            continue;
+          }
           if (name === config[key].name &&
               !config[key].auto_verify_group) {
             return 'hideAgreementsTextBox';
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
index aec52cb..f1b65d9 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-cla-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-cla-view.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
index b3e6990..53a30c3 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -29,40 +30,64 @@
       <section>
         <span class="title">Tab width</span>
         <span class="value">
-          <input
-              is="iron-input"
+          <iron-input
               type="number"
               prevent-invalid-input
               allowed-pattern="[0-9]"
               bind-value="{{editPrefs.tab_size}}"
               on-keypress="_handleEditPrefsChanged"
               on-change="_handleEditPrefsChanged">
+            <input
+                is="iron-input"
+                type="number"
+                prevent-invalid-input
+                allowed-pattern="[0-9]"
+                bind-value="{{editPrefs.tab_size}}"
+                on-keypress="_handleEditPrefsChanged"
+                on-change="_handleEditPrefsChanged">
+          </iron-input>
         </span>
       </section>
       <section>
         <span class="title">Columns</span>
         <span class="value">
-          <input
-              is="iron-input"
+          <iron-input
               type="number"
               prevent-invalid-input
               allowed-pattern="[0-9]"
               bind-value="{{editPrefs.line_length}}"
               on-keypress="_handleEditPrefsChanged"
               on-change="_handleEditPrefsChanged">
+            <input
+                is="iron-input"
+                type="number"
+                prevent-invalid-input
+                allowed-pattern="[0-9]"
+                bind-value="{{editPrefs.line_length}}"
+                on-keypress="_handleEditPrefsChanged"
+                on-change="_handleEditPrefsChanged">
+          </iron-input>
         </span>
       </section>
       <section>
         <span class="title">Indent unit</span>
         <span class="value">
-          <input
-              is="iron-input"
+          <iron-input
               type="number"
               prevent-invalid-input
               allowed-pattern="[0-9]"
               bind-value="{{editPrefs.indent_unit}}"
               on-keypress="_handleEditPrefsChanged"
               on-change="_handleEditPrefsChanged">
+            <input
+                is="iron-input"
+                type="number"
+                prevent-invalid-input
+                allowed-pattern="[0-9]"
+                bind-value="{{editPrefs.indent_unit}}"
+                on-keypress="_handleEditPrefsChanged"
+                on-change="_handleEditPrefsChanged">
+          </iron-input>
         </span>
       </section>
       <section>
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
index 37bce08..86350f9 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-edit-preferences',
-    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
index 42171b7..c1c5c52 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-edit-preferences</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-edit-preferences.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
index 0a7433e..caaf18b 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -62,20 +62,28 @@
           <template is="dom-repeat" items="[[_emails]]">
             <tr>
               <td class="emailColumn">[[item.email]]</td>
-              <td class="preferredControl" on-tap="_handlePreferredControlTap">
-                <input
-                    is="iron-input"
+              <td class="preferredControl" on-click="_handlePreferredControlClick">
+                <iron-input
                     class="preferredRadio"
                     type="radio"
                     on-change="_handlePreferredChange"
                     name="preferred"
-                    value="[[item.email]]"
+                    bind-value="[[item.email]]"
                     checked$="[[item.preferred]]">
+                  <input
+                      is="iron-input"
+                      class="preferredRadio"
+                      type="radio"
+                      on-change="_handlePreferredChange"
+                      name="preferred"
+                      value="[[item.email]]"
+                      checked$="[[item.preferred]]">
+                </iron-input>
               </td>
               <td>
                 <gr-button
                     data-index$="[[index]]"
-                    on-tap="_handleDeleteButton"
+                    on-click="_handleDeleteButton"
                     disabled="[[item.preferred]]"
                     class="remove-button">Delete</gr-button>
               </td>
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
index 71d75cc..8490b26 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-email-editor',
-    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
@@ -74,7 +73,7 @@
       this.hasUnsavedChanges = true;
     },
 
-    _handlePreferredControlTap(e) {
+    _handlePreferredControlClick(e) {
       if (e.target.classList.contains('preferredControl')) {
         e.target.firstElementChild.click();
       }
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
index e937f8b..8d3f2d2 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-email-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-email-editor.html">
 
@@ -49,7 +51,7 @@
 
       element = fixture('basic');
 
-      element.loadData().then(done);
+      element.loadData().then(flush(done));
     });
 
     test('renders', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
index 0c589c9..cf73d99 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
@@ -28,9 +28,6 @@
   <template>
     <style include="shared-styles"></style>
     <style include="gr-form-styles">
-      .statusHeader {
-        width: 4em;
-      }
       .keyHeader {
         width: 9em;
       }
@@ -38,11 +35,13 @@
         width: 15em;
       }
       #viewKeyOverlay {
-        padding: 2em;
+        padding: var(--spacing-xxl);
         width: 50em;
       }
       .publicKey {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
         overflow-x: scroll;
         overflow-wrap: break-word;
         width: 30em;
@@ -53,11 +52,7 @@
         right: 2em;
       }
       #existing {
-        margin-bottom: 1em;
-      }
-      #existing .commentColumn {
-        min-width: 27em;
-        width: auto;
+        margin-bottom: var(--spacing-l);
       }
     </style>
     <div class="gr-form-styles">
@@ -85,7 +80,7 @@
                 </td>
                 <td class="keyHeader">
                   <gr-button
-                      on-tap="_showKey"
+                      on-click="_showKey"
                       data-index$="[[index]]"
                       link>Click to View</gr-button>
                 </td>
@@ -100,7 +95,7 @@
                 <td>
                   <gr-button
                       data-index$="[[index]]"
-                      on-tap="_handleDeleteKey">Delete</gr-button>
+                      on-click="_handleDeleteKey">Delete</gr-button>
                 </td>
               </tr>
             </template>
@@ -119,10 +114,10 @@
           </fieldset>
           <gr-button
               class="closeButton"
-              on-tap="_closeOverlay">Close</gr-button>
+              on-click="_closeOverlay">Close</gr-button>
         </gr-overlay>
         <gr-button
-            on-tap="save"
+            on-click="save"
             disabled$="[[!hasUnsavedChanges]]">Save changes</gr-button>
       </fieldset>
       <fieldset>
@@ -139,7 +134,7 @@
         <gr-button
             id="addButton"
             disabled$="[[_computeAddButtonDisabled(_newKey)]]"
-            on-tap="_handleAddKey">Add new GPG key</gr-button>
+            on-click="_handleAddKey">Add new GPG key</gr-button>
       </fieldset>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
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 a50509c..890061e 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-gpg-editor',
-    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
index aa53aca..9cfbde5f 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-gpg-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-gpg-editor.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
index 2c7afd3..ca500c8 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
index 4de24aa..d62a241 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-group-list',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _groups: Array,
@@ -40,7 +39,9 @@
     _computeGroupPath(group) {
       if (!group || !group.id) { return; }
 
-      return Gerrit.Nav.getUrlForGroup(group.id);
+      // Group ID is already encoded from the API
+      // Decode it here to match with our router encoding behavior
+      return Gerrit.Nav.getUrlForGroup(decodeURIComponent(group.id));
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
index 3fa5a36..0422d1b 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-group-list.html">
 
@@ -71,7 +73,8 @@
     teardown(() => { sandbox.restore(); });
 
     test('renders', () => {
-      const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+      const rows = Array.from(
+          Polymer.dom(element.root).querySelectorAll('tbody tr'));
 
       assert.equal(rows.length, 3);
 
@@ -90,21 +93,30 @@
     });
 
     test('_computeGroupPath', () => {
-      sandbox.stub(Gerrit.Nav, 'getUrlForGroup',
+      let urlStub = sandbox.stub(Gerrit.Nav, 'getUrlForGroup',
           () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
 
       let group = {
         id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5',
       };
-
       assert.equal(element._computeGroupPath(group),
           '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
 
       group = {
         name: 'admin',
       };
-
       assert.isUndefined(element._computeGroupPath(group));
+
+      urlStub.restore();
+
+      urlStub = sandbox.stub(Gerrit.Nav, 'getUrlForGroup',
+          () => '/admin/groups/user/test');
+
+      group = {
+        id: 'user%2Ftest',
+      };
+      assert.equal(element._computeGroupPath(group),
+          '/admin/groups/user/test');
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
index 54cfd65..0cb9695 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
@@ -28,19 +28,23 @@
     <style include="shared-styles">
       .password {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
       }
       #generatedPasswordOverlay {
-        padding: 2em;
+        padding: var(--spacing-xxl);
         width: 50em;
       }
       #generatedPasswordDisplay {
-        margin: 1em 0;
+        margin: var(--spacing-l) 0;
       }
       #generatedPasswordDisplay .title {
         width: unset;
       }
       #generatedPasswordDisplay .value {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
       }
       #passwordWarning {
         font-style: italic;
@@ -61,7 +65,7 @@
         </section>
         <gr-button
             id="generateButton"
-            on-tap="_handleGenerateTap">Generate new password</gr-button>
+            on-click="_handleGenerateTap">Generate new password</gr-button>
       </div>
       <span hidden$="[[!_passwordUrl]]">
         <a href$="[[_passwordUrl]]" target="_blank" rel="noopener">
@@ -91,7 +95,7 @@
         <gr-button
             link
             class="closeButton"
-            on-tap="_closeOverlay">Close</gr-button>
+            on-click="_closeOverlay">Close</gr-button>
       </div>
     </gr-overlay>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
index 99f4504..003e471 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-http-password',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _username: String,
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
index ca50b2b..8924058 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-http-password.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html
index 73aa65f..ee855cc 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
@@ -50,7 +50,7 @@
         display: none;
       }
       .space {
-        margin-bottom: 1em;
+        margin-bottom: var(--spacing-l);
       }
     </style>
     <div class="gr-form-styles">
@@ -75,7 +75,7 @@
                 <td class="deleteColumn">
                   <gr-button
                       class$="deleteButton [[_computeHideDeleteClass(item.can_delete)]]"
-                      on-tap="_handleDeleteItem">
+                      on-click="_handleDeleteItem">
                     Delete
                   </gr-button>
                 </td>
@@ -84,13 +84,13 @@
           </tbody>
         </table>
       </fieldset>
-      <dom-if if="[[_showLinkAnotherIdentity]]">
+      <template is="dom-if" if="[[_showLinkAnotherIdentity]]">
         <fieldset>
           <a href$="[[_computeLinkAnotherIdentity()]]">
             <gr-button id="linkAnotherIdentity" link>Link Another Identity</gr-button>
           </a>
         </fieldset>
-      </dom-if>
+      </template>
     </div>
     <gr-overlay id="overlay" with-backdrop>
       <gr-confirm-delete-item-dialog
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
index 4560a2e..c927f1e 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -24,7 +24,6 @@
 
   Polymer({
     is: 'gr-identities',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _identities: Object,
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
index 5468d27..1277424 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-identities</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-identities.html">
 
@@ -69,7 +71,8 @@
     });
 
     test('renders', () => {
-      const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+      const rows = Array.from(
+          Polymer.dom(element.root).querySelectorAll('tbody tr'));
 
       assert.equal(rows.length, 2);
 
@@ -82,7 +85,8 @@
     });
 
     test('renders email', () => {
-      const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+      const rows = Array.from(
+          Polymer.dom(element.root).querySelectorAll('tbody tr'));
 
       assert.equal(rows.length, 2);
 
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
index aa42623..1485628 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -61,22 +61,22 @@
               <td class="buttonColumn">
                 <gr-button
                     link
-                    data-index="[[index]]"
-                    on-tap="_handleMoveUpButton"
+                    data-index$="[[index]]"
+                    on-click="_handleMoveUpButton"
                     class="moveUpButton">↑</gr-button>
               </td>
               <td class="buttonColumn">
                 <gr-button
                     link
-                    data-index="[[index]]"
-                    on-tap="_handleMoveDownButton"
+                    data-index$="[[index]]"
+                    on-click="_handleMoveDownButton"
                     class="moveDownButton">↓</gr-button>
               </td>
               <td>
                 <gr-button
                     link
-                    data-index="[[index]]"
-                    on-tap="_handleDeleteButton"
+                    data-index$="[[index]]"
+                    on-click="_handleDeleteButton"
                     class="remove-button">Delete</gr-button>
               </td>
             </tr>
@@ -85,19 +85,30 @@
         <tfoot>
           <tr>
             <th>
-              <input
-                  is="iron-input"
+              <iron-input
                   placeholder="New Title"
                   on-keydown="_handleInputKeydown"
                   bind-value="{{_newName}}">
+                <input
+                    is="iron-input"
+                    placeholder="New Title"
+                    on-keydown="_handleInputKeydown"
+                    bind-value="{{_newName}}">
+              </iron-input>
             </th>
             <th>
-              <input
+              <iron-input
                   class="newUrlInput"
-                  is="iron-input"
                   placeholder="New URL"
                   on-keydown="_handleInputKeydown"
                   bind-value="{{_newUrl}}">
+                <input
+                    class="newUrlInput"
+                    is="iron-input"
+                    placeholder="New URL"
+                    on-keydown="_handleInputKeydown"
+                    bind-value="{{_newUrl}}">
+              </iron-input>
             </th>
             <th></th>
             <th></th>
@@ -105,7 +116,7 @@
               <gr-button
                   link
                   disabled$="[[_computeAddDisabled(_newName, _newUrl)]]"
-                  on-tap="_handleAddButton">Add</gr-button>
+                  on-click="_handleAddButton">Add</gr-button>
             </th>
           </tr>
         </tfoot>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
index 8587338..4f3c0c7 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-menu-editor',
-    _legacyUndefinedCheck: true,
 
     properties: {
       menuItems: Array,
@@ -28,7 +27,7 @@
     },
 
     _handleMoveUpButton(e) {
-      const index = Polymer.dom(e).localTarget.dataIndex;
+      const index = Number(Polymer.dom(e).localTarget.dataset.index);
       if (index === 0) { return; }
       const row = this.menuItems[index];
       const prev = this.menuItems[index - 1];
@@ -36,7 +35,7 @@
     },
 
     _handleMoveDownButton(e) {
-      const index = Polymer.dom(e).localTarget.dataIndex;
+      const index = Number(Polymer.dom(e).localTarget.dataset.index);
       if (index === this.menuItems.length - 1) { return; }
       const row = this.menuItems[index];
       const next = this.menuItems[index + 1];
@@ -44,7 +43,7 @@
     },
 
     _handleDeleteButton(e) {
-      const index = Polymer.dom(e).localTarget.dataIndex;
+      const index = Number(Polymer.dom(e).localTarget.dataset.index);
       this.splice('menuItems', index, 1);
     },
 
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
index c8a54b6..134e018 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-menu-editor.html">
 
@@ -55,7 +57,7 @@
       MockInteractions.tap(button);
     }
 
-    setup(() => {
+    setup(done => {
       element = fixture('basic');
       menu = [
         {url: '/first/url', name: 'first name', target: '_blank'},
@@ -64,6 +66,7 @@
       ];
       element.set('menuItems', menu);
       Polymer.dom.flush();
+      flush(done);
     });
 
     test('renders', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
index 5f1794c..f366d2a 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -15,7 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -43,23 +45,23 @@
         display: block;
       }
       hr {
-        margin-top: 1em;
-        margin-bottom: 1em;
+        margin-top: var(--spacing-l);
+        margin-bottom: var(--spacing-l);
       }
       header {
         border-bottom: 1px solid var(--border-color);
         font-weight: var(--font-weight-bold);
-        margin-bottom: 1em;
+        margin-bottom: var(--spacing-l);
       }
       .container {
-        padding: .5em 1.5em;
+        padding: var(--spacing-m) var(--spacing-xl);
       }
       footer {
         display: flex;
         justify-content: flex-end;
       }
       footer gr-button {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
       }
       input {
         width: 20em;
@@ -81,19 +83,27 @@
         <hr>
         <section>
           <div class="title">Full Name</div>
-          <input
-              is="iron-input"
-              id="name"
+          <iron-input
               bind-value="{{_account.name}}"
               disabled="[[_saving]]">
+            <input
+                is="iron-input"
+                id="name"
+                bind-value="{{_account.name}}"
+                disabled="[[_saving]]">
+          </iron-input>
         </section>
         <section class$="[[_computeUsernameClass(_usernameMutable)]]">
           <div class="title">Username</div>
-          <input
-              is="iron-input"
-              id="username"
+          <iron-input
               bind-value="{{_account.username}}"
               disabled="[[_saving]]">
+            <input
+                is="iron-input"
+                id="username"
+                bind-value="{{_account.username}}"
+                disabled="[[_saving]]">
+          </iron-input>
         </section>
         <section>
           <div class="title">Preferred Email</div>
@@ -109,7 +119,7 @@
         <hr>
         <p>
           More configuration options for Gerrit may be found in the
-          <a on-tap="close" href$="[[settingsUrl]]">settings</a>.
+          <a on-click="close" href$="[[settingsUrl]]">settings</a>.
         </p>
       </main>
       <footer>
@@ -117,13 +127,13 @@
             id="closeButton"
             link
             disabled="[[_saving]]"
-            on-tap="_handleClose">Close</gr-button>
+            on-click="_handleClose">Close</gr-button>
         <gr-button
             id="saveButton"
             primary
             link
             disabled="[[_computeSaveDisabled(_account.name, _account.email, _saving)]]"
-            on-tap="_handleSave">Save</gr-button>
+            on-click="_handleSave">Save</gr-button>
       </footer>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
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 6b4ee18..0633416 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-registration-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when account details are changed.
@@ -60,6 +59,10 @@
       _serverConfig: Object,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     hostAttributes: {
       role: 'dialog',
     },
@@ -120,6 +123,14 @@
     },
 
     _computeUsernameMutable(config, username) {
+      // Polymer 2: check for undefined
+      if ([
+        config,
+        username,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       return config.auth.editable_account_fields.includes('USER_NAME') &&
           !username;
     },
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
index 93a3188..d1b5c80 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-registration-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-registration-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
index 30a3801..937ee79 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
@@ -15,14 +15,14 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-settings-item">
   <template>
     <style>
       :host {
         display: block;
-        margin-bottom: 2em;
+        margin-bottom: var(--spacing-xxl);
       }
     </style>
     <h2 id="[[anchor]]">[[title]]</h2>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
index dc1aa93..dae3b68 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
@@ -19,7 +19,7 @@
 
   Polymer({
     is: 'gr-settings-item',
-    _legacyUndefinedCheck: true,
+
     properties: {
       anchor: String,
       title: String,
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
index f64d898..846f776 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-page-nav-styles.html">
 
 <dom-module id="gr-settings-menu-item">
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
index 2a56b09..5db0031 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
@@ -19,7 +19,7 @@
 
   Polymer({
     is: 'gr-settings-menu-item',
-    _legacyUndefinedCheck: true,
+
     properties: {
       href: String,
       title: String,
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index f0c4d6e..74971cf 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -15,10 +15,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 
 <link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
+
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-menu-page-styles.html">
 <link rel="import" href="../../../styles/gr-page-nav-styles.html">
@@ -49,18 +51,18 @@
       :host {
         color: var(--primary-text-color);
       }
-      #newEmailInput {
+      .newEmailInput {
         width: 20em;
       }
       #email {
-        margin-bottom: 1em;
+        margin-bottom: var(--spacing-l);
       }
       main section.darkToggle {
         display: block;
       }
       .filters p,
       .darkToggle p {
-        margin-bottom: 1em;
+        margin-bottom: var(--spacing-l);
       }
       .queryExample em {
         color: violet;
@@ -68,8 +70,8 @@
       .toggle {
         align-items: center;
         display: flex;
-        margin-bottom: 1rem;
-        margin-right: 1rem;
+        margin-bottom: var(--spacing-l);
+        margin-right: var(--spacing-l);
       }
     </style>
     <style include="gr-form-styles"></style>
@@ -132,7 +134,7 @@
               mutable="{{_accountNameMutable}}"
               has-unsaved-changes="{{_accountInfoChanged}}"></gr-account-info>
           <gr-button
-              on-tap="_handleSaveAccountInfo"
+              on-click="_handleSaveAccountInfo"
               disabled="[[!_accountInfoChanged]]">Save changes</gr-button>
         </fieldset>
         <h2
@@ -278,7 +280,7 @@
           </section>
           <gr-button
               id="savePrefs"
-              on-tap="_handleSavePreferences"
+              on-click="_handleSavePreferences"
               disabled="[[!_prefsChanged]]">Save changes</gr-button>
         </fieldset>
         <h2
@@ -292,7 +294,7 @@
               has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
           <gr-button
               id="saveDiffPrefs"
-              on-tap="_handleSaveDiffPreferences"
+              on-click="_handleSaveDiffPreferences"
               disabled$="[[!_diffPrefsChanged]]">Save changes</gr-button>
         </fieldset>
         <h2
@@ -306,7 +308,7 @@
               has-unsaved-changes="{{_editPrefsChanged}}"></gr-edit-preferences>
           <gr-button
               id="saveEditPrefs"
-              on-tap="_handleSaveEditPreferences"
+              on-click="_handleSaveEditPreferences"
               disabled$="[[!_editPrefsChanged]]">Save changes</gr-button>
         </fieldset>
         <h2 id="Menu" class$="[[_computeHeaderClass(_menuChanged)]]">Menu</h2>
@@ -315,12 +317,12 @@
               menu-items="{{_localMenu}}"></gr-menu-editor>
           <gr-button
               id="saveMenu"
-              on-tap="_handleSaveMenu"
+              on-click="_handleSaveMenu"
               disabled="[[!_menuChanged]]">Save changes</gr-button>
           <gr-button
               id="resetMenu"
               link
-              on-tap="_handleResetMenuButton">Reset</gr-button>
+              on-click="_handleResetMenuButton">Reset</gr-button>
         </fieldset>
         <h2 id="ChangeTableColumns"
             class$="[[_computeHeaderClass(_changeTableChanged)]]">
@@ -333,7 +335,7 @@
           </gr-change-table-editor>
           <gr-button
               id="saveChangeTable"
-              on-tap="_handleSaveChangeTable"
+              on-click="_handleSaveChangeTable"
               disabled="[[!_changeTableChanged]]">Save changes</gr-button>
         </fieldset>
         <h2
@@ -346,7 +348,7 @@
               has-unsaved-changes="{{_watchedProjectsChanged}}"
               id="watchedProjectsEditor"></gr-watched-projects-editor>
           <gr-button
-              on-tap="_handleSaveWatchedProjects"
+              on-click="_handleSaveWatchedProjects"
               disabled$="[[!_watchedProjectsChanged]]"
               id="_handleSaveWatchedProjects">Save changes</gr-button>
         </fieldset>
@@ -360,21 +362,29 @@
               id="emailEditor"
               has-unsaved-changes="{{_emailsChanged}}"></gr-email-editor>
           <gr-button
-              on-tap="_handleSaveEmails"
+              on-click="_handleSaveEmails"
               disabled$="[[!_emailsChanged]]">Save changes</gr-button>
         </fieldset>
         <fieldset id="newEmail">
           <section>
             <span class="title">New email address</span>
             <span class="value">
-              <input
-                  id="newEmailInput"
+              <iron-input
+                  class="newEmailInput"
                   bind-value="{{_newEmail}}"
-                  is="iron-input"
                   type="text"
                   disabled="[[_addingEmail]]"
                   on-keydown="_handleNewEmailKeydown"
                   placeholder="email@example.com">
+                <input
+                    class="newEmailInput"
+                    bind-value="{{_newEmail}}"
+                    is="iron-input"
+                    type="text"
+                    disabled="[[_addingEmail]]"
+                    on-keydown="_handleNewEmailKeydown"
+                    placeholder="email@example.com">
+              </iron-input>
             </span>
           </section>
           <section
@@ -387,7 +397,7 @@
           </section>
           <gr-button
               disabled="[[!_computeAddEmailButtonEnabled(_newEmail, _addingEmail)]]"
-              on-tap="_handleAddEmailButton">Send verification</gr-button>
+              on-click="_handleAddEmailButton">Send verification</gr-button>
         </fieldset>
         <template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]">
           <div>
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 a66c38d..714faab 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
@@ -47,7 +47,6 @@
 
   Polymer({
     is: 'gr-settings-view',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -149,6 +148,7 @@
     behaviors: [
       Gerrit.DocsUrlBehavior,
       Gerrit.ChangeTableBehavior,
+      Gerrit.FireBehavior,
     ],
 
     observers: [
@@ -392,7 +392,7 @@
     },
 
     _isNewEmailValid(newEmail) {
-      return newEmail.includes('@');
+      return newEmail && newEmail.includes('@');
     },
 
     _computeAddEmailButtonEnabled(newEmail, addingEmail) {
@@ -435,6 +435,7 @@
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {message: RELOAD_MESSAGE},
         bubbles: true,
+        composed: true,
       }));
       this.async(() => {
         window.location.reload();
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index 506c6af..6dcf124 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-settings-view.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
index 57458f8..2a27194 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
@@ -35,11 +35,13 @@
         width: 7.5em;
       }
       #viewKeyOverlay {
-        padding: 2em;
+        padding: var(--spacing-xxl);
         width: 50em;
       }
       .publicKey {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
         overflow-x: scroll;
         overflow-wrap: break-word;
         width: 30em;
@@ -50,7 +52,7 @@
         right: 2em;
       }
       #existing {
-        margin-bottom: 1em;
+        margin-bottom: var(--spacing-l);
       }
       #existing .commentColumn {
         min-width: 27em;
@@ -77,7 +79,7 @@
                 <td>
                   <gr-button
                       link
-                      on-tap="_showKey"
+                      on-click="_showKey"
                       data-index$="[[index]]"
                       link>Click to View</gr-button>
                 </td>
@@ -93,7 +95,7 @@
                   <gr-button
                       link
                       data-index$="[[index]]"
-                      on-tap="_handleDeleteKey">Delete</gr-button>
+                      on-click="_handleDeleteKey">Delete</gr-button>
                 </td>
               </tr>
             </template>
@@ -116,10 +118,10 @@
           </fieldset>
           <gr-button
               class="closeButton"
-              on-tap="_closeOverlay">Close</gr-button>
+              on-click="_closeOverlay">Close</gr-button>
         </gr-overlay>
         <gr-button
-            on-tap="save"
+            on-click="save"
             disabled$="[[!hasUnsavedChanges]]">Save changes</gr-button>
       </fieldset>
       <fieldset>
@@ -137,7 +139,7 @@
             id="addButton"
             link
             disabled$="[[_computeAddButtonDisabled(_newKey)]]"
-            on-tap="_handleAddKey">Add new SSH key</gr-button>
+            on-click="_handleAddKey">Add new SSH key</gr-button>
       </fieldset>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
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 4c423e8..874173a 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-ssh-editor',
-    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
index 8ed0730..d313f5a 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-ssh-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-ssh-editor.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
index 85fe368..360ea2d 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -14,7 +14,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -27,7 +28,7 @@
     <style include="gr-form-styles">
       #watchedProjects .notifType {
         text-align: center;
-        padding: 0 0.4em;
+        padding: 0 var(--spacing-s);
       }
       .notifControl {
         cursor: pointer;
@@ -39,7 +40,7 @@
       .projectFilter {
         color: var(--deemphasized-text-color);
         font-style: italic;
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
       }
       .newFilterInput {
         width: 100%;
@@ -73,7 +74,7 @@
                   is="dom-repeat"
                   items="[[_getTypes()]]"
                   as="type">
-                <td class="notifControl" on-tap="_handleNotifCellTap">
+                <td class="notifControl" on-click="_handleNotifCellClick">
                   <input
                       type="checkbox"
                       data-index$="[[projectIndex]]"
@@ -86,7 +87,7 @@
                 <gr-button
                     link
                     data-index$="[[projectIndex]]"
-                    on-tap="_handleRemoveProject">Delete</gr-button>
+                    on-click="_handleRemoveProject">Delete</gr-button>
               </td>
             </tr>
           </template>
@@ -103,14 +104,18 @@
                   placeholder="Repo"></gr-autocomplete>
             </th>
             <th colspan$="[[_getTypeCount()]]">
-              <input
-                  id="newFilter"
+              <iron-input
                   class="newFilterInput"
-                  is="iron-input"
                   placeholder="branch:name, or other search expression">
+                <input
+                    id="newFilter"
+                    class="newFilterInput"
+                    is="iron-input"
+                    placeholder="branch:name, or other search expression">
+              </iron-input>
             </th>
             <th>
-              <gr-button link on-tap="_handleAddProject">Add</gr-button>
+              <gr-button link on-click="_handleAddProject">Add</gr-button>
             </th>
           </tr>
         </tfoot>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
index bd18456..a40094d 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -27,7 +27,6 @@
 
   Polymer({
     is: 'gr-watched-projects-editor',
-    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
@@ -170,7 +169,7 @@
       this.hasUnsavedChanges = true;
     },
 
-    _handleNotifCellTap(e) {
+    _handleNotifCellClick(e) {
       const checkbox = Polymer.dom(e.target).querySelector('input');
       if (checkbox) { checkbox.click(); }
     },
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
index 9022bcc..7a238ec 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-watched-projects-editor.html">
 
@@ -187,6 +189,7 @@
       element.$.newProject.value = {id: 'project b'};
       element.$.newProject.setText('project b');
       element.$.newFilter.bindValue = 'filter 1';
+      element.$.newFilter.value = 'filter 1';
 
       element._handleAddProject();
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
index 543ed85..5a095a4 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../gr-account-link/gr-account-link.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../gr-icons/gr-icons.html">
@@ -34,28 +35,34 @@
         background: var(--chip-background-color);
         border-radius: .75em;
         display: inline-flex;
-        padding: 0 .5em;
+        padding: 0 var(--spacing-m);
       }
       :host([show-avatar]) .container {
         padding-left: 0;
       }
+      gr-button.remove {
+        --gr-remove-button-style: {
+          border: 0;
+          color: var(--deemphasized-text-color);
+          font-weight: normal;
+          height: .6em;
+          line-height: 10px;
+          margin-left: var(--spacing-xs);
+          padding: 0;
+          text-decoration: none;
+        }
+      }
+
       gr-button.remove:hover,
       gr-button.remove:focus {
         --gr-button: {
+          @apply --gr-remove-button-style;
           color: #333;
         }
       }
       gr-button.remove {
         --gr-button: {
-          border: 0;
-          color: var(--deemphasized-text-color);
-          font-size: 1.7rem;
-          font-weight: normal;
-          height: .6em;
-          line-height: .6;
-          margin-left: .15em;
-          padding: 0;
-          text-decoration: none;
+          @apply --gr-remove-button-style;
         }
       }
       :host:focus {
@@ -93,7 +100,7 @@
           tabindex="-1"
           aria-label="Remove"
           class$="remove [[_getBackgroundClass(transparentBackground)]]"
-          on-tap="_handleRemoveTap">
+          on-click="_handleRemoveTap">
         <iron-icon icon="gr-icons:close"></iron-icon>
       </gr-button>
     </div>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index 827e33b..10c876d 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -20,7 +20,6 @@
 
   Polymer({
     is: 'gr-account-chip',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired to indicate a key was pressed while this chip was focused.
@@ -57,6 +56,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     ready() {
       this._getHasAvatars().then(hasAvatars => {
         this.showAvatar = hasAvatars;
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
similarity index 74%
rename from polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
rename to polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
index 582c83b..ae656fd 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-autocomplete/gr-autocomplete.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-account-entry">
   <template>
@@ -35,14 +35,13 @@
         borderless="[[borderless]]"
         placeholder="[[placeholder]]"
         threshold="[[suggestFrom]]"
-        query="[[query]]"
+        query="[[querySuggestions]]"
         allow-non-suggested-values="[[allowAnyInput]]"
         on-commit="_handleInputCommit"
         clear-on-commit
         warn-uncommitted
         text="{{_inputText}}">
     </gr-autocomplete>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-account-entry.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
new file mode 100644
index 0000000..b2e0973
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
@@ -0,0 +1,101 @@
+/**
+ * @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.
+ */
+(function() {
+  'use strict';
+
+  /**
+   * gr-account-entry is an element for entering account
+   * and/or group with autocomplete support.
+   */
+  Polymer({
+    is: 'gr-account-entry',
+
+    /**
+     * Fired when an account is entered.
+     *
+     * @event add
+     */
+
+    /**
+     * When allowAnyInput is true, account-text-changed is fired when input text
+     * changed. This is needed so that the reply dialog's save button can be
+     * enabled for arbitrary cc's, which don't need a 'commit'.
+     *
+     * @event account-text-changed
+     */
+    properties: {
+      allowAnyInput: Boolean,
+      borderless: Boolean,
+      placeholder: String,
+
+      // suggestFrom = 0 to enable default suggestions.
+      suggestFrom: {
+        type: Number,
+        value: 0,
+      },
+
+      /** @type {!function(string): !Promise<Array<{name, value}>>} */
+      querySuggestions: {
+        type: Function,
+        notify: true,
+        value() {
+          return input => Promise.resolve([]);
+        },
+      },
+
+      _config: Object,
+      /** The value of the autocomplete entry. */
+      _inputText: {
+        type: String,
+        observer: '_inputTextChanged',
+      },
+
+    },
+
+    get focusStart() {
+      return this.$.input.focusStart;
+    },
+
+    focus() {
+      this.$.input.focus();
+    },
+
+    clear() {
+      this.$.input.clear();
+    },
+
+    setText(text) {
+      this.$.input.setText(text);
+    },
+
+    getText() {
+      return this.$.input.text;
+    },
+
+    _handleInputCommit(e) {
+      this.fire('add', {value: e.detail.value});
+      this.$.input.focus();
+    },
+
+    _inputTextChanged(text) {
+      if (text.length && this.allowAnyInput) {
+        this.dispatchEvent(new CustomEvent(
+            'account-text-changed', {bubbles: true, composed: true}));
+      }
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
new file mode 100644
index 0000000..6896af9
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-account-entry</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="gr-account-entry.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-account-entry></gr-account-entry>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-account-entry tests', () => {
+    let sandbox;
+
+    const suggestion1 = {
+      email: 'email1@example.com',
+      _account_id: 1,
+      some_property: 'value',
+    };
+    const suggestion2 = {
+      email: 'email2@example.com',
+      _account_id: 2,
+    };
+    const suggestion3 = {
+      email: 'email25@example.com',
+      _account_id: 25,
+      some_other_property: 'other value',
+    };
+
+    setup(done => {
+      element = fixture('basic');
+      sandbox = sinon.sandbox.create();
+      return flush(done);
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('stubbed values for querySuggestions', () => {
+      setup(() => {
+        element.querySuggestions = input => {
+          return Promise.resolve([
+            suggestion1,
+            suggestion2,
+            suggestion3,
+          ]);
+        };
+      });
+    });
+
+    test('account-text-changed fired when input text changed and allowAnyInput',
+        () => {
+          // Spy on query, as that is called when _updateSuggestions proceeds.
+          const changeStub = sandbox.stub();
+          element.allowAnyInput = true;
+          element.querySuggestions = input => Promise.resolve([]);
+          element.addEventListener('account-text-changed', changeStub);
+          element.$.input.text = 'a';
+          assert.isTrue(changeStub.calledOnce);
+          element.$.input.text = 'ab';
+          assert.isTrue(changeStub.calledTwice);
+        });
+
+    test('account-text-changed not fired when input text changed without ' +
+        'allowAnyInput', () => {
+      // Spy on query, as that is called when _updateSuggestions proceeds.
+      const changeStub = sandbox.stub();
+      element.querySuggestions = input => Promise.resolve([]);
+      element.addEventListener('account-text-changed', changeStub);
+      element.$.input.text = 'a';
+      assert.isFalse(changeStub.called);
+    });
+
+    test('setText', () => {
+      // Spy on query, as that is called when _updateSuggestions proceeds.
+      const suggestSpy = sandbox.spy(element.$.input, 'query');
+      element.setText('test text');
+      flushAsynchronousOperations();
+
+      assert.equal(element.$.input.$.input.value, 'test text');
+      assert.isFalse(suggestSpy.called);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index bdf37bf..7ed7962 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
+<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../gr-avatar/gr-avatar.html">
 <link rel="import" href="../gr-limited-text/gr-limited-text.html">
@@ -35,7 +35,7 @@
       gr-avatar {
         height: 1.3em;
         width: 1.3em;
-        margin-right: .15em;
+        margin-right: var(--spacing-xs);
         vertical-align: -.25em;
       }
       .text {
@@ -64,7 +64,11 @@
           [[_computeEmailStr(account)]]
         </span>
         <template is="dom-if" if="[[account.status]]">
-          (<gr-limited-text limit="10" text="[[account.status]]"></gr-limited-text>)
+          (<gr-limited-text
+            disable-tooltip="true"
+            limit="[[_computeStatusTextLength(account, _serverConfig)]]"
+            text="[[account.status]]">
+          </gr-limited-text>)
         </template>
       </span>
     </span>
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 7983fad..418d2ea 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,7 +19,6 @@
 
   Polymer({
     is: 'gr-account-label',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -52,7 +51,7 @@
     },
 
     behaviors: [
-      Gerrit.AnonymousNameBehavior,
+      Gerrit.DisplayNameBehavior,
       Gerrit.TooltipBehavior,
     ],
 
@@ -66,18 +65,38 @@
       return this.getUserName(config, account, false);
     },
 
+    _computeStatusTextLength(account, config) {
+      // 35 as the max length of the name + status
+      return Math.max(10, 35 - this._computeName(account, config).length);
+    },
+
     _computeAccountTitle(account, tooltip) {
+      // Polymer 2: check for undefined
+      if ([
+        account,
+        tooltip,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       if (!account) { return; }
       let result = '';
       if (this._computeName(account, this._serverConfig)) {
         result += this._computeName(account, this._serverConfig);
       }
       if (account.email) {
-        result += ' <' + account.email + '>';
+        result += ` <${account.email}>`;
       }
       if (this.additionalText) {
-        return result + ' ' + this.additionalText;
+        result += ` ${this.additionalText}`;
       }
+
+      // Show status in the label tooltip instead of
+      // in a separate tooltip on status
+      if (account.status) {
+        result += ` (${account.status})`;
+      }
+
       return result;
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index cd8e194..45545fe 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-label</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
@@ -66,32 +68,35 @@
           {
             name: 'Andrew Bonventre',
             email: 'andybons+gerrit@gmail.com',
-          }),
+          }, /* additionalText= */ ''),
       'Andrew Bonventre <andybons+gerrit@gmail.com>');
 
       assert.equal(element._computeAccountTitle(
-          {name: 'Andrew Bonventre'}),
+          {name: 'Andrew Bonventre'}, /* additionalText= */ ''),
       'Andrew Bonventre');
 
       assert.equal(element._computeAccountTitle(
           {
             email: 'andybons+gerrit@gmail.com',
-          }),
+          }, /* additionalText= */ ''),
       'Anonymous <andybons+gerrit@gmail.com>');
 
       assert.equal(element._computeShowEmailClass(
           {
             name: 'Andrew Bonventre',
             email: 'andybons+gerrit@gmail.com',
-          }), '');
+          }, /* additionalText= */ ''), '');
 
       assert.equal(element._computeShowEmailClass(
           {
             email: 'andybons+gerrit@gmail.com',
-          }), 'showEmail');
+          }, /* additionalText= */ ''), 'showEmail');
 
-      assert.equal(element._computeShowEmailClass({name: 'Andrew Bonventre'}),
-          '');
+      assert.equal(element._computeShowEmailClass(
+          {name: 'Andrew Bonventre'},
+          /* additionalText= */ ''
+      ),
+      '');
 
       assert.equal(element._computeShowEmailClass(undefined), '');
 
@@ -134,5 +139,44 @@
             'TestAnon');
       });
     });
+
+    suite('status in tooltip', () => {
+      setup(() => {
+        element = fixture('basic');
+        element.account = {
+          name: 'test',
+          email: 'test@google.com',
+          status: 'OOO until Aug 10th',
+        };
+        element._config = {
+          user: {
+            anonymous_coward_name: 'Anonymous Coward',
+          },
+        };
+      });
+
+      test('tooltip should contain status text', () => {
+        assert.deepEqual(element.title,
+            'test <test@google.com> (OOO until Aug 10th)');
+      });
+
+      test('status text should not have tooltip', () => {
+        flushAsynchronousOperations();
+        assert.deepEqual(element.$$('gr-limited-text').title, '');
+      });
+
+      test('status text should honor the name length and total length', () => {
+        assert.deepEqual(
+            element._computeStatusTextLength(element.account, element._config),
+            31
+        );
+        assert.deepEqual(
+            element._computeStatusTextLength({
+              name: 'a very long long long long name',
+            }, element._config),
+            10
+        );
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
index 34b0de6..d3575b2 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -16,7 +16,7 @@
 -->
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../gr-account-label/gr-account-label.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index 03967f1..faaf9c3 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-account-link',
-    _legacyUndefinedCheck: true,
 
     properties: {
       additionalText: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
index 6d1831e..134c579 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-link</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-account-link.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
similarity index 83%
rename from polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
index 1bfc5eb..2ce608be 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
@@ -15,8 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
+<link rel="import" href="../gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../gr-account-entry/gr-account-entry.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -25,7 +26,7 @@
     <style include="shared-styles">
       gr-account-chip {
         display: inline-block;
-        margin: .2em .2em .2em 0;
+        margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
       }
       gr-account-entry {
         display: flex;
@@ -55,7 +56,7 @@
             account="[[account]]"
             class$="[[_computeChipClass(account)]]"
             data-account-id$="[[account._account_id]]"
-            removable="[[_computeRemovable(account)]]"
+            removable="[[_computeRemovable(account, readonly)]]"
             on-keydown="_handleChipKeydown"
             tabindex="-1">
         </gr-account-chip>
@@ -66,13 +67,13 @@
         hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]"
         id="entry"
         change="[[change]]"
-        filter="[[filter]]"
         placeholder="[[placeholder]]"
         on-add="_handleAdd"
         on-input-keydown="_handleInputKeydown"
         allow-any-input="[[allowAnyInput]]"
-        allow-any-user="[[allowAnyUser]]">
+        query-suggestions="[[_querySuggestions]]">
     </gr-account-entry>
+    <slot></slot>
   </template>
   <script src="gr-account-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
similarity index 76%
rename from polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index 7cdffc8..de66c50 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -21,7 +21,6 @@
 
   Polymer({
     is: 'gr-account-list',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when user inputs an invalid email address.
@@ -38,6 +37,20 @@
       change: Object,
       filter: Function,
       placeholder: String,
+      disabled: {
+        type: Function,
+        value: false,
+      },
+
+      /**
+       * Returns suggestions and convert them to list item
+       *
+       * @type {Gerrit.GrSuggestionsProvider}
+       */
+      suggestionsProvider: {
+        type: Object,
+      },
+
       /**
        * Needed for template checking since value is initially set to null.
        *
@@ -51,21 +64,6 @@
         type: Boolean,
         value: false,
       },
-
-      /**
-       * When true, the account-entry autocomplete uses the account suggest API
-       * endpoint, which suggests any account in that Gerrit instance (and does
-       * not suggest groups).
-       *
-       * When false/undefined, account-entry uses the suggest_reviewers API
-       * endpoint, which suggests any account or group in that Gerrit instance
-       * that is not already a reviewer (or is not CCed) on that change.
-       */
-      allowAnyUser: {
-        type: Boolean,
-        value: false,
-      },
-
       /**
        * When true, allows for non-suggested inputs to be added.
        */
@@ -83,14 +81,29 @@
         type: Number,
         value: 0,
       },
+
+      /** Returns suggestion items
+       *
+       * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>}
+       */
+      _querySuggestions: {
+        type: Function,
+        value() {
+          return this._getSuggestions.bind(this);
+        },
+      },
     },
 
+    behaviors: [
+      // Used in the tests for gr-account-list and other elements tests.
+      Gerrit.FireBehavior,
+    ],
+
     listeners: {
       remove: '_handleRemove',
     },
 
     get accountChips() {
-      // Polymer2: querySelectorAll returns NodeList instead of Array.
       return Array.from(
           Polymer.dom(this.root).querySelectorAll('gr-account-chip'));
     },
@@ -99,36 +112,54 @@
       return this.$.entry.focusStart;
     },
 
-    _handleAdd(e) {
-      this._addReviewer(e.detail.value);
+    _getSuggestions(input) {
+      const provider = this.suggestionsProvider;
+      if (!provider) {
+        return Promise.resolve([]);
+      }
+      return provider.getSuggestions(input).then(suggestions => {
+        if (!suggestions) { return []; }
+        if (this.filter) {
+          suggestions = suggestions.filter(this.filter);
+        }
+        return suggestions.map(suggestion =>
+          provider.makeSuggestionItem(suggestion));
+      });
     },
 
-    _addReviewer(reviewer) {
+    _handleAdd(e) {
+      this._addAccountItem(e.detail.value);
+    },
+
+    _addAccountItem(item) {
       // Append new account or group to the accounts property. We add our own
       // internal properties to the account/group here, so we clone the object
       // to avoid cluttering up the shared change object.
-      if (reviewer.account) {
+      if (item.account) {
         const account =
-            Object.assign({}, reviewer.account, {_pendingAdd: true});
+            Object.assign({}, item.account, {_pendingAdd: true});
         this.push('accounts', account);
-      } else if (reviewer.group) {
-        if (reviewer.confirm) {
-          this.pendingConfirmation = reviewer;
+      } else if (item.group) {
+        if (item.confirm) {
+          this.pendingConfirmation = item;
           return;
         }
-        const group = Object.assign({}, reviewer.group,
+        const group = Object.assign({}, item.group,
             {_pendingAdd: true, _group: true});
         this.push('accounts', group);
       } else if (this.allowAnyInput) {
-        if (!reviewer.includes('@')) {
+        if (!item.includes('@')) {
           // Repopulate the input with what the user tried to enter and have
           // a toast tell them why they can't enter it.
-          this.$.entry.setText(reviewer);
-          this.dispatchEvent(new CustomEvent('show-alert',
-              {detail: {message: VALID_EMAIL_ALERT}, bubbles: true}));
+          this.$.entry.setText(item);
+          this.dispatchEvent(new CustomEvent('show-alert', {
+            detail: {message: VALID_EMAIL_ALERT},
+            bubbles: true,
+            composed: true,
+          }));
           return false;
         } else {
-          const account = {email: reviewer, _pendingAdd: true};
+          const account = {email: item, _pendingAdd: true};
           this.push('accounts', account);
         }
       }
@@ -166,8 +197,8 @@
       return a === b;
     },
 
-    _computeRemovable(account) {
-      if (this.readonly) { return false; }
+    _computeRemovable(account, readonly) {
+      if (readonly) { return false; }
       if (this.removableValues) {
         for (let i = 0; i < this.removableValues.length; i++) {
           if (this._accountMatches(this.removableValues[i], account)) {
@@ -186,7 +217,9 @@
     },
 
     _removeAccount(toRemove) {
-      if (!toRemove || !this._computeRemovable(toRemove)) { return; }
+      if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+        return;
+      }
       for (let i = 0; i < this.accounts.length; i++) {
         let matches;
         const account = this.accounts[i];
@@ -203,8 +236,13 @@
       console.warn('received remove event for missing account', toRemove);
     },
 
+    _getNativeInput(paperInput) {
+      // In Polymer 2 inputElement isn't nativeInput anymore
+      return paperInput.$.nativeInput || paperInput.inputElement;
+    },
+
     _handleInputKeydown(e) {
-      const input = e.detail.input.inputElement;
+      const input = this._getNativeInput(e.detail.input);
       if (input.selectionStart !== input.selectionEnd ||
           input.selectionStart !== 0) {
         return;
@@ -270,7 +308,7 @@
     submitEntryText() {
       const text = this.$.entry.getText();
       if (!text.length) { return true; }
-      const wasSubmitted = this._addReviewer(text);
+      const wasSubmitted = this._addAccountItem(text);
       if (wasSubmitted) { this.$.entry.clear(); }
       return wasSubmitted;
     },
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
similarity index 65%
rename from polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
index 544238b..f931a69 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-account-list.html">
 
@@ -33,6 +35,16 @@
 </test-fixture>
 
 <script>
+  class MockSuggestionsProvider {
+    getSuggestions(input) {
+      return Promise.resolve([]);
+    }
+
+    makeSuggestionItem(item) {
+      return item;
+    }
+  }
+
   suite('gr-account-list tests', () => {
     let _nextAccountId = 0;
     const makeAccount = function() {
@@ -49,10 +61,11 @@
       };
     };
 
-    let existingReviewer1;
-    let existingReviewer2;
+    let existingAccount1;
+    let existingAccount2;
     let sandbox;
     let element;
+    let suggestionsProvider;
 
     function getChips() {
       return Polymer.dom(element.root).querySelectorAll('gr-account-chip');
@@ -60,14 +73,16 @@
 
     setup(() => {
       sandbox = sinon.sandbox.create();
-      existingReviewer1 = makeAccount();
-      existingReviewer2 = makeAccount();
+      existingAccount1 = makeAccount();
+      existingAccount2 = makeAccount();
 
       stub('gr-rest-api-interface', {
         getConfig() { return Promise.resolve({}); },
       });
       element = fixture('basic');
-      element.accounts = [existingReviewer1, existingReviewer2];
+      element.accounts = [existingAccount1, existingAccount2];
+      suggestionsProvider = new MockSuggestionsProvider();
+      element.suggestionsProvider = suggestionsProvider;
     });
 
     teardown(() => {
@@ -107,7 +122,7 @@
       assert.isTrue(chips[2].classList.contains('pendingAdd'));
 
       // Removed accounts are taken out of the list.
-      element.fire('remove', {account: existingReviewer1});
+      element.fire('remove', {account: existingAccount1});
       flushAsynchronousOperations();
       chips = getChips();
       assert.equal(chips.length, 2);
@@ -115,7 +130,7 @@
       assert.isTrue(chips[1].classList.contains('pendingAdd'));
 
       // Invalid remove is ignored.
-      element.fire('remove', {account: existingReviewer1});
+      element.fire('remove', {account: existingAccount1});
       element.fire('remove', {account: newAccount});
       flushAsynchronousOperations();
       chips = getChips();
@@ -145,6 +160,52 @@
       assert.isFalse(chips[0].classList.contains('pendingAdd'));
     });
 
+    test('_getSuggestions uses filter correctly', done => {
+      const originalSuggestions = [
+        {
+          email: 'abc@example.com',
+          text: 'abcd',
+          _account_id: 3,
+        },
+        {
+          email: 'qwe@example.com',
+          text: 'qwer',
+          _account_id: 1,
+        },
+        {
+          email: 'xyz@example.com',
+          text: 'aaaaa',
+          _account_id: 25,
+        },
+      ];
+      sandbox.stub(suggestionsProvider, 'getSuggestions')
+          .returns(Promise.resolve(originalSuggestions));
+      sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => {
+        return {
+          name: suggestion.email,
+          value: suggestion._account_id,
+        };
+      });
+
+
+      element._getSuggestions().then(suggestions => {
+        // Default is no filtering.
+        assert.equal(suggestions.length, 3);
+
+        // Set up filter that only accepts suggestion1.
+        const accountId = originalSuggestions[0]._account_id;
+        element.filter = function(suggestion) {
+          return suggestion._account_id === accountId;
+        };
+
+        element._getSuggestions().then(suggestions => {
+          assert.deepEqual(suggestions,
+              [{name: originalSuggestions[0].email,
+                value: originalSuggestions[0]._account_id}]);
+        }).then(done);
+      });
+    });
+
     test('_computeChipClass', () => {
       const account = makeAccount();
       assert.equal(element._computeChipClass(account), '');
@@ -161,18 +222,18 @@
       newAccount._pendingAdd = true;
       element.readonly = false;
       element.removableValues = [];
-      assert.isFalse(element._computeRemovable(existingReviewer1));
-      assert.isTrue(element._computeRemovable(newAccount));
+      assert.isFalse(element._computeRemovable(existingAccount1, false));
+      assert.isTrue(element._computeRemovable(newAccount, false));
 
 
-      element.removableValues = [existingReviewer1];
-      assert.isTrue(element._computeRemovable(existingReviewer1));
-      assert.isTrue(element._computeRemovable(newAccount));
-      assert.isFalse(element._computeRemovable(existingReviewer2));
+      element.removableValues = [existingAccount1];
+      assert.isTrue(element._computeRemovable(existingAccount1, false));
+      assert.isTrue(element._computeRemovable(newAccount, false));
+      assert.isFalse(element._computeRemovable(existingAccount2, false));
 
       element.readonly = true;
-      assert.isFalse(element._computeRemovable(existingReviewer1));
-      assert.isFalse(element._computeRemovable(newAccount));
+      assert.isFalse(element._computeRemovable(existingAccount1, true));
+      assert.isFalse(element._computeRemovable(newAccount, true));
     });
 
     test('submitEntryText', () => {
@@ -291,13 +352,40 @@
       assert.isTrue(element.$.entry.hasAttribute('hidden'));
     });
 
-    suite('allowAnyInput', () => {
-      let entry;
+    test('enter text calls suggestions provider', done => {
+      const suggestions = [
+        {
+          email: 'abc@example.com',
+          text: 'abcd',
+        },
+        {
+          email: 'qwe@example.com',
+          text: 'qwer',
+        },
+      ];
+      const getSuggestionsStub =
+          sandbox.stub(suggestionsProvider, 'getSuggestions')
+              .returns(Promise.resolve(suggestions));
 
+      const makeSuggestionItemStub =
+          sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
+
+      const input = element.$.entry.$.input;
+
+      input.text = 'newTest';
+      MockInteractions.focus(input.$.input);
+      input.noDebounce = true;
+      flushAsynchronousOperations();
+      flush(() => {
+        assert.isTrue(getSuggestionsStub.calledOnce);
+        assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
+        assert.equal(makeSuggestionItemStub.getCalls().length, 2);
+        done();
+      });
+    });
+
+    suite('allowAnyInput', () => {
       setup(() => {
-        entry = element.$.entry;
-        sandbox.stub(entry, '_getReviewerSuggestions');
-        sandbox.stub(entry.$.input, '_updateSuggestions');
         element.allowAnyInput = true;
       });
 
@@ -330,44 +418,51 @@
     });
 
     suite('keyboard interactions', () => {
-      test('backspace at text input start removes last account', () => {
+      test('backspace at text input start removes last account', done => {
         const input = element.$.entry.$.input;
-        sandbox.stub(element.$.entry, '_getReviewerSuggestions');
         sandbox.stub(input, '_updateSuggestions');
         sandbox.stub(element, '_computeRemovable').returns(true);
-        // Next line is a workaround for Firefix not moving cursor
-        // on input field update
-        assert.equal(input.$.input.inputElement.selectionStart, 0);
-        input.text = 'test';
-        MockInteractions.focus(input.$.input);
-        flushAsynchronousOperations();
-        assert.equal(element.accounts.length, 2);
-        MockInteractions.pressAndReleaseKeyOn(
-            input.$.input.inputElement, 8); // Backspace
-        assert.equal(element.accounts.length, 2);
-        input.text = '';
-        MockInteractions.pressAndReleaseKeyOn(
-            input.$.input.inputElement, 8); // Backspace
-        assert.equal(element.accounts.length, 1);
+        flush(() => {
+          // Next line is a workaround for Firefix not moving cursor
+          // on input field update
+          assert.equal(
+              element._getNativeInput(input.$.input).selectionStart, 0);
+          input.text = 'test';
+          MockInteractions.focus(input.$.input);
+          flushAsynchronousOperations();
+          assert.equal(element.accounts.length, 2);
+          MockInteractions.pressAndReleaseKeyOn(
+              element._getNativeInput(input.$.input), 8); // Backspace
+          assert.equal(element.accounts.length, 2);
+          input.text = '';
+          MockInteractions.pressAndReleaseKeyOn(
+              element._getNativeInput(input.$.input), 8); // Backspace
+          flushAsynchronousOperations();
+          assert.equal(element.accounts.length, 1);
+          done();
+        });
       });
 
-      test('arrow key navigation', () => {
+      test('arrow key navigation', done => {
         const input = element.$.entry.$.input;
         input.text = '';
         element.accounts = [makeAccount(), makeAccount()];
-        MockInteractions.focus(input.$.input);
-        flushAsynchronousOperations();
-        const chips = element.accountChips;
-        const chipsOneSpy = sandbox.spy(chips[1], 'focus');
-        MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
-        assert.isTrue(chipsOneSpy.called);
-        const chipsZeroSpy = sandbox.spy(chips[0], 'focus');
-        MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left
-        assert.isTrue(chipsZeroSpy.called);
-        MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left
-        assert.isTrue(chipsZeroSpy.calledOnce);
-        MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right
-        assert.isTrue(chipsOneSpy.calledTwice);
+        flush(() => {
+          MockInteractions.focus(input.$.input);
+          flushAsynchronousOperations();
+          const chips = element.accountChips;
+          const chipsOneSpy = sandbox.spy(chips[1], 'focus');
+          MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
+          assert.isTrue(chipsOneSpy.called);
+          const chipsZeroSpy = sandbox.spy(chips[0], 'focus');
+          MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left
+          assert.isTrue(chipsZeroSpy.called);
+          MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left
+          assert.isTrue(chipsZeroSpy.calledOnce);
+          MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right
+          assert.isTrue(chipsOneSpy.calledTwice);
+          done();
+        });
       });
 
       test('delete', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
index b00fded..b0018df 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -31,11 +31,10 @@
       :host([toast]) {
         background-color: var(--tooltip-background-color);
         bottom: 1.25rem;
-        border-radius: 3px;
+        border-radius: var(--border-radius);
         box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
         color: var(--view-background-color);
         left: 1.25rem;
-        padding: 1em 1.5em;
         position: fixed;
         transform: translateY(5rem);
         transition: transform var(--gr-alert-transition-duration, 80ms) ease-out;
@@ -44,6 +43,17 @@
       :host([shown]) {
         transform: translateY(0);
       }
+      /**
+       * NOTE: To avoid style being overwritten by outside of the shadow DOM
+       * (as outside styles always win), .content-wrapper is introduced as a
+       * wrapper around main content to have better encapsulation, styles that
+       * may be affected by outside should be defined on it.
+       * In this case, `padding:0px` is defined in main.css for all elements
+       * with the universal selector: *.
+       */
+      .content-wrapper {
+        padding: var(--spacing-l) var(--spacing-xl);
+      }
       .text {
         color: var(--tooltip-text-color);
         display: inline-block;
@@ -55,19 +65,21 @@
       .action {
         color: var(--link-color);
         font-weight: var(--font-weight-bold);
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
         text-decoration: none;
         --gr-button: {
           padding: 0;
         }
       }
     </style>
-    <span class="text">[[text]]</span>
-    <gr-button
-        link
-        class="action"
-        hidden$="[[_hideActionButton]]"
-        on-tap="_handleActionTap">[[actionText]]</gr-button>
+    <div class="content-wrapper">
+      <span class="text">[[text]]</span>
+      <gr-button
+          link
+          class="action"
+          hidden$="[[_hideActionButton]]"
+          on-click="_handleActionTap">[[actionText]]</gr-button>
+    </div>
   </template>
   <script src="gr-alert.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index ec7b6eb..e7c8b2c 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-alert',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the action button is pressed.
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
index 095e640..2338d55 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-alert</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-alert.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
index 7b82635..9208068 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
@@ -15,10 +15,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/iron-fit-behavior/iron-fit-behavior.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <script src="../../../scripts/rootElement.js"></script>
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -40,7 +42,7 @@
         cursor: pointer;
         display: flex;
         justify-content: space-between;
-        padding: .5em .75em;
+        padding: var(--spacing-m) var(--spacing-l);
       }
       li:last-of-type {
         border: none;
@@ -67,7 +69,7 @@
       }
       .label {
         color: var(--deemphasized-text-color);
-        padding-left: 1em;
+        padding-left: var(--spacing-l);
       }
       .hide {
         display: none;
@@ -86,7 +88,7 @@
               aria-label$="[[item.name]]"
               class="autocompleteOption"
               role="option"
-              on-tap="_handleTapItem">
+              on-click="_handleClickItem">
             <span>[[item.text]]</span>
             <span class$="label [[_computeLabelClass(item)]]">[[item.label]]</span>
           </li>
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 1af629d2..b8c76ff 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-autocomplete-dropdown',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the dropdown is closed.
@@ -53,13 +52,11 @@
         value: () => [],
         observer: '_resetCursorStops',
       },
-      _suggestionEls: {
-        type: Array,
-        observer: '_resetCursorIndex',
-      },
+      _suggestionEls: Array,
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Polymer.IronFitBehavior,
     ],
@@ -78,9 +75,9 @@
 
     open() {
       this.isHidden = false;
-      this.refit();
       this._resetCursorStops();
-      this._resetCursorIndex();
+      // Refit should run after we call Polymer.flush inside _resetCursorStops
+      this.refit();
     },
 
     getCurrentText() {
@@ -138,7 +135,7 @@
       this.close();
     },
 
-    _handleTapItem(e) {
+    _handleClickItem(e) {
       e.preventDefault();
       e.stopPropagation();
       let selected = e.target;
@@ -147,7 +144,7 @@
         selected = selected.parentElement;
       }
       this.fire('item-selected', {
-        trigger: 'tap',
+        trigger: 'click',
         selected,
       });
     },
@@ -162,10 +159,12 @@
 
     _resetCursorStops() {
       if (this.suggestions.length > 0) {
-        Polymer.dom.flush();
-        // Polymer2: querySelectorAll returns NodeList instead of Array.
-        this._suggestionEls = Array.from(
-            this.$.suggestions.querySelectorAll('li'));
+        if (!this.isHidden) {
+          Polymer.dom.flush();
+          this._suggestionEls = Array.from(
+              this.$.suggestions.querySelectorAll('li'));
+          this._resetCursorIndex();
+        }
       } else {
         this._suggestionEls = [];
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
index d4d54ff..a7b59d7 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-autocomplete-dropdown</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-autocomplete-dropdown.html">
 
@@ -126,7 +128,7 @@
       MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[1]);
       flushAsynchronousOperations();
       assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
-        trigger: 'tap',
+        trigger: 'click',
         selected: element.$.suggestions.querySelectorAll('li')[1],
       });
     });
@@ -139,7 +141,7 @@
           .lastElementChild);
       flushAsynchronousOperations();
       assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
-        trigger: 'tap',
+        trigger: 'click',
         selected: element.$.suggestions.querySelectorAll('li')[0],
       });
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index a878174..c9d12ce 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -14,8 +14,9 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/paper-input/paper-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/paper-input/paper-input.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
@@ -32,21 +33,23 @@
         display: inline-block;
       }
       iron-icon {
-        margin: 0 .25em;
+        margin: 0 var(--spacing-xs);
+        vertical-align: top;
       }
       paper-input:not(.borderless) {
         border: 1px solid var(--border-color);
       }
       paper-input {
-        height: 100%;
+        height: var(--line-height-normal);
         width: 100%;
         @apply --gr-autocomplete;
         --paper-input-container: {
           padding: 0;
-        }
+        };
         --paper-input-container-input: {
           font-size: var(--font-size-normal);
-        }
+          line-height: var(--line-height-normal);
+        };
         --paper-input-container-underline: {
           display: none;
         };
@@ -60,7 +63,7 @@
       paper-input.warnUncommitted {
         --paper-input-container-input: {
           color: var(--error-text-color);
-          font-size: var(--font-size-normal);
+          font-size: inherit;
         }
       }
     </style>
@@ -75,14 +78,14 @@
         on-focus="_onInputFocus"
         on-blur="_onInputBlur"
         autocomplete="off">
-      <!-- slot is for future use (2.x) while prefix attribute is for 1.x
-        (current) -->
-      <iron-icon
+
+      <!-- prefix as attribute is required to for polymer 1 -->
+      <div slot="prefix" prefix>
+        <iron-icon
           icon="gr-icons:search"
-          slot="prefix"
-          prefix
           class$="searchIcon [[_computeShowSearchIconClass(showSearchIcon)]]">
-      </iron-icon>
+        </iron-icon>
+      </div>
     </paper-input>
     <gr-autocomplete-dropdown
         vertical-align="top"
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 9d16b9c..ee087cc 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-autocomplete',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a value is chosen.
@@ -168,6 +167,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
     ],
 
@@ -176,12 +176,17 @@
       '_updateSuggestions(text, threshold, noDebounce)',
     ],
 
+    get _nativeInput() {
+      // In Polymer 2 inputElement isn't nativeInput anymore
+      return this.$.input.$.nativeInput || this.$.input.inputElement;
+    },
+
     attached() {
-      this.listen(document.body, 'tap', '_handleBodyTap');
+      this.listen(document.body, 'click', '_handleBodyClick');
     },
 
     detached() {
-      this.unlisten(document.body, 'tap', '_handleBodyTap');
+      this.unlisten(document.body, 'click', '_handleBodyClick');
       this.cancelDebouncer('update-suggestions');
     },
 
@@ -190,11 +195,11 @@
     },
 
     focus() {
-      this.$.input.focus();
+      this._nativeInput.focus();
     },
 
     selectAll() {
-      const nativeInputElement = this.$.input.inputElement;
+      const nativeInputElement = this._nativeInput;
       if (!this.$.input.value) { return; }
       nativeInputElement.setSelectionRange(0, this.$.input.value.length);
     },
@@ -205,7 +210,7 @@
 
     _handleItemSelect(e) {
       // Let _handleKeydown deal with keyboard interaction.
-      if (e.detail.trigger !== 'tap') { return; }
+      if (e.detail.trigger !== 'click') { return; }
       this._selected = e.detail.selected;
       this._commit();
     },
@@ -242,8 +247,13 @@
     },
 
     _updateSuggestions(text, threshold, noDebounce) {
+      // Polymer 2: check for undefined
+      if ([text, threshold, noDebounce].some(arg => arg === undefined)) {
+        return;
+      }
+
       if (this._disableSuggestions) { return; }
-      if (text === undefined || text.length < threshold) {
+      if (text.length < threshold) {
         this._suggestions = [];
         this.value = '';
         return;
@@ -320,7 +330,7 @@
         default:
           // For any normal keypress, return focus to the input to allow for
           // unbroken user input.
-          this.$.input.inputElement.focus();
+          this.focus();
 
           // Since this has been a normal keypress, the suggestions will have
           // been based on a previous input. Clear them. This prevents an
@@ -365,7 +375,7 @@
       }
     },
 
-    _handleBodyTap(e) {
+    _handleBodyClick(e) {
       const eventPath = Polymer.dom(e).path;
       for (let i = 0; i < eventPath.length; i++) {
         if (eventPath[i] === this) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 1a76f98..ea1fd50 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reviewer-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-autocomplete.html">
 
@@ -80,16 +82,19 @@
       });
     });
 
-    test('selectAll', () => {
-      const nativeInput = element.$.input.inputElement;
-      const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange');
+    test('selectAll', done => {
+      flush(() => {
+        const nativeInput = element._nativeInput;
+        const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange');
 
-      element.selectAll();
-      assert.isFalse(selectionStub.called);
+        element.selectAll();
+        assert.isFalse(selectionStub.called);
 
-      element.$.input.value = 'test';
-      element.selectAll();
-      assert.isTrue(selectionStub.called);
+        element.$.input.value = 'test';
+        element.selectAll();
+        assert.isTrue(selectionStub.called);
+        done();
+      });
     });
 
     test('esc key behavior', done => {
@@ -318,12 +323,15 @@
       });
     });
 
-    test('search icon shows with showSearchIcon property', () => {
-      assert.equal(getComputedStyle(element.$$('iron-icon')).display,
-          'none');
-      element.showSearchIcon = true;
-      assert.notEqual(getComputedStyle(element.$$('iron-icon')).display,
-          'none');
+    test('search icon shows with showSearchIcon property', done => {
+      flush(() => {
+        assert.equal(getComputedStyle(element.$$('iron-icon')).display,
+            'none');
+        element.showSearchIcon = true;
+        assert.notEqual(getComputedStyle(element.$$('iron-icon')).display,
+            'none');
+        done();
+      });
     });
 
     test('vertical offset overridden by param if it exists', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
index bc63acf..1daffa2 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
@@ -28,7 +28,7 @@
         display: inline-block;
         border-radius: 50%;
         background-size: cover;
-        background-color: var(--background-color, #f1f2f3);
+        background-color: var(--avatar-background-color, #f1f2f3);
       }
     </style>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 2435e58..bf56382 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-avatar',
-    _legacyUndefinedCheck: true,
 
     properties: {
       account: {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index 63718c5..de05a5c 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-avatar</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-avatar.html">
 
@@ -115,7 +117,7 @@
       assert.strictEqual(element.style.backgroundImage, '');
 
       // Emulate plugins loaded.
-      Gerrit._setPluginsPending([]);
+      Gerrit._loadPlugins([]);
 
       Promise.all([
         element.$.restAPI.getConfig(),
@@ -127,16 +129,33 @@
             element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
       });
     });
+  });
+
+  suite('plugin has avatars', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+
+      stub('gr-avatar', {
+        _getConfig: () => {
+          return Promise.resolve({plugin: {has_avatars: true}});
+        },
+      });
+
+      element = fixture('basic');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
 
     test('dom for non available account', () => {
       assert.isFalse(element.hasAttribute('hidden'));
 
-      sandbox.stub(element, '_getConfig', () => {
-        return Promise.resolve({plugin: {has_avatars: true}});
-      });
-
       // Emulate plugins loaded.
-      Gerrit._setPluginsPending([]);
+      Gerrit._loadPlugins([]);
 
       return Promise.all([
         element.$.restAPI.getConfig(),
@@ -147,45 +166,45 @@
         assert.strictEqual(element.style.backgroundImage, '');
       });
     });
+  });
 
-    test('avatar config not set and account not set', () => {
-      assert.isFalse(element.hasAttribute('hidden'));
+  suite('config not set', () => {
+    let element;
+    let sandbox;
 
-      sandbox.stub(element, '_getConfig', () => {
-        return Promise.resolve({});
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+
+      stub('gr-avatar', {
+        _getConfig: () => {
+          return Promise.resolve({});
+        },
       });
 
-      // Emulate plugins loaded.
-      Gerrit._setPluginsPending([]);
-
-      return Promise.all([
-        element.$.restAPI.getConfig(),
-        Gerrit.awaitPluginsLoaded(),
-      ]).then(() => {
-        assert.isTrue(element.hasAttribute('hidden'));
-      });
+      element = fixture('basic');
     });
 
-    test('avatar config not set and account set', () => {
-      assert.isFalse(element.hasAttribute('hidden'));
+    teardown(() => {
+      sandbox.restore();
+    });
 
-      sandbox.stub(element, '_getConfig', () => {
-        return Promise.resolve({});
-      });
+    test('avatar hidden when account set', () => {
+      flush(() => {
+        assert.isFalse(element.hasAttribute('hidden'));
 
-      element.imageSize = 64;
-      element.account = {
-        _account_id: 123,
-      };
+        element.imageSize = 64;
+        element.account = {
+          _account_id: 123,
+        };
+        // Emulate plugins loaded.
+        Gerrit._loadPlugins([]);
 
-      // Emulate plugins loaded.
-      Gerrit._setPluginsPending([]);
-
-      return Promise.all([
-        element.$.restAPI.getConfig(),
-        Gerrit.awaitPluginsLoaded(),
-      ]).then(() => {
-        assert.isTrue(element.hasAttribute('hidden'));
+        return Promise.all([
+          element.$.restAPI.getConfig(),
+          Gerrit.awaitPluginsLoaded(),
+        ]).then(() => {
+          assert.isTrue(element.hasAttribute('hidden'));
+        });
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index cdf617f..87caf64 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/paper-button/paper-button.html">
+<link rel="import" href="/bower_components/paper-button/paper-button.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-button">
@@ -39,6 +39,37 @@
         text-transform: none;
       }
       paper-button {
+        /* The next lines contains a copy of paper-button style.
+          Without a copy, the @apply works incorrectly with Polymer 2.
+          @apply is deprecated and is not recommended to use. It is expected
+          that @apply will be replaced with the ::part CSS pseudo-element.
+          After replacecment copied lines can be removed.
+        */
+        @apply --layout-inline;
+        @apply --layout-center-center;
+        position: relative;
+        box-sizing: border-box;
+        min-width: 5.14em;
+        margin: 0 0.29em;
+        background: transparent;
+        -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+        -webkit-tap-highlight-color: transparent;
+        font: inherit;
+        text-transform: uppercase;
+        outline-width: 0;
+        border-radius: var(--border-radius);
+        -moz-user-select: none;
+        -ms-user-select: none;
+        -webkit-user-select: none;
+        user-select: none;
+        cursor: pointer;
+        z-index: 0;
+        padding: var(--spacing-m);
+
+        @apply --paper-font-common-base;
+        @apply --paper-button;
+        /* End of copy*/
+
         /* paper-button sets this to anti-aliased, which appears different than
           bold font elsewhere on macOS. */
         -webkit-font-smoothing: initial;
@@ -53,6 +84,24 @@
         padding: var(--padding, 4px 8px);
         @apply --gr-button;
       }
+      /* https://github.com/PolymerElements/paper-button/blob/2.x/paper-button.html */
+      /* BEGIN: Copy from paper-button */
+      paper-button[elevation="1"] {
+        @apply --paper-material-elevation-1;
+      }
+      paper-button[elevation="2"] {
+        @apply --paper-material-elevation-2;
+      }
+      paper-button[elevation="3"] {
+        @apply --paper-material-elevation-3;
+      }
+      paper-button[elevation="4"] {
+        @apply --paper-material-elevation-4;
+      }
+      paper-button[elevation="5"] {
+        @apply --paper-material-elevation-5;
+      }
+      /* END: Copy from paper-button */
       paper-button:hover {
         background: linear-gradient(
           rgba(0, 0, 0, .12),
@@ -91,6 +140,8 @@
       }
       :host([disabled][link]) {
         --background-color: transparent;
+        --text-color: var(--deemphasized-text-color);
+        cursor: default;
       }
 
       /* Styles for the optional down arrow */
@@ -101,8 +152,8 @@
         border-top: .36em solid #ccc;
         border-left: .36em solid transparent;
         border-right: .36em solid transparent;
-        margin-bottom: .05em;
-        margin-left: .5em;
+        margin-bottom: var(--spacing-xxs);
+        margin-left: var(--spacing-m);
         transition: border-top-color 200ms;
       }
       :host([down-arrow]) paper-button:hover .downArrow {
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 5988cde..afc6ba8 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-button',
-    _legacyUndefinedCheck: true,
 
     properties: {
       tooltip: String,
@@ -53,7 +52,6 @@
     },
 
     listeners: {
-      tap: '_handleAction',
       click: '_handleAction',
       keydown: '_handleKeydown',
     },
@@ -81,7 +79,7 @@
 
     _disabledChanged(disabled) {
       if (disabled) {
-        this._enabledTabindex = this.getAttribute('tabindex');
+        this._enabledTabindex = this.getAttribute('tabindex') || '0';
       }
       this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex);
       this.updateStyles();
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
index ed0da2e..ef593c0 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-button</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-button.html">
 
@@ -39,7 +41,11 @@
 
     const addSpyOn = function(eventName) {
       const spy = sandbox.spy();
-      element.addEventListener(eventName, spy);
+      if (eventName == 'tap') {
+        Polymer.Gestures.addListener(element, eventName, spy);
+      } else {
+        element.addEventListener(eventName, spy);
+      }
       return spy;
     };
 
@@ -62,6 +68,23 @@
       assert.isTrue(element.$$('paper-button').disabled);
     });
 
+    test('tabindex should be -1 if disabled', () => {
+      element.disabled = true;
+      assert.isTrue(element.getAttribute('tabindex') === '-1');
+    });
+
+    // Regression tests for Issue: 11969
+    test('tabindex should be reset to 0 if enabled', () => {
+      element.disabled = false;
+      assert.isTrue(element.getAttribute('tabindex') === '0');
+      element.disabled = true;
+      assert.isTrue(element.getAttribute('tabindex') === '-1');
+      element.disabled = false;
+      assert.isTrue(element.getAttribute('tabindex') === '0');
+    });
+
+    // 'tap' event is tested so we don't loose backward compatibility with older
+    // plugins who didn't move to on-click which is faster and well supported.
     for (const eventName of ['tap', 'click']) {
       test('dispatches ' + eventName + ' event', () => {
         const spy = addSpyOn(eventName);
@@ -72,16 +95,16 @@
 
     // Keycodes: 32 for Space, 13 for Enter.
     for (const key of [32, 13]) {
-      test('dispatches tap event on keycode ' + key, () => {
+      test('dispatches click event on keycode ' + key, () => {
         const tapSpy = sandbox.spy();
-        element.addEventListener('tap', tapSpy);
+        element.addEventListener('click', tapSpy);
         MockInteractions.pressAndReleaseKeyOn(element, key);
         assert.isTrue(tapSpy.calledOnce);
       });
 
-      test('dispatches no tap event with modifier on keycode ' + key, () => {
+      test('dispatches no click event with modifier on keycode ' + key, () => {
         const tapSpy = sandbox.spy();
-        element.addEventListener('tap', tapSpy);
+        element.addEventListener('click', tapSpy);
         MockInteractions.pressAndReleaseKeyOn(element, key, 'shift');
         MockInteractions.pressAndReleaseKeyOn(element, key, 'ctrl');
         MockInteractions.pressAndReleaseKeyOn(element, key, 'meta');
@@ -105,9 +128,9 @@
 
       // Keycodes: 32 for Space, 13 for Enter.
       for (const key of [32, 13]) {
-        test('stops tap event on keycode ' + key, () => {
+        test('stops click event on keycode ' + key, () => {
           const tapSpy = sandbox.spy();
-          element.addEventListener('tap', tapSpy);
+          element.addEventListener('click', tapSpy);
           MockInteractions.pressAndReleaseKeyOn(element, key);
           assert.isFalse(tapSpy.called);
         });
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
index a14c652..3774529 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -30,7 +30,7 @@
         fill: var(--link-color);
       }
     </style>
-    <button aria-label="Change star" on-tap="toggleStar">
+    <button aria-label="Change star" on-click="toggleStar">
       <iron-icon
           class$="[[_computeStarClass(change.starred)]]"
           icon$="[[_computeStarIcon(change.starred)]]"></iron-icon>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 44f8c00..a83bc2b 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-change-star',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when star state is toggled.
@@ -49,6 +48,7 @@
       this.set('change.starred', newVal);
       this.dispatchEvent(new CustomEvent('toggle-star', {
         bubbles: true,
+        composed: true,
         detail: {change: this.change, starred: newVal},
       }));
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
index 0ca9368..7ee22a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-star</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-star.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
index 99ddff1..55623b3 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
@@ -25,11 +25,9 @@
   <template>
     <style include="shared-styles">
       .chip {
-        border-radius: 4px;
+        border-radius: var(--border-radius);
         background-color: var(--chip-background-color);
-        font-family: var(--font-family);
-        font-size: var(--font-size-normal);
-        padding: .1em .5em;
+        padding: var(--spacing-xxs) var(--spacing-m);
         white-space: nowrap;
       }
       :host(.merged) .chip {
@@ -66,7 +64,7 @@
       }
       :host([flat]) .chip {
         background-color: transparent;
-        padding: .1em;
+        padding: var(--spacing-xxs);
       }
       :host(:not([flat])) .chip {
         color: white;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 70c5d72..e6f52c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -29,12 +29,15 @@
       'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
       'and email notifications will be silenced until the review is started.';
 
+  const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' +
+      'Download the patch and run "git rebase master". ' +
+      'Upload a new patchset after resolving all merge conflicts.';
+
   const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
       'current reviewers (or anyone with "View Private Changes" permission).';
 
   Polymer({
     is: 'gr-change-status',
-    _legacyUndefinedCheck: true,
 
     properties: {
       flat: {
@@ -76,6 +79,9 @@
         case ChangeStates.PRIVATE:
           this.tooltipText = PRIVATE_TOOLTIP;
           break;
+        case ChangeStates.MERGE_CONFLICT:
+          this.tooltipText = MERGE_CONFLICT_TOOLTIP;
+          break;
         default:
           this.tooltipText = '';
           break;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
index f73fc02..421c6ab5 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-status</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-status.html">
 
@@ -33,6 +35,17 @@
 </test-fixture>
 
 <script>
+  const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
+      'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
+      'and email notifications will be silenced until the review is started.';
+
+  const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' +
+    'Download the patch and run "git rebase master". ' +
+    'Upload a new patchset after resolving all merge conflicts.';
+
+  const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
+      'current reviewers (or anyone with "View Private Changes" permission).';
+
   suite('gr-change-status tests', () => {
     let element;
     let sandbox;
@@ -49,7 +62,7 @@
     test('WIP', () => {
       element.status = 'WIP';
       assert.equal(element.$$('.chip').innerText, 'Work in Progress');
-      assert.isDefined(element.tooltipText);
+      assert.equal(element.tooltipText, WIP_TOOLTIP);
       assert.isTrue(element.classList.contains('wip'));
     });
 
@@ -79,14 +92,14 @@
     test('merge conflict', () => {
       element.status = 'Merge Conflict';
       assert.equal(element.$$('.chip').innerText, element.status);
-      assert.equal(element.tooltipText, '');
+      assert.equal(element.tooltipText, MERGE_CONFLICT_TOOLTIP);
       assert.isTrue(element.classList.contains('merge-conflict'));
     });
 
     test('private', () => {
       element.status = 'Private';
       assert.equal(element.$$('.chip').innerText, element.status);
-      assert.isDefined(element.tooltipText);
+      assert.equal(element.tooltipText, PRIVATE_TOOLTIP);
       assert.isTrue(element.classList.contains('private'));
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
index 8c80b37..bbd7ddf 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
@@ -27,20 +28,32 @@
 <dom-module id="gr-comment-thread">
   <template>
     <style include="shared-styles">
+      :host {
+        font-family: var(--font-family);
+        font-size: var(--font-size-normal);
+        font-weight: var(--font-weight-normal);
+        line-height: var(--line-height-normal);
+      }
       gr-button {
-        margin-left: .5em;
+        margin-left: var(--spacing-m);
       }
       #actions {
         margin-left: auto;
-        padding: .5em .7em;
+        padding: var(--spacing-m);
       }
       #container {
         background-color: var(--comment-background-color);
-        border: 1px solid var(--border-color);
         color: var(--comment-text-color);
         display: block;
-        margin-bottom: 1px;
+        margin: 0 4px 4px 4px;
         white-space: normal;
+        box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
+        border-radius: var(--border-radius);
+        /** This is required for firefox to continue the inheritance */
+        -webkit-user-select: inherit;
+        -moz-user-select: inherit;
+        -ms-user-select: inherit;
+        user-select: inherit;
       }
       #container.unresolved {
         background-color: var(--unresolved-comment-background-color);
@@ -52,14 +65,14 @@
       #unresolvedLabel {
         font-family: var(--font-family);
         margin: auto 0;
-        padding: .5em .7em;
+        padding: var(--spacing-m);
       }
       .pathInfo {
         display: flex;
         align-items: baseline;
       }
       .descriptionText {
-        margin-left: .5rem;
+        margin-left: var(--spacing-m);
         font-style: italic;
       }
     </style>
@@ -96,25 +109,25 @@
               link
               secondary
               class="action reply"
-              on-tap="_handleCommentReply">Reply</gr-button>
+              on-click="_handleCommentReply">Reply</gr-button>
           <gr-button
               id="quoteBtn"
               link
               secondary
               class="action quote"
-              on-tap="_handleCommentQuote">Quote</gr-button>
+              on-click="_handleCommentQuote">Quote</gr-button>
           <gr-button
               id="ackBtn"
               link
               secondary
               class="action ack"
-              on-tap="_handleCommentAck">Ack</gr-button>
+              on-click="_handleCommentAck">Ack</gr-button>
           <gr-button
               id="doneBtn"
               link
               secondary
               class="action done"
-              on-tap="_handleCommentDone">Done</gr-button>
+              on-click="_handleCommentDone">Done</gr-button>
         </div>
       </div>
     </div>
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 0b4f806..c220ecf 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
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-comment-thread',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the thread should be discarded.
@@ -127,6 +126,10 @@
     },
 
     behaviors: [
+      /**
+       * Not used in this element rather other elements tests
+       */
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PathListBehavior,
     ],
@@ -209,7 +212,8 @@
     },
 
     _hideActions(_showActions, _lastComment) {
-      return !_showActions || !_lastComment || !!_lastComment.__draft;
+      return !_showActions || !_lastComment || !!_lastComment.__draft ||
+        !!_lastComment.robot_id;
     },
 
     _getLastComment() {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
index 74c6f5f..d3946e6 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-comment-thread</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
@@ -159,6 +161,9 @@
       showActions = true;
       lastComment.__draft = true;
       assert.equal(element._hideActions(showActions, lastComment), true);
+      const robotComment = {};
+      robotComment.robot_id = true;
+      assert.equal(element._hideActions(showActions, robotComment), true);
     });
 
     test('setting project name loads the project config', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
index a470285..8ad261c 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
@@ -15,9 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
@@ -41,7 +42,7 @@
       :host {
         display: block;
         font-family: var(--font-family);
-        padding: .7em .7em;
+        padding: var(--spacing-m);
         --iron-autogrow-textarea: {
           box-sizing: border-box;
           padding: 2px;
@@ -62,23 +63,17 @@
         align-items: baseline;
         cursor: pointer;
         display: flex;
-        font-family: 'Open Sans', sans-serif;
-        margin: -.7em -.7em 0 -.7em;
-        padding: .7em;
+        margin: calc(0px - var(--spacing-m)) calc(0px - var(--spacing-m)) 0 calc(0px - var(--spacing-m));
+        padding: var(--spacing-m);
       }
       .container.collapsed .header {
-        margin-bottom: -.7em;
+        margin-bottom: calc(0 - var(--spacing-m));
       }
       .headerMiddle {
         color: var(--deemphasized-text-color);
         flex: 1;
         overflow: hidden;
       }
-      .authorName,
-      .draftLabel,
-      .draftTooltip {
-        font-weight: var(--font-weight-bold);
-      }
       .draftLabel,
       .draftTooltip {
         color: var(--deemphasized-text-color);
@@ -103,25 +98,33 @@
         padding-top: 0;
       }
       .action {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
       }
       .robotActions {
         display: flex;
         justify-content: flex-start;
-        padding-top: 0;
+        padding-top: var(--spacing-m);
+        border-top: 1px solid var(--border-color);
       }
       .robotActions .action {
         /* Keep button text lined up with output text */
-        margin-left: -.3rem;
-        margin-right: 1em;
+        margin-left: -4px;
+        margin-right: var(--spacing-l);
       }
       .rightActions {
         display: flex;
         justify-content: flex-end;
       }
+      .rightActions gr-button {
+        --gr-button: {
+          height: 20px;
+          padding: 0 var(--spacing-s);
+          color: var(--default-button-text-color);
+        }
+      }
       .editMessage {
         display: none;
-        margin: .5em 0;
+        margin: var(--spacing-m) 0;
         width: 100%;
       }
       .container:not(.draft) .actions .hideOnPublished {
@@ -155,38 +158,37 @@
         display: block;
       }
       .show-hide {
-        margin-left: .4em;
+        margin-left: var(--spacing-s);
       }
       .robotId {
         color: var(--deemphasized-text-color);
-        margin-bottom: .8em;
+        margin-bottom: var(--spacing-m);
         margin-top: -.4em;
       }
       .robotIcon {
-        margin-right: .2em;
+        margin-right: var(--spacing-xs);
         /* because of the antenna of the robot, it looks off center even when it
          is centered. artificially adjust margin to account for this. */
-        margin-top: -.3em;
+        margin-top: -4px;
       }
       .runIdInformation {
-        margin: .7em 0;
+        margin: var(--spacing-m) 0;
       }
       .robotRun {
-        margin-left: .5em;
+        margin-left: var(--spacing-m);
       }
       .robotRunLink {
-        margin-left: .5em;
+        margin-left: var(--spacing-m);
       }
       input.show-hide {
         display: none;
       }
       label.show-hide {
-        color: var(--comment-text-color);
         cursor: pointer;
         display: block;
-        font-size: .8rem;
-        height: 1.1em;
-        margin-top: .1em;
+      }
+      label.show-hide iron-icon {
+        vertical-align: top;
       }
       #container .collapsedContent {
         display: none;
@@ -231,9 +233,19 @@
       #deleteBtn.showDeleteButtons {
         display: block;
       }
+
+      /** Disable select for the caret and actions */
+      .actions,
+      .show-hide {
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+      }
+
     </style>
     <div id="container" class="container">
-      <div class="header" id="header" on-tap="_handleToggleCollapsed">
+      <div class="header" id="header" on-click="_handleToggleCollapsed">
         <div class="headerLeft">
           <span class="authorName">[[comment.author.name]]</span>
           <span class="draftLabel">DRAFT</span>
@@ -251,10 +263,10 @@
             link
             secondary
             class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]"
-            on-tap="_handleCommentDelete">
+            on-click="_handleCommentDelete">
           (Delete)
         </gr-button>
-        <span class="date" on-tap="_handleAnchorTap">
+        <span class="date" on-click="_handleAnchorClick">
           <gr-date-formatter
               has-tooltip
               date-str="[[comment.updated]]"></gr-date-formatter>
@@ -264,7 +276,10 @@
             <input type="checkbox" class="show-hide"
                checked$="[[collapsed]]"
                on-change="_handleToggleCollapsed">
-            [[_computeShowHideText(collapsed)]]
+            <iron-icon
+                id="icon"
+                icon="[[_computeShowHideIcon(collapsed)]]">
+            </iron-icon>
           </label>
         </div>
       </div>
@@ -280,7 +295,7 @@
               id="editTextarea"
               class="editMessage"
               autocomplete="on"
-              monospace
+              code
               disabled="{{disabled}}"
               rows="4"
               text="{{_messageText}}"></gr-textarea>
@@ -320,23 +335,23 @@
                 link
                 secondary
                 class="action cancel hideOnPublished"
-                on-tap="_handleCancel">Cancel</gr-button>
+                on-click="_handleCancel">Cancel</gr-button>
             <gr-button
                 link
                 secondary
                 class="action discard hideOnPublished"
-                on-tap="_handleDiscard">Discard</gr-button>
+                on-click="_handleDiscard">Discard</gr-button>
             <gr-button
                 link
                 secondary
                 class="action edit hideOnPublished"
-                on-tap="_handleEdit">Edit</gr-button>
+                on-click="_handleEdit">Edit</gr-button>
             <gr-button
                 link
                 secondary
                 disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
                 class="action save hideOnPublished"
-                on-tap="_handleSave">Save</gr-button>
+                on-click="_handleSave">Save</gr-button>
           </div>
         </div>
         <div class="robotActions" hidden$="[[!_showRobotActions]]">
@@ -345,7 +360,7 @@
                 link
                 secondary
                 class="action fix"
-                on-tap="_handleFix"
+                on-click="_handleFix"
                 disabled="[[robotButtonDisabled]]">
               Please Fix
             </gr-button>
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 b699b8b..6881929 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -33,7 +33,6 @@
 
   Polymer({
     is: 'gr-comment',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the create fix comment action is triggered.
@@ -128,7 +127,8 @@
 
       _numPendingDraftRequests: {
         type: Object,
-        value: {number: 0}, // Intentional to share the object across instances.
+        value:
+            {number: 0}, // Intentional to share the object across instances.
       },
 
       _enableOverlay: {
@@ -156,6 +156,7 @@
     ],
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
     ],
 
@@ -204,11 +205,16 @@
       return this._overlays.confirmDiscard;
     },
 
-    _computeShowHideText(collapsed) {
-      return collapsed ? '◀' : '▼';
+    _computeShowHideIcon(collapsed) {
+      return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
     },
 
     _calculateActionstoShow(showActions, isRobotComment) {
+      // Polymer 2: check for undefined
+      if ([showActions, isRobotComment].some(arg => arg === undefined)) {
+        return;
+      }
+
       this._showHumanActions = showActions && !isRobotComment;
       this._showRobotActions = showActions && isRobotComment;
     },
@@ -230,7 +236,9 @@
      */
     save(opt_comment) {
       let comment = opt_comment;
-      if (!comment) { comment = this.comment; }
+      if (!comment) {
+        comment = this.comment;
+      }
 
       this.set('comment.message', this._messageText);
       this.editing = false;
@@ -315,6 +323,11 @@
     },
 
     _editingChanged(editing, previousValue) {
+      // Polymer 2: observer fires when at least one property is defined.
+      // Do nothing to prevent comment.__editing being overwritten
+      // if previousValue is undefined
+      if (previousValue === undefined) return;
+
       this.$.container.classList.toggle('editing', editing);
       if (this.comment && this.comment.id) {
         this.$$('.cancel').hidden = !editing;
@@ -340,7 +353,9 @@
 
     _computeSaveDisabled(draft, comment, resolved) {
       // If resolved state has changed and a msg exists, save should be enabled.
-      if (comment.unresolved === resolved && draft) { return false; }
+      if (!comment || comment.unresolved === resolved && draft) {
+        return false;
+      }
       return !draft || draft.trim() === '';
     },
 
@@ -376,7 +391,9 @@
     },
 
     _messageTextChanged(newValue, oldValue) {
-      if (!this.comment || (this.comment && this.comment.id)) { return; }
+      if (!this.comment || (this.comment && this.comment.id)) {
+        return;
+      }
 
       this.debounce('store', () => {
         const message = this._messageText;
@@ -398,11 +415,14 @@
       }, STORAGE_DEBOUNCE_INTERVAL);
     },
 
-    _handleAnchorTap(e) {
+    _handleAnchorClick(e) {
       e.preventDefault();
-      if (!this.comment.line) { return; }
+      if (!this.comment.line) {
+        return;
+      }
       this.dispatchEvent(new CustomEvent('comment-anchor-tap', {
         bubbles: true,
+        composed: true,
         detail: {
           number: this.comment.line || FILE,
           side: this.side,
@@ -421,7 +441,9 @@
       e.preventDefault();
 
       // Ignore saves started while already saving.
-      if (this.disabled) { return; }
+      if (this.disabled) {
+        return;
+      }
       const timingLabel = this.comment.id ?
         REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
       const timer = this.$.reporting.getTimer(timingLabel);
@@ -450,6 +472,7 @@
     _handleFix() {
       this.dispatchEvent(new CustomEvent('create-fix-comment', {
         bubbles: true,
+        composed: true,
         detail: this._getEventPayload(),
       }));
     },
@@ -512,7 +535,9 @@
     },
 
     _getSavingMessage(numPending) {
-      if (numPending === 0) { return SAVED_MESSAGE; }
+      if (numPending === 0) {
+        return SAVED_MESSAGE;
+      }
       return [
         SAVING_MESSAGE,
         numPending,
@@ -544,8 +569,8 @@
         // 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
         // case the event would not bubble.
-        document.body.dispatchEvent(new CustomEvent('show-alert',
-            {detail: {message}, bubbles: true}));
+        document.body.dispatchEvent(new CustomEvent(
+            'show-alert', {detail: {message}, bubbles: true, composed: true}));
       }, TOAST_DEBOUNCE_INTERVAL);
     },
 
@@ -580,6 +605,11 @@
     },
 
     _loadLocalDraft(changeNum, patchNum, comment) {
+      // Polymer 2: check for undefined
+      if ([changeNum, patchNum, comment].some(arg => arg === undefined)) {
+        return;
+      }
+
       // Only apply local drafts to comments that haven't been saved
       // remotely, and haven't been given a default message already.
       //
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
index 7ca5242..c829343 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-comment</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="gr-comment.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
index 9decfa9..62ab307 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
@@ -15,8 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -42,6 +43,8 @@
       }
       iron-autogrow-textarea {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
         padding: 0;
         width: 73ch; /* Add a char to account for the border. */
 
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
index 4ac059d..c2075f0 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-confirm-delete-comment-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -37,17 +36,23 @@
       message: String,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     resetFocus() {
       this.$.messageInput.textarea.focus();
     },
 
     _handleConfirmTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('confirm', {reason: this.message}, {bubbles: false});
     },
 
     _handleCancelTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('cancel', null, {bubbles: false});
     },
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
index 32ca557..f58db39 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
@@ -31,36 +31,53 @@
       }
       .copyText {
         flex-grow: 1;
-        margin-right: .3em;
+        margin-right: var(--spacing-s);
       }
       .hideInput {
         display: none;
       }
-      input {
+      input#input {
         font-family: var(--monospace-font-family);
-        font-size: inherit;
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
         @apply --text-container-style;
+        width: 100%;
       }
       #icon {
         height: 1.2em;
         width: 1.2em;
       }
+      gr-button {
+        --gr-button: {
+          padding: 1px 4px;
+        }
+      }
+
     </style>
     <div class="text">
-        <input id="input" is="iron-input"
-            class$="copyText [[_computeInputClass(hideInput)]]"
+      <iron-input
+          class="copyText"
+          type="text"
+          bind-value="[[text]]"
+          on-tap="_handleInputClick"
+          readonly>
+        <input
+            id="input"
+            is="iron-input"
+            class$="[[_computeInputClass(hideInput)]]"
             type="text"
             bind-value="[[text]]"
-            on-tap="_handleInputTap"
+            on-click="_handleInputClick"
             readonly>
-        <gr-button id="button"
-            link
-            has-tooltip="[[hasTooltip]]"
-            class="copyToClipboard"
-            title="[[buttonTitle]]"
-            on-tap="_copyToClipboard">
-          <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
-        </gr-button>
+      </iron-input>
+      <gr-button id="button"
+          link
+          has-tooltip="[[hasTooltip]]"
+          class="copyToClipboard"
+          title="[[buttonTitle]]"
+          on-click="_copyToClipboard">
+        <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
+      </gr-button>
     </div>
   </template>
   <script src="gr-copy-clipboard.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
index 550f1df..3e87202 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -21,7 +21,6 @@
 
   Polymer({
     is: 'gr-copy-clipboard',
-    _legacyUndefinedCheck: true,
 
     properties: {
       text: String,
@@ -44,7 +43,7 @@
       return hideInput ? 'hideInput' : '';
     },
 
-    _handleInputTap(e) {
+    _handleInputClick(e) {
       e.preventDefault();
       Polymer.dom(e).rootTarget.select();
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
index d6e9dca..9cec20e 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-copy-clipboard</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-copy-clipboard.html">
 
@@ -37,12 +39,13 @@
     let element;
     let sandbox;
 
-    setup(() => {
+    setup(done => {
       sandbox = sinon.sandbox.create();
       element = fixture('basic');
       element.text = `git fetch http://gerrit@localhost:8080/a/test-project
           refs/changes/05/5/1 && git checkout FETCH_HEAD`;
       flushAsynchronousOperations();
+      flush(done);
     });
 
     teardown(() => {
@@ -62,7 +65,7 @@
           element.$$('.copyToClipboard'));
     });
 
-    test('_handleInputTap', () => {
+    test('_handleInputClick', () => {
       const inputElement = element.$$('input');
       MockInteractions.tap(inputElement);
       assert.equal(inputElement.selectionStart, 0);
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
index e4d896b..d061ac2 100644
--- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-count-string-formatter</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-count-string-formatter.html"/>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
index d619b18..94d7aaa 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-cursor-manager">
   <template></template>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 766ac7f..b97726e 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -24,7 +24,6 @@
 
   Polymer({
     is: 'gr-cursor-manager',
-    _legacyUndefinedCheck: true,
 
     properties: {
       stops: {
@@ -92,8 +91,22 @@
       this.unsetCursor();
     },
 
-    next(opt_condition, opt_getTargetHeight) {
-      this._moveCursor(1, opt_condition, opt_getTargetHeight);
+    /**
+     * Move the cursor forward. Clipped to the ends of the stop list.
+     *
+     * @param {!Function=} opt_condition Optional stop condition. If a condition
+     *    is passed the cursor will continue to move in the specified direction
+     *    until the condition is met.
+     * @param {!Function=} opt_getTargetHeight Optional function to calculate the
+     *    height of the target's 'section'. The height of the target itself is
+     *    sometimes different, used by the diff cursor.
+     * @param {boolean=} opt_clipToTop When none of the next indices match, move
+     *     back to first instead of to last.
+     * @private
+     */
+
+    next(opt_condition, opt_getTargetHeight, opt_clipToTop) {
+      this._moveCursor(1, opt_condition, opt_getTargetHeight, opt_clipToTop);
     },
 
     previous(opt_condition) {
@@ -148,8 +161,8 @@
     },
 
     /**
-     * Move the cursor forward or backward by delta. Noop if moving past either
-     * end of the stop list.
+     * Move the cursor forward or backward by delta. Clipped to the beginning or
+     * end of stop list.
      *
      * @param {number} delta either -1 or 1.
      * @param {!Function=} opt_condition Optional stop condition. If a condition
@@ -158,9 +171,11 @@
      * @param {!Function=} opt_getTargetHeight Optional function to calculate the
      *    height of the target's 'section'. The height of the target itself is
      *    sometimes different, used by the diff cursor.
+     * @param {boolean=} opt_clipToTop When none of the next indices match, move
+     *     back to first instead of to last.
      * @private
      */
-    _moveCursor(delta, opt_condition, opt_getTargetHeight) {
+    _moveCursor(delta, opt_condition, opt_getTargetHeight, opt_clipToTop) {
       if (!this.stops.length) {
         this.unsetCursor();
         return;
@@ -168,7 +183,7 @@
 
       this._unDecorateTarget();
 
-      const newIndex = this._getNextindex(delta, opt_condition);
+      const newIndex = this._getNextindex(delta, opt_condition, opt_clipToTop);
 
       let newTarget = null;
       if (newIndex !== -1) {
@@ -208,10 +223,12 @@
      *
      * @param {number} delta either -1 or 1.
      * @param {!Function=} opt_condition Optional stop condition.
+     * @param {boolean=} opt_clipToTop When none of the next indices match, move
+     *     back to first instead of to last.
      * @return {number} the new index.
      * @private
      */
-    _getNextindex(delta, opt_condition) {
+    _getNextindex(delta, opt_condition, opt_clipToTop) {
       if (!this.stops.length || this.index === -1) {
         return -1;
       }
@@ -227,10 +244,10 @@
 
       // If we failed to satisfy the condition:
       if (opt_condition && !opt_condition(this.stops[newIndex])) {
-        if (delta > 0) {
-          return this.stops.length - 1;
-        } else if (delta < 0) {
+        if (delta < 0 || opt_clipToTop) {
           return 0;
+        } else if (delta > 0) {
+          return this.stops.length - 1;
         }
         return this.index;
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
index adbe618..0793ccd 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-cursor-manager</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-cursor-manager.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index 481dd2f..ae5a945 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -31,7 +31,7 @@
       }
     </style>
     <span>
-      [[_computeDateStr(dateStr, _timeFormat, _relative, showDateAndTime)]]
+      [[_computeDateStr(dateStr, _timeFormat, _dateFormat, _relative, showDateAndTime)]]
     </span>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
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 a9ce4c9..545a7c3 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
@@ -27,13 +27,33 @@
     TIME_12_WITH_SEC: 'h:mm:ss A', // 2:14:00 PM
     TIME_24: 'HH:mm', // 14:14
     TIME_24_WITH_SEC: 'HH:mm:ss', // 14:14:00
-    MONTH_DAY: 'MMM DD', // Aug 29
-    MONTH_DAY_YEAR: 'MMM DD, YYYY', // Aug 29, 1997
+  };
+
+  const DateFormats = {
+    STD: {
+      short: 'MMM DD', // Aug 29
+      full: 'MMM DD, YYYY', // Aug 29, 1997
+    },
+    US: {
+      short: 'MM/DD', // 08/29
+      full: 'MM/DD/YY', // 08/29/97
+    },
+    ISO: {
+      short: 'MM-DD', // 08-29
+      full: 'YYYY-MM-DD', // 1997-08-29
+    },
+    EURO: {
+      short: 'DD. MMM', // 29. Aug
+      full: 'DD.MM.YYYY', // 29.08.1997
+    },
+    UK: {
+      short: 'DD/MM', // 29/08
+      full: 'DD/MM/YYYY', // 29/08/1997
+    },
   };
 
   Polymer({
     is: 'gr-date-formatter',
-    _legacyUndefinedCheck: true,
 
     properties: {
       dateStr: {
@@ -58,9 +78,11 @@
       title: {
         type: String,
         reflectToAttribute: true,
-        computed: '_computeFullDateStr(dateStr, _timeFormat)',
+        computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)',
       },
 
+      /** @type {?{short: string, full: string}} */
+      _dateFormat: Object,
       _timeFormat: String, // No default value to prevent flickering.
       _relative: Boolean, // No default value to prevent flickering.
     },
@@ -81,6 +103,7 @@
       return this._getLoggedIn().then(loggedIn => {
         if (!loggedIn) {
           this._timeFormat = TimeFormats.TIME_24;
+          this._dateFormat = DateFormats.STD;
           this._relative = false;
           return;
         }
@@ -94,19 +117,47 @@
     _loadTimeFormat() {
       return this._getPreferences().then(preferences => {
         const timeFormat = preferences && preferences.time_format;
-        switch (timeFormat) {
-          case 'HHMM_12':
-            this._timeFormat = TimeFormats.TIME_12;
-            break;
-          case 'HHMM_24':
-            this._timeFormat = TimeFormats.TIME_24;
-            break;
-          default:
-            throw Error('Invalid time format: ' + timeFormat);
-        }
+        const dateFormat = preferences && preferences.date_format;
+        this._decideTimeFormat(timeFormat);
+        this._decideDateFormat(dateFormat);
       });
     },
 
+    _decideTimeFormat(timeFormat) {
+      switch (timeFormat) {
+        case 'HHMM_12':
+          this._timeFormat = TimeFormats.TIME_12;
+          break;
+        case 'HHMM_24':
+          this._timeFormat = TimeFormats.TIME_24;
+          break;
+        default:
+          throw Error('Invalid time format: ' + timeFormat);
+      }
+    },
+
+    _decideDateFormat(dateFormat) {
+      switch (dateFormat) {
+        case 'STD':
+          this._dateFormat = DateFormats.STD;
+          break;
+        case 'US':
+          this._dateFormat = DateFormats.US;
+          break;
+        case 'ISO':
+          this._dateFormat = DateFormats.ISO;
+          break;
+        case 'EURO':
+          this._dateFormat = DateFormats.EURO;
+          break;
+        case 'UK':
+          this._dateFormat = DateFormats.UK;
+          break;
+        default:
+          throw Error('Invalid date format: ' + dateFormat);
+      }
+    },
+
     _loadRelative() {
       return this._getPreferences().then(prefs => {
         // prefs.relative_date_in_change_table is not set when false.
@@ -139,8 +190,10 @@
           diff < 180 * Duration.DAY;
     },
 
-    _computeDateStr(dateStr, timeFormat, relative, showDateAndTime) {
-      if (!dateStr) { return ''; }
+    _computeDateStr(
+        dateStr, timeFormat, dateFormat, relative, showDateAndTime
+    ) {
+      if (!dateStr || !timeFormat || !dateFormat) { return ''; }
       const date = moment(util.parseDate(dateStr));
       if (!date.isValid()) { return ''; }
       if (relative) {
@@ -152,12 +205,12 @@
         }
       }
       const now = new Date();
-      let format = TimeFormats.MONTH_DAY_YEAR;
+      let format = dateFormat.full;
       if (this._isWithinDay(now, date)) {
         format = timeFormat;
       } else {
         if (this._isWithinHalfYear(now, date)) {
-          format = TimeFormats.MONTH_DAY;
+          format = dateFormat.short;
         }
         if (this.showDateAndTime) {
           format = `${format} ${timeFormat}`;
@@ -172,11 +225,20 @@
         TimeFormats.TIME_24_WITH_SEC;
     },
 
-    _computeFullDateStr(dateStr, timeFormat) {
+    _computeFullDateStr(dateStr, timeFormat, dateFormat) {
+      // Polymer 2: check for undefined
+      if ([
+        dateStr,
+        timeFormat,
+        dateFormat,
+      ].some(arg => arg === undefined)) {
+        return undefined;
+      }
+
       if (!dateStr) { return ''; }
       const date = moment(util.parseDate(dateStr));
       if (!date.isValid()) { return ''; }
-      let format = TimeFormats.MONTH_DAY_YEAR + ', ';
+      let format = dateFormat.full + ', ';
       format += this._timeToSecondsFormat(timeFormat);
       return date.format(format) + this._getUtcOffsetString();
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index 798aa68b1..d51b5d5 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-date-formatter</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
@@ -84,16 +86,16 @@
       return Promise.all([loggedInPromise, preferencesPromise]);
     }
 
-    suite('24 hours time format preference', () => {
-      setup(() => {
-        return stubRestAPI(
-            {time_format: 'HHMM_24', relative_date_in_change_table: false}
-        ).then(() => {
-          element = fixture('basic');
-          sandbox.stub(element, '_getUtcOffsetString').returns('');
-          return element._loadPreferences();
-        });
-      });
+    suite('STD + 24 hours time format preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_24',
+        date_format: 'STD',
+        relative_date_in_change_table: false,
+      }).then(() => {
+        element = fixture('basic');
+        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        return element._loadPreferences();
+      }));
 
       test('invalid dates are quietly rejected', () => {
         assert.notOk((new Date('foo')).valueOf());
@@ -133,17 +135,161 @@
       });
     });
 
-    suite('12 hours time format preference', () => {
-      setup(() => {
+    suite('US + 24 hours time format preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_24',
+        date_format: 'US',
+        relative_date_in_change_table: false,
+      }).then(() => {
+        element = fixture('basic');
+        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        return element._loadPreferences();
+      }));
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '15:34',
+            '15:34',
+            '07/29/15, 15:34:14', done);
+      });
+
+      test('Within 24 hours on different days', done => {
+        testDates('2015-07-29 03:34:14.985000000',
+            '2015-07-28 20:25:14.985000000',
+            '07/28',
+            '07/28 20:25',
+            '07/28/15, 20:25:14', done);
+      });
+
+      test('More than 24 hours but less than six months', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-06-15 03:25:14.985000000',
+            '06/15',
+            '06/15 03:25',
+            '06/15/15, 03:25:14', done);
+      });
+    });
+
+    suite('ISO + 24 hours time format preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_24',
+        date_format: 'ISO',
+        relative_date_in_change_table: false,
+      }).then(() => {
+        element = fixture('basic');
+        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        return element._loadPreferences();
+      }));
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '15:34',
+            '15:34',
+            '2015-07-29, 15:34:14', done);
+      });
+
+      test('Within 24 hours on different days', done => {
+        testDates('2015-07-29 03:34:14.985000000',
+            '2015-07-28 20:25:14.985000000',
+            '07-28',
+            '07-28 20:25',
+            '2015-07-28, 20:25:14', done);
+      });
+
+      test('More than 24 hours but less than six months', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-06-15 03:25:14.985000000',
+            '06-15',
+            '06-15 03:25',
+            '2015-06-15, 03:25:14', done);
+      });
+    });
+
+    suite('EURO + 24 hours time format preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_24',
+        date_format: 'EURO',
+        relative_date_in_change_table: false,
+      }).then(() => {
+        element = fixture('basic');
+        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        return element._loadPreferences();
+      }));
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '15:34',
+            '15:34',
+            '29.07.2015, 15:34:14', done);
+      });
+
+      test('Within 24 hours on different days', done => {
+        testDates('2015-07-29 03:34:14.985000000',
+            '2015-07-28 20:25:14.985000000',
+            '28. Jul',
+            '28. Jul 20:25',
+            '28.07.2015, 20:25:14', done);
+      });
+
+      test('More than 24 hours but less than six months', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-06-15 03:25:14.985000000',
+            '15. Jun',
+            '15. Jun 03:25',
+            '15.06.2015, 03:25:14', done);
+      });
+    });
+
+    suite('UK + 24 hours time format preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_24',
+        date_format: 'UK',
+        relative_date_in_change_table: false,
+      }).then(() => {
+        element = fixture('basic');
+        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        return element._loadPreferences();
+      }));
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '15:34',
+            '15:34',
+            '29/07/2015, 15:34:14', done);
+      });
+
+      test('Within 24 hours on different days', done => {
+        testDates('2015-07-29 03:34:14.985000000',
+            '2015-07-28 20:25:14.985000000',
+            '28/07',
+            '28/07 20:25',
+            '28/07/2015, 20:25:14', done);
+      });
+
+      test('More than 24 hours but less than six months', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-06-15 03:25:14.985000000',
+            '15/06',
+            '15/06 03:25',
+            '15/06/2015, 03:25:14', done);
+      });
+    });
+
+    suite('STD + 12 hours time format preference', () => {
+      setup(() =>
         // relative_date_in_change_table is not set when false.
-        return stubRestAPI(
-            {time_format: 'HHMM_12'}
+        stubRestAPI(
+            {time_format: 'HHMM_12', date_format: 'STD'}
         ).then(() => {
           element = fixture('basic');
           sandbox.stub(element, '_getUtcOffsetString').returns('');
           return element._loadPreferences();
-        });
-      });
+        })
+      );
 
       test('Within 24 hours on same day', done => {
         testDates('2015-07-29 20:34:14.985000000',
@@ -154,16 +300,100 @@
       });
     });
 
-    suite('relative date preference', () => {
-      setup(() => {
-        return stubRestAPI(
-            {time_format: 'HHMM_12', relative_date_in_change_table: true}
+    suite('US + 12 hours time format preference', () => {
+      setup(() =>
+        // relative_date_in_change_table is not set when false.
+        stubRestAPI(
+            {time_format: 'HHMM_12', date_format: 'US'}
         ).then(() => {
           element = fixture('basic');
           sandbox.stub(element, '_getUtcOffsetString').returns('');
           return element._loadPreferences();
-        });
+        })
+      );
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '3:34 PM',
+            '3:34 PM',
+            '07/29/15, 3:34:14 PM', done);
       });
+    });
+
+    suite('ISO + 12 hours time format preference', () => {
+      setup(() =>
+        // relative_date_in_change_table is not set when false.
+        stubRestAPI(
+            {time_format: 'HHMM_12', date_format: 'ISO'}
+        ).then(() => {
+          element = fixture('basic');
+          sandbox.stub(element, '_getUtcOffsetString').returns('');
+          return element._loadPreferences();
+        })
+      );
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '3:34 PM',
+            '3:34 PM',
+            '2015-07-29, 3:34:14 PM', done);
+      });
+    });
+
+    suite('EURO + 12 hours time format preference', () => {
+      setup(() =>
+        // relative_date_in_change_table is not set when false.
+        stubRestAPI(
+            {time_format: 'HHMM_12', date_format: 'EURO'}
+        ).then(() => {
+          element = fixture('basic');
+          sandbox.stub(element, '_getUtcOffsetString').returns('');
+          return element._loadPreferences();
+        })
+      );
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '3:34 PM',
+            '3:34 PM',
+            '29.07.2015, 3:34:14 PM', done);
+      });
+    });
+
+    suite('UK + 12 hours time format preference', () => {
+      setup(() =>
+        // relative_date_in_change_table is not set when false.
+        stubRestAPI(
+            {time_format: 'HHMM_12', date_format: 'UK'}
+        ).then(() => {
+          element = fixture('basic');
+          sandbox.stub(element, '_getUtcOffsetString').returns('');
+          return element._loadPreferences();
+        })
+      );
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '3:34 PM',
+            '3:34 PM',
+            '29/07/2015, 3:34:14 PM', done);
+      });
+    });
+
+    suite('relative date preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_12',
+        date_format: 'STD',
+        relative_date_in_change_table: true,
+      }).then(() => {
+        element = fixture('basic');
+        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        return element._loadPreferences();
+      }));
 
       test('Within 24 hours on same day', done => {
         testDates('2015-07-29 20:34:14.985000000',
@@ -183,31 +413,33 @@
     });
 
     suite('logged in', () => {
-      setup(() => {
-        return stubRestAPI(
-            {time_format: 'HHMM_12', relative_date_in_change_table: true}
-        ).then(() => {
-          element = fixture('basic');
-          return element._loadPreferences();
-        });
-      });
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_12',
+        date_format: 'US',
+        relative_date_in_change_table: true,
+      }).then(() => {
+        element = fixture('basic');
+        return element._loadPreferences();
+      }));
 
       test('Preferences are respected', () => {
         assert.equal(element._timeFormat, 'h:mm A');
+        assert.equal(element._dateFormat.short, 'MM/DD');
+        assert.equal(element._dateFormat.full, 'MM/DD/YY');
         assert.isTrue(element._relative);
       });
     });
 
     suite('logged out', () => {
-      setup(() => {
-        return stubRestAPI(null).then(() => {
-          element = fixture('basic');
-          return element._loadPreferences();
-        });
-      });
+      setup(() => stubRestAPI(null).then(() => {
+        element = fixture('basic');
+        return element._loadPreferences();
+      }));
 
       test('Default preferences are respected', () => {
         assert.equal(element._timeFormat, 'HH:mm');
+        assert.equal(element._dateFormat.short, 'MMM DD');
+        assert.equal(element._dateFormat.full, 'MMM DD, YYYY');
         assert.isFalse(element._relative);
       });
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
index 797c8ea..2ef5539 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -45,10 +46,10 @@
       header,
       main,
       footer {
-        padding: .5em 1.5em;
+        padding: var(--spacing-m) var(--spacing-xl);
       }
       gr-button {
-        margin-left: 1em;
+        margin-left: var(--spacing-l);
       }
       footer {
         display: flex;
@@ -63,10 +64,10 @@
       <header><slot name="header"></slot></header>
       <main><slot name="main"></slot></main>
       <footer>
-        <gr-button id="cancel" class$="[[_computeCancelClass(cancelLabel)]]" link on-tap="_handleCancelTap">
+        <gr-button id="cancel" class$="[[_computeCancelClass(cancelLabel)]]" link on-click="_handleCancelTap">
           [[cancelLabel]]
         </gr-button>
-        <gr-button id="confirm" link primary on-tap="_handleConfirm" disabled="[[disabled]]">
+        <gr-button id="confirm" link primary on-click="_handleConfirm" disabled="[[disabled]]">
           [[confirmLabel]]
         </gr-button>
       </footer>
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
index b8b2af4..68dc537 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-dialog',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -53,6 +52,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     hostAttributes: {
       role: 'dialog',
     },
@@ -61,11 +64,13 @@
       if (this.disabled) { return; }
 
       e.preventDefault();
+      e.stopPropagation();
       this.fire('confirm', null, {bubbles: false});
     },
 
     _handleCancelTap(e) {
       e.preventDefault();
+      e.stopPropagation();
       this.fire('cancel', null, {bubbles: false});
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
index 4a5a181..1456e77 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
index 1c9f469..9d85d44 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
@@ -60,43 +60,67 @@
       <section>
         <span class="title">Diff width</span>
         <span class="value">
-          <input
-              is="iron-input"
+          <iron-input
               type="number"
-              id="columnsInput"
               prevent-invalid-input
               allowed-pattern="[0-9]"
               bind-value="{{diffPrefs.line_length}}"
               on-keypress="_handleDiffPrefsChanged"
               on-change="_handleDiffPrefsChanged">
+            <input
+                is="iron-input"
+                type="number"
+                id="columnsInput"
+                prevent-invalid-input
+                allowed-pattern="[0-9]"
+                bind-value="{{diffPrefs.line_length}}"
+                on-keypress="_handleDiffPrefsChanged"
+                on-change="_handleDiffPrefsChanged">
+          </iron-input>
         </span>
       </section>
       <section>
         <span class="title">Tab width</span>
         <span class="value">
-          <input
-              is="iron-input"
+          <iron-input
               type="number"
-              id="tabSizeInput"
               prevent-invalid-input
               allowed-pattern="[0-9]"
               bind-value="{{diffPrefs.tab_size}}"
               on-keypress="_handleDiffPrefsChanged"
               on-change="_handleDiffPrefsChanged">
+            <input
+                is="iron-input"
+                type="number"
+                id="tabSizeInput"
+                prevent-invalid-input
+                allowed-pattern="[0-9]"
+                bind-value="{{diffPrefs.tab_size}}"
+                on-keypress="_handleDiffPrefsChanged"
+                on-change="_handleDiffPrefsChanged">
+          </iron-input>
         </span>
       </section>
       <section hidden$="[[!diffPrefs.font_size]]">
         <span class="title">Font size</span>
         <span class="value">
-          <input
-              is="iron-input"
+          <iron-input
               type="number"
-              id="fontSizeInput"
               prevent-invalid-input
               allowed-pattern="[0-9]"
               bind-value="{{diffPrefs.font_size}}"
               on-keypress="_handleDiffPrefsChanged"
               on-change="_handleDiffPrefsChanged">
+            <input
+                is="iron-input"
+                type="number"
+                id="fontSizeInput"
+                prevent-invalid-input
+                allowed-pattern="[0-9]"
+                bind-value="{{diffPrefs.font_size}}"
+                on-keypress="_handleDiffPrefsChanged"
+                on-change="_handleDiffPrefsChanged">
+          </iron-input>
         </span>
       </section>
       <section>
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
index 89c3d74..36fdf5b 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-diff-preferences',
-    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
index 055d7aa..4d2a1a4 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-preferences</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-diff-preferences.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
index 6aec5a6..14a65b2 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/paper-tabs/paper-tabs.html">
+<link rel="import" href="/bower_components/paper-tabs/paper-tabs.html">
 <link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -29,7 +29,7 @@
     <style include="shared-styles">
       paper-tabs {
         height: 3rem;
-        margin-bottom: .5em;
+        margin-bottom: var(--spacing-m);
         --paper-tabs-selection-bar-color: var(--link-color);
       }
       paper-tab {
@@ -54,7 +54,7 @@
       }
       gr-shell-command {
         width: 60em;
-        margin-bottom: .5em;
+        margin-bottom: var(--spacing-m);
       }
       .hidden {
         display: none;
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 ed7c2cc..121aa35 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
@@ -19,7 +19,7 @@
 
   Polymer({
     is: 'gr-download-commands',
-    _legacyUndefinedCheck: true,
+
     properties: {
       commands: Array,
       _loggedIn: {
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
index c59e56a..85a5b1f 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-download-commands</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-download-commands.html">
 
@@ -65,12 +67,13 @@
     });
 
     suite('unauthenticated', () => {
-      setup(() => {
+      setup(done => {
         element = fixture('basic');
         element.schemes = SCHEMES;
         element.commands = COMMANDS;
         element.selectedScheme = SELECTED_SCHEME;
         flushAsynchronousOperations();
+        flush(done);
       });
 
       test('focusOnCopy', () => {
@@ -89,13 +92,13 @@
         assert.isTrue(isHidden(element.$$('.commands')));
       });
 
-      test('tab selection', () => {
+      test('tab selection', done => {
         assert.equal(element.$.downloadTabs.selected, '0');
         MockInteractions.tap(element.$$('[data-scheme="ssh"]'));
         flushAsynchronousOperations();
-
         assert.equal(element.selectedScheme, 'ssh');
         assert.equal(element.$.downloadTabs.selected, '2');
+        done();
       });
 
       test('loads scheme from preferences', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
index 42b6f94..98d7bf6 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -14,11 +14,11 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="../../../bower_components/paper-item/paper-item.html">
-<link rel="import" href="../../../bower_components/paper-listbox/paper-listbox.html">
+<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/paper-item/paper-item.html">
+<link rel="import" href="/bower_components/paper-listbox/paper-listbox.html">
 
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -46,9 +46,9 @@
         background-color: var(--dropdown-background-color);
         box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
         max-height: 70vh;
-        margin-top: 2em;
+        margin-top: var(--spacing-xxl);
         min-width: 266px;
-        @apply --dropdown-content-style
+        @apply --dropdown-content-style;
       }
       paper-listbox {
         --paper-listbox: {
@@ -58,7 +58,7 @@
       paper-item {
         cursor: pointer;
         flex-direction: column;
-        font-size: var(--font-size-normal);
+        font-size: inherit;
         --paper-item: {
           min-height: 0;
           padding: 10px 16px;
@@ -78,20 +78,10 @@
       }
       .bottomContent {
         color: var(--deemphasized-text-color);
-        /*
-         * Should be 16px when the base font size is 13px (browser default of
-         * 16px.
-         */
-        line-height: 1.37rem;
       }
       .bottomContent,
       .topContent {
         display: flex;
-        /*
-         * Should be 16px when the base font size is 13px (browser default of
-         * 16px.
-         */
-        line-height: 1.37rem;
         justify-content: space-between;
         flex-direction: row;
         width: 100%;
@@ -103,7 +93,7 @@
       }
       gr-date-formatter {
         color: var(--deemphasized-text-color);
-        margin-left: 2em;
+        margin-left: var(--spacing-xxl);
         white-space: nowrap;
       }
       gr-select {
@@ -135,11 +125,12 @@
       }
     </style>
     <gr-button
+        disabled="[[disabled]]"
         down-arrow
         link
         id="trigger"
         class="dropdown-trigger"
-        on-tap="_showDropdownTapHandler"
+        on-click="_showDropdownTapHandler"
         slot="dropdown-trigger">
       <span id="triggerText">[[text]]</span>
     </gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
index 6b3905f..efd1d0c 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -47,7 +47,6 @@
 
   Polymer({
     is: 'gr-dropdown-list',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the selected value changes
@@ -62,6 +61,10 @@
       /** @type {!Array<!Defs.item>} */
       items: Object,
       text: String,
+      disabled: {
+        type: Boolean,
+        value: false,
+      },
       value: {
         type: String,
         notify: true,
@@ -106,6 +109,11 @@
     },
 
     _handleValueChange(value, items) {
+      // Polymer 2: check for undefined
+      if ([value, items].some(arg => arg === undefined)) {
+        return;
+      }
+
       if (!value) { return; }
       const selectedObj = items.find(item => {
         return item.value + '' === value + '';
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
index 87fd8de..2b63d99 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-dropdown-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-dropdown-list.html">
 
@@ -66,7 +68,7 @@
       assert.equal(element._computeMobileText(item), item.mobileText);
     });
 
-    test('options are selected and laid out correctly', () => {
+    test('options are selected and laid out correctly', done => {
       element.value = 2;
       element.items = [
         {
@@ -92,77 +94,79 @@
       ];
       assert.equal(element.$$('paper-listbox').selected, element.value);
       assert.equal(element.text, 'Button Text 2');
-      flushAsynchronousOperations();
-      const items = Polymer.dom(element.root).querySelectorAll('paper-item');
-      const mobileItems = Polymer.dom(element.root).querySelectorAll('option');
-      assert.equal(items.length, 3);
-      assert.equal(mobileItems.length, 3);
+      flush(() => {
+        const items = Polymer.dom(element.root).querySelectorAll('paper-item');
+        const mobileItems = Polymer.dom(element.root).querySelectorAll('option');
+        assert.equal(items.length, 3);
+        assert.equal(mobileItems.length, 3);
 
-      // First Item
-      // The first item should be disabled, has no bottom text, and no date.
-      assert.isFalse(!!items[0].disabled);
-      assert.isFalse(mobileItems[0].disabled);
-      assert.isFalse(items[0].classList.contains('iron-selected'));
-      assert.isFalse(mobileItems[0].selected);
+        // First Item
+        // The first item should be disabled, has no bottom text, and no date.
+        assert.isFalse(!!items[0].disabled);
+        assert.isFalse(mobileItems[0].disabled);
+        assert.isFalse(items[0].classList.contains('iron-selected'));
+        assert.isFalse(mobileItems[0].selected);
 
-      assert.isNotOk(Polymer.dom(items[0]).querySelector('gr-date-formatter'));
-      assert.isNotOk(Polymer.dom(items[0]).querySelector('.bottomContent'));
-      assert.equal(items[0].value, element.items[0].value);
-      assert.equal(mobileItems[0].value, element.items[0].value);
-      assert.equal(Polymer.dom(items[0]).querySelector('.topContent div')
-          .innerText, element.items[0].text);
+        assert.isNotOk(Polymer.dom(items[0]).querySelector('gr-date-formatter'));
+        assert.isNotOk(Polymer.dom(items[0]).querySelector('.bottomContent'));
+        assert.equal(items[0].value, element.items[0].value);
+        assert.equal(mobileItems[0].value, element.items[0].value);
+        assert.equal(Polymer.dom(items[0]).querySelector('.topContent div')
+            .innerText, element.items[0].text);
 
-      // Since no mobile specific text, it should fall back to text.
-      assert.equal(mobileItems[0].text, element.items[0].text);
+        // Since no mobile specific text, it should fall back to text.
+        assert.equal(mobileItems[0].text, element.items[0].text);
 
 
-      // Second Item
-      // The second item should have top text, bottom text, and no date.
-      assert.isFalse(!!items[1].disabled);
-      assert.isFalse(mobileItems[1].disabled);
-      assert.isTrue(items[1].classList.contains('iron-selected'));
-      assert.isTrue(mobileItems[1].selected);
+        // Second Item
+        // The second item should have top text, bottom text, and no date.
+        assert.isFalse(!!items[1].disabled);
+        assert.isFalse(mobileItems[1].disabled);
+        assert.isTrue(items[1].classList.contains('iron-selected'));
+        assert.isTrue(mobileItems[1].selected);
 
-      assert.isNotOk(Polymer.dom(items[1]).querySelector('gr-date-formatter'));
-      assert.isOk(Polymer.dom(items[1]).querySelector('.bottomContent'));
-      assert.equal(items[1].value, element.items[1].value);
-      assert.equal(mobileItems[1].value, element.items[1].value);
-      assert.equal(Polymer.dom(items[1]).querySelector('.topContent div')
-          .innerText, element.items[1].text);
+        assert.isNotOk(Polymer.dom(items[1]).querySelector('gr-date-formatter'));
+        assert.isOk(Polymer.dom(items[1]).querySelector('.bottomContent'));
+        assert.equal(items[1].value, element.items[1].value);
+        assert.equal(mobileItems[1].value, element.items[1].value);
+        assert.equal(Polymer.dom(items[1]).querySelector('.topContent div')
+            .innerText, element.items[1].text);
 
-      // Since there is mobile specific text, it should that.
-      assert.equal(mobileItems[1].text, element.items[1].mobileText);
+        // Since there is mobile specific text, it should that.
+        assert.equal(mobileItems[1].text, element.items[1].mobileText);
 
-      // Since this item is selected, and it has triggerText defined, that
-      // should be used.
-      assert.equal(element.text, element.items[1].triggerText);
+        // Since this item is selected, and it has triggerText defined, that
+        // should be used.
+        assert.equal(element.text, element.items[1].triggerText);
 
-      // Third item
-      // The third item should be disabled, and have a date, and bottom content.
-      assert.isTrue(!!items[2].disabled);
-      assert.isTrue(mobileItems[2].disabled);
-      assert.isFalse(items[2].classList.contains('iron-selected'));
-      assert.isFalse(mobileItems[2].selected);
+        // Third item
+        // The third item should be disabled, and have a date, and bottom content.
+        assert.isTrue(!!items[2].disabled);
+        assert.isTrue(mobileItems[2].disabled);
+        assert.isFalse(items[2].classList.contains('iron-selected'));
+        assert.isFalse(mobileItems[2].selected);
 
-      assert.isOk(Polymer.dom(items[2]).querySelector('gr-date-formatter'));
-      assert.isOk(Polymer.dom(items[2]).querySelector('.bottomContent'));
-      assert.equal(items[2].value, element.items[2].value);
-      assert.equal(mobileItems[2].value, element.items[2].value);
-      assert.equal(Polymer.dom(items[2]).querySelector('.topContent div')
-          .innerText, element.items[2].text);
+        assert.isOk(Polymer.dom(items[2]).querySelector('gr-date-formatter'));
+        assert.isOk(Polymer.dom(items[2]).querySelector('.bottomContent'));
+        assert.equal(items[2].value, element.items[2].value);
+        assert.equal(mobileItems[2].value, element.items[2].value);
+        assert.equal(Polymer.dom(items[2]).querySelector('.topContent div')
+            .innerText, element.items[2].text);
 
-      // Since there is mobile specific text, it should that.
-      assert.equal(mobileItems[2].text, element.items[2].mobileText);
+        // Since there is mobile specific text, it should that.
+        assert.equal(mobileItems[2].text, element.items[2].mobileText);
 
-      // Select a new item.
-      MockInteractions.tap(items[0]);
-      flushAsynchronousOperations();
-      assert.equal(element.value, 1);
-      assert.isTrue(items[0].classList.contains('iron-selected'));
-      assert.isTrue(mobileItems[0].selected);
+        // Select a new item.
+        MockInteractions.tap(items[0]);
+        flushAsynchronousOperations();
+        assert.equal(element.value, 1);
+        assert.isTrue(items[0].classList.contains('iron-selected'));
+        assert.isTrue(mobileItems[0].selected);
 
-      // Since no triggerText, the fallback is used.
-      assert.equal(element.text, element.items[0].text);
+        // Since no triggerText, the fallback is used.
+        assert.equal(element.text, element.items[0].text);
+        done();
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index 373e77d..d76721f 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -17,8 +17,8 @@
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -63,7 +63,7 @@
       li .itemAction {
         cursor: pointer;
         display: block;
-        padding: .85em 1em;
+        padding: var(--spacing-m) var(--spacing-l);
       }
       li .itemAction {
         @apply --gr-dropdown-item;
@@ -90,7 +90,7 @@
       }
       .topContent {
         display: block;
-        padding: .85em 1em;
+        padding: var(--spacing-m) var(--spacing-l);
         @apply --gr-dropdown-item;
       }
       .bold-text {
@@ -101,7 +101,7 @@
         link="[[link]]"
         class="dropdown-trigger" id="trigger"
         down-arrow="[[downArrow]]"
-        on-tap="_dropdownTriggerTapHandler">
+        on-click="_dropdownTriggerTapHandler">
       <slot></slot>
     </gr-button>
     <iron-dropdown id="dropdown"
@@ -139,7 +139,7 @@
                 <span
                     class$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]"
                     data-id$="[[link.id]]"
-                    on-tap="_handleItemTap"
+                    on-click="_handleItemTap"
                     hidden$="[[link.url]]"
                     tabindex="-1">[[link.name]]</span>
                 <a
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 b4b7087..11825c5 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-dropdown',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a non-link dropdown item with the given ID is tapped.
@@ -143,10 +142,10 @@
       e.preventDefault();
       e.stopPropagation();
       if (this.$.dropdown.opened) {
-        // TODO(kaspern): This solution will not work in Shadow DOM, and
-        // is not particularly robust in general. Find a better solution
-        // when page.js has been abstracted away from components.
-        const el = this.$.cursor.target.querySelector(':not([hidden])');
+        // TODO(milutin): This solution is not particularly robust in general.
+        // Since gr-tooltip-content click on shadow dom is not propagated down,
+        // we have to target `a` inside it.
+        const el = this.$.cursor.target.querySelector(':not([hidden]) a');
         if (el) { el.click(); }
       } else {
         this._open();
@@ -182,8 +181,8 @@
      */
     _open() {
       this.$.dropdown.open();
+      this._resetCursorStops();
       this.$.cursor.setCursorAtIndex(0);
-      Polymer.dom.flush();
       this.$.cursor.target.focus();
     },
 
@@ -295,10 +294,11 @@
      * Recompute the stops for the dropdown item cursor.
      */
     _resetCursorStops() {
-      Polymer.dom.flush();
-      // Polymer2: querySelectorAll returns NodeList instead of Array.
-      this._listElements = Array.from(
-          Polymer.dom(this.root).querySelectorAll('li'));
+      if (this.items && this.items.length > 0 && this.$.dropdown.opened) {
+        Polymer.dom.flush();
+        this._listElements = Array.from(
+            Polymer.dom(this.root).querySelectorAll('li'));
+      }
     },
 
     _computeHasTooltip(tooltip) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index 7bb4dce..295f746 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-dropdown</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-dropdown.html">
 
@@ -194,7 +196,7 @@
         MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
         assert.isTrue(element.$.dropdown.opened);
 
-        const el = element.$.cursor.target.querySelector(':not([hidden])');
+        const el = element.$.cursor.target.querySelector(':not([hidden]) a');
         const stub = sandbox.stub(el, 'click');
         MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
         assert.isTrue(stub.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
index 6cd87f5..45ddfc8 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
@@ -15,10 +15,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../gr-storage/gr-storage.html">
+<link rel="import" href="../gr-button/gr-button.html">
 
 <dom-module id="gr-editable-content">
   <template>
@@ -29,10 +31,18 @@
       :host([disabled]) iron-autogrow-textarea {
         opacity: .5;
       }
-      iron-autogrow-textarea {
+      .viewer {
+        background-color: var(--view-background-color);
+        border: 1px solid var(--border-color);
+        border-radius: var(--border-radius);
+        padding: var(--spacing-m);
+      }
+      .editor iron-autogrow-textarea {
+        background-color: var(--view-background-color);
         width: 100%;
 
         --iron-autogrow-textarea: {
+          padding: var(--spacing-m);
           box-sizing: border-box;
           overflow-y: hidden;
           white-space: pre;
@@ -43,7 +53,7 @@
         justify-content: space-between;
       }
     </style>
-    <div hidden$="[[editing]]">
+    <div class="viewer" hidden$="[[editing]]">
       <slot></slot>
     </div>
     <div class="editor" hidden$="[[!editing]]">
@@ -53,10 +63,10 @@
           disabled="[[disabled]]"></iron-autogrow-textarea>
       <div class="editButtons">
         <gr-button primary
-            on-tap="_handleSave"
+            on-click="_handleSave"
             disabled="[[_saveDisabled]]">Save</gr-button>
         <gr-button
-            on-tap="_handleCancel"
+            on-click="_handleCancel"
             disabled="[[disabled]]">Cancel</gr-button>
       </div>
     </div>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
index 5463564..ee41103 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-editable-content',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the save button is pressed.
@@ -71,6 +70,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     focusTextarea() {
       this.$$('iron-autogrow-textarea').textarea.focus();
     },
@@ -117,8 +120,11 @@
             this.$.storage.getEditableContentItem(this.storageKey);
         if (storedContent && storedContent.message) {
           content = storedContent.message;
-          this.dispatchEvent(new CustomEvent('show-alert',
-              {detail: {message: RESTORED_MESSAGE}, bubbles: true}));
+          this.dispatchEvent(new CustomEvent('show-alert', {
+            detail: {message: RESTORED_MESSAGE},
+            bubbles: true,
+            composed: true,
+          }));
         }
       }
       if (!content) {
@@ -132,6 +138,15 @@
     },
 
     _computeSaveDisabled(disabled, content, newContent) {
+      // Polymer 2: check for undefined
+      if ([
+        disabled,
+        content,
+        newContent,
+      ].some(arg => arg === undefined)) {
+        return true;
+      }
+
       return disabled || !newContent || content === newContent;
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
index ca500cb..ee5adb0 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-editable-content</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
 
 <link rel="import" href="gr-editable-content.html">
 
@@ -47,6 +49,7 @@
     teardown(() => { sandbox.restore(); });
 
     test('save event', done => {
+      element.content = '';
       element._newContent = 'foo';
       element.addEventListener('editable-content-save', e => {
         assert.equal(e.detail.content, 'foo');
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
index ddc35bf..78465e1 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -14,12 +14,14 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="../../../bower_components/paper-input/paper-input.html">
+<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/paper-input/paper-input.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../gr-button/gr-button.html">
 
 <dom-module id="gr-editable-label">
   <template>
@@ -35,13 +37,9 @@
       label {
         width: 100%;
       }
-      input {
-        font: inherit;
-      }
       label {
         color: var(--deemphasized-text-color);
         display: inline-block;
-        font-weight: var(--font-weight-bold);
         overflow: hidden;
         text-overflow: ellipsis;
         white-space: nowrap;
@@ -56,17 +54,17 @@
       }
       .inputContainer {
         background-color: var(--dialog-background-color);
-        padding: .8em;
+        padding: var(--spacing-m);
         @apply --input-style;
       }
       .buttons {
         display: flex;
         justify-content: flex-end;
-        padding-top: 1.2em;
+        padding-top: var(--spacing-l);
         width: 100%;
       }
       .buttons gr-button {
-        margin-left: .5em;
+        margin-left: var(--spacing-m);
       }
       paper-input {
         --paper-input-container: {
@@ -74,7 +72,7 @@
           min-width: 15em;
         }
         --paper-input-container-input: {
-          font-size: var(--font-size-normal);
+          font-size: inherit;
         }
         --paper-input-container-focus-color: var(--link-color);
       }
@@ -82,7 +80,7 @@
       <label
           class$="[[_computeLabelClass(readOnly, value, placeholder)]]"
           title$="[[_computeLabel(value, placeholder)]]"
-          on-tap="_showDropdown">[[_computeLabel(value, placeholder)]]</label>
+          on-click="_showDropdown">[[_computeLabel(value, placeholder)]]</label>
       <iron-dropdown id="dropdown"
           vertical-align="auto"
           horizontal-align="auto"
@@ -97,8 +95,8 @@
                 maxlength="[[maxLength]]"
                 value="{{_inputText}}"></paper-input>
             <div class="buttons">
-              <gr-button link id="cancelBtn" on-tap="_cancel">cancel</gr-button>
-              <gr-button link id="saveBtn" on-tap="_save">save</gr-button>
+              <gr-button link id="cancelBtn" on-click="_cancel">cancel</gr-button>
+              <gr-button link id="saveBtn" on-click="_save">save</gr-button>
             </div>
           </div>
         </div>
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 f23afea..4485551 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
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-editable-label',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the value is changed.
@@ -68,6 +67,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
     ],
 
@@ -94,15 +94,15 @@
     _showDropdown() {
       if (this.readOnly || this.editing) { return; }
       return this._open().then(() => {
-        this.$.input.$.input.focus();
+        this._nativeInput.focus();
         if (!this.$.input.value) { return; }
-        this.$.input.$.input.setSelectionRange(0, this.$.input.value.length);
+        this._nativeInput.setSelectionRange(0, this.$.input.value.length);
       });
     },
 
     open() {
       return this._open().then(() => {
-        this.$.input.$.input.focus();
+        this._nativeInput.focus();
       });
     },
 
@@ -154,29 +154,25 @@
       this._inputText = this.value;
     },
 
-    /**
-     * @suppress {checkTypes}
-     * Closure doesn't think 'e' is an Event.
-     * TODO(beckysiegel) figure out why.
-     */
+    get _nativeInput() {
+      // In Polymer 2, the namespace of nativeInput
+      // changed from input to nativeInput
+      return this.$.input.$.nativeInput || this.$.input.$.input;
+    },
+
     _handleEnter(e) {
       e = this.getKeyboardEvent(e);
       const target = Polymer.dom(e).rootTarget;
-      if (target === this.$.input.$.input) {
+      if (target === this._nativeInput) {
         e.preventDefault();
         this._save();
       }
     },
 
-    /**
-     * @suppress {checkTypes}
-     * Closure doesn't think 'e' is an Event.
-     * TODO(beckysiegel) figure out why.
-     */
     _handleEsc(e) {
       e = this.getKeyboardEvent(e);
       const target = Polymer.dom(e).rootTarget;
-      if (target === this.$.input.$.input) {
+      if (target === this._nativeInput) {
         e.preventDefault();
         this._cancel();
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
index 6815173..7ff0a14 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-editable-label</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
 
 <link rel="import" href="gr-editable-label.html">
 
@@ -59,13 +61,17 @@
     let label;
     let sandbox;
 
-    setup(() => {
+    setup(done => {
       element = fixture('basic');
       elementNoPlaceholder = fixture('no-placeholder');
 
-      input = element.$.input.$.input;
       label = element.$$('label');
       sandbox = sinon.sandbox.create();
+      flush(() => {
+        // In Polymer 2 inputElement isn't nativeInput anymore
+        input = element.$.input.$.nativeInput || element.$.input.inputElement;
+        done();
+      });
     });
 
     teardown(() => {
@@ -77,7 +83,7 @@
       assert.isFalse(element.$.dropdown.opened);
       assert.isTrue(label.classList.contains('editable'));
       assert.equal(label.textContent, 'value text');
-      const focusSpy = sandbox.spy(element.$.input.$.input, 'focus');
+      const focusSpy = sandbox.spy(input, 'focus');
       const showSpy = sandbox.spy(element, '_showDropdown');
 
       MockInteractions.tap(label);
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
index 674ff97..226092f 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
@@ -15,13 +15,14 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-fixed-panel">
   <template>
     <style include="shared-styles">
       :host {
+        box-sizing: border-box;
         display: block;
         min-height: var(--header-height);
         position: relative;
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
index 87cd4b4..2c32709 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-fixed-panel',
-    _legacyUndefinedCheck: true,
 
     properties: {
       floatingDisabled: Boolean,
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
index 9eac7f7..75e9901 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-fixed-panel</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-fixed-panel.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
index 3995595..a98952c 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../gr-linked-text/gr-linked-text.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -29,7 +29,7 @@
       ul,
       blockquote,
       gr-linked-text.pre {
-        margin: 0 0 .8em 0;
+        margin: 0 0 var(--spacing-m) 0;
       }
       p,
       ul,
@@ -44,14 +44,16 @@
       }
       blockquote {
         border-left: 1px solid #aaa;
-        padding: 0 .7em;
+        padding: 0 var(--spacing-m);
       }
       li {
         list-style-type: disc;
-        margin-left: 1.4em;
+        margin-left: var(--spacing-xl);
       }
       gr-linked-text.pre {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-code);
+        line-height: var(--line-height-code);
       }
 
     </style>
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 d84c38e..feae173 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
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-formatted-text',
-    _legacyUndefinedCheck: true,
 
     properties: {
       content: {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
index ad036c5..801190a 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-editable-label</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-formatted-text.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html
index 7e3246f..c77ea81 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -34,10 +34,10 @@
         visibility: visible;
         opacity: 1;
       }
-      :host ::content #hovercard {
+      #hovercard {
         background: var(--dialog-background-color);
         box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
-        padding: 1em;
+        padding: var(--spacing-l);
       }
     </style>
     <div id="hovercard" role="tooltip" tabindex="-1">
@@ -46,4 +46,4 @@
   </template>
   <script src="../../../scripts/rootElement.js"></script>
   <script src="gr-hovercard.js"></script>
-</dom-module>
\ No newline at end of file
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
index f8ff549..5692b38 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -27,7 +27,6 @@
 
   Polymer({
     is: 'gr-hovercard',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -98,7 +97,7 @@
       this.listen(this._target, 'focus', 'show');
       this.listen(this._target, 'mouseleave', 'hide');
       this.listen(this._target, 'blur', 'hide');
-      this.listen(this._target, 'tap', 'hide');
+      this.listen(this._target, 'click', 'hide');
     },
 
     ready() {
@@ -119,7 +118,7 @@
       this.unlisten(this._target, 'focus', 'show');
       this.unlisten(this._target, 'mouseleave', 'hide');
       this.unlisten(this._target, 'blur', 'hide');
-      this.unlisten(this._target, 'tap', 'hide');
+      this.unlisten(this._target, 'click', 'hide');
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
index e3e252f..8e79f65 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-hovercard</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
 
 <link rel="import" href="gr-hovercard.html">
 
@@ -39,7 +41,8 @@
   suite('gr-hovercard tests', () => {
     let element;
     let sandbox;
-    const TRANSITION_TIME = 200;
+    // For css animations
+    const TRANSITION_TIME = 500;
 
     setup(() => {
       sandbox = sinon.sandbox.create();
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
index 1f885af..7bd6f48 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
@@ -14,8 +14,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../../bower_components/iron-iconset-svg/iron-iconset-svg.html">
+<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
+<link rel="import" href="/bower_components/iron-iconset-svg/iron-iconset-svg.html">
 
 <iron-iconset-svg name="gr-icons" size="24">
   <svg>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js
index 2581ff9..708e5b6 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js
@@ -45,26 +45,27 @@
    *
    * @param {number} offset The char offset where the update starts.
    * @param {number} length The number of chars that the update covers.
-   * @param {string} cssClass The name of a CSS class created using Gerrit.css.
+   * @param {GrStyleObject} styleObject The style object for the range.
    * @param {string} side The side of the update. ('left' or 'right')
    */
   GrAnnotationActionsContext.prototype.annotateRange = function(
-      offset, length, cssClass, side) {
+      offset, length, styleObject, side) {
     if (this._contentEl && this._contentEl.getAttribute('data-side') == side) {
-      GrAnnotation.annotateElement(this._contentEl, offset, length, cssClass);
+      GrAnnotation.annotateElement(this._contentEl, offset, length,
+          styleObject.getClassName(this._contentEl));
     }
   };
 
   /**
    * Method to add a CSS class to the line number TD element.
    *
-   * @param {string} cssClass The name of a CSS class created using Gerrit.css.
+   * @param {GrStyleObject} styleObject The style object for the range.
    * @param {string} side The side of the update. ('left' or 'right')
    */
   GrAnnotationActionsContext.prototype.annotateLineNumber = function(
-      cssClass, side) {
+      styleObject, side) {
     if (this._lineNumberEl && this._lineNumberEl.classList.contains(side)) {
-      this._lineNumberEl.classList.add(cssClass);
+      styleObject.apply(this._lineNumberEl);
     }
   };
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
index 03c8c5e..3223636 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-annotation-actions-context</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <script src="../../diff/gr-diff-highlight/gr-annotation.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
@@ -40,9 +42,13 @@
     let sandbox;
     let el;
     let lineNumberEl;
+    let plugin;
 
     setup(() => {
       sandbox = sinon.sandbox.create();
+      Gerrit.install(p => { plugin = p; }, '0.1',
+          'http://test.com/plugins/testplugin/static/test.js');
+
       const str = 'lorem ipsum blah blah';
       const line = {text: str};
       el = document.createElement('div');
@@ -50,6 +56,7 @@
       el.setAttribute('data-side', 'right');
       lineNumberEl = document.createElement('td');
       lineNumberEl.classList.add('right');
+      document.body.appendChild(el);
       instance = new GrAnnotationActionsContext(
           el, lineNumberEl, line, 'dummy/path', '123', '1');
     });
@@ -62,32 +69,34 @@
       annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
       const start = 0;
       const end = 100;
-      const cssClass = Gerrit.css('background-color: #000000');
+      const cssStyleObject = plugin.styles().css('background-color: #000000');
 
       // Assert annotateElement is not called when side is different.
-      instance.annotateRange(start, end, cssClass, 'left');
+      instance.annotateRange(start, end, cssStyleObject, 'left');
       assert.equal(annotateElementSpy.callCount, 0);
 
       // Assert annotateElement is called once when side is the same.
-      instance.annotateRange(start, end, cssClass, 'right');
+      instance.annotateRange(start, end, cssStyleObject, 'right');
       assert.equal(annotateElementSpy.callCount, 1);
       const args = annotateElementSpy.getCalls()[0].args;
       assert.equal(args[0], el);
       assert.equal(args[1], start);
       assert.equal(args[2], end);
-      assert.equal(args[3], cssClass);
+      assert.equal(args[3], cssStyleObject.getClassName(el));
     });
 
     test('test annotateLineNumber', () => {
-      const cssClass = Gerrit.css('background-color: #000000');
+      const cssStyleObject = plugin.styles().css('background-color: #000000');
+
+      const className = cssStyleObject.getClassName(lineNumberEl);
 
       // Assert that css class is *not* applied when side is different.
-      instance.annotateLineNumber(cssClass, 'left');
-      assert.isFalse(lineNumberEl.classList.contains(cssClass));
+      instance.annotateLineNumber(cssStyleObject, 'left');
+      assert.isFalse(lineNumberEl.classList.contains(className));
 
       // Assert that css class is applied when side is the same.
-      instance.annotateLineNumber(cssClass, 'right');
-      assert.isTrue(lineNumberEl.classList.contains(cssClass));
+      instance.annotateLineNumber(cssStyleObject, 'right');
+      assert.isTrue(lineNumberEl.classList.contains(className));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js
index 3f91a82..4ba9820 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js
@@ -67,10 +67,8 @@
    * providers are not supported. A second call will just overwrite the
    * provider of the first call.
    *
-   * TODO(brohlfs): Replace Array<Object> type by Array<Gerrit.CoverageRange>.
-   *
    * @param {function(changeNum, path, basePatchNum, patchNum):
-   * !Promise<!Array<Object>>} coverageProvider
+   * !Promise<!Array<!Gerrit.CoverageRange>>} coverageProvider
    * @return {GrAnnotationActionsInterface}
    */
   GrAnnotationActionsInterface.prototype.setCoverageProvider = function(
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
index bf7c2cb..987b551 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-annotation-actions-js-api-js-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../change/gr-change-actions/gr-change-actions.html">
 
@@ -28,7 +30,9 @@
   <template>
     <span hidden id="annotation-span">
       <label for="annotation-checkbox" id="annotation-label"></label>
-      <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
+      <iron-input type="checkbox" disabled>
+        <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
+      </iron-input>
     </span>
   </template>
 </test-fixture>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
new file mode 100644
index 0000000..2925736
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
@@ -0,0 +1,112 @@
+/**
+ * @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.
+ */
+
+(function(window) {
+  'use strict';
+
+  const PRELOADED_PROTOCOL = 'preloaded:';
+  const PLUGIN_LOADING_TIMEOUT_MS = 10000;
+
+  let _restAPI;
+  function getRestAPI() {
+    if (!_restAPI) {
+      _restAPI = document.createElement('gr-rest-api-interface');
+    }
+    return _restAPI;
+  }
+
+  function getBaseUrl() {
+    return Gerrit.BaseUrlBehavior.getBaseUrl();
+  }
+
+  /**
+   * Retrieves the name of the plugin base on the url.
+   *
+   * @param {string|URL} url
+   */
+  function getPluginNameFromUrl(url) {
+    if (!(url instanceof URL)) {
+      try {
+        url = new URL(url);
+      } catch (e) {
+        console.warn(e);
+        return null;
+      }
+    }
+    if (url.protocol === PRELOADED_PROTOCOL) {
+      return url.pathname;
+    }
+    const base = Gerrit.BaseUrlBehavior.getBaseUrl();
+    const pathname = url.pathname.replace(base, '');
+    // Site theme is server from predefined path.
+    if (pathname === '/static/gerrit-theme.html') {
+      return 'gerrit-theme';
+    } else if (!pathname.startsWith('/plugins')) {
+      console.warn('Plugin not being loaded from /plugins base path:',
+          url.href, '— Unable to determine name.');
+      return null;
+    }
+
+    // Pathname should normally look like this:
+    // /plugins/PLUGINNAME/static/SCRIPTNAME.html
+    // Or, for app/samples:
+    // /plugins/PLUGINNAME.html
+    // TODO(taoalpha): guard with a regex
+    return pathname.split('/')[2].split('.')[0];
+  }
+
+  // TODO(taoalpha): to be deprecated.
+  function send(method, url, opt_callback, opt_payload) {
+    return getRestAPI().send(method, url, opt_payload).then(response => {
+      if (response.status < 200 || response.status >= 300) {
+        return response.text().then(text => {
+          if (text) {
+            return Promise.reject(text);
+          } else {
+            return Promise.reject(response.status);
+          }
+        });
+      } else {
+        return getRestAPI().getResponseObject(response);
+      }
+    }).then(response => {
+      if (opt_callback) {
+        opt_callback(response);
+      }
+      return response;
+    });
+  }
+
+
+  // TEST only methods / properties
+
+  function testOnly_resetInternalState() {
+    _restAPI = undefined;
+  }
+
+  window._apiUtils = {
+    getPluginNameFromUrl,
+    send,
+    getRestAPI,
+    getBaseUrl,
+    PRELOADED_PROTOCOL,
+    PLUGIN_LOADING_TIMEOUT_MS,
+
+    // TEST only methods
+    testOnly_resetInternalState,
+  };
+})(window);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
new file mode 100644
index 0000000..c407aa8
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-api-interface</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-js-api-interface.html">
+
+<script>void(0);</script>
+
+<script>
+
+  const PRELOADED_PROTOCOL = 'preloaded:';
+
+  suite('gr-api-utils tests', () => {
+    suite('test getPluginNameFromUrl', () => {
+      const {getPluginNameFromUrl} = window._apiUtils;
+
+      test('with empty string', () => {
+        assert.equal(getPluginNameFromUrl(''), null);
+      });
+
+      test('with invalid url', () => {
+        assert.equal(getPluginNameFromUrl('test'), null);
+      });
+
+      test('with random invalid url', () => {
+        assert.equal(getPluginNameFromUrl('http://example.com'), null);
+        assert.equal(
+            getPluginNameFromUrl('http://example.com/static/a.html'),
+            null
+        );
+      });
+
+      test('with valid urls', () => {
+        assert.equal(
+            getPluginNameFromUrl('http://example.com/plugins/a.html'),
+            'a'
+        );
+        assert.equal(
+            getPluginNameFromUrl('http://example.com/plugins/a/static/t.html'),
+            'a'
+        );
+      });
+
+      test('with preloaded urls', () => {
+        assert.equal(getPluginNameFromUrl(`${PRELOADED_PROTOCOL}a`), 'a');
+      });
+
+      test('with gerrit-theme override', () => {
+        assert.equal(
+            getPluginNameFromUrl('http://example.com/static/gerrit-theme.html'),
+            'gerrit-theme'
+        );
+      });
+    });
+  });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index fef4fc9..7332877 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-actions-js-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <!--
 This must refer to the element this interface is wrapping around. Otherwise
@@ -52,17 +54,18 @@
 
     suite('early init', () => {
       setup(() => {
-        Gerrit._resetPlugins();
+        Gerrit._testOnly_resetPlugins();
         Gerrit.install(p => { plugin = p; }, '0.1',
             'http://test.com/plugins/testplugin/static/test.js');
         // Mimic all plugins loaded.
-        Gerrit._setPluginsPending([]);
+        Gerrit._loadPlugins([]);
         changeActions = plugin.changeActions();
         element = fixture('basic');
       });
 
       teardown(() => {
         changeActions = null;
+        Gerrit._testOnly_resetPlugins();
       });
 
       test('does not throw', ()=> {
@@ -74,7 +77,7 @@
 
     suite('normal init', () => {
       setup(() => {
-        Gerrit._resetPlugins();
+        Gerrit._testOnly_resetPlugins();
         element = fixture('basic');
         sinon.stub(element, '_editStatusChanged');
         element.change = {};
@@ -83,11 +86,12 @@
             'http://test.com/plugins/testplugin/static/test.js');
         changeActions = plugin.changeActions();
         // Mimic all plugins loaded.
-        Gerrit._setPluginsPending([]);
+        Gerrit._loadPlugins([]);
       });
 
       teardown(() => {
         changeActions = null;
+        Gerrit._testOnly_resetPlugins();
       });
 
       test('property existence', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
index 278f95a..842a2fe 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-reply-js-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <!--
 This must refer to the element this interface is wrapping around. Otherwise
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
new file mode 100644
index 0000000..73a2480
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
@@ -0,0 +1,196 @@
+/**
+ * @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.
+ */
+
+/**
+ * This defines the Gerrit instance. All methods directly attached to Gerrit
+ * should be defined or linked here.
+ */
+
+(function(window) {
+  'use strict';
+
+  // Import utils methods
+  const {
+    send,
+    getRestAPI,
+  } = window._apiUtils;
+
+  /**
+   * Trigger the preinstalls for bundled plugins.
+   * This needs to happen before Gerrit as plugin bundle overrides the Gerrit.
+   */
+  function flushPreinstalls() {
+    if (window.Gerrit.flushPreinstalls) {
+      window.Gerrit.flushPreinstalls();
+    }
+  }
+  flushPreinstalls();
+
+  window.Gerrit = window.Gerrit || {};
+  const Gerrit = window.Gerrit;
+  Gerrit._pluginLoader = new PluginLoader();
+
+  Gerrit._endpoints = new GrPluginEndpoints();
+
+  // Provide reset plugins function to clear installed plugins between tests.
+  const app = document.querySelector('#app');
+  if (!app) {
+    // No gr-app found (running tests)
+    const {
+      testOnly_resetInternalState,
+    } = window._apiUtils;
+    Gerrit._testOnly_installPreloadedPlugins = (...args) => Gerrit._pluginLoader
+        .installPreloadedPlugins(...args);
+    Gerrit._testOnly_flushPreinstalls = flushPreinstalls;
+    Gerrit._testOnly_resetPlugins = () => {
+      testOnly_resetInternalState();
+      Gerrit._endpoints = new GrPluginEndpoints();
+      Gerrit._pluginLoader = new PluginLoader();
+    };
+  }
+
+  /**
+   * @deprecated Use plugin.styles().css(rulesStr) instead. Please, consult
+   * the documentation how to replace it accordingly.
+   */
+  Gerrit.css = function(rulesStr) {
+    console.warn('Gerrit.css(rulesStr) is deprecated!',
+        'Use plugin.styles().css(rulesStr)');
+    if (!Gerrit._customStyleSheet) {
+      const styleEl = document.createElement('style');
+      document.head.appendChild(styleEl);
+      Gerrit._customStyleSheet = styleEl.sheet;
+    }
+
+    const name = '__pg_js_api_class_' +
+        Gerrit._customStyleSheet.cssRules.length;
+    Gerrit._customStyleSheet.insertRule('.' + name + '{' + rulesStr + '}', 0);
+    return name;
+  };
+
+  Gerrit.install = function(callback, opt_version, opt_src) {
+    Gerrit._pluginLoader.install(callback, opt_version, opt_src);
+  };
+
+  Gerrit.getLoggedIn = function() {
+    console.warn('Gerrit.getLoggedIn() is deprecated! ' +
+        'Use plugin.restApi().getLoggedIn()');
+    return document.createElement('gr-rest-api-interface').getLoggedIn();
+  };
+
+  Gerrit.get = function(url, callback) {
+    console.warn('.get() is deprecated! Use plugin.restApi().get()');
+    send('GET', url, callback);
+  };
+
+  Gerrit.post = function(url, payload, callback) {
+    console.warn('.post() is deprecated! Use plugin.restApi().post()');
+    send('POST', url, callback, payload);
+  };
+
+  Gerrit.put = function(url, payload, callback) {
+    console.warn('.put() is deprecated! Use plugin.restApi().put()');
+    send('PUT', url, callback, payload);
+  };
+
+  Gerrit.delete = function(url, opt_callback) {
+    console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
+    return getRestAPI().send('DELETE', url).then(response => {
+      if (response.status !== 204) {
+        return response.text().then(text => {
+          if (text) {
+            return Promise.reject(text);
+          } else {
+            return Promise.reject(response.status);
+          }
+        });
+      }
+      if (opt_callback) {
+        opt_callback(response);
+      }
+      return response;
+    });
+  };
+
+  Gerrit.awaitPluginsLoaded = function() {
+    return Gerrit._pluginLoader.awaitPluginsLoaded();
+  };
+
+  // TODO(taoalpha): consider removing these proxy methods
+  // and using _pluginLoader directly
+
+  Gerrit._loadPlugins = function(plugins, opt_option) {
+    Gerrit._pluginLoader.loadPlugins(plugins, opt_option);
+  };
+
+  Gerrit._arePluginsLoaded = function() {
+    return Gerrit._pluginLoader.arePluginsLoaded;
+  };
+
+  Gerrit._isPluginPreloaded = function(url) {
+    return Gerrit._pluginLoader.isPluginPreloaded(url);
+  };
+
+  Gerrit._isPluginEnabled = function(pathOrUrl) {
+    return Gerrit._pluginLoader.isPluginEnabled(pathOrUrl);
+  };
+
+  Gerrit._isPluginLoaded = function(pathOrUrl) {
+    return Gerrit._pluginLoader.isPluginLoaded(pathOrUrl);
+  };
+
+  // Preloaded plugins should be installed after Gerrit.install() is set,
+  // since plugin preloader substitutes Gerrit.install() temporarily.
+  Gerrit._pluginLoader.installPreloadedPlugins();
+
+  // TODO(taoalpha): List all internal supported event names.
+  // Also convert this to inherited class once we move Gerrit to class.
+  Gerrit._eventEmitter = new EventEmitter();
+  ['addListener',
+    'dispatch',
+    'emit',
+    'off',
+    'on',
+    'once',
+    'removeAllListeners',
+    'removeListener',
+  ].forEach(method => {
+    /**
+     * Enabling EventEmitter interface on Gerrit.
+     *
+     * This will enable to signal across different parts of js code without relying on DOM,
+     * including core to core, plugin to plugin and also core to plugin.
+     *
+     * @example
+     *
+     * // Emit this event from pluginA
+     * Gerrit.install(pluginA => {
+     *   fetch("some-api").then(() => {
+     *     Gerrit.on("your-special-event", {plugin: pluginA});
+     *   });
+     * });
+     *
+     * // Listen on your-special-event from pluignB
+     * Gerrit.install(pluginB => {
+     *   Gerrit.on("your-special-event", ({plugin}) => {
+     *     // do something, plugin is pluginA
+     *   });
+     * });
+     */
+    Gerrit[method] = Gerrit._eventEmitter[method].bind(Gerrit._eventEmitter);
+  });
+})(window);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
new file mode 100644
index 0000000..e81b8aa
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-api-interface</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-js-api-interface.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-js-api-interface></gr-js-api-interface>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-gerrit tests', () => {
+    let element;
+    let sandbox;
+    let sendStub;
+
+    setup(() => {
+      this.clock = sinon.useFakeTimers();
+      sandbox = sinon.sandbox.create();
+      sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+      stub('gr-rest-api-interface', {
+        getAccount() {
+          return Promise.resolve({name: 'Judy Hopps'});
+        },
+        send(...args) {
+          return sendStub(...args);
+        },
+      });
+      element = fixture('basic');
+    });
+
+    teardown(() => {
+      this.clock.restore();
+      sandbox.restore();
+      element._removeEventCallbacks();
+      Gerrit._testOnly_resetPlugins();
+    });
+
+    suite('proxy methods', () => {
+      test('Gerrit._isPluginEnabled proxy to pluginLoader', () => {
+        const stubFn = sandbox.stub();
+        sandbox.stub(
+            Gerrit._pluginLoader,
+            'isPluginEnabled',
+            (...args) => stubFn(...args)
+        );
+        Gerrit._isPluginEnabled('test_plugin');
+        assert.isTrue(stubFn.calledWith('test_plugin'));
+      });
+
+      test('Gerrit._isPluginLoaded proxy to pluginLoader', () => {
+        const stubFn = sandbox.stub();
+        sandbox.stub(
+            Gerrit._pluginLoader,
+            'isPluginLoaded',
+            (...args) => stubFn(...args)
+        );
+        Gerrit._isPluginLoaded('test_plugin');
+        assert.isTrue(stubFn.calledWith('test_plugin'));
+      });
+
+      test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => {
+        const stubFn = sandbox.stub();
+        sandbox.stub(
+            Gerrit._pluginLoader,
+            'isPluginPreloaded',
+            (...args) => stubFn(...args)
+        );
+        Gerrit._isPluginPreloaded('test_plugin');
+        assert.isTrue(stubFn.calledWith('test_plugin'));
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index d95fd0a..72fc3d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
@@ -26,10 +26,19 @@
 <link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html">
 <link rel="import" href="../../plugins/gr-repo-api/gr-repo-api.html">
 <link rel="import" href="../../plugins/gr-settings-api/gr-settings-api.html">
+<link rel="import" href="../../plugins/gr-styles-api/gr-styles-api.html">
 <link rel="import" href="../../plugins/gr-theme-api/gr-theme-api.html">
 <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-js-api-interface">
+  <!--
+    Note: the order matters as files depend on each other.
+    1. gr-api-utils will be used in multiple files below.
+    2. gr-gerrit depends on gr-plugin-loader, gr-public-js-api and
+      also gr-plugin-endpoints
+    3. gr-public-js-api depends on gr-plugin-rest-api
+  -->
+  <script src="gr-api-utils.js"></script>
   <script src="../gr-event-interface/gr-event-interface.js"></script>
   <script src="gr-annotation-actions-context.js"></script>
   <script src="gr-annotation-actions-js-api.js"></script>
@@ -40,4 +49,6 @@
   <script src="gr-plugin-action-context.js"></script>
   <script src="gr-plugin-rest-api.js"></script>
   <script src="gr-public-js-api.js"></script>
+  <script src="gr-plugin-loader.js"></script>
+  <script src="gr-gerrit.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index ad318d7..2c00c89 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -17,11 +17,13 @@
 (function() {
   'use strict';
 
+  // Note: for new events, naming convention should be: `a-b`
   const EventType = {
     HISTORY: 'history',
     LABEL_CHANGE: 'labelchange',
     SHOW_CHANGE: 'showchange',
     SUBMIT_CHANGE: 'submitchange',
+    SHOW_REVISION_ACTIONS: 'show-revision-actions',
     COMMIT_MSG_EDIT: 'commitmsgedit',
     COMMENT: 'comment',
     REVERT: 'revert',
@@ -38,7 +40,6 @@
 
   Polymer({
     is: 'gr-js-api-interface',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _elements: {
@@ -71,6 +72,9 @@
           case EventType.LABEL_CHANGE:
             this._handleLabelChange(detail);
             break;
+          case EventType.SHOW_REVISION_ACTIONS:
+            this._handleShowRevisionActions(detail);
+            break;
           case EventType.HIGHLIGHTJS_LOADED:
             this._handleHighlightjsLoaded(detail);
             break;
@@ -136,13 +140,16 @@
       //
       // This clone and getter can be removed after plugins migrate to use
       // info.mergeable.
-      const change = Object.assign({
+      //
+      // 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 ' +
               'deprecated! Use info.mergeable instead.');
-          return detail.info.mergeable;
+          return detail.info && detail.info.mergeable;
         },
-      }, detail.change);
+      });
       const patchNum = detail.patchNum;
       const info = detail.info;
 
@@ -163,6 +170,22 @@
       }
     },
 
+    /**
+     * @param {!{change: !Object, revisionActions: !Object}} detail
+     */
+    _handleShowRevisionActions(detail) {
+      const registeredCallbacks = this._getEventCallbacks(
+          EventType.SHOW_REVISION_ACTIONS
+      );
+      for (const cb of registeredCallbacks) {
+        try {
+          cb(detail.revisionActions, detail.change);
+        } catch (err) {
+          console.error(err);
+        }
+      }
+    },
+
     handleCommitMessage(change, msg) {
       for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
         try {
@@ -232,30 +255,13 @@
      * Retrieves coverage data possibly provided by a plugin.
      *
      * Will wait for plugins to be loaded. If multiple plugins offer a coverage
-     * provider, the first one is used. If no plugin offers a coverage provider,
-     * will resolve to [].
-     *
-     * TODO(brohlfs): Replace Array<Object> type by Array<Gerrit.CoverageRange>.
-     *
-     * @param {string|number} changeNum
-     * @param {string} path
-     * @param {string|number} basePatchNum
-     * @param {string|number} patchNum
-     * @return {!Promise<!Array<Object>>}
+     * provider, the first one is returned. If no plugin offers a coverage provider,
+     * will resolve to null.
      */
-    getCoverageRanges(changeNum, path, basePatchNum, patchNum) {
-      return Gerrit.awaitPluginsLoaded().then(() => {
-        for (const annotationApi of
-          this._getEventCallbacks(EventType.ANNOTATE_DIFF)) {
-          const provider = annotationApi.getCoverageProvider();
-          // Only one coverage provider makes sense. If there are more, then we
-          // simply ignore them.
-          if (provider) {
-            return provider(changeNum, path, basePatchNum, patchNum);
-          }
-        }
-        return [];
-      });
+    getCoverageAnnotationApi() {
+      return Gerrit.awaitPluginsLoaded()
+          .then(() => this._getEventCallbacks(EventType.ANNOTATE_DIFF)
+              .find(api => api.getCoverageProvider()));
     },
 
     getAdminMenuLinks() {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index f03aa09..14928e6 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-api-interface</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-js-api-interface.html">
 
@@ -33,6 +35,7 @@
 </test-fixture>
 
 <script>
+  const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
   suite('gr-js-api-interface tests', () => {
     let element;
     let plugin;
@@ -46,6 +49,7 @@
     };
 
     setup(() => {
+      this.clock = sinon.useFakeTimers();
       sandbox = sinon.sandbox.create();
       getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
       sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
@@ -62,32 +66,16 @@
       errorStub = sandbox.stub(console, 'error');
       Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
-      Gerrit._setPluginsPending([]);
+      Gerrit._loadPlugins([]);
     });
 
     teardown(() => {
+      this.clock.restore();
       sandbox.restore();
       element._removeEventCallbacks();
       plugin = null;
     });
 
-    test('reuse plugin for install calls', () => {
-      let otherPlugin;
-      Gerrit.install(p => { otherPlugin = p; }, '0.1',
-          'http://test.com/plugins/testplugin/static/test.js');
-      assert.strictEqual(plugin, otherPlugin);
-    });
-
-    test('flushes preinstalls if provided', () => {
-      assert.doesNotThrow(() => {
-        Gerrit._flushPreinstalls();
-      });
-      window.Gerrit.flushPreinstalls = sandbox.stub();
-      Gerrit._flushPreinstalls();
-      assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
-      delete window.Gerrit.flushPreinstalls;
-    });
-
     test('url', () => {
       assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
       assert.equal(plugin.url('/static/test.js'),
@@ -203,18 +191,37 @@
           {change: testChange, patchNum: 1, info: {mergeable: false}});
     });
 
+    test('show-revision-actions event', done => {
+      const testChange = {
+        _number: 42,
+        revisions: {def: {_number: 2}, abc: {_number: 1}},
+      };
+      plugin.on(element.EventType.SHOW_REVISION_ACTIONS, throwErrFn);
+      plugin.on(element.EventType.SHOW_REVISION_ACTIONS, (actions, change) => {
+        assert.deepEqual(change, testChange);
+        assert.deepEqual(actions, {test: {}});
+        assert.isTrue(errorStub.calledOnce);
+        done();
+      });
+      element.handleEvent(element.EventType.SHOW_REVISION_ACTIONS,
+          {change: testChange, revisionActions: {test: {}}});
+    });
+
     test('handleEvent awaits plugins load', done => {
       const testChange = {
         _number: 42,
         revisions: {def: {_number: 2}, abc: {_number: 1}},
       };
       const spy = sandbox.spy();
-      Gerrit._setPluginsCount(1);
+      Gerrit._loadPlugins(['plugins/test.html']);
       plugin.on(element.EventType.SHOW_CHANGE, spy);
       element.handleEvent(element.EventType.SHOW_CHANGE,
           {change: testChange, patchNum: 1});
       assert.isFalse(spy.called);
-      Gerrit._setPluginsCount(0);
+
+      // Timeout on loading plugins
+      this.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
+
       flush(() => {
         assert.isTrue(spy.called);
         done();
@@ -311,90 +318,13 @@
       element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
     });
 
-    test('versioning', () => {
-      const callback = sandbox.spy();
-      Gerrit.install(callback, '0.0pre-alpha');
-      assert(callback.notCalled);
-    });
-
     test('getAccount', done => {
-      Gerrit.getLoggedIn().then(loggedIn => {
+      plugin.restApi().getLoggedIn().then(loggedIn => {
         assert.isTrue(loggedIn);
         done();
       });
     });
 
-    test('_setPluginsCount', done => {
-      stub('gr-reporting', {
-        pluginsLoaded() {
-          done();
-        },
-      });
-      Gerrit._setPluginsCount(0);
-    });
-
-    test('_arePluginsLoaded', () => {
-      assert.isTrue(Gerrit._arePluginsLoaded());
-      Gerrit._setPluginsCount(1);
-      assert.isFalse(Gerrit._arePluginsLoaded());
-      Gerrit._setPluginsCount(0);
-      assert.isTrue(Gerrit._arePluginsLoaded());
-    });
-
-    test('_pluginInstalled', () => {
-      const pluginsLoadedStub = sandbox.stub();
-      stub('gr-reporting', {
-        pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-      });
-      const plugins = [
-        'http://test.com/plugins/foo/static/test.js',
-        'http://test.com/plugins/bar/static/test.js',
-      ];
-      Gerrit._setPluginsPending(plugins);
-      Gerrit._pluginInstalled(plugins[0]);
-      Gerrit._pluginInstalled(plugins[1]);
-      assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
-    });
-
-    test('install calls _pluginInstalled', () => {
-      sandbox.stub(Gerrit, '_pluginInstalled');
-      Gerrit.install(p => { plugin = p; }, '0.1',
-          'http://test.com/plugins/testplugin/static/test.js');
-
-      // testplugin has already been installed once (in setup).
-      assert.isFalse(Gerrit._pluginInstalled.called);
-
-      // testplugin2 plugin has not yet been installed.
-      Gerrit.install(p => { plugin = p; }, '0.1',
-          'http://test.com/plugins/testplugin2/static/test.js');
-      assert.isTrue(Gerrit._pluginInstalled.calledOnce);
-    });
-
-    test('plugin install errors mark plugins as loaded', () => {
-      Gerrit._setPluginsCount(1);
-      Gerrit.install(() => {}, '0.0pre-alpha');
-      return Gerrit.awaitPluginsLoaded();
-    });
-
-    test('multiple ui plugins per java plugin', () => {
-      const file1 = 'http://test.com/plugins/qaz/static/foo.nocache.js';
-      const file2 = 'http://test.com/plugins/qaz/static/bar.js';
-      Gerrit._setPluginsPending([file1, file2]);
-      Gerrit.install(() => {}, '0.1', file1);
-      Gerrit.install(() => {}, '0.1', file2);
-      return Gerrit.awaitPluginsLoaded();
-    });
-
-    test('plugin install errors shows toasts', () => {
-      const alertStub = sandbox.stub();
-      document.addEventListener('show-alert', alertStub);
-      Gerrit._setPluginsCount(1);
-      Gerrit.install(() => {}, '0.0pre-alpha');
-      return Gerrit.awaitPluginsLoaded().then(() => {
-        assert.isTrue(alertStub.calledOnce);
-      });
-    });
-
     test('attributeHelper', () => {
       assert.isOk(plugin.attributeHelper());
     });
@@ -420,40 +350,12 @@
           element.EventType.ADMIN_MENU_LINKS);
     });
 
-    test('Gerrit._isPluginPreloaded', () => {
-      Gerrit._preloadedPlugins = {baz: ()=>{}};
-      assert.isFalse(Gerrit._isPluginPreloaded('plugins/foo/bar'));
-      assert.isFalse(Gerrit._isPluginPreloaded('http://a.com/42'));
-      assert.isTrue(Gerrit._isPluginPreloaded('preloaded:baz'));
-      Gerrit._preloadedPlugins = null;
-    });
-
-    test('preloaded plugins are installed', () => {
-      const installStub = sandbox.stub();
-      Gerrit._preloadedPlugins = {foo: installStub};
-      Gerrit._installPreloadedPlugins();
-      assert.isTrue(installStub.called);
-      const pluginApi = installStub.lastCall.args[0];
-      assert.strictEqual(pluginApi.getPluginName(), 'foo');
-    });
-
-    test('installing preloaded plugin', () => {
-      let plugin;
-      window.ASSETS_PATH = 'http://blips.com/chitz';
-      Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
-      assert.strictEqual(plugin.getPluginName(), 'foo');
-      assert.strictEqual(plugin.url('/some/thing.html'),
-          'http://blips.com/chitz/plugins/foo/some/thing.html');
-      delete window.ASSETS_PATH;
-    });
-
     suite('test plugin with base url', () => {
       let baseUrlPlugin;
 
       setup(() => {
         sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
 
-        Gerrit._setPluginsCount(1);
         Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
             'http://test.com/r/plugins/baseurlplugin/static/test.js');
       });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
index ca87956..6da117f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-action-context</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-js-api-interface.html"/>
 
@@ -40,7 +42,6 @@
 
     setup(() => {
       sandbox = sinon.sandbox.create();
-      Gerrit._setPluginsCount(1);
       Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
       instance = new GrPluginActionContext(plugin);
@@ -140,12 +141,12 @@
         send: sendStub,
       });
       const errorStub = sandbox.stub();
-      document.addEventListener('network-error', errorStub);
+      document.addEventListener('show-alert', errorStub);
       instance.call();
       flush(() => {
         assert.isTrue(errorStub.calledOnce);
         assert.equal(errorStub.args[0][0].detail.message,
-            'Plugin network error: boom');
+            'Plugin network error: Error: boom');
         done();
       });
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
index b00b5ac..8ed7f14 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-endpoints</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-js-api-interface.html"/>
 
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
new file mode 100644
index 0000000..201b683
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
@@ -0,0 +1,397 @@
+/**
+ * @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.
+ */
+
+(function(window) {
+  'use strict';
+
+  // Import utils methods
+  const {
+    PLUGIN_LOADING_TIMEOUT_MS,
+    PRELOADED_PROTOCOL,
+    getPluginNameFromUrl,
+    getBaseUrl,
+  } = window._apiUtils;
+
+  /**
+   * @enum {string}
+   */
+  const PluginState = {
+    /**
+     * State that indicates the plugin is pending to be loaded.
+     */
+    PENDING: 'PENDING',
+
+    /**
+     * State that indicates the plugin is already loaded.
+     */
+    LOADED: 'LOADED',
+
+    /**
+     * State that indicates the plugin is already loaded.
+     */
+    PRE_LOADED: 'PRE_LOADED',
+
+    /**
+     * State that indicates the plugin failed to load.
+     */
+    LOAD_FAILED: 'LOAD_FAILED',
+  };
+
+  // Prefix for any unrecognized plugin urls.
+  // Url should match following patterns:
+  // /plugins/PLUGINNAME/static/SCRIPTNAME.(html|js)
+  // /plugins/PLUGINNAME.(js|html)
+  const UNKNOWN_PLUGIN_PREFIX = '__$$__';
+
+  // Current API version for Plugin,
+  // plugins with incompatible version will not be laoded.
+  const API_VERSION = '0.1';
+
+  /**
+   * PluginLoader, responsible for:
+   *
+   * Loading all plugins and handling errors etc.
+   * Recording plugin state.
+   * Reporting on plugin loading status.
+   * Retrieve plugin.
+   * Check plugin status and if all plugins loaded.
+   */
+  class PluginLoader {
+    constructor() {
+      this._pluginListLoaded = false;
+
+      /** @type {Map<string,PluginLoader.PluginObject>} */
+      this._plugins = new Map();
+
+      this._reporting = null;
+
+      // Promise that resolves when all plugins loaded
+      this._loadingPromise = null;
+
+      // Resolver to resolve _loadingPromise once all plugins loaded
+      this._loadingResolver = null;
+    }
+
+    _getReporting() {
+      if (!this._reporting) {
+        this._reporting = document.createElement('gr-reporting');
+      }
+      return this._reporting;
+    }
+
+    /**
+     * Use the plugin name or use the full url if not recognized.
+     *
+     * @see gr-api-utils#getPluginNameFromUrl
+     * @param {string|URL} url
+     */
+    _getPluginKeyFromUrl(url) {
+      return getPluginNameFromUrl(url) ||
+        `${UNKNOWN_PLUGIN_PREFIX}${url}`;
+    }
+
+    /**
+     * Load multiple plugins with certain options.
+     *
+     * @param {Array<string>} plugins
+     * @param {Object<string, PluginLoader.PluginOption>} opts
+     */
+    loadPlugins(plugins = [], opts = {}) {
+      this._pluginListLoaded = true;
+
+      plugins.forEach(path => {
+        const url = this._urlFor(path);
+        // Skip if preloaded, for bundling.
+        if (this.isPluginPreloaded(url)) return;
+
+        const pluginKey = this._getPluginKeyFromUrl(url);
+        // Skip if already installed.
+        if (this._plugins.has(pluginKey)) return;
+        this._plugins.set(pluginKey, {
+          name: pluginKey,
+          url,
+          state: PluginState.PENDING,
+          plugin: null,
+        });
+
+        if (this._isPathEndsWith(url, '.html')) {
+          this._importHtmlPlugin(url, opts && opts[path]);
+        } else if (this._isPathEndsWith(url, '.js')) {
+          this._loadJsPlugin(url);
+        } else {
+          this._failToLoad(`Unrecognized plugin url ${url}`, url);
+        }
+      });
+
+      this.awaitPluginsLoaded().then(() => {
+        console.info('Plugins loaded');
+        this._getReporting().pluginsLoaded(this._getAllInstalledPluginNames());
+      });
+    }
+
+    _isPathEndsWith(url, suffix) {
+      if (!(url instanceof URL)) {
+        try {
+          url = new URL(url);
+        } catch (e) {
+          console.warn(e);
+          return false;
+        }
+      }
+
+      return url.pathname && url.pathname.endsWith(suffix);
+    }
+
+    _getAllInstalledPluginNames() {
+      const installedPlugins = [];
+      for (const plugin of this._plugins.values()) {
+        if (plugin.state === PluginState.LOADED) {
+          installedPlugins.push(plugin.name);
+        }
+      }
+      return installedPlugins;
+    }
+
+    install(callback, opt_version, opt_src) {
+      // HTML import polyfill adds __importElement pointing to the import tag.
+      const script = document.currentScript &&
+          (document.currentScript.__importElement || document.currentScript);
+      let src = opt_src || (script && script.src);
+      if (!src || src.startsWith('data:')) {
+        src = script && script.baseURI;
+      }
+
+      if (opt_version && opt_version !== API_VERSION) {
+        this._failToLoad(`Plugin ${src} install error: only version ` +
+            API_VERSION + ' is supported in PolyGerrit. ' + opt_version +
+            ' was given.', src);
+        return;
+      }
+
+      const pluginObject = this.getPlugin(src);
+      let plugin = pluginObject && pluginObject.plugin;
+      if (!plugin) {
+        plugin = new Plugin(src);
+      }
+      try {
+        callback(plugin);
+        this._pluginInstalled(src, plugin);
+      } catch (e) {
+        this._failToLoad(`${e.name}: ${e.message}`, src);
+      }
+    }
+
+    get arePluginsLoaded() {
+      // As the size of plugins is relatively small,
+      // so the performance of this check should be reasonable
+      if (!this._pluginListLoaded) return false;
+      for (const plugin of this._plugins.values()) {
+        if (plugin.state === PluginState.PENDING) return false;
+      }
+      return true;
+    }
+
+    _checkIfCompleted() {
+      if (this.arePluginsLoaded && this._loadingResolver) {
+        this._loadingResolver();
+        this._loadingResolver = null;
+        this._loadingPromise = null;
+      }
+    }
+
+    _timeout() {
+      const pendingPlugins = [];
+      for (const plugin of this._plugins.values()) {
+        if (plugin.state === PluginState.PENDING) {
+          this._updatePluginState(plugin.url, PluginState.LOAD_FAILED);
+          this._checkIfCompleted();
+          pendingPlugins.push(plugin.url);
+        }
+      }
+      return `Timeout when loading plugins: ${pendingPlugins.join(',')}`;
+    }
+
+    _failToLoad(message, pluginUrl) {
+      // Show an alert with the error
+      document.dispatchEvent(new CustomEvent('show-alert', {
+        detail: {
+          message: `Plugin install error: ${message} from ${pluginUrl}`,
+        },
+      }));
+      this._updatePluginState(pluginUrl, PluginState.LOAD_FAILED);
+      this._checkIfCompleted();
+    }
+
+    _updatePluginState(pluginUrl, state) {
+      const key = this._getPluginKeyFromUrl(pluginUrl);
+      if (this._plugins.has(key)) {
+        this._plugins.get(key).state = state;
+      } else {
+        // Plugin is not recorded for some reason.
+        console.warn(`Plugin loaded separately: ${pluginUrl}`);
+        this._plugins.set(key, {
+          name: key,
+          url: pluginUrl,
+          state,
+          plugin: null,
+        });
+      }
+      return this._plugins.get(key);
+    }
+
+    _pluginInstalled(url, plugin) {
+      const pluginObj = this._updatePluginState(url, PluginState.LOADED);
+      pluginObj.plugin = plugin;
+      this._getReporting().pluginLoaded(plugin.getPluginName() || url);
+      console.log(`Plugin ${plugin.getPluginName() || url} installed.`);
+      this._checkIfCompleted();
+    }
+
+    installPreloadedPlugins() {
+      if (!window.Gerrit || !window.Gerrit._preloadedPlugins) { return; }
+      const Gerrit = window.Gerrit;
+      for (const name in Gerrit._preloadedPlugins) {
+        if (!Gerrit._preloadedPlugins.hasOwnProperty(name)) { continue; }
+        const callback = Gerrit._preloadedPlugins[name];
+        this.install(callback, API_VERSION, PRELOADED_PROTOCOL + name);
+      }
+    }
+
+    isPluginPreloaded(pathOrUrl) {
+      const url = this._urlFor(pathOrUrl);
+      const name = getPluginNameFromUrl(url);
+      if (name && window.Gerrit._preloadedPlugins) {
+        return window.Gerrit._preloadedPlugins.hasOwnProperty(name);
+      } else {
+        return false;
+      }
+    }
+
+    /**
+     * Checks if given plugin path/url is enabled or not.
+     *
+     * @param {string} pathOrUrl
+     */
+    isPluginEnabled(pathOrUrl) {
+      const url = this._urlFor(pathOrUrl);
+      if (this.isPluginPreloaded(url)) return true;
+      const key = this._getPluginKeyFromUrl(url);
+      return this._plugins.has(key);
+    }
+
+    /**
+     * Returns the plugin object with a given url.
+     *
+     * @param {string} pathOrUrl
+     */
+    getPlugin(pathOrUrl) {
+      const key = this._getPluginKeyFromUrl(this._urlFor(pathOrUrl));
+      return this._plugins.get(key);
+    }
+
+    /**
+     * Checks if given plugin path/url is loaded or not.
+     *
+     * @param {string} pathOrUrl
+     */
+    isPluginLoaded(pathOrUrl) {
+      const url = this._urlFor(pathOrUrl);
+      const key = this._getPluginKeyFromUrl(url);
+      return this._plugins.has(key) ?
+        this._plugins.get(key).state === PluginState.LOADED :
+        false;
+    }
+
+    _importHtmlPlugin(pluginUrl, opts = {}) {
+      // onload (second param) needs to be a function. When null or undefined
+      // were passed, plugins were not loaded correctly.
+      (Polymer.importHref || Polymer.Base.importHref)(
+          this._urlFor(pluginUrl), () => {},
+          () => this._failToLoad(`${pluginUrl} import error`, pluginUrl),
+          !opts.sync);
+    }
+
+    _loadJsPlugin(pluginUrl) {
+      this._createScriptTag(this._urlFor(pluginUrl));
+    }
+
+    _createScriptTag(url) {
+      const el = document.createElement('script');
+      el.defer = true;
+      el.src = url;
+      el.onerror = () => this._failToLoad(`${url} load error`, url);
+      return document.body.appendChild(el);
+    }
+
+    _urlFor(pathOrUrl) {
+      if (!pathOrUrl) {
+        return pathOrUrl;
+      }
+      if (pathOrUrl.startsWith(PRELOADED_PROTOCOL) ||
+          pathOrUrl.startsWith('http')) {
+        // Plugins are loaded from another domain or preloaded.
+        return pathOrUrl;
+      }
+      if (!pathOrUrl.startsWith('/')) {
+        pathOrUrl = '/' + pathOrUrl;
+      }
+      return window.location.origin + getBaseUrl() + pathOrUrl;
+    }
+
+    awaitPluginsLoaded() {
+      // Resolve if completed.
+      this._checkIfCompleted();
+
+      if (this.arePluginsLoaded) {
+        return Promise.resolve();
+      }
+      if (!this._loadingPromise) {
+        let timerId;
+        this._loadingPromise =
+          Promise.race([
+            new Promise(resolve => this._loadingResolver = resolve),
+            new Promise((_, reject) => timerId = setTimeout(
+                () => {
+                  reject(this._timeout());
+                }, PLUGIN_LOADING_TIMEOUT_MS)),
+          ]).then(() => {
+            if (timerId) clearTimeout(timerId);
+          });
+      }
+      return this._loadingPromise;
+    }
+  }
+
+  /**
+   * @typedef {{
+   *            name:string,
+   *            url:string,
+   *            state:PluginState,
+   *            plugin:Object
+   *          }}
+   */
+  PluginLoader.PluginObject;
+
+  /**
+   * @typedef {{
+   *            sync:boolean,
+   *          }}
+   */
+  PluginLoader.PluginOption;
+
+  window.PluginLoader = PluginLoader;
+})(window);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
new file mode 100644
index 0000000..ee54319
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
@@ -0,0 +1,502 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-plugin-host</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-js-api-interface.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-js-api-interface></gr-js-api-interface>
+  </template>
+</test-fixture>
+
+<script>
+  const {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
+  suite('gr-plugin-loader tests', () => {
+    let plugin;
+    let sandbox;
+    let url;
+    let sendStub;
+
+    setup(() => {
+      this.clock = sinon.useFakeTimers();
+      sandbox = sinon.sandbox.create();
+      sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+      stub('gr-rest-api-interface', {
+        getAccount() {
+          return Promise.resolve({name: 'Judy Hopps'});
+        },
+        send(...args) {
+          return sendStub(...args);
+        },
+      });
+      sandbox.stub(document.body, 'appendChild');
+      fixture('basic');
+      url = window.location.origin;
+    });
+
+    teardown(() => {
+      sandbox.restore();
+      this.clock.restore();
+      Gerrit._testOnly_resetPlugins();
+    });
+
+    test('reuse plugin for install calls', () => {
+      Gerrit.install(p => { plugin = p; }, '0.1',
+          'http://test.com/plugins/testplugin/static/test.js');
+
+      let otherPlugin;
+      Gerrit.install(p => { otherPlugin = p; }, '0.1',
+          'http://test.com/plugins/testplugin/static/test.js');
+      assert.strictEqual(plugin, otherPlugin);
+    });
+
+    test('flushes preinstalls if provided', () => {
+      assert.doesNotThrow(() => {
+        Gerrit._testOnly_flushPreinstalls();
+      });
+      window.Gerrit.flushPreinstalls = sandbox.stub();
+      Gerrit._testOnly_flushPreinstalls();
+      assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
+      delete window.Gerrit.flushPreinstalls;
+    });
+
+    test('versioning', () => {
+      const callback = sandbox.spy();
+      Gerrit.install(callback, '0.0pre-alpha');
+      assert(callback.notCalled);
+    });
+
+    test('report pluginsLoaded', done => {
+      stub('gr-reporting', {
+        pluginsLoaded() {
+          done();
+        },
+      });
+      Gerrit._loadPlugins([]);
+    });
+
+    test('arePluginsLoaded', done => {
+      assert.isFalse(Gerrit._arePluginsLoaded());
+      const plugins = [
+        'http://test.com/plugins/foo/static/test.js',
+        'http://test.com/plugins/bar/static/test.js',
+      ];
+
+      Gerrit._loadPlugins(plugins);
+      assert.isFalse(Gerrit._arePluginsLoaded());
+      // Timeout on loading plugins
+      this.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
+
+      flush(() => {
+        assert.isTrue(Gerrit._arePluginsLoaded());
+        done();
+      });
+    });
+
+    test('plugins installed successfully', done => {
+      sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+        Gerrit.install(() => void 0, undefined, url);
+      });
+      const pluginsLoadedStub = sandbox.stub();
+      stub('gr-reporting', {
+        pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+      });
+
+      const plugins = [
+        'http://test.com/plugins/foo/static/test.js',
+        'http://test.com/plugins/bar/static/test.js',
+      ];
+      Gerrit._loadPlugins(plugins);
+
+      flush(() => {
+        assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
+        assert.isTrue(Gerrit._arePluginsLoaded());
+        done();
+      });
+    });
+
+    test('isPluginEnabled and isPluginLoaded', done => {
+      sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+        Gerrit.install(() => void 0, undefined, url);
+      });
+      const pluginsLoadedStub = sandbox.stub();
+      stub('gr-reporting', {
+        pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+      });
+
+      const plugins = [
+        'http://test.com/plugins/foo/static/test.js',
+        'http://test.com/plugins/bar/static/test.js',
+        'bar/static/test.js',
+      ];
+      Gerrit._loadPlugins(plugins);
+      assert.isTrue(
+          plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
+      );
+
+      flush(() => {
+        assert.isTrue(Gerrit._arePluginsLoaded());
+        assert.isTrue(
+            plugins.every(plugin => Gerrit._pluginLoader.isPluginLoaded(plugin))
+        );
+
+        done();
+      });
+    });
+
+    test('plugins installed mixed result, 1 fail 1 succeed', done => {
+      const plugins = [
+        'http://test.com/plugins/foo/static/test.js',
+        'http://test.com/plugins/bar/static/test.js',
+      ];
+
+      const alertStub = sandbox.stub();
+      document.addEventListener('show-alert', alertStub);
+
+      sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+        Gerrit.install(() => {
+          if (url === plugins[0]) {
+            throw new Error('failed');
+          }
+        }, undefined, url);
+      });
+
+      const pluginsLoadedStub = sandbox.stub();
+      stub('gr-reporting', {
+        pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+      });
+
+      Gerrit._loadPlugins(plugins);
+
+      flush(() => {
+        assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
+        assert.isTrue(Gerrit._arePluginsLoaded());
+        assert.isTrue(alertStub.calledOnce);
+        done();
+      });
+    });
+
+    test('isPluginEnabled and isPluginLoaded for mixed results', done => {
+      const plugins = [
+        'http://test.com/plugins/foo/static/test.js',
+        'http://test.com/plugins/bar/static/test.js',
+      ];
+
+      const alertStub = sandbox.stub();
+      document.addEventListener('show-alert', alertStub);
+
+      sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+        Gerrit.install(() => {
+          if (url === plugins[0]) {
+            throw new Error('failed');
+          }
+        }, undefined, url);
+      });
+
+      const pluginsLoadedStub = sandbox.stub();
+      stub('gr-reporting', {
+        pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+      });
+
+      Gerrit._loadPlugins(plugins);
+      assert.isTrue(
+          plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
+      );
+
+      flush(() => {
+        assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
+        assert.isTrue(Gerrit._arePluginsLoaded());
+        assert.isTrue(alertStub.calledOnce);
+        assert.isTrue(Gerrit._pluginLoader.isPluginLoaded(plugins[1]));
+        assert.isFalse(Gerrit._pluginLoader.isPluginLoaded(plugins[0]));
+        done();
+      });
+    });
+
+    test('plugins installed all failed', done => {
+      const plugins = [
+        'http://test.com/plugins/foo/static/test.js',
+        'http://test.com/plugins/bar/static/test.js',
+      ];
+
+      const alertStub = sandbox.stub();
+      document.addEventListener('show-alert', alertStub);
+
+      sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+        Gerrit.install(() => {
+          throw new Error('failed');
+        }, undefined, url);
+      });
+
+      const pluginsLoadedStub = sandbox.stub();
+      stub('gr-reporting', {
+        pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+      });
+
+      Gerrit._loadPlugins(plugins);
+
+      flush(() => {
+        assert.isTrue(pluginsLoadedStub.calledWithExactly([]));
+        assert.isTrue(Gerrit._arePluginsLoaded());
+        assert.isTrue(alertStub.calledTwice);
+        done();
+      });
+    });
+
+    test('plugins installed failed becasue of wrong version', done => {
+      const plugins = [
+        'http://test.com/plugins/foo/static/test.js',
+        'http://test.com/plugins/bar/static/test.js',
+      ];
+
+      const alertStub = sandbox.stub();
+      document.addEventListener('show-alert', alertStub);
+
+      sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+        Gerrit.install(() => {
+        }, url === plugins[0] ? '' : 'alpha', url);
+      });
+
+      const pluginsLoadedStub = sandbox.stub();
+      stub('gr-reporting', {
+        pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+      });
+
+      Gerrit._loadPlugins(plugins);
+
+      flush(() => {
+        assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo']));
+        assert.isTrue(Gerrit._arePluginsLoaded());
+        assert.isTrue(alertStub.calledOnce);
+        done();
+      });
+    });
+
+    test('multiple assets for same plugin installed successfully', done => {
+      sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+        Gerrit.install(() => void 0, undefined, url);
+      });
+      const pluginsLoadedStub = sandbox.stub();
+      stub('gr-reporting', {
+        pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+      });
+
+      const plugins = [
+        'http://test.com/plugins/foo/static/test.js',
+        'http://test.com/plugins/foo/static/test2.js',
+        'http://test.com/plugins/bar/static/test.js',
+      ];
+      Gerrit._loadPlugins(plugins);
+
+      flush(() => {
+        assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
+        assert.isTrue(Gerrit._arePluginsLoaded());
+        done();
+      });
+    });
+
+    suite('plugin path and url', () => {
+      let importHtmlPluginStub;
+      let loadJsPluginStub;
+      setup(() => {
+        importHtmlPluginStub = sandbox.stub();
+        sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
+          importHtmlPluginStub(url);
+        });
+        loadJsPluginStub = sandbox.stub();
+        sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+          loadJsPluginStub(url);
+        });
+      });
+
+      test('invalid plugin path', () => {
+        const failToLoadStub = sandbox.stub();
+        sandbox.stub(Gerrit._pluginLoader, '_failToLoad', (...args) => {
+          failToLoadStub(...args);
+        });
+
+        Gerrit._loadPlugins([
+          'foo/bar',
+        ]);
+
+        assert.isTrue(failToLoadStub.calledOnce);
+        assert.isTrue(failToLoadStub.calledWithExactly(
+            `Unrecognized plugin url ${url}/foo/bar`,
+            `${url}/foo/bar`
+        ));
+      });
+
+      test('relative path for plugins', () => {
+        Gerrit._loadPlugins([
+          'foo/bar.js',
+          'foo/bar.html',
+        ]);
+
+        assert.isTrue(importHtmlPluginStub.calledOnce);
+        assert.isTrue(
+            importHtmlPluginStub.calledWithExactly(`${url}/foo/bar.html`)
+        );
+        assert.isTrue(loadJsPluginStub.calledOnce);
+        assert.isTrue(
+            loadJsPluginStub.calledWithExactly(`${url}/foo/bar.js`)
+        );
+      });
+
+
+      test('relative path should honor getBaseUrl', () => {
+        const testUrl = '/test';
+        sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => {
+          return testUrl;
+        });
+
+        Gerrit._loadPlugins([
+          'foo/bar.js',
+          'foo/bar.html',
+        ]);
+
+        assert.isTrue(importHtmlPluginStub.calledOnce);
+        assert.isTrue(loadJsPluginStub.calledOnce);
+        assert.isTrue(
+            importHtmlPluginStub.calledWithExactly(
+                `${url}${testUrl}/foo/bar.html`
+            )
+        );
+        assert.isTrue(
+            loadJsPluginStub.calledWithExactly(`${url}${testUrl}/foo/bar.js`)
+        );
+      });
+
+      test('absolute path for plugins', () => {
+        Gerrit._loadPlugins([
+          'http://e.com/foo/bar.js',
+          'http://e.com/foo/bar.html',
+        ]);
+
+        assert.isTrue(importHtmlPluginStub.calledOnce);
+        assert.isTrue(
+            importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
+        );
+        assert.isTrue(loadJsPluginStub.calledOnce);
+        assert.isTrue(
+            loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`)
+        );
+      });
+    });
+
+    test('adds js plugins will call the body', () => {
+      Gerrit._loadPlugins([
+        'http://e.com/foo/bar.js',
+        'http://e.com/bar/foo.js',
+      ]);
+      assert.isTrue(document.body.appendChild.calledTwice);
+    });
+
+    test('can call awaitPluginsLoaded multiple times', done => {
+      const plugins = [
+        'http://e.com/foo/bar.js',
+        'http://e.com/bar/foo.js',
+      ];
+
+      let installed = false;
+      function pluginCallback(url) {
+        if (url === plugins[1]) {
+          installed = true;
+        }
+      }
+      sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+        Gerrit.install(() => pluginCallback(url), undefined, url);
+      });
+
+      Gerrit._loadPlugins(plugins);
+
+      Gerrit.awaitPluginsLoaded().then(() => {
+        assert.isTrue(installed);
+
+        Gerrit.awaitPluginsLoaded().then(() => {
+          done();
+        });
+      });
+    });
+
+    suite('preloaded plugins', () => {
+      test('skips preloaded plugins when load plugins', () => {
+        const importHtmlPluginStub = sandbox.stub();
+        sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
+          importHtmlPluginStub(url);
+        });
+        const loadJsPluginStub = sandbox.stub();
+        sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+          loadJsPluginStub(url);
+        });
+
+        Gerrit._preloadedPlugins = {
+          foo: () => void 0,
+          bar: () => void 0,
+        };
+
+        Gerrit._loadPlugins([
+          'http://e.com/plugins/foo.js',
+          'plugins/bar.html',
+          'http://e.com/plugins/test/foo.js',
+        ]);
+
+        assert.isTrue(importHtmlPluginStub.notCalled);
+        assert.isTrue(loadJsPluginStub.calledOnce);
+      });
+
+      test('isPluginPreloaded', () => {
+        Gerrit._preloadedPlugins = {baz: ()=>{}};
+        assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('plugins/foo/bar'));
+        assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('http://a.com/42'));
+        assert.isTrue(
+            Gerrit._pluginLoader.isPluginPreloaded(PRELOADED_PROTOCOL + 'baz')
+        );
+        Gerrit._preloadedPlugins = null;
+      });
+
+      test('preloaded plugins are installed', () => {
+        const installStub = sandbox.stub();
+        Gerrit._preloadedPlugins = {foo: installStub};
+        Gerrit._pluginLoader.installPreloadedPlugins();
+        assert.isTrue(installStub.called);
+        const pluginApi = installStub.lastCall.args[0];
+        assert.strictEqual(pluginApi.getPluginName(), 'foo');
+      });
+
+      test('installing preloaded plugin', () => {
+        let plugin;
+        window.ASSETS_PATH = 'http://blips.com/chitz';
+        Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
+        assert.strictEqual(plugin.getPluginName(), 'foo');
+        assert.strictEqual(plugin.url('/some/thing.html'),
+            'http://blips.com/chitz/plugins/foo/some/thing.html');
+        delete window.ASSETS_PATH;
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
index ce619a0..ecfafb5 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
@@ -46,6 +46,19 @@
     getRestApi().invalidateReposCache();
   };
 
+  GrPluginRestApi.prototype.getAccount = function() {
+    return getRestApi().getAccount();
+  };
+
+  GrPluginRestApi.prototype.getAccountCapabilities = function(capabilities) {
+    return getRestApi().getAccountCapabilities(capabilities);
+  };
+
+  GrPluginRestApi.prototype.getRepos =
+    function(filter, reposPerPage, opt_offset) {
+      return getRestApi().getRepos(filter, reposPerPage, opt_offset);
+    };
+
   /**
    * Fetch and return native browser REST API Response.
    *
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
index 7a59337..bcbd961 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-rest-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-js-api-interface.html"/>
 
@@ -48,7 +50,6 @@
         a[k] = (...args) => restApiStub[k](...args);
         return a;
       }, {}));
-      Gerrit._setPluginsCount(1);
       Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
       instance = new GrPluginRestApi();
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 7376a12..b261a90 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
@@ -17,71 +17,18 @@
 (function(window) {
   'use strict';
 
-  /**
-   * Hash of loaded and installed plugins, name to Plugin object.
-   */
-  const _plugins = {};
-
-  /**
-   * Array of plugin URLs to be loaded, name to url.
-   */
-  let _pluginsPending = {};
-
-  let _pluginsInstalled = [];
-
-  let _pluginsPendingCount = -1;
-
   const PRELOADED_PROTOCOL = 'preloaded:';
 
-  const UNKNOWN_PLUGIN = 'unknown';
-
   const PANEL_ENDPOINTS_MAPPING = {
     CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK: 'change-view-integration',
     CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK: 'change-metadata-item',
   };
 
-  const PLUGIN_LOADING_TIMEOUT_MS = 10000;
-
-  let _restAPI;
-
-  const getRestAPI = () => {
-    if (!_restAPI) {
-      _restAPI = document.createElement('gr-rest-api-interface');
-    }
-    return _restAPI;
-  };
-
-  let _reporting;
-  const getReporting = () => {
-    if (!_reporting) {
-      _reporting = document.createElement('gr-reporting');
-    }
-    return _reporting;
-  };
-
-  // TODO (viktard): deprecate in favor of GrPluginRestApi.
-  function send(method, url, opt_callback, opt_payload) {
-    return getRestAPI().send(method, url, opt_payload).then(response => {
-      if (response.status < 200 || response.status >= 300) {
-        return response.text().then(text => {
-          if (text) {
-            return Promise.reject(text);
-          } else {
-            return Promise.reject(response.status);
-          }
-        });
-      } else {
-        return getRestAPI().getResponseObject(response);
-      }
-    }).then(response => {
-      if (opt_callback) {
-        opt_callback(response);
-      }
-      return response;
-    });
-  }
-
-  const API_VERSION = '0.1';
+  // Import utils methods
+  const {
+    getPluginNameFromUrl,
+    send,
+  } = window._apiUtils;
 
   /**
    * Plugin-provided custom components can affect content in extension
@@ -99,50 +46,6 @@
     STYLE: 'style',
   };
 
-  function flushPreinstalls() {
-    if (window.Gerrit.flushPreinstalls) {
-      window.Gerrit.flushPreinstalls();
-    }
-  }
-
-  function installPreloadedPlugins() {
-    if (!Gerrit._preloadedPlugins) { return; }
-    for (const name in Gerrit._preloadedPlugins) {
-      if (!Gerrit._preloadedPlugins.hasOwnProperty(name)) { continue; }
-      const callback = Gerrit._preloadedPlugins[name];
-      Gerrit.install(callback, API_VERSION, PRELOADED_PROTOCOL + name);
-    }
-  }
-
-  function getPluginNameFromUrl(url) {
-    if (!(url instanceof URL)) {
-      try {
-        url = new URL(url);
-      } catch (e) {
-        console.warn(e);
-        return null;
-      }
-    }
-    if (url.protocol === PRELOADED_PROTOCOL) {
-      return url.pathname;
-    }
-    const base = Gerrit.BaseUrlBehavior.getBaseUrl();
-    const pathname = url.pathname.replace(base, '');
-    // Site theme is server from predefined path.
-    if (pathname === '/static/gerrit-theme.html') {
-      return 'gerrit-theme';
-    } else if (!pathname.startsWith('/plugins')) {
-      console.warn('Plugin not being loaded from /plugins base path:',
-          url.href, '— Unable to determine name.');
-      return;
-    }
-    // Pathname should normally look like this:
-    // /plugins/PLUGINNAME/static/SCRIPTNAME.html
-    // Or, for app/samples:
-    // /plugins/PLUGINNAME.html
-    return pathname.split('/')[2].split('.')[0];
-  }
-
   function Plugin(opt_url) {
     this._domHooks = new GrDomHooksManager(this);
 
@@ -315,6 +218,10 @@
     return new GrSettingsApi(this);
   };
 
+  Plugin.prototype.styles = function() {
+    return new GrStylesApi();
+  };
+
   /**
    * To make REST requests for plugin-provided endpoints, use
    *
@@ -361,10 +268,14 @@
       return;
     }
     return this.registerCustomComponent(
-        Gerrit._getPluginScreenName(this.getPluginName(), screenName),
+        this._getScreenName(screenName),
         opt_moduleName);
   };
 
+  Plugin.prototype._getScreenName = function(screenName) {
+    return `${this.getPluginName()}-screen-${screenName}`;
+  };
+
   const deprecatedAPI = {
     _loadedGwt: ()=> {},
 
@@ -415,7 +326,7 @@
             'Please use strings for patterns.');
         return;
       }
-      this.hook(Gerrit._getPluginScreenName(this.getPluginName(), pattern))
+      this.hook(this._getScreenName(pattern))
           .onAttached(el => {
             el.style.display = 'none';
             callback({
@@ -472,243 +383,5 @@
     },
   };
 
-  flushPreinstalls();
-
-  const Gerrit = window.Gerrit || {};
-
-  let _resolveAllPluginsLoaded = null;
-  let _allPluginsPromise = null;
-
-  Gerrit._endpoints = new GrPluginEndpoints();
-
-  // Provide reset plugins function to clear installed plugins between tests.
-  const app = document.querySelector('#app');
-  if (!app) {
-    // No gr-app found (running tests)
-    Gerrit._installPreloadedPlugins = installPreloadedPlugins;
-    Gerrit._flushPreinstalls = flushPreinstalls;
-    Gerrit._resetPlugins = () => {
-      _allPluginsPromise = null;
-      _pluginsInstalled = [];
-      _pluginsPending = {};
-      _pluginsPendingCount = -1;
-      _reporting = null;
-      _resolveAllPluginsLoaded = null;
-      _restAPI = null;
-      Gerrit._endpoints = new GrPluginEndpoints();
-      for (const k of Object.keys(_plugins)) {
-        delete _plugins[k];
-      }
-    };
-  }
-
-  Gerrit.getPluginName = function() {
-    console.warn('Gerrit.getPluginName is not supported in PolyGerrit.',
-        'Please use plugin.getPluginName() instead.');
-  };
-
-  Gerrit.css = function(rulesStr) {
-    if (!Gerrit._customStyleSheet) {
-      const styleEl = document.createElement('style');
-      document.head.appendChild(styleEl);
-      Gerrit._customStyleSheet = styleEl.sheet;
-    }
-
-    const name = '__pg_js_api_class_' +
-        Gerrit._customStyleSheet.cssRules.length;
-    Gerrit._customStyleSheet.insertRule('.' + name + '{' + rulesStr + '}', 0);
-    return name;
-  };
-
-  Gerrit.install = function(callback, opt_version, opt_src) {
-    // HTML import polyfill adds __importElement pointing to the import tag.
-    const script = document.currentScript &&
-        (document.currentScript.__importElement || document.currentScript);
-    const src = opt_src || (script && (script.src || script.baseURI));
-    const name = getPluginNameFromUrl(src);
-
-    if (opt_version && opt_version !== API_VERSION) {
-      Gerrit._pluginInstallError(`Plugin ${name} install error: only version ` +
-          API_VERSION + ' is supported in PolyGerrit. ' + opt_version +
-          ' was given.');
-      return;
-    }
-
-    const existingPlugin = _plugins[name];
-    const plugin = existingPlugin || new Plugin(src);
-    try {
-      callback(plugin);
-      if (name) {
-        _plugins[name] = plugin;
-      }
-      if (!existingPlugin) {
-        Gerrit._pluginInstalled(src);
-      }
-    } catch (e) {
-      Gerrit._pluginInstallError(`${e.name}: ${e.message}`);
-    }
-  };
-
-  Gerrit.getLoggedIn = function() {
-    console.warn('Gerrit.getLoggedIn() is deprecated! ' +
-        'Use plugin.restApi().getLoggedIn()');
-    return document.createElement('gr-rest-api-interface').getLoggedIn();
-  };
-
-  Gerrit.get = function(url, callback) {
-    console.warn('.get() is deprecated! Use plugin.restApi().get()');
-    send('GET', url, callback);
-  };
-
-  Gerrit.post = function(url, payload, callback) {
-    console.warn('.post() is deprecated! Use plugin.restApi().post()');
-    send('POST', url, callback, payload);
-  };
-
-  Gerrit.put = function(url, payload, callback) {
-    console.warn('.put() is deprecated! Use plugin.restApi().put()');
-    send('PUT', url, callback, payload);
-  };
-
-  Gerrit.delete = function(url, opt_callback) {
-    console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
-    return getRestAPI().send('DELETE', url).then(response => {
-      if (response.status !== 204) {
-        return response.text().then(text => {
-          if (text) {
-            return Promise.reject(text);
-          } else {
-            return Promise.reject(response.status);
-          }
-        });
-      }
-      if (opt_callback) {
-        opt_callback(response);
-      }
-      return response;
-    });
-  };
-
-  Gerrit.awaitPluginsLoaded = function() {
-    if (!_allPluginsPromise) {
-      if (Gerrit._arePluginsLoaded()) {
-        _allPluginsPromise = Promise.resolve();
-      } else {
-        let timeoutId;
-        _allPluginsPromise =
-          Promise.race([
-            new Promise(resolve => _resolveAllPluginsLoaded = resolve),
-            new Promise(resolve => timeoutId = setTimeout(
-                Gerrit._pluginLoadingTimeout, PLUGIN_LOADING_TIMEOUT_MS)),
-          ]).then(() => clearTimeout(timeoutId));
-      }
-    }
-    return _allPluginsPromise;
-  };
-
-  Gerrit._pluginLoadingTimeout = function() {
-    console.error(`Failed to load plugins: ${Object.keys(_pluginsPending)}`);
-    Gerrit._setPluginsPending([]);
-  };
-
-  Gerrit._setPluginsPending = function(plugins) {
-    _pluginsPending = plugins.reduce((o, url) => {
-      // TODO(viktard): Remove guard (@see Issue 8962)
-      o[getPluginNameFromUrl(url) || UNKNOWN_PLUGIN] = url;
-      return o;
-    }, {});
-    Gerrit._setPluginsCount(Object.keys(_pluginsPending).length);
-  };
-
-  Gerrit._setPluginsCount = function(count) {
-    _pluginsPendingCount = count;
-    if (Gerrit._arePluginsLoaded()) {
-      getReporting().pluginsLoaded(_pluginsInstalled);
-      if (_resolveAllPluginsLoaded) {
-        _resolveAllPluginsLoaded();
-      }
-    }
-  };
-
-  Gerrit._pluginInstallError = function(message) {
-    document.dispatchEvent(new CustomEvent('show-alert', {
-      detail: {
-        message: `Plugin install error: ${message}`,
-      },
-    }));
-    console.info(`Plugin install error: ${message}`);
-    Gerrit._setPluginsCount(_pluginsPendingCount - 1);
-  };
-
-  Gerrit._pluginInstalled = function(url) {
-    const name = getPluginNameFromUrl(url) || UNKNOWN_PLUGIN;
-    if (!_pluginsPending[name]) {
-      console.warn(`Unexpected plugin ${name} installed from ${url}.`);
-    } else {
-      delete _pluginsPending[name];
-      _pluginsInstalled.push(name);
-      Gerrit._setPluginsCount(_pluginsPendingCount - 1);
-      console.log(`Plugin ${name} installed.`);
-    }
-  };
-
-  Gerrit._arePluginsLoaded = function() {
-    return _pluginsPendingCount === 0;
-  };
-
-  Gerrit._getPluginScreenName = function(pluginName, screenName) {
-    return `${pluginName}-screen-${screenName}`;
-  };
-
-  Gerrit._isPluginPreloaded = function(url) {
-    const name = getPluginNameFromUrl(url);
-    if (name && Gerrit._preloadedPlugins) {
-      return name in Gerrit._preloadedPlugins;
-    } else {
-      return false;
-    }
-  };
-
-  // TODO(taoalpha): List all internal supported event names.
-  // Also convert this to inherited class once we move Gerrit to class.
-  Gerrit._eventEmitter = new EventEmitter();
-  ['addListener',
-    'dispatch',
-    'emit',
-    'off',
-    'on',
-    'once',
-    'removeAllListeners',
-    'removeListener',
-  ].forEach(method => {
-    /**
-     * Enabling EventEmitter interface on Gerrit.
-     *
-     * This will enable to signal across different parts of js code without relying on DOM,
-     * including core to core, plugin to plugin and also core to plugin.
-     *
-     * @example
-     *
-     * // Emit this event from pluginA
-     * Gerrit.install(pluginA => {
-     *   fetch("some-api").then(() => {
-     *     Gerrit.on("your-special-event", {plugin: pluginA});
-     *   });
-     * });
-     *
-     * // Listen on your-special-event from pluignB
-     * Gerrit.install(pluginB => {
-     *   Gerrit.on("your-special-event", ({plugin}) => {
-     *     // do something, plugin is pluginA
-     *   });
-     * });
-     */
-    Gerrit[method] = Gerrit._eventEmitter[method].bind(Gerrit._eventEmitter);
-  });
-
-  window.Gerrit = Gerrit;
-
-  // Preloaded plugins should be installed after Gerrit.install() is set,
-  // since plugin preloader substitutes Gerrit.install() temporarily.
-  installPreloadedPlugins();
+  window.Plugin = Plugin;
 })(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
index ca5c49f..63a528e 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
@@ -15,11 +15,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../styles/gr-voting-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../gr-account-label/gr-account-label.html">
+<link rel="import" href="../gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../gr-icons/gr-icons.html">
 <link rel="import" href="../gr-label/gr-label.html">
@@ -31,7 +32,7 @@
     <style include="shared-styles">
       .placeholder {
         color: var(--deemphasized-text-color);
-        padding-top: .2em;
+        padding-top: var(--spacing-xs);
       }
       .hidden {
         display: none;
@@ -39,9 +40,10 @@
       .voteChip {
         display: flex;
         justify-content: center;
-        margin-right: .3em;
-        padding: .05em .85em;
+        margin-right: var(--spacing-s);
+        padding: 0;
         @apply --vote-chip-styles;
+        border-width: 0;
       }
       .max {
         background-color: var(--vote-color-approved);
@@ -59,30 +61,31 @@
         display: none;
       }
       td {
-        vertical-align: middle;
+        vertical-align: top;
       }
       tr {
-        min-height: 2.25em;
+        min-height: var(--line-height-normal);
       }
       gr-button {
+        vertical-align: top;
         --gr-button: {
-          height: 2em;
+          height: var(--line-height-normal);
+          width: var(--line-height-normal);
           padding: 0;
-          width: 2em;
         }
       }
       gr-button[disabled] iron-icon {
         color: var(--border-color);
       }
       gr-account-chip {
-        margin-right: .25em;
+        margin-right: var(--spacing-xs);
       }
       iron-icon {
-        height: 1.2em;
-        width: 1.2em;
+        height: calc(var(--line-height-normal) - 2px);
+        width: calc(var(--line-height-normal) - 2px);
       }
       .labelValueContainer:not(:first-of-type) td {
-        padding-top: .3em;
+        padding-top: var(--spacing-s);
       }
     </style>
     <table>
@@ -111,7 +114,7 @@
             <gr-button
                 link
                 aria-label="Remove"
-                on-tap="_onDeleteVote"
+                on-click="_onDeleteVote"
                 tooltip="Remove vote"
                 data-account-id$="[[mappedLabel.account._account_id]]"
                 class$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]">
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
index 387fe96..ed2dfdd 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-label-info',
-    _legacyUndefinedCheck: true,
 
     properties: {
       labelInfo: Object,
@@ -38,7 +37,7 @@
      */
     _mapLabelInfo(labelInfo, account, changeLabelsRecord) {
       const result = [];
-      if (!labelInfo) { return result; }
+      if (!labelInfo || !account) { return result; }
       if (!labelInfo.values) {
         if (labelInfo.rejected || labelInfo.approved) {
           const ok = labelInfo.approved || !labelInfo.rejected;
@@ -149,10 +148,12 @@
      *    order to trigger computation when a label is removed from the change.
      */
     _computeShowPlaceholder(labelInfo, changeLabelsRecord) {
-      if (!labelInfo.values && (labelInfo.rejected || labelInfo.approved)) {
+      if (labelInfo &&
+          !labelInfo.values && (labelInfo.rejected || labelInfo.approved)) {
         return 'hidden';
       }
-      if (labelInfo.all) {
+
+      if (labelInfo && labelInfo.all) {
         for (const label of labelInfo.all) {
           if (label.value && label.value != labelInfo.default_value) {
             return 'hidden';
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
index e6e72d2..658a9733 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
@@ -17,9 +17,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-label-info</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-label-info.html">
 
@@ -98,7 +100,6 @@
         assert.isTrue(button.disabled);
         return deleteResponse.then(() => {
           assert.isFalse(button.disabled);
-          assert.notOk(element.change.labels.test.recommended);
           assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test'));
         });
       });
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.html b/polygerrit-ui/app/elements/shared/gr-label/gr-label.html
index fe290b7..55ecc98 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <dom-module id="gr-label">
   <template strip-whitespace>
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 c437885..0de0881 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-label',
-    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.TooltipBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
index c001ce7..da0b93f 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -28,7 +28,7 @@
       #container {
         background: var(--chip-background-color);
         border-radius: 1em;
-        padding: .5em;
+        padding: var(--spacing-m);
       }
       #header {
         color: var(--deemphasized-text-color);
@@ -48,7 +48,7 @@
         border-left: 1px solid var(--deemphasized-text-color);
         color: var(--deemphasized-text-color);
         cursor: pointer;
-        padding-left: .4em;
+        padding-left: var(--spacing-s);
       }
       #trigger:hover {
         color: var(--primary-text-color);
@@ -64,7 +64,7 @@
             disabled="[[disabled]]"
             placeholder="[[placeholder]]"
             borderless></gr-autocomplete>
-        <div id="trigger" on-tap="_handleTriggerTap">▼</div>
+        <div id="trigger" on-click="_handleTriggerClick">▼</div>
       </div>
     </div>
   </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
index a892522..fd0f228 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-labeled-autocomplete',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a value is chosen.
@@ -59,7 +58,7 @@
       },
     },
 
-    _handleTriggerTap(e) {
+    _handleTriggerClick(e) {
       // Stop propagation here so we don't confuse gr-autocomplete, which
       // listens for taps on body to try to determine when it's blurred.
       e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
index 6bcaa18..b257746 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-labeled-autocomplete</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-labeled-autocomplete.html">
 
@@ -47,7 +49,7 @@
       const e = {stopPropagation: () => undefined};
       sandbox.stub(e, 'stopPropagation');
       sandbox.stub(element.$.autocomplete, 'focus');
-      element._handleTriggerTap(e);
+      element._handleTriggerClick(e);
       assert.isTrue(e.stopPropagation.calledOnce);
       assert.isTrue(element.$.autocomplete.focus.calledOnce);
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
index 4137485..fb55c67 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-lib-loader">
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
index ba86b66..5ea5dca 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-lib-loader',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _hljsState: {
@@ -68,14 +67,16 @@
      * custom-style DOM element.
      *
      * @return {!Promise<Element>}
+     * @suppress {checkTypes}
      */
     getDarkTheme() {
       return new Promise((resolve, reject) => {
-        this.importHref(this._getLibRoot() + DARK_THEME_PATH, () => {
-          const module = document.createElement('style', 'custom-style');
-          module.setAttribute('include', 'dark-theme');
-          resolve(module);
-        });
+        (this.importHref || Polymer.importHref)(
+            this._getLibRoot() + DARK_THEME_PATH, () => {
+              const module = document.createElement('style', 'custom-style');
+              module.setAttribute('include', 'dark-theme');
+              resolve(module);
+            });
       });
     },
 
@@ -139,7 +140,7 @@
           return;
         }
 
-        script.src = src;
+        script.setAttribute('src', src);
         script.onload = resolve;
         script.onerror = reject;
         Polymer.dom(document.head).appendChild(script);
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
index cf9a41c..10d1608 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-lib-loader</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-lib-loader.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html
index 91866e5..d00416b 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 
 <dom-module id="gr-limited-text">
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 44a8791..048e4f5 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
@@ -26,7 +26,6 @@
 
   Polymer({
     is: 'gr-limited-text',
-    _legacyUndefinedCheck: true,
 
     properties: {
       /** The un-truncated text to display. */
@@ -45,6 +44,15 @@
       },
 
       /**
+       * Disable the tooltip.
+       * When set to true, will not show tooltip even text is over limit
+       */
+      disableTooltip: {
+        type: Boolean,
+        value: false,
+      },
+
+      /**
        * The maximum number of characters to display in the tooltop.
        */
       tooltipLimit: {
@@ -66,8 +74,13 @@
      * enabled.
      */
     _updateTitle(text, limit, tooltipLimit) {
+      // Polymer 2: check for undefined
+      if ([text, limit, tooltipLimit].some(arg => arg === undefined)) {
+        return;
+      }
+
       this.hasTooltip = !!limit && !!text && text.length > limit;
-      if (this.hasTooltip) {
+      if (this.hasTooltip && !this.disableTooltip) {
         this.setAttribute('title', text.substr(0, tooltipLimit));
       } else {
         this.removeAttribute('title');
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
index 16eb960..7946bb6 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-limited-text</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="gr-limited-text.html">
@@ -90,5 +92,14 @@
       assert.equal(element._computeDisplayText('foo bar', 4), 'foo…');
       assert.equal(element._computeDisplayText('foo bar', null), 'foo bar');
     });
+
+    test('when disable tooltip', () => {
+      sandbox.spy(element, '_updateTitle');
+      element.text = 'abcdefghijklmn';
+      element.disableTooltip = true;
+      element.limit = 10;
+      flushAsynchronousOperations();
+      assert.equal(element.getAttribute('title'), null);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
index fab562a..f3e0906 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -15,7 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../gr-icons/gr-icons.html">
@@ -34,25 +35,31 @@
         background: var(--chip-background-color);
         border-radius: .75em;
         display: inline-flex;
-        padding: 0 .5em;
+        padding: 0 var(--spacing-m);
       }
+      gr-button.remove {
+        --gr-remove-button-style: {
+          border: 0;
+          color: var(--deemphasized-text-color);
+          font-weight: normal;
+          height: .6em;
+          line-height: 10px;
+          margin-left: var(--spacing-xs);
+          padding: 0;
+          text-decoration: none;
+        }
+      }
+
       gr-button.remove:hover,
       gr-button.remove:focus {
         --gr-button: {
+          @apply --gr-remove-button-style;
           color: #333;
         }
       }
       gr-button.remove {
         --gr-button: {
-          border: 0;
-          color: var(--deemphasized-text-color);
-          font-size: 1.7rem;
-          font-weight: normal;
-          height: .6em;
-          line-height: .6;
-          margin-left: .15em;
-          padding: 0;
-          text-decoration: none;
+          @apply --gr-remove-button-style;
         }
       }
       .transparentBackground,
@@ -83,7 +90,7 @@
           hidden$="[[!removable]]"
           hidden
           class$="remove [[_getBackgroundClass(transparentBackground)]]"
-          on-tap="_handleRemoveTap">
+          on-click="_handleRemoveTap">
         <iron-icon icon="gr-icons:close"></iron-icon>
       </gr-button>
     </div>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
index 8388a07..33a9c25 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-linked-chip',
-    _legacyUndefinedCheck: true,
 
     properties: {
       href: String,
@@ -42,6 +41,10 @@
       limit: Number,
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     _getBackgroundClass(transparent) {
       return transparent ? 'transparentBackground' : '';
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
index eb57428..22a2eaf 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-linked-chip</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
 
 <link rel="import" href="gr-linked-chip.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
index af70e37..61facc0 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
@@ -15,12 +15,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 
-<script src="../../../bower_components/ba-linkify/ba-linkify.js"></script>
+<script src="/bower_components/ba-linkify/ba-linkify.js"></script>
 <script src="link-text-parser.js"></script>
 <dom-module id="gr-linked-text">
   <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index 157ad5e..229fa19 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-linked-text',
-    _legacyUndefinedCheck: true,
 
     properties: {
       removeZeroWidthSpace: Boolean,
@@ -62,6 +61,7 @@
      *     commentLink patterns
      */
     _contentOrConfigChanged(content, config) {
+      if (!Gerrit.Nav || !Gerrit.Nav.mapCommentlinks) return;
       config = Gerrit.Nav.mapCommentlinks(config);
       const output = Polymer.dom(this.$.output);
       output.textContent = '';
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index 9fc92b1..0deff05 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-linked-text</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
@@ -276,16 +278,58 @@
       let links = element.$.output.querySelectorAll('a');
       assert.equal(links.length, 1);
       assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+      assert.equal(links[0].innerHTML, 'mailto:test@google.com');
 
       element.content = 'xx http://google.com yy';
       links = element.$.output.querySelectorAll('a');
       assert.equal(links.length, 1);
       assert.equal(links[0].getAttribute('href'), 'http://google.com');
+      assert.equal(links[0].innerHTML, 'http://google.com');
 
       element.content = 'xx https://google.com yy';
       links = element.$.output.querySelectorAll('a');
       assert.equal(links.length, 1);
       assert.equal(links[0].getAttribute('href'), 'https://google.com');
+      assert.equal(links[0].innerHTML, 'https://google.com');
+
+      element.content = 'xx ssh://google.com yy';
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 0);
+
+      element.content = 'xx ftp://google.com yy';
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 0);
+    });
+
+    test('links without leading whitespace are linkified', () => {
+      element.content = 'xx abcmailto:test@google.com yy';
+      assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx abc');
+      let links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+      assert.equal(links[0].innerHTML, 'mailto:test@google.com');
+
+      element.content = 'xx defhttp://google.com yy';
+      assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx def');
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'http://google.com');
+      assert.equal(links[0].innerHTML, 'http://google.com');
+
+      element.content = 'xx qwehttps://google.com yy';
+      assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx qwe');
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'https://google.com');
+      assert.equal(links[0].innerHTML, 'https://google.com');
+
+      // Non-latin character
+      element.content = 'xx абвhttps://google.com yy';
+      assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx абв');
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'https://google.com');
+      assert.equal(links[0].innerHTML, 'https://google.com');
 
       element.content = 'xx ssh://google.com yy';
       links = element.$.output.querySelectorAll('a');
@@ -328,26 +372,4 @@
       assert.isTrue(contentConfigStub.called);
     });
   });
-
-  suite('gr-linked-text with null config', () => {
-    let element;
-    let sandbox;
-
-    setup(() => {
-      element = fixture('basic');
-      sandbox = sinon.sandbox.create();
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('_contentOrConfigChanged not called without config', () => {
-      const contentStub = sandbox.stub(element, '_contentChanged');
-      const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
-      element.content = 'some text';
-      assert.isTrue(contentStub.called);
-      assert.isFalse(contentConfigStub.called);
-    });
-  });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
index 027c632..fa38a66 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -17,23 +17,12 @@
 (function() {
   'use strict';
 
-  const Defs = {};
-
-  /**
-   * @typedef {{
-   *    html: Node,
-   *    position: number,
-   *    length: number,
-   * }}
-   */
-  Defs.CommentLinkItem;
-
   /**
    * Pattern describing URLs with supported protocols.
    *
    * @type {RegExp}
    */
-  const URL_PROTOCOL_PATTERN = /^(https?:\/\/|mailto:)/;
+  const URL_PROTOCOL_PATTERN = /^(.*)(https?:\/\/|mailto:)/;
 
   /**
    * Construct a parser for linkifying text. Will linkify plain URLs that appear
@@ -73,7 +62,7 @@
    *
    * @param {string} text The chuml of source text over which the outputArray
    *     items range.
-   * @param {!Array<Defs.CommentLinkItem>} outputArray The list of items to add
+   * @param {!Array<Gerrit.CommentLinkItem>} outputArray The list of items to add
    *     resulting from commentlink matches.
    */
   GrLinkTextParser.prototype.processLinks = function(text, outputArray) {
@@ -109,7 +98,7 @@
    * Sort the given array of CommentLinkItems such that the positions are in
    * reverse order.
    *
-   * @param {!Array<Defs.CommentLinkItem>} outputArray
+   * @param {!Array<Gerrit.CommentLinkItem>} outputArray
    */
   GrLinkTextParser.prototype.sortArrayReverse = function(outputArray) {
     outputArray.sort((a, b) => b.position - a.position);
@@ -132,7 +121,7 @@
    *     starts.
    * @param {number} length The number of characters in the source text
    *     represented by the item.
-   * @param {!Array<Defs.CommentLinkItem>} outputArray The array to which the
+   * @param {!Array<Gerrit.CommentLinkItem>} outputArray The array to which the
    *     new item is to be appended.
    */
   GrLinkTextParser.prototype.addItem =
@@ -174,7 +163,7 @@
    *     starts.
    * @param {number} length The number of characters in the source text
    *     represented by the link.
-   * @param {!Array<Defs.CommentLinkItem>} outputArray The array to which the
+   * @param {!Array<Gerrit.CommentLinkItem>} outputArray The array to which the
    *     new item is to be appended.
    */
   GrLinkTextParser.prototype.addLink =
@@ -196,7 +185,7 @@
    *     starts.
    * @param {number} length The number of characters in the source text
    *     represented by the item.
-   * @param {!Array<Defs.CommentLinkItem>} outputArray The array to which the
+   * @param {!Array<Gerrit.CommentLinkItem>} outputArray The array to which the
    *     new item is to be appended.
    */
   GrLinkTextParser.prototype.addHTML =
@@ -214,7 +203,7 @@
    *
    * @param {number} position
    * @param {number} length
-   * @param {!Array<Defs.CommentLinkItem>} outputArray
+   * @param {!Array<Gerrit.CommentLinkItem>} outputArray
    */
   GrLinkTextParser.prototype.hasOverlap =
       function(position, length, outputArray) {
@@ -238,9 +227,11 @@
    * @param {string} text
    */
   GrLinkTextParser.prototype.parse = function(text) {
-    linkify(text, {
-      callback: this.parseChunk.bind(this),
-    });
+    if (text) {
+      linkify(text, {
+        callback: this.parseChunk.bind(this),
+      });
+    }
   };
 
   /**
@@ -265,13 +256,29 @@
     // the source text does not include a protocol, the protocol will be added
     // by ba-linkify. Create the link if the href is provided and its protocol
     // matches the expected pattern.
-    if (href && URL_PROTOCOL_PATTERN.test(href)) {
-      this.addText(text, href);
-    } else {
-      // For the sections of text that lie between the links found by
-      // ba-linkify, we search for the project-config-specified link patterns.
-      this.parseLinks(text, this.linkConfig);
+    if (href) {
+      const result = URL_PROTOCOL_PATTERN.exec(href);
+      if (result) {
+        const prefixText = result[1];
+        if (prefixText.length > 0) {
+          // Fix for simple cases from
+          // https://bugs.chromium.org/p/gerrit/issues/detail?id=11697
+          // When leading whitespace is missed before link,
+          // linkify add this text before link as a schema name to href.
+          // We suppose, that prefixText just a single word
+          // before link and add this word as is, without processing
+          // any patterns in it.
+          this.parseLinks(prefixText, []);
+          text = text.substring(prefixText.length);
+          href = href.substring(prefixText.length);
+        }
+        this.addText(text, href);
+        return;
+      }
     }
+    // For the sections of text that lie between the links found by
+    // ba-linkify, we search for the project-config-specified link patterns.
+    this.parseLinks(text, this.linkConfig);
   };
 
   /**
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
index be02d40..3d41a7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
@@ -14,9 +14,12 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -25,7 +28,6 @@
   <template>
     <style include="shared-styles">
       #filter {
-        font-size: var(--font-size-normal);
         max-width: 25em;
       }
       #filter:focus {
@@ -36,7 +38,7 @@
         display: flex;
         height: 3rem;
         justify-content: space-between;
-        margin: 0 1em;
+        margin: 0 var(--spacing-l);
       }
       #createNewContainer:not(.show) {
         display: none;
@@ -68,20 +70,26 @@
     <div id="topContainer">
       <div class="filterContainer">
         <label>Filter:</label>
-        <input is="iron-input"
+        <iron-input
             type="text"
-            id="filter"
             bind-value="{{filter}}">
+          <input
+              is="iron-input"
+              type="text"
+              id="filter"
+              bind-value="{{filter}}">
+        </iron-input>
       </div>
       <div id="createNewContainer"
           class$="[[_computeCreateClass(createNew)]]">
-        <gr-button primary link id="createNew" on-tap="_createNewItem">
+        <gr-button primary link id="createNew" on-click="_createNewItem">
           Create New
         </gr-button>
       </div>
     </div>
     <slot></slot>
     <nav>
+      Page [[_computePage(offset, itemsPerPage)]]
       <a id="prevArrow"
           href$="[[_computeNavLink(offset, -1, itemsPerPage, filter, path)]]"
           hidden$="[[_hidePrevArrow(loading, offset)]]" hidden>
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 53d05e1..6840e97 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
@@ -21,7 +21,6 @@
 
   Polymer({
     is: 'gr-list-view',
-    _legacyUndefinedCheck: true,
 
     properties: {
       createNew: Boolean,
@@ -38,6 +37,7 @@
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
+      Gerrit.FireBehavior,
       Gerrit.URLEncodingBehavior,
     ],
 
@@ -90,11 +90,18 @@
     },
 
     _hideNextArrow(loading, items) {
-      let lastPage = false;
-      if (items.length < this.itemsPerPage + 1) {
-        lastPage = true;
+      if (loading || !items || !items.length) {
+        return true;
       }
-      return loading || lastPage || !items || !items.length;
+      const lastPage = items.length < this.itemsPerPage + 1;
+      return lastPage;
+    },
+
+    // TODO: fix offset (including itemsPerPage)
+    // to either support a decimal or make it go to the nearest
+    // whole number (e.g 3).
+    _computePage(offset, itemsPerPage) {
+      return offset / itemsPerPage + 1;
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
index 09e68dd..ea1dcbb 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-list-view</title>
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-list-view.html">
@@ -153,5 +155,10 @@
       element.path = TAGS_PATH;
       assert.equal(element._computeNavLink.lastCall.args[4], TAGS_PATH);
     });
+
+    test('_computePage', () => {
+      assert.equal(element._computePage(0, 25), 1);
+      assert.equal(element._computePage(50, 25), 3);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
index e94b655..2b4b982 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -15,8 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-overlay">
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 c167b3b..8623458 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -23,7 +23,6 @@
 
   Polymer({
     is: 'gr-overlay',
-    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a fullscreen overlay is closed
@@ -45,6 +44,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Polymer.IronOverlayBehavior,
     ],
 
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
index ee05b69..08b7497 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-overlay</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
index 3885497..f1c3a6f 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-page-nav">
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
index 181c7bc..2e05607 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-page-nav',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _headerHeight: Number,
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
index 428bab3..b384b47 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-page-nav</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html
index d794dd6..ce596f8 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html
@@ -14,8 +14,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-icon/iron-icon.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
@@ -33,7 +33,7 @@
         display: inline-block;
       }
       iron-icon {
-        margin-bottom: 1.2em;
+        margin-bottom: var(--spacing-l);
       }
     </style>
     <div>
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 2fccc8d..e2298c3 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
@@ -22,7 +22,6 @@
 
   Polymer({
     is: 'gr-repo-branch-picker',
-    _legacyUndefinedCheck: true,
 
     properties: {
       repo: {
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
index 989e838..1ed9151 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-branch-picker</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-branch-picker.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
index f203594..dc07d0f 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-auth</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html
index c5a0dfe..d3500d8 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-etag-decorator">
   <script src="gr-etag-decorator.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
index 09ae1da..76c8c2c 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-etag-decorator</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <script src="gr-etag-decorator.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 562980c..7461ac4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -15,19 +15,21 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="gr-etag-decorator.html">
 
 <!-- NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 -->
-<script src="../../../bower_components/es6-promise/dist/es6-promise.min.js"></script>
-<script src="../../../bower_components/fetch/fetch.js"></script>
+<script src="/bower_components/es6-promise/dist/es6-promise.min.js"></script>
+<script src="/bower_components/fetch/fetch.js"></script>
 
 <dom-module id="gr-rest-api-interface">
   <!-- NB: Order is important, because of namespaced classes. -->
+  <script src="gr-rest-apis/gr-rest-api-helper.js"></script>
   <script src="gr-auth.js"></script>
   <script src="gr-reviewer-updates-parser.js"></script>
   <script src="gr-rest-api-interface.js"></script>
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 546d004..8c34e33 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
@@ -17,113 +17,15 @@
 (function() {
   'use strict';
 
-  const Defs = {};
-
-  /**
-   * @typedef {{
-   *    basePatchNum: (string|number),
-   *    patchNum: (number),
-   * }}
-   */
-  Defs.patchRange;
-
-  /**
-   * @typedef {{
-   *    url: string,
-   *    fetchOptions: (Object|null|undefined),
-   *    anonymizedUrl: (string|undefined),
-   * }}
-   */
-  Defs.FetchRequest;
-
-  /**
-   * Object to describe a request for passing into _fetchJSON or _fetchRawJSON.
-   * - url is the URL for the request (excluding get params)
-   * - errFn is a function to invoke when the request fails.
-   * - cancelCondition is a function that, if provided and returns true, will
-   *     cancel the response after it resolves.
-   * - params is a key-value hash to specify get params for the request URL.
-   *
-   * @typedef {{
-   *    url: string,
-   *    errFn: (function(?Response, string=)|null|undefined),
-   *    cancelCondition: (function()|null|undefined),
-   *    params: (Object|null|undefined),
-   *    fetchOptions: (Object|null|undefined),
-   *    anonymizedUrl: (string|undefined),
-   *    reportUrlAsIs: (boolean|undefined),
-   * }}
-   */
-  Defs.FetchJSONRequest;
-
-  /**
-   * @typedef {{
-   *   changeNum: (string|number),
-   *   endpoint: string,
-   *   patchNum: (string|number|null|undefined),
-   *   errFn: (function(?Response, string=)|null|undefined),
-   *   params: (Object|null|undefined),
-   *   fetchOptions: (Object|null|undefined),
-   *   anonymizedEndpoint: (string|undefined),
-   *   reportEndpointAsIs: (boolean|undefined),
-   * }}
-   */
-  Defs.ChangeFetchRequest;
-
-  /**
-   * Object to describe a request for passing into _send.
-   * - method is the HTTP method to use in the request.
-   * - url is the URL for the request
-   * - body is a request payload.
-   *     TODO (beckysiegel) remove need for number at least.
-   * - errFn is a function to invoke when the request fails.
-   * - cancelCondition is a function that, if provided and returns true, will
-   *   cancel the response after it resolves.
-   * - contentType is the content type of the body.
-   * - headers is a key-value hash to describe HTTP headers for the request.
-   * - parseResponse states whether the result should be parsed as a JSON
-   *     object using getResponseObject.
-   *
-   * @typedef {{
-   *   method: string,
-   *   url: string,
-   *   body: (string|number|Object|null|undefined),
-   *   errFn: (function(?Response, string=)|null|undefined),
-   *   contentType: (string|null|undefined),
-   *   headers: (Object|undefined),
-   *   parseResponse: (boolean|undefined),
-   *   anonymizedUrl: (string|undefined),
-   *   reportUrlAsIs: (boolean|undefined),
-   * }}
-   */
-  Defs.SendRequest;
-
-  /**
-   * @typedef {{
-   *   changeNum: (string|number),
-   *   method: string,
-   *   patchNum: (string|number|undefined),
-   *   endpoint: string,
-   *   body: (string|number|Object|null|undefined),
-   *   errFn: (function(?Response, string=)|null|undefined),
-   *   contentType: (string|null|undefined),
-   *   headers: (Object|undefined),
-   *   parseResponse: (boolean|undefined),
-   *   anonymizedEndpoint: (string|undefined),
-   *   reportEndpointAsIs: (boolean|undefined),
-   * }}
-   */
-  Defs.ChangeSendRequest;
-
   const DiffViewMode = {
     SIDE_BY_SIDE: 'SIDE_BY_SIDE',
     UNIFIED: 'UNIFIED_DIFF',
   };
   const JSON_PREFIX = ')]}\'';
   const MAX_PROJECT_RESULTS = 25;
-  const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900;
+  // 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 FAILED_TO_FETCH_ERROR = 'Failed to fetch';
 
   const Requests = {
     SEND_DIFF_DRAFT: 'sendDiffDraft',
@@ -137,57 +39,11 @@
   const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
       '/revisions/*';
 
-  /**
-   * Wrapper around Map for caching server responses. Site-based so that
-   * changes to CANONICAL_PATH will result in a different cache going into
-   * effect.
-   */
-  class SiteBasedCache {
-    constructor() {
-      // Container of per-canonical-path caches.
-      this._data = new Map();
-    }
-
-    // Returns the cache for the current canonical path.
-    _cache() {
-      if (!this._data.has(window.CANONICAL_PATH)) {
-        this._data.set(window.CANONICAL_PATH, new Map());
-      }
-      return this._data.get(window.CANONICAL_PATH);
-    }
-
-    has(key) {
-      return this._cache().has(key);
-    }
-
-    get(key) {
-      return this._cache().get(key);
-    }
-
-    set(key, value) {
-      this._cache().set(key, value);
-    }
-
-    delete(key) {
-      this._cache().delete(key);
-    }
-
-    invalidatePrefix(prefix) {
-      const newMap = new Map();
-      for (const [key, value] of this._cache().entries()) {
-        if (!key.startsWith(prefix)) {
-          newMap.set(key, value);
-        }
-      }
-      this._data.set(window.CANONICAL_PATH, newMap);
-    }
-  }
-
   Polymer({
     is: 'gr-rest-api-interface',
-    _legacyUndefinedCheck: true,
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.PathListBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.RESTClientBehavior,
@@ -228,7 +84,7 @@
       },
       _sharedFetchPromises: {
         type: Object,
-        value: {}, // Intentional to share the object across instances.
+        value: new FetchPromisesCache(), // Shared across instances.
       },
       _pendingRequests: {
         type: Object,
@@ -253,131 +109,48 @@
 
     JSON_PREFIX,
 
-    /**
-     * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
-     * with timing and logging.
-     *
-     * @param {Defs.FetchRequest} req
-     */
-    _fetch(req) {
-      const start = Date.now();
-      const xhr = this._auth.fetch(req.url, req.fetchOptions);
+    created() {
+      /* Polymer 1 and Polymer 2 have slightly different lifecycle.
+      * Differences are not very well documented (see
+      * https://github.com/Polymer/old-docs-site/issues/2322).
+      * In Polymer 1, created() is called when properties values is not set
+      * and ready() is always called later, even if element is not added
+      * to a DOM. I.e. in Polymer 1 _cache and other properties are undefined,
+      * while in Polymer 2 they are set to default values.
+      * In Polymer 2, created() is called after properties values set and
+      * ready() is called only after element is attached to a DOM.
+      * There are several places in the code, where element is created with
+      * document.createElement('gr-rest-api-interface') and is not added
+      * to a DOM.
+      * In such cases, Polymer 1 calls both created() and ready() methods,
+      * but Polymer 2 calls only created() method.
+      * To workaround these differences, we should try to create _restApiHelper
+      * in both methods.
+      */
+      //
 
-      // Log the call after it completes.
-      xhr.then(res => this._logCall(req, start, res.status));
-
-      // Return the XHR directly (without the log).
-      return xhr;
+      this._initRestApiHelper();
     },
 
-    /**
-     * Log information about a REST call. Because the elapsed time is determined
-     * by this method, it should be called immediately after the request
-     * finishes.
-     *
-     * @param {Defs.FetchRequest} req
-     * @param {number} startTime the time that the request was started.
-     * @param {number} status the HTTP status of the response. The status value
-     *     is used here rather than the response object so there is no way this
-     *     method can read the body stream.
-     */
-    _logCall(req, startTime, status) {
-      const method = (req.fetchOptions && req.fetchOptions.method) ?
-        req.fetchOptions.method : 'GET';
-      const elapsed = (Date.now() - startTime);
-      console.log([
-        'HTTP',
-        status,
-        method,
-        elapsed + 'ms',
-        req.anonymizedUrl || req.url,
-      ].join(' '));
-      if (req.anonymizedUrl) {
-        this.fire('rpc-log',
-            {status, method, elapsed, anonymizedUrl: req.anonymizedUrl});
+    ready() {
+      // See comments in created()
+      this._initRestApiHelper();
+    },
+
+    _initRestApiHelper() {
+      if (this._restApiHelper) {
+        return;
+      }
+      if (this._cache && this._auth && this._sharedFetchPromises
+          && this._credentialCheck) {
+        this._restApiHelper = new GrRestApiHelper(this._cache, this._auth,
+            this._sharedFetchPromises, this._credentialCheck, this);
       }
     },
 
-    /**
-     * Fetch JSON from url provided.
-     * Returns a Promise that resolves to a native Response.
-     * Doesn't do error checking. Supports cancel condition. Performs auth.
-     * Validates auth expiry errors.
-     *
-     * @param {Defs.FetchJSONRequest} req
-     */
-    _fetchRawJSON(req) {
-      const urlWithParams = this._urlWithParams(req.url, req.params);
-      const fetchReq = {
-        url: urlWithParams,
-        fetchOptions: req.fetchOptions,
-        anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
-      };
-      return this._fetch(fetchReq).then(res => {
-        if (req.cancelCondition && req.cancelCondition()) {
-          res.body.cancel();
-          return;
-        }
-        return res;
-      }).catch(err => {
-        const isLoggedIn = !!this._cache.get('/accounts/self/detail');
-        if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) {
-          this.checkCredentials();
-          return;
-        }
-        if (req.errFn) {
-          req.errFn.call(undefined, null, err);
-        } else {
-          this.fire('network-error', {error: err});
-        }
-        throw err;
-      });
-    },
-
-    /**
-     * Fetch JSON from url provided.
-     * Returns a Promise that resolves to a parsed response.
-     * Same as {@link _fetchRawJSON}, plus error handling.
-     *
-     * @param {Defs.FetchJSONRequest} req
-     */
-    _fetchJSON(req) {
-      return this._fetchRawJSON(req).then(response => {
-        if (!response) {
-          return;
-        }
-        if (!response.ok) {
-          if (req.errFn) {
-            req.errFn.call(null, response);
-            return;
-          }
-          this.fire('server-error', {request: req, response});
-          return;
-        }
-        return response && this.getResponseObject(response);
-      });
-    },
-
-    /**
-     * @param {string} url
-     * @param {?Object|string=} opt_params URL params, key-value hash.
-     * @return {string}
-     */
-    _urlWithParams(url, opt_params) {
-      if (!opt_params) { return this.getBaseUrl() + url; }
-
-      const params = [];
-      for (const p in opt_params) {
-        if (!opt_params.hasOwnProperty(p)) { continue; }
-        if (opt_params[p] == null) {
-          params.push(encodeURIComponent(p));
-          continue;
-        }
-        for (const value of [].concat(opt_params[p])) {
-          params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`);
-        }
-      }
-      return this.getBaseUrl() + url + '?' + params.join('&');
+    _fetchSharedCacheURL(req) {
+      // Cache is shared across instances
+      return this._restApiHelper.fetchCacheURL(req);
     },
 
     /**
@@ -385,32 +158,7 @@
      * @return {?}
      */
     getResponseObject(response) {
-      return this._readResponsePayload(response)
-          .then(payload => payload.parsed);
-    },
-
-    /**
-     * @param {!Object} response
-     * @return {!Object}
-     */
-    _readResponsePayload(response) {
-      return response.text().then(text => {
-        let result;
-        try {
-          result = this._parsePrefixedJSON(text);
-        } catch (_) {
-          result = null;
-        }
-        return {parsed: result, raw: text};
-      });
-    },
-
-    /**
-     * @param {string} source
-     * @return {?}
-     */
-    _parsePrefixedJSON(source) {
-      return JSON.parse(source.substring(JSON_PREFIX.length));
+      return this._restApiHelper.getResponseObject(response);
     },
 
     getConfig(noCache) {
@@ -421,7 +169,7 @@
         });
       }
 
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/config/server/info',
         reportUrlAsIs: true,
       });
@@ -471,7 +219,7 @@
       // supports it.
       const url = `/projects/${encodeURIComponent(repo)}/config`;
       this._cache.delete(url);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url,
         body: config,
@@ -485,7 +233,7 @@
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
       const encodeName = encodeURIComponent(repo);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'POST',
         url: `/projects/${encodeName}/gc`,
         body: '',
@@ -503,7 +251,7 @@
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
       const encodeName = encodeURIComponent(config.name);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/projects/${encodeName}`,
         body: config,
@@ -519,7 +267,7 @@
     createGroup(config, opt_errFn) {
       if (!config.name) { return ''; }
       const encodeName = encodeURIComponent(config.name);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/groups/${encodeName}`,
         body: config,
@@ -529,7 +277,7 @@
     },
 
     getGroupConfig(group, opt_errFn) {
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: `/groups/${encodeURIComponent(group)}/detail`,
         errFn: opt_errFn,
         anonymizedUrl: '/groups/*/detail',
@@ -547,7 +295,7 @@
       // supports it.
       const encodeName = encodeURIComponent(repo);
       const encodeRef = encodeURIComponent(ref);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'DELETE',
         url: `/projects/${encodeName}/branches/${encodeRef}`,
         body: '',
@@ -567,7 +315,7 @@
       // supports it.
       const encodeName = encodeURIComponent(repo);
       const encodeRef = encodeURIComponent(ref);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'DELETE',
         url: `/projects/${encodeName}/tags/${encodeRef}`,
         body: '',
@@ -588,7 +336,7 @@
       // supports it.
       const encodeName = encodeURIComponent(name);
       const encodeBranch = encodeURIComponent(branch);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/projects/${encodeName}/branches/${encodeBranch}`,
         body: revision,
@@ -609,7 +357,7 @@
       // supports it.
       const encodeName = encodeURIComponent(name);
       const encodeTag = encodeURIComponent(tag);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/projects/${encodeName}/tags/${encodeTag}`,
         body: revision,
@@ -625,8 +373,8 @@
     getIsGroupOwner(groupName) {
       const encodeName = encodeURIComponent(groupName);
       const req = {
-        url: `/groups/?owned&q=${encodeName}`,
-        anonymizedUrl: '/groups/owned&q=*',
+        url: `/groups/?owned&g=${encodeName}`,
+        anonymizedUrl: '/groups/owned&g=*',
       };
       return this._fetchSharedCacheURL(req)
           .then(configs => configs.hasOwnProperty(groupName));
@@ -634,7 +382,7 @@
 
     getGroupMembers(groupName, opt_errFn) {
       const encodeName = encodeURIComponent(groupName);
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: `/groups/${encodeName}/members/`,
         errFn: opt_errFn,
         anonymizedUrl: '/groups/*/members',
@@ -642,7 +390,7 @@
     },
 
     getIncludedGroup(groupName) {
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: `/groups/${encodeURIComponent(groupName)}/groups/`,
         anonymizedUrl: '/groups/*/groups',
       });
@@ -650,7 +398,7 @@
 
     saveGroupName(groupId, name) {
       const encodeId = encodeURIComponent(groupId);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/groups/${encodeId}/name`,
         body: {name},
@@ -660,7 +408,7 @@
 
     saveGroupOwner(groupId, ownerId) {
       const encodeId = encodeURIComponent(groupId);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/groups/${encodeId}/owner`,
         body: {owner: ownerId},
@@ -670,7 +418,7 @@
 
     saveGroupDescription(groupId, description) {
       const encodeId = encodeURIComponent(groupId);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/groups/${encodeId}/description`,
         body: {description},
@@ -680,7 +428,7 @@
 
     saveGroupOptions(groupId, options) {
       const encodeId = encodeURIComponent(groupId);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/groups/${encodeId}/options`,
         body: options,
@@ -699,7 +447,7 @@
     saveGroupMembers(groupName, groupMembers) {
       const encodeName = encodeURIComponent(groupName);
       const encodeMember = encodeURIComponent(groupMembers);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/groups/${encodeName}/members/${encodeMember}`,
         parseResponse: true,
@@ -716,7 +464,7 @@
         errFn: opt_errFn,
         anonymizedUrl: '/groups/*/groups/*',
       };
-      return this._send(req).then(response => {
+      return this._restApiHelper.send(req).then(response => {
         if (response.ok) {
           return this.getResponseObject(response);
         }
@@ -726,7 +474,7 @@
     deleteGroupMembers(groupName, groupMembers) {
       const encodeName = encodeURIComponent(groupName);
       const encodeMember = encodeURIComponent(groupMembers);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'DELETE',
         url: `/groups/${encodeName}/members/${encodeMember}`,
         anonymizedUrl: '/groups/*/members/*',
@@ -736,7 +484,7 @@
     deleteIncludedGroup(groupName, includedGroup) {
       const encodeName = encodeURIComponent(groupName);
       const encodeIncludedGroup = encodeURIComponent(includedGroup);
-      return this._send({
+      return this._restApiHelper.send({
         method: 'DELETE',
         url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
         anonymizedUrl: '/groups/*/groups/*',
@@ -823,7 +571,7 @@
         prefs.download_scheme = prefs.download_scheme.toLowerCase();
       }
 
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: '/accounts/self/preferences',
         body: prefs,
@@ -839,7 +587,7 @@
     saveDiffPreferences(prefs, opt_errFn) {
       // Invalidate the cache.
       this._cache.delete('/accounts/self/preferences.diff');
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: '/accounts/self/preferences.diff',
         body: prefs,
@@ -855,7 +603,7 @@
     saveEditPreferences(prefs, opt_errFn) {
       // Invalidate the cache.
       this._cache.delete('/accounts/self/preferences.edit');
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: '/accounts/self/preferences.edit',
         body: prefs,
@@ -889,14 +637,14 @@
     },
 
     getExternalIds() {
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/accounts/self/external.ids',
         reportUrlAsIs: true,
       });
     },
 
     deleteAccountIdentity(id) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'POST',
         url: '/accounts/self/external.ids:delete',
         body: id,
@@ -910,7 +658,7 @@
      * @return {!Promise<!Object>}
      */
     getAccountDetails(userId) {
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: `/accounts/${encodeURIComponent(userId)}/detail`,
         anonymizedUrl: '/accounts/*/detail',
       });
@@ -928,7 +676,7 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     addAccountEmail(email, opt_errFn) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: '/accounts/self/emails/' + encodeURIComponent(email),
         errFn: opt_errFn,
@@ -941,7 +689,7 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     deleteAccountEmail(email, opt_errFn) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'DELETE',
         url: '/accounts/self/emails/' + encodeURIComponent(email),
         errFn: opt_errFn,
@@ -961,7 +709,7 @@
         errFn: opt_errFn,
         anonymizedUrl: '/accounts/self/emails/*/preferred',
       };
-      return this._send(req).then(() => {
+      return this._restApiHelper.send(req).then(() => {
         // If result of getAccountEmails is in cache, update it in the cache
         // so we don't have to invalidate it.
         const cachedEmails = this._cache.get('/accounts/self/emails');
@@ -1005,7 +753,7 @@
         parseResponse: true,
         reportUrlAsIs: true,
       };
-      return this._send(req)
+      return this._restApiHelper.send(req)
           .then(newName => this._updateCachedAccount({name: newName}));
     },
 
@@ -1022,7 +770,7 @@
         parseResponse: true,
         reportUrlAsIs: true,
       };
-      return this._send(req)
+      return this._restApiHelper.send(req)
           .then(newName => this._updateCachedAccount({username: newName}));
     },
 
@@ -1039,33 +787,33 @@
         parseResponse: true,
         reportUrlAsIs: true,
       };
-      return this._send(req)
+      return this._restApiHelper.send(req)
           .then(newStatus => this._updateCachedAccount({status: newStatus}));
     },
 
     getAccountStatus(userId) {
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: `/accounts/${encodeURIComponent(userId)}/status`,
         anonymizedUrl: '/accounts/*/status',
       });
     },
 
     getAccountGroups() {
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/accounts/self/groups',
         reportUrlAsIs: true,
       });
     },
 
     getAccountAgreements() {
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/accounts/self/agreements',
         reportUrlAsIs: true,
       });
     },
 
     saveAccountAgreement(name) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: '/accounts/self/agreements',
         body: name,
@@ -1092,6 +840,8 @@
     getLoggedIn() {
       return this.getAccount().then(account => {
         return account != null;
+      }).catch(() => {
+        return false;
       });
     },
 
@@ -1108,29 +858,7 @@
     },
 
     checkCredentials() {
-      if (this._credentialCheck.checking) {
-        return;
-      }
-      this._credentialCheck.checking = true;
-      const req = {url: '/accounts/self/detail', reportUrlAsIs: true};
-      // Skip the REST response cache.
-      return this._fetchRawJSON(req).then(res => {
-        if (!res) { return; }
-        if (res.status === 403) {
-          this.fire('auth-error');
-          this._cache.delete('/accounts/self/detail');
-        } else if (res.ok) {
-          return this.getResponseObject(res);
-        }
-      }).then(res => {
-        this._credentialCheck.checking = false;
-        if (res) {
-          this._cache.delete('/accounts/self/detail');
-        }
-        return res;
-      }).catch(err => {
-        this._credentialCheck.checking = false;
-      });
+      return this._restApiHelper.checkCredentials();
     },
 
     getDefaultPreferences() {
@@ -1146,6 +874,8 @@
           const req = {url: '/accounts/self/preferences', reportUrlAsIs: true};
           return this._fetchSharedCacheURL(req).then(res => {
             if (this._isNarrowScreen()) {
+              // Note that this can be problematic, because the diff will stay
+              // unified even after increasing the window width.
               res.default_diff_view = DiffViewMode.UNIFIED;
             } else {
               res.default_diff_view = res.diff_view;
@@ -1176,7 +906,7 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     saveWatchedProjects(projects, opt_errFn) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'POST',
         url: '/accounts/self/watched.projects',
         body: projects,
@@ -1191,7 +921,7 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     deleteWatchedProjects(projects, opt_errFn) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'POST',
         url: '/accounts/self/watched.projects:delete',
         body: projects,
@@ -1200,45 +930,6 @@
       });
     },
 
-    /**
-     * @param {Defs.FetchJSONRequest} req
-     */
-    _fetchSharedCacheURL(req) {
-      if (this._sharedFetchPromises[req.url]) {
-        return this._sharedFetchPromises[req.url];
-      }
-      // TODO(andybons): Periodic cache invalidation.
-      if (this._cache.has(req.url)) {
-        return Promise.resolve(this._cache.get(req.url));
-      }
-      this._sharedFetchPromises[req.url] = this._fetchJSON(req)
-          .then(response => {
-            if (response !== undefined) {
-              this._cache.set(req.url, response);
-            }
-            this._sharedFetchPromises[req.url] = undefined;
-            return response;
-          }).catch(err => {
-            this._sharedFetchPromises[req.url] = undefined;
-            throw err;
-          });
-      return this._sharedFetchPromises[req.url];
-    },
-
-    /**
-     * @param {string} prefix
-     */
-    _invalidateSharedFetchPromisesPrefix(prefix) {
-      const newObject = {};
-      Object.entries(this._sharedFetchPromises).forEach(([key, value]) => {
-        if (!key.startsWith(prefix)) {
-          newObject[key] = value;
-        }
-      });
-      this._sharedFetchPromises = newObject;
-      this._cache.invalidatePrefix(prefix);
-    },
-
     _isNarrowScreen() {
       return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX;
     },
@@ -1280,7 +971,7 @@
         params,
         reportUrlAsIs: true,
       };
-      return this._fetchJSON(req).then(response => {
+      return this._restApiHelper.fetchJSON(req).then(response => {
         // Response may be an array of changes OR an array of arrays of
         // changes.
         if (opt_query instanceof Array) {
@@ -1336,13 +1027,13 @@
         this.ListChangesOption.ALL_COMMITS,
         this.ListChangesOption.ALL_REVISIONS,
         this.ListChangesOption.CHANGE_ACTIONS,
-        this.ListChangesOption.CURRENT_ACTIONS,
         this.ListChangesOption.DETAILED_LABELS,
         this.ListChangesOption.DOWNLOAD_COMMANDS,
         this.ListChangesOption.MESSAGES,
         this.ListChangesOption.SUBMITTABLE,
         this.ListChangesOption.WEB_LINKS,
         this.ListChangesOption.SKIP_MERGEABLE,
+        this.ListChangesOption.SKIP_DIFFSTAT,
       ];
       return this.getConfig(false).then(config => {
         if (config.receive && config.receive.enable_signed_push) {
@@ -1364,7 +1055,8 @@
       const optionsHex = this.listChangesOptionsToHex(
           this.ListChangesOption.ALL_COMMITS,
           this.ListChangesOption.ALL_REVISIONS,
-          this.ListChangesOption.SKIP_MERGEABLE
+          this.ListChangesOption.SKIP_MERGEABLE,
+          this.ListChangesOption.SKIP_DIFFSTAT
       );
       return this._getChangeDetail(changeNum, optionsHex, opt_errFn,
           opt_cancelCondition);
@@ -1378,9 +1070,10 @@
      */
     _getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) {
       return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
-        const urlWithParams = this._urlWithParams(url, optionsHex);
+        const urlWithParams = this._restApiHelper
+            .urlWithParams(url, optionsHex);
         const params = {O: optionsHex};
-        const req = {
+        let req = {
           url,
           errFn: opt_errFn,
           cancelCondition: opt_cancelCondition,
@@ -1388,9 +1081,10 @@
           fetchOptions: this._etags.getOptions(urlWithParams),
           anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
         };
-        return this._fetchRawJSON(req).then(response => {
+        req = this._restApiHelper.addAcceptJsonHeader(req);
+        return this._restApiHelper.fetchRawJSON(req).then(response => {
           if (response && response.status === 304) {
-            return Promise.resolve(this._parsePrefixedJSON(
+            return Promise.resolve(this._restApiHelper.parsePrefixedJSON(
                 this._etags.getCachedPayload(urlWithParams)));
           }
 
@@ -1404,7 +1098,7 @@
           }
 
           const payloadPromise = response ?
-            this._readResponsePayload(response) :
+            this._restApiHelper.readResponsePayload(response) :
             Promise.resolve(null);
 
           return payloadPromise.then(payload => {
@@ -1433,7 +1127,7 @@
 
     /**
      * @param {number|string} changeNum
-     * @param {Defs.patchRange} patchRange
+     * @param {Gerrit.PatchRange} patchRange
      * @param {number=} opt_parentIndex
      */
     getChangeFiles(changeNum, patchRange, opt_parentIndex) {
@@ -1454,7 +1148,7 @@
 
     /**
      * @param {number|string} changeNum
-     * @param {Defs.patchRange} patchRange
+     * @param {Gerrit.PatchRange} patchRange
      */
     getChangeEditFiles(changeNum, patchRange) {
       let endpoint = '/edit?list';
@@ -1487,7 +1181,7 @@
 
     /**
      * @param {number|string} changeNum
-     * @param {Defs.patchRange} patchRange
+     * @param {Gerrit.PatchRange} patchRange
      * @return {!Promise<!Array<!Object>>}
      */
     getChangeOrEditFiles(changeNum, patchRange) {
@@ -1498,18 +1192,6 @@
       return this.getChangeFiles(changeNum, patchRange);
     },
 
-    /**
-     * The closure compiler doesn't realize this.specialFilePathCompare is
-     * valid.
-     *
-     * @suppress {checkTypes}
-     */
-    getChangeFilePathsAsSpeciallySortedArray(changeNum, patchRange) {
-      return this.getChangeFiles(changeNum, patchRange).then(files => {
-        return Object.keys(files).sort(this.specialFilePathCompare);
-      });
-    },
-
     getChangeRevisionActions(changeNum, patchNum) {
       const req = {
         changeNum,
@@ -1517,15 +1199,7 @@
         patchNum,
         reportEndpointAsIs: true,
       };
-      return this._getChangeURLAndFetch(req).then(revisionActions => {
-        // The rebase button on change screen is always enabled.
-        if (revisionActions.rebase) {
-          revisionActions.rebase.rebaseOnCurrent =
-              !!revisionActions.rebase.enabled;
-          revisionActions.rebase.enabled = true;
-        }
-        return revisionActions;
-      });
+      return this._getChangeURLAndFetch(req);
     },
 
     /**
@@ -1534,9 +1208,24 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) {
+      return this._getChangeSuggestedGroup('REVIEWER', changeNum, inputVal,
+          opt_errFn);
+    },
+
+    /**
+     * @param {number|string} changeNum
+     * @param {string} inputVal
+     * @param {function(?Response, string=)=} opt_errFn
+     */
+    getChangeSuggestedCCs(changeNum, inputVal, opt_errFn) {
+      return this._getChangeSuggestedGroup('CC', changeNum, inputVal,
+          opt_errFn);
+    },
+
+    _getChangeSuggestedGroup(reviewerState, changeNum, inputVal, opt_errFn) {
       // More suggestions may obscure content underneath in the reply dialog,
       // see issue 10793.
-      const params = {n: 6};
+      const params = {'n': 6, 'reviewer-state': reviewerState};
       if (inputVal) { params.q = inputVal; }
       return this._getChangeURLAndFetch({
         changeNum,
@@ -1618,11 +1307,11 @@
     },
 
     invalidateGroupsCache() {
-      this._invalidateSharedFetchPromisesPrefix('/groups/?');
+      this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?');
     },
 
     invalidateReposCache() {
-      this._invalidateSharedFetchPromisesPrefix('/projects/?');
+      this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?');
     },
 
     /**
@@ -1660,7 +1349,7 @@
     setRepoHead(repo, ref) {
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/projects/${encodeURIComponent(repo)}/HEAD`,
         body: {ref},
@@ -1684,7 +1373,7 @@
       const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`;
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url,
         errFn: opt_errFn,
         anonymizedUrl: '/projects/*/branches?*',
@@ -1708,7 +1397,7 @@
           encodedFilter;
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url,
         errFn: opt_errFn,
         anonymizedUrl: '/projects/*/tags',
@@ -1727,7 +1416,7 @@
       const encodedFilter = this._computeFilter(filter);
       const n = pluginsPerPage + 1;
       const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`;
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url,
         errFn: opt_errFn,
         anonymizedUrl: '/plugins/?all',
@@ -1737,7 +1426,7 @@
     getRepoAccessRights(repoName, opt_errFn) {
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: `/projects/${encodeURIComponent(repoName)}/access`,
         errFn: opt_errFn,
         anonymizedUrl: '/projects/*/access',
@@ -1747,7 +1436,7 @@
     setRepoAccessRights(repoName, repoInfo) {
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._send({
+      return this._restApiHelper.send({
         method: 'POST',
         url: `/projects/${encodeURIComponent(repoName)}/access`,
         body: repoInfo,
@@ -1756,7 +1445,7 @@
     },
 
     setRepoAccessRightsForReview(projectName, projectInfo) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: `/projects/${encodeURIComponent(projectName)}/access:review`,
         body: projectInfo,
@@ -1773,7 +1462,7 @@
     getSuggestedGroups(inputVal, opt_n, opt_errFn) {
       const params = {s: inputVal};
       if (opt_n) { params.n = opt_n; }
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/groups/',
         errFn: opt_errFn,
         params,
@@ -1793,7 +1482,7 @@
         type: 'ALL',
       };
       if (opt_n) { params.n = opt_n; }
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/projects/',
         errFn: opt_errFn,
         params,
@@ -1812,7 +1501,7 @@
       }
       const params = {suggest: null, q: inputVal};
       if (opt_n) { params.n = opt_n; }
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/accounts/',
         errFn: opt_errFn,
         params,
@@ -1843,7 +1532,7 @@
                 throw Error('Unsupported HTTP method: ' + method);
             }
 
-            return this._send({method, url, body});
+            return this._restApiHelper.send({method, url, body});
           });
     },
 
@@ -1873,7 +1562,7 @@
         O: options,
         q: 'status:open is:mergeable conflicts:' + changeNum,
       };
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/changes/',
         params,
         anonymizedUrl: '/changes/conflicts:*',
@@ -1895,7 +1584,7 @@
         O: options,
         q: query,
       };
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/changes/',
         params,
         anonymizedUrl: '/changes/change:*',
@@ -1918,7 +1607,7 @@
         O: options,
         q: query,
       };
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/changes/',
         params,
         anonymizedUrl: '/changes/topic:*',
@@ -1964,7 +1653,7 @@
         this.getChangeActionURL(changeNum, patchNum, '/review'),
       ];
       return Promise.all(promises).then(([, url]) => {
-        return this._send({
+        return this._restApiHelper.send({
           method: 'POST',
           url,
           body: review,
@@ -1998,7 +1687,7 @@
      */
     createChange(project, branch, subject, opt_topic, opt_isPrivate,
         opt_workInProgress, opt_baseChange, opt_baseCommit) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'POST',
         url: '/changes/',
         body: {
@@ -2175,7 +1864,7 @@
       return this.getFromProjectLookup(changeNum).then(project => {
         const url = '/accounts/self/starred.changes/' +
             (project ? encodeURIComponent(project) + '~' : '') + changeNum;
-        return this._send({
+        return this._restApiHelper.send({
           method: starred ? 'PUT' : 'DELETE',
           url,
           anonymizedUrl: '/accounts/self/starred.changes/*',
@@ -2192,60 +1881,7 @@
     },
 
     /**
-     * Send an XHR.
-     *
-     * @param {Defs.SendRequest} req
-     * @return {Promise}
-     */
-    _send(req) {
-      const options = {method: req.method};
-      if (req.body) {
-        options.headers = new Headers();
-        options.headers.set(
-            'Content-Type', req.contentType || 'application/json');
-        options.body = typeof req.body === 'string' ?
-          req.body : JSON.stringify(req.body);
-      }
-      if (req.headers) {
-        if (!options.headers) { options.headers = new Headers(); }
-        for (const header in req.headers) {
-          if (!req.headers.hasOwnProperty(header)) { continue; }
-          options.headers.set(header, req.headers[header]);
-        }
-      }
-      const url = req.url.startsWith('http') ?
-        req.url : this.getBaseUrl() + req.url;
-      const fetchReq = {
-        url,
-        fetchOptions: options,
-        anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl,
-      };
-      const xhr = this._fetch(fetchReq).then(response => {
-        if (!response.ok) {
-          if (req.errFn) {
-            return req.errFn.call(undefined, response);
-          }
-          this.fire('server-error', {request: fetchReq, response});
-        }
-        return response;
-      }).catch(err => {
-        this.fire('network-error', {error: err});
-        if (req.errFn) {
-          return req.errFn.call(undefined, null, err);
-        } else {
-          throw err;
-        }
-      });
-
-      if (req.parseResponse) {
-        return xhr.then(res => this.getResponseObject(res));
-      }
-
-      return xhr;
-    },
-
-    /**
-     * Public version of the _send method preserved for plugins.
+     * Public version of the _restApiHelper.send method preserved for plugins.
      *
      * @param {string} method
      * @param {string} url
@@ -2259,7 +1895,7 @@
      */
     send(method, url, opt_body, opt_errFn, opt_contentType,
         opt_headers) {
-      return this._send({
+      return this._restApiHelper.send({
         method,
         url,
         body: opt_body,
@@ -2524,7 +2160,7 @@
     },
 
     getCommitInfo(project, commit) {
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/projects/' + encodeURIComponent(project) +
             '/commits/' + encodeURIComponent(commit),
         anonymizedUrl: '/projects/*/comments/*',
@@ -2532,7 +2168,7 @@
     },
 
     _fetchB64File(url) {
-      return this._fetch({url: this.getBaseUrl() + url})
+      return this._restApiHelper.fetch({url: this.getBaseUrl() + url})
           .then(response => {
             if (!response.ok) {
               return Promise.reject(new Error(response.statusText));
@@ -2656,7 +2292,7 @@
     },
 
     deleteAccountHttpPassword() {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'DELETE',
         url: '/accounts/self/password.http',
         reportUrlAsIs: true,
@@ -2669,7 +2305,7 @@
      * parameter.
      */
     generateAccountHttpPassword() {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'PUT',
         url: '/accounts/self/password.http',
         body: {generate: true},
@@ -2693,7 +2329,7 @@
         contentType: 'text/plain',
         reportUrlAsIs: true,
       };
-      return this._send(req)
+      return this._restApiHelper.send(req)
           .then(response => {
             if (response.status < 200 && response.status >= 300) {
               return Promise.reject(new Error('error'));
@@ -2707,7 +2343,7 @@
     },
 
     deleteAccountSSHKey(id) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'DELETE',
         url: '/accounts/self/sshkeys/' + id,
         anonymizedUrl: '/accounts/self/sshkeys/*',
@@ -2715,7 +2351,7 @@
     },
 
     getAccountGPGKeys() {
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/accounts/self/gpgkeys',
         reportUrlAsIs: true,
       });
@@ -2728,7 +2364,7 @@
         body: key,
         reportUrlAsIs: true,
       };
-      return this._send(req)
+      return this._restApiHelper.send(req)
           .then(response => {
             if (response.status < 200 && response.status >= 300) {
               return Promise.reject(new Error('error'));
@@ -2742,7 +2378,7 @@
     },
 
     deleteAccountGPGKey(id) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'DELETE',
         url: '/accounts/self/gpgkeys/' + id,
         anonymizedUrl: '/accounts/self/gpgkeys/*',
@@ -2775,7 +2411,7 @@
         body: {token},
         reportUrlAsIs: true,
       };
-      return this._send(req).then(response => {
+      return this._restApiHelper.send(req).then(response => {
         if (response.status === 204) {
           return 'Email confirmed successfully.';
         }
@@ -2784,7 +2420,7 @@
     },
 
     getCapabilities(opt_errFn) {
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: '/config/server/capabilities',
         errFn: opt_errFn,
         reportUrlAsIs: true,
@@ -2792,7 +2428,7 @@
     },
 
     getTopMenus(opt_errFn) {
-      return this._fetchJSON({
+      return this._fetchSharedCacheURL({
         url: '/config/server/top-menus',
         errFn: opt_errFn,
         reportUrlAsIs: true,
@@ -2890,7 +2526,7 @@
      */
     getChange(changeNum, opt_errFn) {
       // Cannot use _changeBaseURL, as this function is used by _projectLookup.
-      return this._fetchJSON({
+      return this._restApiHelper.fetchJSON({
         url: `/changes/?q=change:${changeNum}`,
         errFn: opt_errFn,
         anonymizedUrl: '/changes/?q=change:*',
@@ -2941,7 +2577,7 @@
      * Alias for _changeBaseURL.then(send).
      *
      * @todo(beckysiegel) clean up comments
-     * @param {Defs.ChangeSendRequest} req
+     * @param {Gerrit.ChangeSendRequest} req
      * @return {!Promise<!Object>}
      */
     _getChangeURLAndSend(req) {
@@ -2951,7 +2587,7 @@
         req.endpoint : req.anonymizedEndpoint;
 
       return this._changeBaseURL(req.changeNum, req.patchNum).then(url => {
-        return this._send({
+        return this._restApiHelper.send({
           method: req.method,
           url: url + req.endpoint,
           body: req.body,
@@ -2968,7 +2604,7 @@
     /**
      * Alias for _changeBaseURL.then(_fetchJSON).
      *
-     * @param {Defs.ChangeFetchRequest} req
+     * @param {Gerrit.ChangeFetchRequest} req
      * @return {!Promise<!Object>}
      */
     _getChangeURLAndFetch(req) {
@@ -2977,7 +2613,7 @@
       const anonymizedBaseUrl = req.patchNum ?
         ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
       return this._changeBaseURL(req.changeNum, req.patchNum).then(url => {
-        return this._fetchJSON({
+        return this._restApiHelper.fetchJSON({
           url: url + req.endpoint,
           errFn: req.errFn,
           params: req.params,
@@ -3108,7 +2744,7 @@
     },
 
     deleteDraftComments(query) {
-      return this._send({
+      return this._restApiHelper.send({
         method: 'POST',
         url: '/accounts/self/drafts:delete',
         body: {query},
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index ef4e401..635e0f5 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-rest-api-interface</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
@@ -61,93 +63,8 @@
       sandbox.restore();
     });
 
-    test('JSON prefix is properly removed', done => {
-      element._fetchJSON('/dummy/url').then(obj => {
-        assert.deepEqual(obj, {hello: 'bonjour'});
-        done();
-      });
-    });
-
-    test('cached results', done => {
-      let n = 0;
-      sandbox.stub(element, '_fetchJSON', () => {
-        return Promise.resolve(++n);
-      });
-      const promises = [];
-      promises.push(element._fetchSharedCacheURL('/foo'));
-      promises.push(element._fetchSharedCacheURL('/foo'));
-      promises.push(element._fetchSharedCacheURL('/foo'));
-
-      Promise.all(promises).then(results => {
-        assert.deepEqual(results, [1, 1, 1]);
-        element._fetchSharedCacheURL('/foo').then(foo => {
-          assert.equal(foo, 1);
-          done();
-        });
-      });
-    });
-
-    test('cached promise', done => {
-      const promise = Promise.reject(new Error('foo'));
-      element._cache.set('/foo', promise);
-      element._fetchSharedCacheURL({url: '/foo'}).catch(p => {
-        assert.equal(p.message, 'foo');
-        done();
-      });
-    });
-
-    test('cache invalidation', () => {
-      element._cache.set('/foo/bar', 1);
-      element._cache.set('/bar', 2);
-      element._sharedFetchPromises['/foo/bar'] = 3;
-      element._sharedFetchPromises['/bar'] = 4;
-      element._invalidateSharedFetchPromisesPrefix('/foo/');
-      assert.isFalse(element._cache.has('/foo/bar'));
-      assert.isTrue(element._cache.has('/bar'));
-      assert.isUndefined(element._sharedFetchPromises['/foo/bar']);
-      assert.strictEqual(4, element._sharedFetchPromises['/bar']);
-    });
-
-    test('params are properly encoded', () => {
-      let url = element._urlWithParams('/path/', {
-        sp: 'hola',
-        gr: 'guten tag',
-        noval: null,
-      });
-      assert.equal(url,
-          window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval');
-
-      url = element._urlWithParams('/path/', {
-        sp: 'hola',
-        en: ['hey', 'hi'],
-      });
-      assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi');
-
-      // Order must be maintained with array params.
-      url = element._urlWithParams('/path/', {
-        l: ['c', 'b', 'a'],
-      });
-      assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a');
-    });
-
-    test('request callbacks can be canceled', done => {
-      let cancelCalled = false;
-      window.fetch.returns(Promise.resolve({
-        body: {
-          cancel() { cancelCalled = true; },
-        },
-      }));
-      const cancelCondition = () => { return true; };
-      element._fetchJSON({url: '/dummy/url', cancelCondition}).then(
-          obj => {
-            assert.isUndefined(obj);
-            assert.isTrue(cancelCalled);
-            done();
-          });
-    });
-
     test('parent diff comments are properly grouped', done => {
-      sandbox.stub(element, '_fetchJSON', () => {
+      sandbox.stub(element._restApiHelper, 'fetchJSON', () => {
         return Promise.resolve({
           '/COMMIT_MSG': [],
           'sieve.go': [
@@ -290,7 +207,7 @@
     test('differing patch diff comments are properly grouped', done => {
       sandbox.stub(element, 'getFromProjectLookup')
           .returns(Promise.resolve('test'));
-      sandbox.stub(element, '_fetchJSON', request => {
+      sandbox.stub(element._restApiHelper, 'fetchJSON', request => {
         const url = request.url;
         if (url === '/changes/test~42/revisions/1') {
           return Promise.resolve({
@@ -404,37 +321,6 @@
           ]);
     });
 
-    suite('rebase action', () => {
-      let resolve_fetchJSON;
-      setup(() => {
-        sandbox.stub(element, '_fetchJSON').returns(
-            new Promise(resolve => {
-              resolve_fetchJSON = resolve;
-            }));
-      });
-
-      test('no rebase on current', done => {
-        element.getChangeRevisionActions('42', '1337').then(
-            response => {
-              assert.isTrue(response.rebase.enabled);
-              assert.isFalse(response.rebase.rebaseOnCurrent);
-              done();
-            });
-        resolve_fetchJSON({rebase: {}});
-      });
-
-      test('rebase on current', done => {
-        element.getChangeRevisionActions('42', '1337').then(
-            response => {
-              assert.isTrue(response.rebase.enabled);
-              assert.isTrue(response.rebase.rebaseOnCurrent);
-              done();
-            });
-        resolve_fetchJSON({rebase: {enabled: true}});
-      });
-    });
-
-
     test('server error', done => {
       const getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
       window.fetch.returns(Promise.resolve({ok: false}));
@@ -442,7 +328,7 @@
         element.addEventListener('server-error', resolve);
       });
 
-      element._fetchJSON({}).then(response => {
+      element._restApiHelper.fetchJSON({}).then(response => {
         assert.isUndefined(response);
         assert.isTrue(getResponseObjectStub.notCalled);
         serverErrorEventPromise.then(() => done());
@@ -458,12 +344,12 @@
           Promise.reject(new Error('Failed to fetch')));
       window.fetch.onSecondCall().returns(Promise.resolve(fakeAuthResponse));
       // Emulate logged in.
-      element._cache.set('/accounts/self/detail', {});
+      element._restApiHelper._cache.set('/accounts/self/detail', {});
       const serverErrorStub = sandbox.stub();
       element.addEventListener('server-error', serverErrorStub);
       const authErrorStub = sandbox.stub();
       element.addEventListener('auth-error', authErrorStub);
-      element._fetchJSON('/bar').then(r => {
+      element._restApiHelper.fetchJSON({url: '/bar'}).finally(r => {
         flush(() => {
           assert.isTrue(authErrorStub.called);
           assert.isFalse(serverErrorStub.called);
@@ -473,6 +359,34 @@
       });
     });
 
+    test('auth failure - test all failed to fetch', done => {
+      window.fetch.returns(
+          Promise.reject(new Error('Failed to fetch')));
+      // Emulate logged in.
+      element._cache.set('/accounts/self/detail', {});
+      const serverErrorStub = sandbox.stub();
+      element.addEventListener('server-error', serverErrorStub);
+      const authErrorStub = sandbox.stub();
+      element.addEventListener('auth-error', authErrorStub);
+      element._restApiHelper.fetchJSON({url: '/bar'}).finally(r => {
+        flush(() => {
+          assert.isTrue(authErrorStub.called);
+          assert.isFalse(serverErrorStub.called);
+          assert.isFalse(element._cache.has('/accounts/self/detail'));
+          done();
+        });
+      });
+    });
+
+    test('getLoggedIn returns false when network/auth failure', done => {
+      window.fetch.returns(
+          Promise.reject(new Error('Failed to fetch')));
+      element.getLoggedIn().then(isLoggedIn => {
+        assert.isFalse(isLoggedIn);
+        done();
+      });
+    });
+
     test('checkCredentials', done => {
       const responses = [
         {
@@ -505,7 +419,8 @@
     test('checkCredentials promise rejection', () => {
       window.fetch.restore();
       element._cache.set('/accounts/self/detail', true);
-      sandbox.spy(element, 'checkCredentials');
+      const checkCredentialsSpy =
+          sandbox.spy(element._restApiHelper, 'checkCredentials');
       sandbox.stub(window, 'fetch', url => {
         return Promise.reject(new Error('Failed to fetch'));
       });
@@ -517,13 +432,22 @@
             // The second fetch call also fails, which leads to a second
             // invocation of checkCredentials, which should immediately
             // return instead of making further fetch calls.
-            assert.isTrue(element.checkCredentials.calledTwice);
+            assert.isTrue(checkCredentialsSpy .calledTwice);
             assert.isTrue(window.fetch.calledTwice);
           });
     });
 
+    test('checkCredentials accepts only json', () => {
+      const authFetchStub = sandbox.stub(element._auth, 'fetch')
+          .returns(Promise.resolve());
+      element.checkCredentials();
+      assert.isTrue(authFetchStub.called);
+      assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
+          'application/json');
+    });
+
     test('legacy n,z key in change url is replaced', () => {
-      const stub = sandbox.stub(element, '_fetchJSON')
+      const stub = sandbox.stub(element._restApiHelper, 'fetchJSON')
           .returns(Promise.resolve([]));
       element.getChanges(1, null, 'n,z');
       assert.equal(stub.lastCall.args[0].params.S, 0);
@@ -531,38 +455,38 @@
 
     test('saveDiffPreferences invalidates cache line', () => {
       const cacheKey = '/accounts/self/preferences.diff';
-      sandbox.stub(element, '_send');
+      const sendStub = sandbox.stub(element._restApiHelper, 'send');
       element._cache.set(cacheKey, {tab_size: 4});
       element.saveDiffPreferences({tab_size: 8});
-      assert.isTrue(element._send.called);
-      assert.isFalse(element._cache.has(cacheKey));
+      assert.isTrue(sendStub.called);
+      assert.isFalse(element._restApiHelper._cache.has(cacheKey));
     });
 
     test('getAccount when resp is null does not add anything to the cache',
         done => {
           const cacheKey = '/accounts/self/detail';
-          const stub = sandbox.stub(element, '_fetchSharedCacheURL',
+          const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
               () => Promise.resolve());
 
           element.getAccount().then(() => {
-            assert.isTrue(element._fetchSharedCacheURL.called);
-            assert.isFalse(element._cache.has(cacheKey));
+            assert.isTrue(stub.called);
+            assert.isFalse(element._restApiHelper._cache.has(cacheKey));
             done();
           });
 
-          element._cache.set(cacheKey, 'fake cache');
+          element._restApiHelper._cache.set(cacheKey, 'fake cache');
           stub.lastCall.args[0].errFn();
         });
 
     test('getAccount does not add to the cache when resp.status is 403',
         done => {
           const cacheKey = '/accounts/self/detail';
-          const stub = sandbox.stub(element, '_fetchSharedCacheURL',
+          const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
               () => Promise.resolve());
 
           element.getAccount().then(() => {
-            assert.isTrue(element._fetchSharedCacheURL.called);
-            assert.isFalse(element._cache.has(cacheKey));
+            assert.isTrue(stub.called);
+            assert.isFalse(element._restApiHelper._cache.has(cacheKey));
             done();
           });
           element._cache.set(cacheKey, 'fake cache');
@@ -571,15 +495,15 @@
 
     test('getAccount when resp is successful', done => {
       const cacheKey = '/accounts/self/detail';
-      const stub = sandbox.stub(element, '_fetchSharedCacheURL',
+      const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
           () => Promise.resolve());
 
       element.getAccount().then(response => {
-        assert.isTrue(element._fetchSharedCacheURL.called);
-        assert.equal(element._cache.get(cacheKey), 'fake cache');
+        assert.isTrue(stub.called);
+        assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache');
         done();
       });
-      element._cache.set(cacheKey, 'fake cache');
+      element._restApiHelper._cache.set(cacheKey, 'fake cache');
 
       stub.lastCall.args[0].errFn({});
     });
@@ -591,7 +515,7 @@
       sandbox.stub(element, '_isNarrowScreen', () => {
         return smallScreen;
       });
-      sandbox.stub(element, '_fetchSharedCacheURL', () => {
+      sandbox.stub(element._restApiHelper, 'fetchCacheURL', () => {
         return Promise.resolve(testJSON);
       });
     };
@@ -656,10 +580,10 @@
         });
 
     test('savPreferences normalizes download scheme', () => {
-      sandbox.stub(element, '_send');
+      const sendStub = sandbox.stub(element._restApiHelper, 'send');
       element.savePreferences({download_scheme: 'HTTP'});
-      assert.isTrue(element._send.called);
-      assert.equal(element._send.lastCall.args[0].body.download_scheme, 'http');
+      assert.isTrue(sendStub.called);
+      assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http');
     });
 
     test('getDiffPreferences returns correct defaults', done => {
@@ -685,10 +609,10 @@
     });
 
     test('saveDiffPreferences set show_tabs to false', () => {
-      sandbox.stub(element, '_send');
+      const sendStub = sandbox.stub(element._restApiHelper, 'send');
       element.saveDiffPreferences({show_tabs: false});
-      assert.isTrue(element._send.called);
-      assert.equal(element._send.lastCall.args[0].body.show_tabs, false);
+      assert.isTrue(sendStub.called);
+      assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
     });
 
     test('getEditPreferences returns correct defaults', done => {
@@ -718,34 +642,36 @@
     });
 
     test('saveEditPreferences set show_tabs to false', () => {
-      sandbox.stub(element, '_send');
+      const sendStub = sandbox.stub(element._restApiHelper, 'send');
       element.saveEditPreferences({show_tabs: false});
-      assert.isTrue(element._send.called);
-      assert.equal(element._send.lastCall.args[0].body.show_tabs, false);
+      assert.isTrue(sendStub.called);
+      assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
     });
 
     test('confirmEmail', () => {
-      sandbox.spy(element, '_send');
+      const sendStub = sandbox.spy(element._restApiHelper, 'send');
       element.confirmEmail('foo');
-      assert.isTrue(element._send.calledOnce);
-      assert.equal(element._send.lastCall.args[0].method, 'PUT');
-      assert.equal(element._send.lastCall.args[0].url,
+      assert.isTrue(sendStub.calledOnce);
+      assert.equal(sendStub.lastCall.args[0].method, 'PUT');
+      assert.equal(sendStub.lastCall.args[0].url,
           '/config/server/email.confirm');
-      assert.deepEqual(element._send.lastCall.args[0].body, {token: 'foo'});
+      assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
     });
 
     test('setAccountStatus', () => {
-      sandbox.stub(element, '_send').returns(Promise.resolve('OOO'));
+      const sendStub = sandbox.stub(element._restApiHelper, 'send')
+          .returns(Promise.resolve('OOO'));
       element._cache.set('/accounts/self/detail', {});
       return element.setAccountStatus('OOO').then(() => {
-        assert.isTrue(element._send.calledOnce);
-        assert.equal(element._send.lastCall.args[0].method, 'PUT');
-        assert.equal(element._send.lastCall.args[0].url,
+        assert.isTrue(sendStub.calledOnce);
+        assert.equal(sendStub.lastCall.args[0].method, 'PUT');
+        assert.equal(sendStub.lastCall.args[0].url,
             '/accounts/self/status');
-        assert.deepEqual(element._send.lastCall.args[0].body,
+        assert.deepEqual(sendStub.lastCall.args[0].body,
             {status: 'OOO'});
-        assert.deepEqual(element._cache.get('/accounts/self/detail'),
-            {status: 'OOO'});
+        assert.deepEqual(element._restApiHelper
+            ._cache.get('/accounts/self/detail'),
+        {status: 'OOO'});
       });
     });
 
@@ -834,18 +760,20 @@
       const change_num = '1';
       const file_name = 'index.php';
       const file_contents = '<?php';
-      sandbox.stub(element, '_send').returns(
+      sandbox.stub(element._restApiHelper, 'send').returns(
           Promise.resolve([change_num, file_name, file_contents]));
       sandbox.stub(element, 'getResponseObject')
           .returns(Promise.resolve([change_num, file_name, file_contents]));
       element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
       return element.saveChangeEdit(change_num, file_name, file_contents)
           .then(() => {
-            assert.isTrue(element._send.calledOnce);
-            assert.equal(element._send.lastCall.args[0].method, 'PUT');
-            assert.equal(element._send.lastCall.args[0].url,
+            assert.isTrue(element._restApiHelper.send.calledOnce);
+            assert.equal(element._restApiHelper.send.lastCall.args[0].method,
+                'PUT');
+            assert.equal(element._restApiHelper.send.lastCall.args[0].url,
                 '/changes/test~1/edit/' + file_name);
-            assert.equal(element._send.lastCall.args[0].body, file_contents);
+            assert.equal(element._restApiHelper.send.lastCall.args[0].body,
+                file_contents);
           });
     });
 
@@ -853,17 +781,18 @@
       element._projectLookup = {1: 'test'};
       const change_num = '1';
       const message = 'this is a commit message';
-      sandbox.stub(element, '_send').returns(
+      sandbox.stub(element._restApiHelper, 'send').returns(
           Promise.resolve([change_num, message]));
       sandbox.stub(element, 'getResponseObject')
           .returns(Promise.resolve([change_num, message]));
       element._cache.set('/changes/' + change_num + '/message', {});
       return element.putChangeCommitMessage(change_num, message).then(() => {
-        assert.isTrue(element._send.calledOnce);
-        assert.equal(element._send.lastCall.args[0].method, 'PUT');
-        assert.equal(element._send.lastCall.args[0].url,
+        assert.isTrue(element._restApiHelper.send.calledOnce);
+        assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT');
+        assert.equal(element._restApiHelper.send.lastCall.args[0].url,
             '/changes/test~1/message');
-        assert.deepEqual(element._send.lastCall.args[0].body, {message});
+        assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body,
+            {message});
       });
     });
 
@@ -919,7 +848,7 @@
     });
 
     test('createRepo encodes name', () => {
-      const sendStub = sandbox.stub(element, '_send')
+      const sendStub = sandbox.stub(element._restApiHelper, 'send')
           .returns(Promise.resolve());
       return element.createRepo({name: 'x/y'}).then(() => {
         assert.isTrue(sendStub.calledOnce);
@@ -965,64 +894,65 @@
 
     suite('getRepos', () => {
       const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
-
+      let fetchCacheURLStub;
       setup(() => {
-        sandbox.stub(element, '_fetchSharedCacheURL');
+        fetchCacheURLStub =
+            sandbox.stub(element._restApiHelper, 'fetchCacheURL');
       });
 
       test('normal use', () => {
         element.getRepos('test', 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=test');
 
         element.getRepos(null, 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             `/projects/?n=26&S=0&query=${defaultQuery}`);
 
         element.getRepos('test', 25, 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/projects/?n=26&S=25&query=test');
       });
 
       test('with blank', () => {
         element.getRepos('test/test', 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest');
       });
 
       test('with hyphen', () => {
         element.getRepos('foo-bar', 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
       });
 
       test('with leading hyphen', () => {
         element.getRepos('-bar', 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Abar');
       });
 
       test('with trailing hyphen', () => {
         element.getRepos('foo-bar-', 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
       });
 
       test('with underscore', () => {
         element.getRepos('foo_bar', 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
       });
 
       test('with underscore', () => {
         element.getRepos('foo_bar', 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
       });
 
       test('hyphen only', () => {
         element.getRepos('-', 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             `/projects/?n=26&S=0&query=${defaultQuery}`);
       });
     });
@@ -1051,43 +981,45 @@
     });
 
     suite('getGroups', () => {
+      let fetchCacheURLStub;
       setup(() => {
-        sandbox.stub(element, '_fetchSharedCacheURL');
+        fetchCacheURLStub =
+            sandbox.stub(element._restApiHelper, 'fetchCacheURL');
       });
 
       test('normal use', () => {
         element.getGroups('test', 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/groups/?n=26&S=0&m=test');
 
         element.getGroups(null, 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/groups/?n=26&S=0');
 
         element.getGroups('test', 25, 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/groups/?n=26&S=25&m=test');
       });
 
       test('regex', () => {
         element.getGroups('^test.*', 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/groups/?n=26&S=0&r=%5Etest.*');
 
         element.getGroups('^test.*', 25, 25);
-        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
             '/groups/?n=26&S=25&r=%5Etest.*');
       });
     });
 
     test('gerrit auth is used', () => {
       sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve());
-      element._fetchJSON('foo');
+      element._restApiHelper.fetchJSON({url: 'foo'});
       assert(Gerrit.Auth.fetch.called);
     });
 
     test('getSuggestedAccounts does not return _fetchJSON', () => {
-      const _fetchJSONSpy = sandbox.spy(element, '_fetchJSON');
+      const _fetchJSONSpy = sandbox.spy(element._restApiHelper, 'fetchJSON');
       return element.getSuggestedAccounts().then(accts => {
         assert.isFalse(_fetchJSONSpy.called);
         assert.equal(accts.length, 0);
@@ -1095,7 +1027,7 @@
     });
 
     test('_fetchJSON gets called by getSuggestedAccounts', () => {
-      const _fetchJSONStub = sandbox.stub(element, '_fetchJSON',
+      const _fetchJSONStub = sandbox.stub(element._restApiHelper, 'fetchJSON',
           () => Promise.resolve());
       return element.getSuggestedAccounts('own').then(() => {
         assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, {
@@ -1167,24 +1099,35 @@
         const errFn = sinon.stub();
         sandbox.stub(element, 'getChangeActionURL')
             .returns(Promise.resolve(''));
-        sandbox.stub(element, '_fetchRawJSON')
+        sandbox.stub(element._restApiHelper, 'fetchRawJSON')
             .returns(Promise.resolve({ok: false, status: 500}));
         return element._getChangeDetail(123, '516714', errFn).then(() => {
           assert.isTrue(errFn.called);
         });
       });
 
+      test('_getChangeDetail accepts only json', () => {
+        const authFetchStub = sandbox.stub(element._auth, 'fetch')
+            .returns(Promise.resolve());
+        const errFn = sinon.stub();
+        element._getChangeDetail(123, '516714', errFn);
+        assert.isTrue(authFetchStub.called);
+        assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
+            'application/json');
+      });
+
       test('_getChangeDetail populates _projectLookup', () => {
         sandbox.stub(element, 'getChangeActionURL')
             .returns(Promise.resolve(''));
-        sandbox.stub(element, '_fetchRawJSON')
+        sandbox.stub(element._restApiHelper, 'fetchRawJSON')
             .returns(Promise.resolve({ok: true}));
 
         const mockResponse = {_number: 1, project: 'test'};
-        sandbox.stub(element, '_readResponsePayload').returns(Promise.resolve({
-          parsed: mockResponse,
-          raw: JSON.stringify(mockResponse),
-        }));
+        sandbox.stub(element._restApiHelper, 'readResponsePayload')
+            .returns(Promise.resolve({
+              parsed: mockResponse,
+              raw: JSON.stringify(mockResponse),
+            }));
         return element._getChangeDetail(1, '516714').then(() => {
           assert.equal(Object.keys(element._projectLookup).length, 1);
           assert.equal(element._projectLookup[1], 'test');
@@ -1202,7 +1145,8 @@
           const mockResponse = {foo: 'bar', baz: 42};
           mockResponseSerial = element.JSON_PREFIX +
               JSON.stringify(mockResponse);
-          sandbox.stub(element, '_urlWithParams').returns(requestUrl);
+          sandbox.stub(element._restApiHelper, 'urlWithParams')
+              .returns(requestUrl);
           sandbox.stub(element, 'getChangeActionURL')
               .returns(Promise.resolve(requestUrl));
           collectSpy = sandbox.spy(element._etags, 'collect');
@@ -1210,11 +1154,12 @@
         });
 
         test('contributes to cache', () => {
-          sandbox.stub(element, '_fetchRawJSON').returns(Promise.resolve({
-            text: () => Promise.resolve(mockResponseSerial),
-            status: 200,
-            ok: true,
-          }));
+          sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+              .returns(Promise.resolve({
+                text: () => Promise.resolve(mockResponseSerial),
+                status: 200,
+                ok: true,
+              }));
 
           return element._getChangeDetail(123, '516714').then(detail => {
             assert.isFalse(getPayloadSpy.called);
@@ -1225,11 +1170,12 @@
         });
 
         test('uses cache on HTTP 304', () => {
-          sandbox.stub(element, '_fetchRawJSON').returns(Promise.resolve({
-            text: () => Promise.resolve(mockResponseSerial),
-            status: 304,
-            ok: true,
-          }));
+          sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+              .returns(Promise.resolve({
+                text: () => Promise.resolve(mockResponseSerial),
+                status: 304,
+                ok: true,
+              }));
 
           return element._getChangeDetail(123, {}).then(detail => {
             assert.isFalse(collectSpy.called);
@@ -1274,7 +1220,7 @@
 
     suite('getChanges populates _projectLookup', () => {
       test('multiple queries', () => {
-        sandbox.stub(element, '_fetchJSON')
+        sandbox.stub(element._restApiHelper, 'fetchJSON')
             .returns(Promise.resolve([
               [
                 {_number: 1, project: 'test'},
@@ -1294,7 +1240,7 @@
       });
 
       test('no query', () => {
-        sandbox.stub(element, '_fetchJSON')
+        sandbox.stub(element._restApiHelper, 'fetchJSON')
             .returns(Promise.resolve([
               {_number: 1, project: 'test'},
               {_number: 2, project: 'test'},
@@ -1314,7 +1260,7 @@
 
     test('_getChangeURLAndFetch', () => {
       element._projectLookup = {1: 'test'};
-      const fetchStub = sandbox.stub(element, '_fetchJSON')
+      const fetchStub = sandbox.stub(element._restApiHelper, 'fetchJSON')
           .returns(Promise.resolve());
       const req = {changeNum: 1, endpoint: '/test', patchNum: 1};
       return element._getChangeURLAndFetch(req).then(() => {
@@ -1325,7 +1271,7 @@
 
     test('_getChangeURLAndSend', () => {
       element._projectLookup = {1: 'test'};
-      const sendStub = sandbox.stub(element, '_send')
+      const sendStub = sandbox.stub(element._restApiHelper, 'send')
           .returns(Promise.resolve());
 
       const req = {
@@ -1347,16 +1293,17 @@
         const mockObject = {foo: 'bar', baz: 'foo'};
         const serial = element.JSON_PREFIX + JSON.stringify(mockObject);
         const mockResponse = {text: () => Promise.resolve(serial)};
-        return element._readResponsePayload(mockResponse).then(payload => {
-          assert.deepEqual(payload.parsed, mockObject);
-          assert.equal(payload.raw, serial);
-        });
+        return element._restApiHelper.readResponsePayload(mockResponse)
+            .then(payload => {
+              assert.deepEqual(payload.parsed, mockObject);
+              assert.equal(payload.raw, serial);
+            });
       });
 
       test('_parsePrefixedJSON', () => {
         const obj = {x: 3, y: {z: 4}, w: 23};
         const serial = element.JSON_PREFIX + JSON.stringify(obj);
-        const result = element._parsePrefixedJSON(serial);
+        const result = element._restApiHelper.parsePrefixedJSON(serial);
         assert.deepEqual(result, obj);
       });
     });
@@ -1378,7 +1325,7 @@
     });
 
     test('generateAccountHttpPassword', () => {
-      const sendSpy = sandbox.spy(element, '_send');
+      const sendSpy = sandbox.spy(element._restApiHelper, 'send');
       return element.generateAccountHttpPassword().then(() => {
         assert.isTrue(sendSpy.calledOnce);
         assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
@@ -1463,11 +1410,12 @@
     });
 
     test('getDashboard', () => {
-      const fetchStub = sandbox.stub(element, '_fetchSharedCacheURL');
+      const fetchCacheURLStub = sandbox.stub(element._restApiHelper,
+          'fetchCacheURL');
       element.getDashboard('gerrit/project', 'default:main');
-      assert.isTrue(fetchStub.calledOnce);
+      assert.isTrue(fetchCacheURLStub.calledOnce);
       assert.equal(
-          fetchStub.lastCall.args[0].url,
+          fetchCacheURLStub.lastCall.args[0].url,
           '/projects/gerrit%2Fproject/dashboards/default%3Amain');
     });
 
@@ -1535,7 +1483,7 @@
     });
 
     test('_fetch forwards request and logs', () => {
-      const logStub = sandbox.stub(element, '_logCall');
+      const logStub = sandbox.stub(element._restApiHelper, '_logCall');
       const response = {status: 404, text: sinon.stub()};
       const url = 'my url';
       const fetchOptions = {method: 'DELETE'};
@@ -1543,7 +1491,7 @@
       const startTime = 123;
       sandbox.stub(Date, 'now').returns(startTime);
       const req = {url, fetchOptions};
-      return element._fetch(req).then(() => {
+      return element._restApiHelper.fetch(req).then(() => {
         assert.isTrue(logStub.calledOnce);
         assert.isTrue(logStub.calledWith(req, startTime, response.status));
         assert.isFalse(response.text.called);
@@ -1555,10 +1503,11 @@
       const handler = sinon.stub();
       element.addEventListener('rpc-log', handler);
 
-      element._logCall({url: 'url'}, 100, 200);
+      element._restApiHelper._logCall({url: 'url'}, 100, 200);
       assert.isFalse(handler.called);
 
-      element._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200);
+      element._restApiHelper
+          ._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200);
       flushAsynchronousOperations();
       assert.isTrue(handler.calledOnce);
     });
@@ -1567,7 +1516,7 @@
       sandbox.stub(element, 'getFromProjectLookup')
           .returns(Promise.resolve('test'));
       const sendStub =
-          sandbox.stub(element, '_send').returns(Promise.resolve());
+          sandbox.stub(element._restApiHelper, 'send').returns(Promise.resolve());
 
       await element.saveChangeStarred(123, true);
       assert.isTrue(sendStub.calledOnce);
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
new file mode 100644
index 0000000..3908a00
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
@@ -0,0 +1,431 @@
+/**
+ * @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.
+ */
+(function(window) {
+  'use strict';
+
+  const JSON_PREFIX = ')]}\'';
+  const FAILED_TO_FETCH_ERROR = 'Failed to fetch';
+
+  /**
+   * Wrapper around Map for caching server responses. Site-based so that
+   * changes to CANONICAL_PATH will result in a different cache going into
+   * effect.
+   */
+  class SiteBasedCache {
+    constructor() {
+      // Container of per-canonical-path caches.
+      this._data = new Map();
+      if (window.INITIAL_DATA != undefined) {
+        // Put all data shipped with index.html into the cache. This makes it
+        // so that we spare more round trips to the server when the app loads
+        // initially.
+        Object
+            .entries(window.INITIAL_DATA)
+            .forEach(e => this._cache().set(e[0], e[1]));
+      }
+    }
+
+    // Returns the cache for the current canonical path.
+    _cache() {
+      if (!this._data.has(window.CANONICAL_PATH)) {
+        this._data.set(window.CANONICAL_PATH, new Map());
+      }
+      return this._data.get(window.CANONICAL_PATH);
+    }
+
+    has(key) {
+      return this._cache().has(key);
+    }
+
+    get(key) {
+      return this._cache().get(key);
+    }
+
+    set(key, value) {
+      this._cache().set(key, value);
+    }
+
+    delete(key) {
+      this._cache().delete(key);
+    }
+
+    invalidatePrefix(prefix) {
+      const newMap = new Map();
+      for (const [key, value] of this._cache().entries()) {
+        if (!key.startsWith(prefix)) {
+          newMap.set(key, value);
+        }
+      }
+      this._data.set(window.CANONICAL_PATH, newMap);
+    }
+  }
+
+  class FetchPromisesCache {
+    constructor() {
+      this._data = {};
+    }
+
+    has(key) {
+      return !!this._data[key];
+    }
+
+    get(key) {
+      return this._data[key];
+    }
+
+    set(key, value) {
+      this._data[key] = value;
+    }
+
+    invalidatePrefix(prefix) {
+      const newData = {};
+      Object.entries(this._data).forEach(([key, value]) => {
+        if (!key.startsWith(prefix)) {
+          newData[key] = value;
+        }
+      });
+      this._data = newData;
+    }
+  }
+
+  class GrRestApiHelper {
+    /**
+     * @param {SiteBasedCache} cache
+     * @param {object} auth
+     * @param {FetchPromisesCache} fetchPromisesCache
+     * @param {object} credentialCheck
+     * @param {object} restApiInterface
+     */
+    constructor(cache, auth, fetchPromisesCache, credentialCheck,
+        restApiInterface) {
+      this._cache = cache;// TODO: make it public
+      this._auth = auth;
+      this._fetchPromisesCache = fetchPromisesCache;
+      this._credentialCheck = credentialCheck;
+      this._restApiInterface = restApiInterface;
+    }
+
+    /**
+     * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
+     * with timing and logging.
+     *
+     * @param {Gerrit.FetchRequest} req
+     */
+    fetch(req) {
+      const start = Date.now();
+      const xhr = this._auth.fetch(req.url, req.fetchOptions);
+
+      // Log the call after it completes.
+      xhr.then(res => this._logCall(req, start, res ? res.status : null));
+
+      // Return the XHR directly (without the log).
+      return xhr;
+    }
+
+    /**
+     * Log information about a REST call. Because the elapsed time is determined
+     * by this method, it should be called immediately after the request
+     * finishes.
+     *
+     * @param {Gerrit.FetchRequest} req
+     * @param {number} startTime the time that the request was started.
+     * @param {number} status the HTTP status of the response. The status value
+     *     is used here rather than the response object so there is no way this
+     *     method can read the body stream.
+     */
+    _logCall(req, startTime, status) {
+      const method = (req.fetchOptions && req.fetchOptions.method) ?
+        req.fetchOptions.method : 'GET';
+      const endTime = Date.now();
+      const elapsed = (endTime - startTime);
+      const startAt = new Date(startTime);
+      const endAt = new Date(endTime);
+      console.log([
+        'HTTP',
+        status,
+        method,
+        elapsed + 'ms',
+        req.anonymizedUrl || req.url,
+        `(${startAt.toISOString()}, ${endAt.toISOString()})`,
+      ].join(' '));
+      if (req.anonymizedUrl) {
+        this.fire('rpc-log',
+            {status, method, elapsed, anonymizedUrl: req.anonymizedUrl});
+      }
+    }
+
+    /**
+     * Fetch JSON from url provided.
+     * Returns a Promise that resolves to a native Response.
+     * Doesn't do error checking. Supports cancel condition. Performs auth.
+     * Validates auth expiry errors.
+     *
+     * @param {Gerrit.FetchJSONRequest} req
+     */
+    fetchRawJSON(req) {
+      const urlWithParams = this.urlWithParams(req.url, req.params);
+      const fetchReq = {
+        url: urlWithParams,
+        fetchOptions: req.fetchOptions,
+        anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
+      };
+      return this.fetch(fetchReq).then(res => {
+        if (req.cancelCondition && req.cancelCondition()) {
+          res.body.cancel();
+          return;
+        }
+        return res;
+      }).catch(err => {
+        const isLoggedIn = !!this._cache.get('/accounts/self/detail');
+        if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) {
+          this.checkCredentials();
+        } else {
+          if (req.errFn) {
+            req.errFn.call(undefined, null, err);
+          } else {
+            this.fire('network-error', {error: err});
+          }
+        }
+        throw err;
+      });
+    }
+
+    /**
+     * Fetch JSON from url provided.
+     * Returns a Promise that resolves to a parsed response.
+     * Same as {@link fetchRawJSON}, plus error handling.
+     *
+     * @param {Gerrit.FetchJSONRequest} req
+     */
+    fetchJSON(req) {
+      req = this.addAcceptJsonHeader(req);
+      return this.fetchRawJSON(req).then(response => {
+        if (!response) {
+          return;
+        }
+        if (!response.ok) {
+          if (req.errFn) {
+            req.errFn.call(null, response);
+            return;
+          }
+          this.fire('server-error', {request: req, response});
+          return;
+        }
+        return response && this.getResponseObject(response);
+      });
+    }
+
+    /**
+     * @param {string} url
+     * @param {?Object|string=} opt_params URL params, key-value hash.
+     * @return {string}
+     */
+    urlWithParams(url, opt_params) {
+      if (!opt_params) { return this.getBaseUrl() + url; }
+
+      const params = [];
+      for (const p in opt_params) {
+        if (!opt_params.hasOwnProperty(p)) { continue; }
+        if (opt_params[p] == null) {
+          params.push(encodeURIComponent(p));
+          continue;
+        }
+        for (const value of [].concat(opt_params[p])) {
+          params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`);
+        }
+      }
+      return this.getBaseUrl() + url + '?' + params.join('&');
+    }
+
+    /**
+     * @param {!Object} response
+     * @return {?}
+     */
+    getResponseObject(response) {
+      return this.readResponsePayload(response)
+          .then(payload => payload.parsed);
+    }
+
+    /**
+     * @param {!Object} response
+     * @return {!Object}
+     */
+    readResponsePayload(response) {
+      return response.text().then(text => {
+        let result;
+        try {
+          result = this.parsePrefixedJSON(text);
+        } catch (_) {
+          result = null;
+        }
+        return {parsed: result, raw: text};
+      });
+    }
+
+    /**
+     * @param {string} source
+     * @return {?}
+     */
+    parsePrefixedJSON(source) {
+      return JSON.parse(source.substring(JSON_PREFIX.length));
+    }
+
+    /**
+     * @param {Gerrit.FetchJSONRequest} req
+     * @return {Gerrit.FetchJSONRequest}
+     */
+    addAcceptJsonHeader(req) {
+      if (!req.fetchOptions) req.fetchOptions = {};
+      if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
+      if (!req.fetchOptions.headers.has('Accept')) {
+        req.fetchOptions.headers.append('Accept', 'application/json');
+      }
+      return req;
+    }
+
+    getBaseUrl() {
+      return this._restApiInterface.getBaseUrl();
+    }
+
+    fire(type, detail, options) {
+      return this._restApiInterface.fire(type, detail, options);
+    }
+
+    /**
+     * @param {Gerrit.FetchJSONRequest} req
+     */
+    fetchCacheURL(req) {
+      if (this._fetchPromisesCache.has(req.url)) {
+        return this._fetchPromisesCache.get(req.url);
+      }
+      // TODO(andybons): Periodic cache invalidation.
+      if (this._cache.has(req.url)) {
+        return Promise.resolve(this._cache.get(req.url));
+      }
+      this._fetchPromisesCache.set(req.url,
+          this.fetchJSON(req).then(response => {
+            if (response !== undefined) {
+              this._cache.set(req.url, response);
+            }
+            this._fetchPromisesCache.set(req.url, undefined);
+            return response;
+          }).catch(err => {
+            this._fetchPromisesCache.set(req.url, undefined);
+            throw err;
+          })
+      );
+      return this._fetchPromisesCache.get(req.url);
+    }
+
+    /**
+     * Send an XHR.
+     *
+     * @param {Gerrit.SendRequest} req
+     * @return {Promise}
+     */
+    send(req) {
+      const options = {method: req.method};
+      if (req.body) {
+        options.headers = new Headers();
+        options.headers.set(
+            'Content-Type', req.contentType || 'application/json');
+        options.body = typeof req.body === 'string' ?
+          req.body : JSON.stringify(req.body);
+      }
+      if (req.headers) {
+        if (!options.headers) { options.headers = new Headers(); }
+        for (const header in req.headers) {
+          if (!req.headers.hasOwnProperty(header)) { continue; }
+          options.headers.set(header, req.headers[header]);
+        }
+      }
+      const url = req.url.startsWith('http') ?
+        req.url : this.getBaseUrl() + req.url;
+      const fetchReq = {
+        url,
+        fetchOptions: options,
+        anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl,
+      };
+      const xhr = this.fetch(fetchReq).then(response => {
+        if (!response.ok) {
+          if (req.errFn) {
+            return req.errFn.call(undefined, response);
+          }
+          this.fire('server-error', {request: fetchReq, response});
+        }
+        return response;
+      }).catch(err => {
+        this.fire('network-error', {error: err});
+        if (req.errFn) {
+          return req.errFn.call(undefined, null, err);
+        } else {
+          throw err;
+        }
+      });
+
+      if (req.parseResponse) {
+        return xhr.then(res => this.getResponseObject(res));
+      }
+
+      return xhr;
+    }
+
+    checkCredentials() {
+      if (this._credentialCheck.checking) {
+        return;
+      }
+      this._credentialCheck.checking = true;
+      let req = {url: '/accounts/self/detail', reportUrlAsIs: true};
+      req = this.addAcceptJsonHeader(req);
+      // Skip the REST response cache.
+      return this.fetchRawJSON(req).then(res => {
+        if (!res) { return; }
+        if (res.status === 403) {
+          this.fire('auth-error');
+          this._cache.delete('/accounts/self/detail');
+        } else if (res.ok) {
+          return this.getResponseObject(res);
+        }
+      }).then(res => {
+        this._credentialCheck.checking = false;
+        if (res) {
+          this._cache.set('/accounts/self/detail', res);
+        }
+        return res;
+      }).catch(err => {
+        this._credentialCheck.checking = false;
+        if (err && err.message === FAILED_TO_FETCH_ERROR) {
+          this.fire('auth-error');
+          this._cache.delete('/accounts/self/detail');
+        }
+      });
+    }
+
+    /**
+     * @param {string} prefix
+     */
+    invalidateFetchPromisesPrefix(prefix) {
+      this._fetchPromisesCache.invalidatePrefix(prefix);
+      this._cache.invalidatePrefix(prefix);
+    }
+  }
+
+  window.SiteBasedCache = SiteBasedCache;
+  window.FetchPromisesCache = FetchPromisesCache;
+  window.GrRestApiHelper = GrRestApiHelper;
+})(window);
+
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
new file mode 100644
index 0000000..4eaf1bc
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-rest-api-helper</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../../test/common-test-setup.html"/>
+<script src="../../../../scripts/util.js"></script>
+<script src="../gr-auth.js"></script>
+<script src="gr-rest-api-helper.js"></script>
+
+<script>void(0);</script>
+
+<script>
+  suite('gr-rest-api-helper tests', () => {
+    let helper;
+    let sandbox;
+    let cache;
+    let fetchPromisesCache;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      cache = new SiteBasedCache();
+      fetchPromisesCache = new FetchPromisesCache();
+      const credentialCheck = {checking: false};
+
+      window.CANONICAL_PATH = 'testhelper';
+
+      const mockRestApiInterface = {
+        getBaseUrl: sinon.stub().returns(window.CANONICAL_PATH),
+        fire: sinon.stub(),
+      };
+
+      const testJSON = ')]}\'\n{"hello": "bonjour"}';
+      sandbox.stub(window, 'fetch').returns(Promise.resolve({
+        ok: true,
+        text() {
+          return Promise.resolve(testJSON);
+        },
+      }));
+
+      helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache,
+          credentialCheck, mockRestApiInterface);
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('fetchJSON()', () => {
+      test('Sets header to accept application/json', () => {
+        const authFetchStub = sandbox.stub(helper._auth, 'fetch')
+            .returns(Promise.resolve());
+        helper.fetchJSON({url: '/dummy/url'});
+        assert.isTrue(authFetchStub.called);
+        assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
+            'application/json');
+      });
+
+      test('Use header option accept when provided', () => {
+        const authFetchStub = sandbox.stub(helper._auth, 'fetch')
+            .returns(Promise.resolve());
+        const headers = new Headers();
+        headers.append('Accept', '*/*');
+        const fetchOptions = {headers};
+        helper.fetchJSON({url: '/dummy/url', fetchOptions});
+        assert.isTrue(authFetchStub.called);
+        assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
+            '*/*');
+      });
+    });
+
+    test('JSON prefix is properly removed', done => {
+      helper.fetchJSON({url: '/dummy/url'}).then(obj => {
+        assert.deepEqual(obj, {hello: 'bonjour'});
+        done();
+      });
+    });
+
+    test('cached results', done => {
+      let n = 0;
+      sandbox.stub(helper, 'fetchJSON', () => {
+        return Promise.resolve(++n);
+      });
+      const promises = [];
+      promises.push(helper.fetchCacheURL('/foo'));
+      promises.push(helper.fetchCacheURL('/foo'));
+      promises.push(helper.fetchCacheURL('/foo'));
+
+      Promise.all(promises).then(results => {
+        assert.deepEqual(results, [1, 1, 1]);
+        helper.fetchCacheURL('/foo').then(foo => {
+          assert.equal(foo, 1);
+          done();
+        });
+      });
+    });
+
+    test('cached promise', done => {
+      const promise = Promise.reject(new Error('foo'));
+      cache.set('/foo', promise);
+      helper.fetchCacheURL({url: '/foo'}).catch(p => {
+        assert.equal(p.message, 'foo');
+        done();
+      });
+    });
+
+    test('cache invalidation', () => {
+      cache.set('/foo/bar', 1);
+      cache.set('/bar', 2);
+      fetchPromisesCache.set('/foo/bar', 3);
+      fetchPromisesCache.set('/bar', 4);
+      helper.invalidateFetchPromisesPrefix('/foo/');
+      assert.isFalse(cache.has('/foo/bar'));
+      assert.isTrue(cache.has('/bar'));
+      assert.isUndefined(fetchPromisesCache.get('/foo/bar'));
+      assert.strictEqual(4, fetchPromisesCache.get('/bar'));
+    });
+
+    test('params are properly encoded', () => {
+      let url = helper.urlWithParams('/path/', {
+        sp: 'hola',
+        gr: 'guten tag',
+        noval: null,
+      });
+      assert.equal(url,
+          window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval');
+
+      url = helper.urlWithParams('/path/', {
+        sp: 'hola',
+        en: ['hey', 'hi'],
+      });
+      assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi');
+
+      // Order must be maintained with array params.
+      url = helper.urlWithParams('/path/', {
+        l: ['c', 'b', 'a'],
+      });
+      assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a');
+    });
+
+    test('request callbacks can be canceled', done => {
+      let cancelCalled = false;
+      window.fetch.returns(Promise.resolve({
+        body: {
+          cancel() { cancelCalled = true; },
+        },
+      }));
+      const cancelCondition = () => { return true; };
+      helper.fetchJSON({url: '/dummy/url', cancelCondition}).then(
+          obj => {
+            assert.isUndefined(obj);
+            assert.isTrue(cancelCalled);
+            done();
+          });
+    });
+  });
+</script>
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 8ddb255..d883ef6 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
@@ -21,7 +21,6 @@
   if (window.GrReviewerUpdatesParser) { return; }
 
   function GrReviewerUpdatesParser(change) {
-    // TODO (viktard): Polyfill Object.assign for IE.
     this.result = Object.assign({}, change);
     this._lastState = {};
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
index 202c52a..fdf79af 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reviewer-updates-parser</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 <script src="gr-reviewer-updates-parser.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
index 05c2cee..015d71e 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <dom-module id="mock-diff-response">
   <template></template>
@@ -153,7 +153,7 @@
 
       Polymer({
         is: 'mock-diff-response',
-        _legacyUndefinedCheck: true,
+
         properties: {
           diffResponse: {
             type: Object,
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select.html
index e73d41c..f1ef86a 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.html
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.html
@@ -15,7 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
+
 <dom-module id="gr-select">
   <slot></slot>
   <script src="gr-select.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index 85e1a61..05267ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -19,7 +19,7 @@
 
   Polymer({
     is: 'gr-select',
-    _legacyUndefinedCheck: true,
+
     properties: {
       bindValue: {
         type: String,
@@ -28,6 +28,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
     listeners: {
       'change': '_valueChanged',
       'dom-change': '_updateValue',
@@ -55,9 +59,15 @@
       this.bindValue = this.nativeSelect.value;
     },
 
+    focus() {
+      this.nativeSelect.focus();
+    },
+
     ready() {
       // If not set via the property, set bind-value to the element value.
-      if (!this.bindValue) { this.bindValue = this.nativeSelect.value; }
+      if (this.bindValue == undefined && this.nativeSelect.options.length > 0) {
+        this.bindValue = this.nativeSelect.value;
+      }
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
index 1748ec06..b3abe5f 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-select</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-select.html">
 
@@ -38,6 +40,15 @@
   </template>
 </test-fixture>
 
+<test-fixture id="noOptions">
+  <template>
+    <gr-select>
+      <select>
+      </select>
+    </gr-select>
+  </template>
+</test-fixture>
+
 <script>
   suite('gr-select tests', () => {
     let element;
@@ -46,6 +57,10 @@
       element = fixture('basic');
     });
 
+    test('bindValue must be set to the first option value', () => {
+      assert.equal(element.bindValue, '1');
+    });
+
     test('value of 0 should still trigger value updates', () => {
       element.bindValue = 0;
       assert.equal(element.nativeSelect.value, 0);
@@ -88,4 +103,16 @@
       assert.isTrue(changeStub.called);
     });
   });
+
+  suite('gr-select no options tests', () => {
+    let element;
+
+    setup(() => {
+      element = fixture('noOptions');
+    });
+
+    test('bindValue must not be changed', () => {
+      assert.isUndefined(element.bindValue);
+    });
+  });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html
index fe6ed88..15e282f 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
 
@@ -23,25 +23,30 @@
   <template>
     <style include="shared-styles">
       .commandContainer {
-        margin-bottom: .75em;
+        margin-bottom: var(--spacing-m);
       }
       .commandContainer {
         background-color: var(--shell-command-background-color);
-        padding: .5em .5em .5em 2.5em;
+        /* Should be spacing-m larger than the :before width. */
+        padding: var(--spacing-m) var(--spacing-m) var(--spacing-m) calc(3*var(--spacing-m) + 0.5em);
         position: relative;
         width: 100%;
       }
       .commandContainer:before {
-        background: var(--shell-command-decoration-background-color);
-        bottom: 0;
-        box-sizing: border-box;
         content: '$';
-        display: block;
-        left: 0;
-        padding: .8em;
         position: absolute;
+        display: block;
+        box-sizing: border-box;
+        background: var(--shell-command-decoration-background-color);
         top: 0;
-        width: 2em;
+        bottom: 0;
+        left: 0;
+        /* Should be spacing-m smaller than the .commandContainer padding-left. */
+        width: calc(2*var(--spacing-m) + 0.5em);
+        /* Should vertically match the padding of .commandContainer. */
+        padding: var(--spacing-m);
+        /* Should roughly match the height of .commandContainer without padding. */
+        line-height: 26px;
       }
       .commandContainer gr-copy-clipboard {
         --text-container-style: {
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
index 901b8ce..2c546cc 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-shell-command',
-    _legacyUndefinedCheck: true,
 
     properties: {
       command: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
index a49f76f..3f2f8ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-shell-command</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-shell-command.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
index 6fc2f3f..7215b26 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <dom-module id="gr-storage">
   <script src="gr-storage.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 83cd06c..f6ade6e 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -30,7 +30,6 @@
 
   Polymer({
     is: 'gr-storage',
-    _legacyUndefinedCheck: true,
 
     properties: {
       _lastCleanup: Number,
@@ -76,14 +75,6 @@
       this._storage.removeItem(this._getEditableContentKey(key));
     },
 
-    getPreferences() {
-      return this._getObject('localPrefs');
-    },
-
-    savePreferences(localPrefs) {
-      this._setObject('localPrefs', localPrefs || null);
-    },
-
     _getDraftKey(location) {
       const range = location.range ?
         `${location.range.start_line}-${location.range.start_character}` +
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
index 6b89af2..0482584 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-storage</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-storage.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
index 10c9111..6803eb9 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
@@ -14,25 +14,36 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 
 <dom-module id="gr-textarea">
   <template>
     <style include="shared-styles">
       :host {
-        display: block;
+        display: flex;
         position: relative;
       }
       :host(.monospace) {
         font-family: var(--monospace-font-family);
+        font-size: var(--font-size-mono);
+        line-height: var(--line-height-mono);
+        font-weight: var(--font-weight-normal);
+      }
+      :host(.code) {
+        font-family: var(--monospace-font-family);
+        font-size: var(--font-size-code);
+        line-height: var(--line-height-code);
+        font-weight: var(--font-weight-normal);
       }
       #emojiSuggestions {
         font-family: var(--font-family);
@@ -54,6 +65,9 @@
       iron-autogrow-textarea {
         padding: 2px;
         position: relative;
+
+        /** This is needed for firefox */
+        --iron-autogrow-textarea_-_white-space: pre-wrap;
       }
       #textarea.noBorder {
         border: none;
@@ -92,6 +106,7 @@
         max-rows="[[maxRows]]"
         value="{{text}}"
         on-bind-value-changed="_onValueChanged"></iron-autogrow-textarea>
+    <gr-reporting id="reporting"></gr-reporting>
   </template>
   <script src="gr-textarea.js"></script>
 </dom-module>
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 7929fbe..4c4b038 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -20,41 +20,40 @@
   const MAX_ITEMS_DROPDOWN = 10;
 
   const ALL_SUGGESTIONS = [
+    {value: '😊', match: 'smile :)'},
+    {value: '👍', match: 'thumbs up'},
+    {value: '😄', match: 'laugh :D'},
+    {value: '🎉', match: 'party'},
+    {value: '😞', match: 'sad :('},
+    {value: '😂', match: 'tears :\')'},
+    {value: '🙏', match: 'pray'},
+    {value: '😐', match: 'neutral :|'},
+    {value: '😮', match: 'shock :O'},
+    {value: '👎', match: 'thumbs down'},
+    {value: '😎', match: 'cool |;)'},
+    {value: '😕', match: 'confused'},
+    {value: '👌', match: 'ok'},
+    {value: '🔥', match: 'fire'},
+    {value: '👊', match: 'fistbump'},
     {value: '💯', match: '100'},
     {value: '💔', match: 'broken heart'},
     {value: '🍺', match: 'beer'},
     {value: '✔', match: 'check'},
-    {value: '😎', match: 'cool'},
-    {value: '😕', match: 'confused'},
-    {value: '😭', match: 'crying'},
-    {value: '🔥', match: 'fire'},
-    {value: '👊', match: 'fistbump'},
+    {value: '😋', match: 'tongue'},
+    {value: '😭', match: 'crying :\'('},
     {value: '🐨', match: 'koala'},
-    {value: '😄', match: 'laugh'},
     {value: '🤓', match: 'glasses'},
     {value: '😆', match: 'grin'},
-    {value: '😐', match: 'neutral'},
-    {value: '👌', match: 'ok'},
-    {value: '🎉', match: 'party'},
     {value: '💩', match: 'poop'},
-    {value: '🙏', match: 'pray'},
-    {value: '😞', match: 'sad'},
-    {value: '😮', match: 'shock'},
-    {value: '😊', match: 'smile'},
     {value: '😢', match: 'tear'},
-    {value: '😂', match: 'tears'},
-    {value: '😋', match: 'tongue'},
-    {value: '👍', match: 'thumbs up'},
-    {value: '👎', match: 'thumbs down'},
     {value: '😒', match: 'unamused'},
-    {value: '😉', match: 'wink'},
+    {value: '😉', match: 'wink ;)'},
     {value: '🍷', match: 'wine'},
-    {value: '😜', match: 'winking tongue'},
+    {value: '😜', match: 'winking tongue ;)'},
   ];
 
   Polymer({
     is: 'gr-textarea',
-    _legacyUndefinedCheck: true,
 
     /**
      * @event bind-value-changed
@@ -75,15 +74,21 @@
         type: Boolean,
         value: false,
       },
+      /** Text input should be rendered in monspace font.  */
       monospace: {
         type: Boolean,
         value: false,
       },
+      /** Text input should be rendered in code font, which is smaller than the
+          standard monospace font. */
+      code: {
+        type: Boolean,
+        value: false,
+      },
       /** @type(?number) */
       _colonIndex: Number,
       _currentSearchString: {
         type: String,
-        value: '',
         observer: '_determineSuggestions',
       },
       _hideAutocomplete: {
@@ -101,6 +106,7 @@
     },
 
     behaviors: [
+      Gerrit.FireBehavior,
       Gerrit.KeyboardShortcutBehavior,
     ],
 
@@ -113,10 +119,12 @@
     },
 
     ready() {
-      this._resetEmojiDropdown();
       if (this.monospace) {
         this.classList.add('monospace');
       }
+      if (this.code) {
+        this.classList.add('code');
+      }
       if (this.hideBorder) {
         this.$.textarea.classList.add('noBorder');
       }
@@ -153,6 +161,7 @@
       e.stopPropagation();
       this.$.emojiSuggestions.cursorUp();
       this.$.textarea.textarea.focus();
+      this.disableEnterKeyForSelectingEmoji = false;
     },
 
     _handleDownKey(e) {
@@ -161,24 +170,34 @@
       e.stopPropagation();
       this.$.emojiSuggestions.cursorDown();
       this.$.textarea.textarea.focus();
+      this.disableEnterKeyForSelectingEmoji = false;
     },
 
     _handleEnterByKey(e) {
-      if (this._hideAutocomplete) { return; }
+      if (this._hideAutocomplete || this.disableEnterKeyForSelectingEmoji) {
+        return;
+      }
       e.preventDefault();
       e.stopPropagation();
-      this.text = this._getText(this.$.emojiSuggestions.getCurrentText());
-      this._resetEmojiDropdown();
+      this._setEmoji(this.$.emojiSuggestions.getCurrentText());
     },
 
     _handleEmojiSelect(e) {
-      this.text = this._getText(e.detail.selected.dataset.value);
+      this._setEmoji(e.detail.selected.dataset.value);
+    },
+
+    _setEmoji(text) {
+      const colonIndex = this._colonIndex;
+      this.text = this._getText(text);
+      this.$.textarea.selectionStart = colonIndex + 1;
+      this.$.textarea.selectionEnd = colonIndex + 1;
+      this.$.reporting.reportInteraction('select-emoji');
       this._resetEmojiDropdown();
     },
 
     _getText(value) {
       return this.text.substr(0, this._colonIndex || 0) +
-          value + this.text.substr(this.$.textarea.selectionStart) + ' ';
+          value + this.text.substr(this.$.textarea.selectionStart);
     },
     /**
      * Uses a hidden element with the same width and styling of the textarea and
@@ -218,41 +237,45 @@
       // If cursor is not in textarea (just opened with colon as last char),
       // Don't do anything.
       if (!e.currentTarget.focused) { return; }
-      const newChar = e.detail.value[this.$.textarea.selectionStart - 1];
 
-      // When a colon is detected, set a colon index, but don't do anything else
-      // yet.
-      if (newChar === ':') {
-        this._colonIndex = this.$.textarea.selectionStart - 1;
-      // If the colon index exists, continue to determine what needs to be done
-      // with the dropdown. It may be open or closed at this point.
-      } else if (this._colonIndex !== null) {
-        // The search string is a substring of the textarea's value from (1
-        // position after) the colon index to the cursor position.
-        this._currentSearchString = e.detail.value.substr(this._colonIndex + 1,
-            this.$.textarea.selectionStart);
-        // Under the following conditions, close and reset the dropdown:
-        // - The cursor is no longer at the end of the current search string
-        // - The search string is an space or new line
-        // - The colon has been removed
-        // - There are no suggestions that match the search string
-        if (this.$.textarea.selectionStart !==
-            this._currentSearchString.length + this._colonIndex + 1 ||
-            this._currentSearchString === ' ' ||
-            this._currentSearchString === '\n' ||
-            !(e.detail.value[this._colonIndex] === ':') ||
-            !this._suggestions.length) {
-          this._resetEmojiDropdown();
-        // Otherwise open the dropdown and set the position to be just below the
-        // cursor.
-        } else if (this.$.emojiSuggestions.isHidden) {
-          this._updateCaratPosition();
+      const charAtCursor = e.detail && e.detail.value ?
+        e.detail.value[this.$.textarea.selectionStart - 1] : '';
+      if (charAtCursor !== ':' && this._colonIndex == null) { return; }
+
+      // When a colon is detected, set a colon index. We are interested only on
+      // colons after space or in beginning of textarea
+      if (charAtCursor === ':') {
+        if (this.$.textarea.selectionStart < 2 ||
+            e.detail.value[this.$.textarea.selectionStart - 2] === ' ') {
+          this._colonIndex = this.$.textarea.selectionStart - 1;
         }
-        this.$.textarea.textarea.focus();
       }
+
+      this._currentSearchString = e.detail.value.substr(this._colonIndex + 1,
+          this.$.textarea.selectionStart - this._colonIndex - 1);
+      // Under the following conditions, close and reset the dropdown:
+      // - The cursor is no longer at the end of the current search string
+      // - The search string is an space or new line
+      // - The colon has been removed
+      // - There are no suggestions that match the search string
+      if (this.$.textarea.selectionStart !==
+          this._currentSearchString.length + this._colonIndex + 1 ||
+          this._currentSearchString === ' ' ||
+          this._currentSearchString === '\n' ||
+          !(e.detail.value[this._colonIndex] === ':') ||
+          !this._suggestions.length) {
+        this._resetEmojiDropdown();
+      // Otherwise open the dropdown and set the position to be just below the
+      // cursor.
+      } else if (this.$.emojiSuggestions.isHidden) {
+        this._updateCaratPosition();
+      }
+      this.$.textarea.textarea.focus();
     },
+
     _openEmojiDropdown() {
       this.$.emojiSuggestions.open();
+      this.$.reporting.reportInteraction('open-emoji-dropdown');
     },
 
     _formatSuggestions(matchedSuggestions) {
@@ -268,11 +291,14 @@
     _determineSuggestions(emojiText) {
       if (!emojiText.length) {
         this._formatSuggestions(ALL_SUGGESTIONS);
+        this.disableEnterKeyForSelectingEmoji = true;
+      } else {
+        const matches = ALL_SUGGESTIONS.filter(suggestion => {
+          return suggestion.match.includes(emojiText);
+        }).slice(0, MAX_ITEMS_DROPDOWN);
+        this._formatSuggestions(matches);
+        this.disableEnterKeyForSelectingEmoji = false;
       }
-      const matches = ALL_SUGGESTIONS.filter(suggestion => {
-        return suggestion.match.includes(emojiText);
-      }).splice(0, MAX_ITEMS_DROPDOWN);
-      this._formatSuggestions(matches);
     },
 
     _resetEmojiDropdown() {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index f010fb3..b884ecd 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-textarea</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-textarea.html">
 
@@ -31,6 +33,18 @@
   </template>
 </test-fixture>
 
+<test-fixture id="monospace">
+  <template>
+    <gr-textarea monospace="true"></gr-textarea>
+  </template>
+</test-fixture>
+
+<test-fixture id="hideBorder">
+  <template>
+    <gr-textarea hide-border="true"></gr-textarea>
+  </template>
+</test-fixture>
+
 <script>
   suite('gr-textarea tests', () => {
     let element;
@@ -39,6 +53,7 @@
     setup(() => {
       sandbox = sinon.sandbox.create();
       element = fixture('basic');
+      sandbox.stub(element.$.reporting, 'reportInteraction');
     });
 
     teardown(() => {
@@ -47,16 +62,10 @@
 
     test('monospace is set properly', () => {
       assert.isFalse(element.classList.contains('monospace'));
-      element.monospace = true;
-      element.ready();
-      assert.isTrue(element.classList.contains('monospace'));
     });
 
     test('hideBorder is set properly', () => {
       assert.isFalse(element.$.textarea.classList.contains('noBorder'));
-      element.hideBorder = true;
-      element.ready();
-      assert.isTrue(element.$.textarea.classList.contains('noBorder'));
     });
 
     test('emoji selector is not open with the textarea lacks focus', () => {
@@ -82,6 +91,49 @@
           element.$.textarea.selectionStart = 1;
           element.$.textarea.selectionEnd = 1;
           element.text = ':';
+          flushAsynchronousOperations();
+          assert.isFalse(element.$.emojiSuggestions.isHidden);
+          assert.equal(element._colonIndex, 0);
+          assert.isFalse(element._hideAutocomplete);
+          assert.equal(element._currentSearchString, '');
+        });
+
+    test('emoji selector opens when a colon is typed after space',
+        () => {
+          MockInteractions.focus(element.$.textarea);
+          // Needed for Safari tests. selectionStart is not updated when text is
+          // updated.
+          element.$.textarea.selectionStart = 2;
+          element.$.textarea.selectionEnd = 2;
+          element.text = ' :';
+          flushAsynchronousOperations();
+          assert.isFalse(element.$.emojiSuggestions.isHidden);
+          assert.equal(element._colonIndex, 1);
+          assert.isFalse(element._hideAutocomplete);
+          assert.equal(element._currentSearchString, '');
+        });
+
+    test('emoji selector doesn\`t open when a colon is typed after character',
+        () => {
+          MockInteractions.focus(element.$.textarea);
+          // Needed for Safari tests. selectionStart is not updated when text is
+          // updated.
+          element.$.textarea.selectionStart = 5;
+          element.$.textarea.selectionEnd = 5;
+          element.text = 'test:';
+          flushAsynchronousOperations();
+          assert.isTrue(element.$.emojiSuggestions.isHidden);
+          assert.isTrue(element._hideAutocomplete);
+        });
+
+    test('emoji selector opens when a colon is typed and some substring',
+        () => {
+          MockInteractions.focus(element.$.textarea);
+          // Needed for Safari tests. selectionStart is not updated when text is
+          // updated.
+          element.$.textarea.selectionStart = 1;
+          element.$.textarea.selectionEnd = 1;
+          element.text = ':';
           element.$.textarea.selectionStart = 2;
           element.$.textarea.selectionEnd = 2;
           element.text = ':t';
@@ -92,6 +144,30 @@
           assert.equal(element._currentSearchString, 't');
         });
 
+    test('emoji selector opens when a colon is typed in middle of text',
+        () => {
+          MockInteractions.focus(element.$.textarea);
+          // Needed for Safari tests. selectionStart is not updated when text is
+          // updated.
+          element.$.textarea.selectionStart = 1;
+          element.$.textarea.selectionEnd = 1;
+          // Since selectionStart is on Chrome set always on end of text, we
+          // stub it to 1
+          const text = ': hello';
+          sandbox.stub(element.$, 'textarea', {
+            selectionStart: 1,
+            value: text,
+            textarea: {
+              focus: () => {},
+            },
+          });
+          element.text = text;
+          flushAsynchronousOperations();
+          assert.isFalse(element.$.emojiSuggestions.isHidden);
+          assert.equal(element._colonIndex, 0);
+          assert.isFalse(element._hideAutocomplete);
+          assert.equal(element._currentSearchString, '');
+        });
     test('emoji selector closes when text changes before the colon', () => {
       const resetStub = sandbox.stub(element, '_resetEmojiDropdown');
       MockInteractions.focus(element.$.textarea);
@@ -131,8 +207,10 @@
       element._determineSuggestions(emojiText);
       assert.isTrue(formatSpy.called);
       assert.isTrue(formatSpy.lastCall.calledWithExactly(
-          [{dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'},
-            {dataValue: '😂', value: '😂', match: 'tears', text: '😂 tears'}]));
+          [{dataValue: '😂', value: '😂', match: 'tears :\')',
+            text: '😂 tears :\')'},
+          {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'},
+          ]));
     });
 
     test('_formatSuggestions', () => {
@@ -153,7 +231,7 @@
       const selectedItem = {dataset: {value: '😂'}};
       const event = {detail: {selected: selectedItem}};
       element._handleEmojiSelect(event);
-      assert.equal(element.text, 'test test 😂 ');
+      assert.equal(element.text, 'test test 😂');
     });
 
     test('_updateCaratPosition', () => {
@@ -228,9 +306,70 @@
         MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
         assert.isTrue(enterSpy.called);
         flushAsynchronousOperations();
-        // A space is automatically added at the end.
-        assert.equal(element.text, '💯 ');
+        assert.equal(element.text, '💯');
       });
+
+      test('enter key - ignored on just colon without more information', () => {
+        const enterSpy = sandbox.spy(element.$.emojiSuggestions,
+            'getCursorTarget');
+        MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+        assert.isFalse(enterSpy.called);
+        MockInteractions.focus(element.$.textarea);
+        element.$.textarea.selectionStart = 1;
+        element.$.textarea.selectionEnd = 1;
+        element.text = ':';
+        flushAsynchronousOperations();
+        MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+        assert.isFalse(enterSpy.called);
+      });
+    });
+  });
+
+  suite('gr-textarea monospace', () => {
+    // gr-textarea set monospace class in the ready() method.
+    // In Polymer2, ready() is called from the fixture(...) method,
+    // If ready() is called again later, some nested elements doesn't
+    // handle it correctly. A separate test-fixture is used to set
+    // properties before ready() is called.
+
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('monospace');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('monospace is set properly', () => {
+      assert.isTrue(element.classList.contains('monospace'));
+    });
+  });
+
+  suite('gr-textarea hideBorder', () => {
+    // gr-textarea set noBorder class in the ready() method.
+    // In Polymer2, ready() is called from the fixture(...) method,
+    // If ready() is called again later, some nested elements doesn't
+    // handle it correctly. A separate test-fixture is used to set
+    // properties before ready() is called.
+
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('hideBorder');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('hideBorder is set properly', () => {
+      assert.isTrue(element.$.textarea.classList.contains('noBorder'));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
index 65f1fda..b4fefe1 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 
 <dom-module id="gr-tooltip-content">
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 b46cafb..c5de8f4 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
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-tooltip-content',
-    _legacyUndefinedCheck: true,
 
     properties: {
       title: {
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
index 438d436..f9350c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-storage</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-tooltip-content.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
index 9947d61..75d9c4b 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-tooltip">
@@ -34,7 +34,7 @@
         max-width: var(--tooltip-max-width);
       }
       :host .tooltip {
-        padding: .5em .85em;
+        padding: var(--spacing-m) var(--spacing-l);
       }
       :host .arrowPositionBelow,
       :host([position-below]) .arrowPositionAbove  {
@@ -54,11 +54,11 @@
       }
       .arrowPositionAbove {
         border-top: var(--gr-tooltip-arrow-size) solid var(--tooltip-background-color);
-        bottom: -var(--gr-tooltip-arrow-size);
+        bottom: calc(-1 * var(--gr-tooltip-arrow-size));
       }
       .arrowPositionBelow {
         border-bottom: var(--gr-tooltip-arrow-size) solid var(--tooltip-background-color);
-        top: -var(--gr-tooltip-arrow-size);
+        top: calc(-1 * var(--gr-tooltip-arrow-size));
       }
     </style>
     <div class="tooltip">
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
index 3e16beb..fb87b558 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -19,7 +19,6 @@
 
   Polymer({
     is: 'gr-tooltip',
-    _legacyUndefinedCheck: true,
 
     properties: {
       text: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
index 3a47288..f59f6e1 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-storage</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-tooltip.html">
 
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info.html b/polygerrit-ui/app/elements/shared/revision-info/revision-info.html
index 0846728..48e488a 100644
--- a/polygerrit-ui/app/elements/shared/revision-info/revision-info.html
+++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info.html
@@ -36,6 +36,9 @@
      * @return {Number}
      */
     RevisionInfo.prototype.getMaxParents = function() {
+      if (!this._change || !this._change.revisions) {
+        return 0;
+      }
       return Object.values(this._change.revisions)
           .reduce((acc, rev) => Math.max(rev.commit.parents.length, acc), 0);
     };
@@ -48,6 +51,9 @@
      */
     RevisionInfo.prototype.getParentCountMap = function() {
       const result = {};
+      if (!this._change || !this._change.revisions) {
+        return {};
+      }
       Object.values(this._change.revisions)
           .forEach(rev => { result[rev._number] = rev.commit.parents.length; });
       return result;
@@ -75,9 +81,7 @@
       return rev.commit.parents[parentIndex].commit;
     };
 
-    if (!window.Gerrit) {
-      window.Gerrit = {};
-    }
+    window.Gerrit = window.Gerrit || {};
     window.Gerrit.RevisionInfo = RevisionInfo;
   })();
 </script>
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
index 433872d..7e5810b 100644
--- a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
+++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>revision-info</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="revision-info.html">
 
diff --git a/polygerrit-ui/app/elements/test/plugin.html b/polygerrit-ui/app/elements/test/plugin.html
index bd29b90..ecd9007 100644
--- a/polygerrit-ui/app/elements/test/plugin.html
+++ b/polygerrit-ui/app/elements/test/plugin.html
@@ -1,18 +1,44 @@
 <dom-module id="my-plugin">
   <script>
-    Gerrit.install(plugin =>
-      plugin.registerStyleModule('app-theme', 'myplugin-app-theme')
-    );
+    Gerrit.install(plugin => {
+      plugin.registerStyleModule('app-theme', 'myplugin-app-theme');
+      plugin.registerStyleModule('app-theme-light', 'myplugin-app-theme-light');
+      plugin.registerStyleModule('app-theme-dark', 'myplugin-app-theme-dark');
+    });
   </script>
 </dom-module>
 
 <dom-module id="myplugin-app-theme">
-  <style>
-    html {
-      --primary-text-color: #F00BAA;
-      --header-background-color: #F01BAA;
-      --header-title-content: "MyGerrit";
-      --footer-background-color: #F02BAA;
-    }
-  </style>
+  <template>
+    <style>
+      html {
+        --primary-text-color: #F00BAA;
+      }
+    </style>
+  </template>
+</dom-module>
+
+<dom-module id="myplugin-app-theme-light">
+  <template>
+    <style>
+      html {
+        --header-background-color: #F01BAA;
+        --header-title-content: "MyGerrit";
+        --footer-background-color: #F02BAA;
+      }
+    </style>
+  </template>
+</dom-module>
+
+<dom-module id="myplugin-app-theme-dark">
+  <template>
+    <style>
+      html {
+        --primary-text-color: red;
+        --header-background-color: black;
+        --header-title-content: "MyGerrit Dark";
+        --footer-background-color: yellow;
+      }
+    </style>
+  </template>
 </dom-module>
diff --git a/polygerrit-ui/app/embed/embed.html b/polygerrit-ui/app/embed/embed.html
index 1b2f20f..64e0137 100644
--- a/polygerrit-ui/app/embed/embed.html
+++ b/polygerrit-ui/app/embed/embed.html
@@ -15,12 +15,9 @@
 limitations under the License.
 -->
 <script>
-  // Needed for JSCompiler to understand it's global.
-  // eslint-disable-next-line no-unused-vars, prefer-const
-  let Gerrit = window.Gerrit || {};
-  window.Gerrit = Gerrit;
+  window.Gerrit = window.Gerrit || {};
 </script>
-<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../elements/change/gr-change-view/gr-change-view.html">
 <link rel="import" href="../elements/core/gr-search-bar/gr-search-bar.html">
 <link rel="import" href="../elements/diff/gr-diff-view/gr-diff-view.html">
diff --git a/polygerrit-ui/app/embed/embed_test.html b/polygerrit-ui/app/embed/embed_test.html
index 7ca75c9..1e3f5d7 100644
--- a/polygerrit-ui/app/embed/embed_test.html
+++ b/polygerrit-ui/app/embed/embed_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>embed_test</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../polygerrit_ui/elements/embed.html"/>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="embed.html"/>
 
 <script>void(0);</script>
 
diff --git a/polygerrit-ui/app/embed/gr-diff.html b/polygerrit-ui/app/embed/gr-diff.html
index 6aa9370..f5f74bd 100644
--- a/polygerrit-ui/app/embed/gr-diff.html
+++ b/polygerrit-ui/app/embed/gr-diff.html
@@ -15,11 +15,7 @@
 limitations under the License.
 -->
 <script>
-  // Needed for JSCompiler to understand it's global.
-  // eslint-disable-next-line no-unused-vars, prefer-const
-  let Gerrit = window.Gerrit || {};
-  window.Gerrit = Gerrit;
+  window.Gerrit = window.Gerrit || {};
 </script>
-<link rel="import" href="../styles/themes/app-theme.html">
 <link rel="import" href="../elements/diff/gr-diff/gr-diff.html">
 <link rel="import" href="../elements/diff/gr-diff-cursor/gr-diff-cursor.html">
diff --git a/polygerrit-ui/app/embed/test.html b/polygerrit-ui/app/embed/test.html
index eed2fef..955eaee 100644
--- a/polygerrit-ui/app/embed/test.html
+++ b/polygerrit-ui/app/embed/test.html
@@ -19,8 +19,8 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>Embed Test Runner</title>
 <meta charset="utf-8">
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <script>
-  WCT.loadSuites(['embed_test.html']);
+  WCT.loadSuites(['../embed/embed_test.html']);
 </script>
diff --git a/polygerrit-ui/app/embed_test.sh b/polygerrit-ui/app/embed_test.sh
index d482796..0d8f58f 100755
--- a/polygerrit-ui/app/embed_test.sh
+++ b/polygerrit-ui/app/embed_test.sh
@@ -4,20 +4,19 @@
 
 t=$(mktemp -d || mktemp -d -t wct-XXXXXXXXXX)
 components=$TEST_SRCDIR/gerrit/polygerrit-ui/app/test_components.zip
-code=$TEST_SRCDIR/gerrit/polygerrit-ui/app/polygerrit_embed_ui.zip
-index=$TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/test.html
-tests=$TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/*_test.html
+code=$TEST_SRCDIR/gerrit/polygerrit-ui/app/pg_code.zip
 
+echo $t
 unzip -qd $t $components
 unzip -qd $t $code
+# Purge test/ directory contents coming from pg_code.zip.
+rm -rf $t/test
 mkdir -p $t/test
-cp $index $t/test/
-cp $tests $t/test/
+cp $TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/test.html $t/test/
 
 if [ "${WCT_HEADLESS_MODE:-0}" != "0" ]; then
     CHROME_OPTIONS=[\'start-maximized\',\'headless\',\'disable-gpu\',\'no-sandbox\']
-    # TODO(paladox): Fix Firefox support for headless mode
-    FIREFOX_OPTIONS=[\'\']
+    FIREFOX_OPTIONS=[\'-headless\']
 else
     CHROME_OPTIONS=[\'start-maximized\']
     FIREFOX_OPTIONS=[\'\']
@@ -62,9 +61,9 @@
     };
 EOF
 
-export PATH="$(dirname $WCT):$(dirname $NPM):$PATH"
+export PATH="$(dirname $NPM):$PATH"
 
 cd $t
 test -n "${WCT}"
 
-$(basename ${WCT}) ${WCT_ARGS}
+${WCT} ${WCT_ARGS}
diff --git a/polygerrit-ui/app/gr-diff/gr-diff-root.html b/polygerrit-ui/app/gr-diff/gr-diff-root.html
index 132654c..b3f0d34 100644
--- a/polygerrit-ui/app/gr-diff/gr-diff-root.html
+++ b/polygerrit-ui/app/gr-diff/gr-diff-root.html
@@ -1,7 +1,4 @@
 <script>
-  // Needed for JSCompiler to understand it's global.
-  // eslint-disable-next-line no-unused-vars, prefer-const
-  let Gerrit = window.Gerrit || {};
-  window.Gerrit = Gerrit;
+  window.Gerrit = window.Gerrit || {};
 </script>
 <link rel="import" href="../elements/diff/gr-diff/gr-diff.html">
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 98387a0..7ef0ee3 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -14,17 +14,14 @@
         # See: https://github.com/google/closure-compiler/issues/2042
         compilation_level = "WHITESPACE_ONLY",
         defs = [
-            "--polymer_version=1",
+            "--polymer_version=2",
             "--jscomp_off=duplicate",
-            "--force_inject_library=es6_runtime",
         ],
-        language = "ECMASCRIPT5",
+        language = "ECMASCRIPT_2017",
         deps = [name + "_closure_lib"],
         dependency_mode = "PRUNE_LEGACY",
     )
 
-    # TODO(davido): Remove JSC_REFERENCE_BEFORE_DECLARE when this is fixed upstream:
-    # https://github.com/Polymer/polymer-resin/issues/7
     closure_js_library(
         name = name + "_closure_lib",
         srcs = [appName + ".js"],
@@ -33,9 +30,7 @@
         # and remove this supression
         suppress = [
             "JSC_JSDOC_MISSING_TYPE_WARNING",
-            "JSC_REFERENCE_BEFORE_DECLARE",
             "JSC_UNNECESSARY_ESCAPE",
-            "JSC_UNUSED_LOCAL_ASSIGNMENT",
         ],
         deps = [
             "//lib/polymer_externs:polymer_closure",
diff --git a/polygerrit-ui/app/samples/bind-parameters.html b/polygerrit-ui/app/samples/bind-parameters.html
index a7eb39a..a28c462 100644
--- a/polygerrit-ui/app/samples/bind-parameters.html
+++ b/polygerrit-ui/app/samples/bind-parameters.html
@@ -15,7 +15,7 @@
   <script>
     Polymer({
       is: 'my-bind-sample',
-      _legacyUndefinedCheck: true,
+
       properties: {
         computedExample: {
           type: String,
diff --git a/polygerrit-ui/app/samples/coverage-plugin.html b/polygerrit-ui/app/samples/coverage-plugin.html
index 0d38c63..d1d96a8 100644
--- a/polygerrit-ui/app/samples/coverage-plugin.html
+++ b/polygerrit-ui/app/samples/coverage-plugin.html
@@ -32,6 +32,11 @@
       const coverageData = {};
       let displayCoverage = false;
       const annotationApi = plugin.annotationApi();
+      const styleApi = plugin.styles();
+
+      const coverageStyle = styleApi.css('background-color: #EF9B9B !important');
+      const emptyStyle = styleApi.css('');
+
       annotationApi.addLayer(context => {
         if (Object.keys(coverageData).length === 0) {
           // Coverage data is not ready yet.
@@ -41,16 +46,16 @@
         const line = context.line;
         // Highlight lines missing coverage with this background color if
         // coverage should be displayed, else do nothing.
-        const cssClass = displayCoverage
-          ? Gerrit.css('background-color: #EF9B9B')
-          : Gerrit.css('');
+        const annotationStyle = displayCoverage
+          ? coverageStyle
+          : emptyStyle;
         if (coverageData[path] &&
               coverageData[path].changeNum === context.changeNum &&
               coverageData[path].patchNum === context.patchNum) {
           const linesMissingCoverage = coverageData[path].linesMissingCoverage;
           if (linesMissingCoverage.includes(line.afterNumber)) {
-            context.annotateRange(0, line.text.length, cssClass, 'right');
-            context.annotateLineNumber(cssClass, 'right');
+            context.annotateRange(0, line.text.length, annotationStyle, 'right');
+            context.annotateLineNumber(annotationStyle, 'right');
           }
         }
       }).enableToggleCheckbox('Display Coverage', checkbox => {
diff --git a/polygerrit-ui/app/samples/repo-command.html b/polygerrit-ui/app/samples/repo-command.html
index 37aca04..526d350 100644
--- a/polygerrit-ui/app/samples/repo-command.html
+++ b/polygerrit-ui/app/samples/repo-command.html
@@ -29,7 +29,7 @@
   <script>
     Polymer({
       is: 'repo-command-low',
-      _legacyUndefinedCheck: true,
+
       attached() {
         console.log(this.repoName);
         console.log(this.config);
diff --git a/polygerrit-ui/app/samples/some-screen.html b/polygerrit-ui/app/samples/some-screen.html
index 527ebce..da025a2 100644
--- a/polygerrit-ui/app/samples/some-screen.html
+++ b/polygerrit-ui/app/samples/some-screen.html
@@ -38,7 +38,7 @@
   <script>
     Polymer({
       is: 'some-screen-main',
-      _legacyUndefinedCheck: true,
+
       properties: {
         rootUrl: String,
       },
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
new file mode 100644
index 0000000..6e503c7
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.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.
+ */
+(function(window) {
+  'use strict';
+
+  if (window.GrDisplayNameUtils) {
+    return;
+  }
+
+  const ANONYMOUS_NAME = 'Anonymous';
+
+  class GrDisplayNameUtils {
+    /**
+     * enableEmail when true enables to fallback to using email if
+     * the account name is not avilable.
+     */
+    static getUserName(config, account, enableEmail) {
+      if (account && account.name) {
+        return account.name;
+      } else if (account && account.username) {
+        return account.username;
+      } else if (enableEmail && 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 getAccountDisplayName(config, account, enableEmail) {
+      const reviewerName = this._accountOrAnon(config, account, enableEmail);
+      const reviewerEmail = this._accountEmail(account.email);
+      const reviewerStatus = account.status ? '(' + account.status + ')' : '';
+      return [reviewerName, reviewerEmail, reviewerStatus]
+          .filter(p => p.length > 0).join(' ');
+    }
+
+    static _accountOrAnon(config, reviewer, enableEmail) {
+      return this.getUserName(config, reviewer, !!enableEmail);
+    }
+
+    static _accountEmail(email) {
+      if (typeof email !== 'undefined') {
+        return '<' + email + '>';
+      }
+      return '';
+    }
+
+    static getGroupDisplayName(group) {
+      return group.name + ' (group)';
+    }
+  }
+
+  window.GrDisplayNameUtils = GrDisplayNameUtils;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
new file mode 100644
index 0000000..25ca4c5
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-display-name-utils</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../test/common-test-setup.html"/>
+<script src="gr-display-name-utils.js"></script>
+
+<script>
+  suite('gr-display-name-utils tests', () => {
+    // eslint-disable-next-line no-unused-vars
+    const config = {
+      user: {
+        anonymous_coward_name: 'Anonymous Coward',
+      },
+    };
+
+
+    test('getUserName name only', () => {
+      const account = {
+        name: 'test-name',
+      };
+      assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+          'test-name');
+    });
+
+    test('getUserName username only', () => {
+      const account = {
+        username: 'test-user',
+      };
+      assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+          'test-user');
+    });
+
+    test('getUserName email only', () => {
+      const account = {
+        email: 'test-user@test-url.com',
+      };
+      assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+          'test-user@test-url.com');
+    });
+
+    test('getUserName returns not Anonymous Coward as the anon name', () => {
+      assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
+          'Anonymous');
+    });
+
+    test('getUserName for the config returning the anon name', () => {
+      const config = {
+        user: {
+          anonymous_coward_name: 'Test Anon',
+        },
+      };
+      assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
+          'Test Anon');
+    });
+
+    test('getAccountDisplayName - account with name only', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config,
+              {name: 'Some user name'}),
+          'Some user name');
+    });
+
+    test('getAccountDisplayName - account with email only', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config,
+              {email: 'my@example.com'}),
+          'Anonymous <my@example.com>');
+    });
+
+    test('getAccountDisplayName - account with email only - allowEmail', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config,
+              {email: 'my@example.com'}, true),
+          'my@example.com <my@example.com>');
+    });
+
+    test('getAccountDisplayName - account with name and status', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config, {
+            name: 'Some name',
+            status: 'OOO',
+          }),
+          'Some name (OOO)');
+    });
+
+    test('getAccountDisplayName - account with name and email', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config, {
+            name: 'Some name',
+            email: 'my@example.com',
+          }),
+          'Some name <my@example.com>');
+    });
+
+    test('getAccountDisplayName - account with name, email and status', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config, {
+            name: 'Some name',
+            email: 'my@example.com',
+            status: 'OOO',
+          }),
+          'Some name <my@example.com> (OOO)');
+    });
+
+    test('getGroupDisplayName', () => {
+      assert.equal(
+          GrDisplayNameUtils.getGroupDisplayName({name: 'Some user name'}),
+          'Some user name (group)');
+    });
+
+    test('_accountEmail', () => {
+      assert.equal(
+          GrDisplayNameUtils._accountEmail('email@gerritreview.com'),
+          '<email@gerritreview.com>');
+      assert.equal(GrDisplayNameUtils._accountEmail(undefined), '');
+    });
+  });
+</script>
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
new file mode 100644
index 0000000..67001d2
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
@@ -0,0 +1,46 @@
+/**
+ * @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.
+ */
+(function(window) {
+  'use strict';
+
+  if (window.GrEmailSuggestionsProvider) {
+    return;
+  }
+
+  class GrEmailSuggestionsProvider {
+    constructor(restAPI) {
+      this._restAPI = restAPI;
+    }
+
+    getSuggestions(input) {
+      return this._restAPI.getSuggestedAccounts(`${input}`)
+          .then(accounts => {
+            if (!accounts) { return []; }
+            return accounts;
+          });
+    }
+
+    makeSuggestionItem(account) {
+      return {
+        name: GrDisplayNameUtils.getAccountDisplayName(null, account, true),
+        value: {account, count: 1},
+      };
+    }
+  }
+
+  window.GrEmailSuggestionsProvider = GrEmailSuggestionsProvider;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
new file mode 100644
index 0000000..0266ab9
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-email-suggestions-provider</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
+<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="gr-email-suggestions-provider.js"></script>
+
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+</test-fixture>
+
+<script>
+  suite('GrEmailSuggestionsProvider tests', () => {
+    let sandbox;
+    let restAPI;
+    let provider;
+    const account1 = {
+      name: 'Some name',
+      email: 'some@example.com',
+    };
+    const account2 = {
+      email: 'other@example.com',
+      _account_id: 3,
+    };
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+
+      stub('gr-rest-api-interface', {
+        getConfig() { return Promise.resolve({}); },
+      });
+      restAPI = fixture('basic');
+      provider = new GrEmailSuggestionsProvider(restAPI);
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('getSuggestions', done => {
+      const getSuggestedAccountsStub =
+          sandbox.stub(restAPI, 'getSuggestedAccounts')
+              .returns(Promise.resolve([account1, account2]));
+
+      provider.getSuggestions('Some input').then(res => {
+        assert.deepEqual(res, [account1, account2]);
+        assert.isTrue(getSuggestedAccountsStub.calledOnce);
+        assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+        done();
+      });
+    });
+
+    test('makeSuggestionItem', () => {
+      assert.deepEqual(provider.makeSuggestionItem(account1), {
+        name: 'Some name <some@example.com>',
+        value: {
+          account: account1,
+          count: 1,
+        },
+      });
+
+      assert.deepEqual(provider.makeSuggestionItem(account2), {
+        name: 'other@example.com <other@example.com>',
+        value: {
+          account: account2,
+          count: 1,
+        },
+      });
+    });
+  });
+</script>
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
new file mode 100644
index 0000000..a95670b
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js
@@ -0,0 +1,47 @@
+/**
+ * @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.
+ */
+(function(window) {
+  'use strict';
+
+  if (window.GrGroupSuggestionsProvider) {
+    return;
+  }
+
+  class GrGroupSuggestionsProvider {
+    constructor(restAPI) {
+      this._restAPI = restAPI;
+    }
+
+    getSuggestions(input) {
+      return this._restAPI.getSuggestedGroups(`${input}`)
+          .then(groups => {
+            if (!groups) { return []; }
+            const keys = Object.keys(groups);
+            return keys.map(key => {
+              return Object.assign({}, groups[key], {name: key});
+            });
+          });
+    }
+
+    makeSuggestionItem(suggestion) {
+      return {name: suggestion.name,
+        value: {group: {name: suggestion.name, id: suggestion.id}}};
+    }
+  }
+
+  window.GrGroupSuggestionsProvider = GrGroupSuggestionsProvider;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
new file mode 100644
index 0000000..b60aaa9
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-group-suggestions-provider</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
+<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="gr-group-suggestions-provider.js"></script>
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+</test-fixture>
+
+<script>
+  suite('GrGroupSuggestionsProvider tests', () => {
+    let sandbox;
+    let restAPI;
+    let provider;
+    const group1 = {
+      name: 'Some name',
+      id: 1,
+    };
+    const group2 = {
+      name: 'Other name',
+      id: 3,
+      url: 'abcd',
+    };
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+
+      stub('gr-rest-api-interface', {
+        getConfig() { return Promise.resolve({}); },
+      });
+      restAPI = fixture('basic');
+      provider = new GrGroupSuggestionsProvider(restAPI);
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('getSuggestions', done => {
+      const getSuggestedAccountsStub =
+          sandbox.stub(restAPI, 'getSuggestedGroups')
+              .returns(Promise.resolve({
+                'Some name': {id: 1},
+                'Other name': {id: 3, url: 'abcd'},
+              }));
+
+      provider.getSuggestions('Some input').then(res => {
+        assert.deepEqual(res, [group1, group2]);
+        assert.isTrue(getSuggestedAccountsStub.calledOnce);
+        assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+        done();
+      });
+    });
+
+    test('makeSuggestionItem', () => {
+      assert.deepEqual(provider.makeSuggestionItem(group1), {
+        name: 'Some name',
+        value: {
+          group: {
+            name: 'Some name',
+            id: 1,
+          },
+        },
+      });
+
+      assert.deepEqual(provider.makeSuggestionItem(group2), {
+        name: 'Other name',
+        value: {
+          group: {
+            name: 'Other name',
+            id: 3,
+          },
+        },
+      });
+    });
+  });
+</script>
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
new file mode 100644
index 0000000..ffcdba9
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
@@ -0,0 +1,114 @@
+/**
+ * @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.
+ */
+(function(window) {
+  'use strict';
+
+  if (window.GrReviewerSuggestionsProvider) {
+    return;
+  }
+
+  /**
+   * @enum {string}
+   */
+  Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES = {
+    REVIEWER: 'reviewers',
+    CC: 'ccs',
+    ANY: 'any',
+  };
+
+  class GrReviewerSuggestionsProvider {
+    static create(restApi, changeNumber, usersType) {
+      switch (usersType) {
+        case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER:
+          return new GrReviewerSuggestionsProvider(restApi, changeNumber,
+              input => restApi.getChangeSuggestedReviewers(changeNumber,
+                  input));
+        case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC:
+          return new GrReviewerSuggestionsProvider(restApi, changeNumber,
+              input => restApi.getChangeSuggestedCCs(changeNumber, input));
+        case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY:
+          return new GrReviewerSuggestionsProvider(restApi, changeNumber,
+              input => restApi.getSuggestedAccounts(
+                  `cansee:${changeNumber} ${input}`));
+        default:
+          throw new Error(`Unknown users type: ${usersType}`);
+      }
+    }
+
+    constructor(restAPI, changeNumber, apiCall) {
+      this._changeNumber = changeNumber;
+      this._apiCall = apiCall;
+      this._restAPI = restAPI;
+    }
+
+    init() {
+      if (this._initPromise) {
+        return this._initPromise;
+      }
+      const getConfigPromise = this._restAPI.getConfig().then(cfg => {
+        this._config = cfg;
+      });
+      const getLoggedInPromise = this._restAPI.getLoggedIn().then(loggedIn => {
+        this._loggedIn = loggedIn;
+      });
+      this._initPromise = Promise.all([getConfigPromise, getLoggedInPromise])
+          .then(() => {
+            this._initialized = true;
+          });
+      return this._initPromise;
+    }
+
+    getSuggestions(input) {
+      if (!this._initialized || !this._loggedIn) {
+        return Promise.resolve([]);
+      }
+
+      return this._apiCall(input)
+          .then(reviewers => (reviewers || []));
+    }
+
+    makeSuggestionItem(suggestion) {
+      if (suggestion.account) {
+        // Reviewer is an account suggestion from getChangeSuggestedReviewers.
+        return {
+          name: GrDisplayNameUtils.getAccountDisplayName(this._config,
+              suggestion.account, false),
+          value: suggestion,
+        };
+      }
+
+      if (suggestion.group) {
+        // Reviewer is a group suggestion from getChangeSuggestedReviewers.
+        return {
+          name: GrDisplayNameUtils.getGroupDisplayName(suggestion.group),
+          value: suggestion,
+        };
+      }
+
+      if (suggestion._account_id) {
+        // Reviewer is an account suggestion from getSuggestedAccounts.
+        return {
+          name: GrDisplayNameUtils.getAccountDisplayName(this._config,
+              suggestion, false),
+          value: {account: suggestion, count: 1},
+        };
+      }
+    }
+  }
+
+  window.GrReviewerSuggestionsProvider = GrReviewerSuggestionsProvider;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
new file mode 100644
index 0000000..ca3c277
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
@@ -0,0 +1,260 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-reviewer-suggestions-provider</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
+<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="gr-reviewer-suggestions-provider.js"></script>
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+</test-fixture>
+
+<script>
+  suite('GrReviewerSuggestionsProvider tests', () => {
+    let sandbox;
+    let _nextAccountId = 0;
+    const makeAccount = function(opt_status) {
+      const accountId = ++_nextAccountId;
+      return {
+        _account_id: accountId,
+        name: 'name ' + accountId,
+        email: 'email ' + accountId,
+        status: opt_status,
+      };
+    };
+    let _nextAccountId2 = 0;
+    const makeAccount2 = function(opt_status) {
+      const accountId2 = ++_nextAccountId2;
+      return {
+        _account_id: accountId2,
+        name: 'name ' + accountId2,
+        status: opt_status,
+      };
+    };
+
+    let owner;
+    let existingReviewer1;
+    let existingReviewer2;
+    let suggestion1;
+    let suggestion2;
+    let suggestion3;
+    let restAPI;
+    let provider;
+
+    let redundantSuggestion1;
+    let redundantSuggestion2;
+    let redundantSuggestion3;
+    let change;
+
+    setup(done => {
+      owner = makeAccount();
+      existingReviewer1 = makeAccount();
+      existingReviewer2 = makeAccount();
+      suggestion1 = {account: makeAccount()};
+      suggestion2 = {account: makeAccount()};
+      suggestion3 = {
+        group: {
+          id: 'suggested group id',
+          name: 'suggested group',
+        },
+      };
+
+      stub('gr-rest-api-interface', {
+        getLoggedIn() { return Promise.resolve(true); },
+        getConfig() { return Promise.resolve({}); },
+      });
+
+      restAPI = fixture('basic');
+      change = {
+        _number: 42,
+        owner,
+        reviewers: {
+          CC: [existingReviewer1],
+          REVIEWER: [existingReviewer2],
+        },
+      };
+      sandbox = sinon.sandbox.create();
+      return flush(done);
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+    suite('allowAnyUser set to false', () => {
+      setup(done => {
+        provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
+            Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
+        provider.init().then(done);
+      });
+      suite('stubbed values for _getReviewerSuggestions', () => {
+        setup(() => {
+          stub('gr-rest-api-interface', {
+            getChangeSuggestedReviewers() {
+              redundantSuggestion1 = {account: existingReviewer1};
+              redundantSuggestion2 = {account: existingReviewer2};
+              redundantSuggestion3 = {account: owner};
+              return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
+                redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+            },
+          });
+        });
+
+        test('makeSuggestionItem formats account or group accordingly', () => {
+          let account = makeAccount();
+          const account3 = makeAccount2();
+          let suggestion = provider.makeSuggestionItem({account});
+          assert.deepEqual(suggestion, {
+            name: account.name + ' <' + account.email + '>',
+            value: {account},
+          });
+
+          const group = {name: 'test'};
+          suggestion = provider.makeSuggestionItem({group});
+          assert.deepEqual(suggestion, {
+            name: group.name + ' (group)',
+            value: {group},
+          });
+
+          suggestion = provider.makeSuggestionItem(account);
+          assert.deepEqual(suggestion, {
+            name: account.name + ' <' + account.email + '>',
+            value: {account, count: 1},
+          });
+
+          suggestion = provider.makeSuggestionItem({account: {}});
+          assert.deepEqual(suggestion, {
+            name: 'Anonymous',
+            value: {account: {}},
+          });
+
+          provider._config = {
+            user: {
+              anonymous_coward_name: 'Anonymous Coward Name',
+            },
+          };
+
+          suggestion = provider.makeSuggestionItem({account: {}});
+          assert.deepEqual(suggestion, {
+            name: 'Anonymous Coward Name',
+            value: {account: {}},
+          });
+
+          account = makeAccount('OOO');
+
+          suggestion = provider.makeSuggestionItem({account});
+          assert.deepEqual(suggestion, {
+            name: account.name + ' <' + account.email + '> (OOO)',
+            value: {account},
+          });
+
+          suggestion = provider.makeSuggestionItem(account);
+          assert.deepEqual(suggestion, {
+            name: account.name + ' <' + account.email + '> (OOO)',
+            value: {account, count: 1},
+          });
+
+          sandbox.stub(GrDisplayNameUtils, '_accountEmail',
+              () => {
+                return '';
+              });
+
+          suggestion = provider.makeSuggestionItem(account3);
+          assert.deepEqual(suggestion, {
+            name: account3.name,
+            value: {account: account3, count: 1},
+          });
+        });
+
+        test('getSuggestions', done => {
+          provider.getSuggestions().then(reviewers => {
+            // Default is no filtering.
+            assert.equal(reviewers.length, 6);
+            assert.deepEqual(reviewers,
+                [redundantSuggestion1, redundantSuggestion2,
+                  redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+          }).then(done);
+        });
+
+        test('getSuggestions short circuits when logged out', () => {
+          // API call is already stubbed.
+          const xhrSpy = restAPI.getChangeSuggestedReviewers;
+          provider._loggedIn = false;
+          return provider.getSuggestions('').then(() => {
+            assert.isFalse(xhrSpy.called);
+            provider._loggedIn = true;
+            return provider.getSuggestions('').then(() => {
+              assert.isTrue(xhrSpy.called);
+            });
+          });
+        });
+      });
+
+      test('getChangeSuggestedReviewers is used', done => {
+        const suggestReviewerStub =
+            sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+                .returns(Promise.resolve([]));
+        const suggestAccountStub =
+            sandbox.stub(restAPI, 'getSuggestedAccounts')
+                .returns(Promise.resolve([]));
+
+        provider.getSuggestions('').then(() => {
+          assert.isTrue(suggestReviewerStub.calledOnce);
+          assert.isTrue(suggestReviewerStub.calledWith(42, ''));
+          assert.isFalse(suggestAccountStub.called);
+          done();
+        });
+      });
+    });
+
+    suite('allowAnyUser set to true', () => {
+      setup(done => {
+        provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
+            Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
+        provider.init().then(done);
+      });
+
+      test('getSuggestedAccounts is used', done => {
+        const suggestReviewerStub =
+            sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+                .returns(Promise.resolve([]));
+        const suggestAccountStub =
+            sandbox.stub(restAPI, 'getSuggestedAccounts')
+                .returns(Promise.resolve([]));
+
+        provider.getSuggestions('').then(() => {
+          assert.isFalse(suggestReviewerStub.called);
+          assert.isTrue(suggestAccountStub.calledOnce);
+          assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
+          done();
+        });
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/scripts/shadow.js b/polygerrit-ui/app/scripts/shadow.js
new file mode 100644
index 0000000..df43766
--- /dev/null
+++ b/polygerrit-ui/app/scripts/shadow.js
@@ -0,0 +1,421 @@
+/**
+ * Copyright 2018 Google LLC
+ *
+ * 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.
+ */
+
+ /* Downloaded from [1] and adapted to be used as HTML-imported JavaScript
+  * rather than import module.
+  * TODO: to be removed when merging to stable-3.2, where the NPM package
+  * management would allow to actually consume the artifact directly without
+  * adaptation.
+  *
+  * [1] https://raw.githubusercontent.com/GoogleChromeLabs/shadow-selection-polyfill/master/shadow.js
+  */
+
+const debug = false;
+
+const hasShadow = 'attachShadow' in Element.prototype && 'getRootNode' in Element.prototype;
+const hasSelection = !!(hasShadow && document.createElement('div').attachShadow({ mode: 'open' }).getSelection);
+const hasShady = window.ShadyDOM && window.ShadyDOM.inUse;
+const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) ||
+  /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+const useDocument = !hasShadow || hasShady || (!hasSelection && !isSafari);
+
+const invalidPartialElements = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|script|source|style|template|track|wbr)$/;
+
+const eventName = '-shadow-selectionchange';
+
+
+const validNodeTypes = [Node.ELEMENT_NODE, Node.TEXT_NODE, Node.DOCUMENT_FRAGMENT_NODE];
+function isValidNode(node) {
+  return validNodeTypes.includes(node.nodeType);
+}
+
+
+/**
+ * @param {!Selection} s selection to use
+ * @param {!Node} node to find caret position within a shadow root
+ * @return {!Node|!ShadowRoot}
+ */
+function findCaretFocus(s, node) {
+  const pending = [];
+  const pushAll = (nodeList) => {
+    for (let i = 0; i < nodeList.length; ++i) {
+      if (nodeList[i].shadowRoot) {
+        pending.push(nodeList[i].shadowRoot);
+      }
+    }
+  };
+
+  // We're told by Safari that a node containing a child with a Shadow Root is selected, but check
+  // the node directly too (just in case they change their mind later).
+  if (node.shadowRoot) {
+    pending.push(node.shadowRoot);
+  }
+  pushAll(node.childNodes);
+
+  while (pending.length) {
+    const root = pending.shift();
+
+    for (let i = 0; i < root.childNodes.length; ++i) {
+      if (s.containsNode(root.childNodes[i], true)) {
+        return root;
+      }
+    }
+
+    // The selection must be inside a further Shadow Root, but there's no good way to get a list of
+    // them. Safari won't tell you what regular node contains the root which has a selection. So,
+    // unfortunately if you stack them this will be slow(-ish).
+    pushAll(root.querySelectorAll('*'));
+  }
+
+  return null;
+}
+
+
+function findNode(s, parentNode, isLeft) {
+  const nodes = parentNode.childNodes || parentNode.children;
+  if (!nodes) {
+    return parentNode;  // found it, probably text
+  }
+
+  for (let i = 0; i < nodes.length; ++i) {
+    const j = isLeft ? i : (nodes.length - 1 - i);
+    const childNode = nodes[j];
+    if (!isValidNode(childNode)) {
+      continue;
+    }
+
+    debug && console.debug('checking child', childNode, 'IsLeft', isLeft);
+    if (s.containsNode(childNode, true)) {
+      if (s.containsNode(childNode, false)) {
+        debug && console.info('found child', childNode);
+        return childNode;
+      }
+      // Special-case elements that cannot have feasible children.
+      if (!invalidPartialElements.exec(childNode.localName || '')) {
+        debug && console.info('descending child', childNode);
+        return findNode(s, childNode, isLeft);
+      }
+    }
+    debug && console.info(parentNode, 'does NOT contain', childNode);
+  }
+  return parentNode;
+}
+
+
+let recentCaretRange = {node: null, offset: -1};
+
+
+(function() {
+  if (hasSelection || useDocument) {
+    // getSelection exists or document API can be used
+    document.addEventListener('selectionchange', (ev) => {
+      document.dispatchEvent(new CustomEvent(eventName));
+    });
+    return () => {};
+  }
+
+  let withinInternals = false;
+
+  document.addEventListener('selectionchange', (ev) => {
+    if (withinInternals) {
+      return;
+    }
+
+    withinInternals = true;
+
+    const s = window.getSelection();
+    if (s.type === 'Caret') {
+      const root = findCaretFocus(s, s.anchorNode);
+      if (root instanceof window.ShadowRoot) {
+        const range = getRange(root);
+        if (range) {
+          const node = range.startContainer;
+          const offset = range.startOffset;
+          recentCaretRange = {node, offset};
+        }
+      }
+    }
+
+    document.dispatchEvent(new CustomEvent('-shadow-selectionchange'));
+    window.requestAnimationFrame(() => {
+      withinInternals = false;
+    });
+  });
+})();
+
+
+/**
+ * @param {!Selection} s the window selection to use
+ * @param {!Node} node the node to walk from
+ * @param {boolean} walkForward should this walk in natural direction
+ * @return {boolean} whether the selection contains the following node (even partially)
+ */
+function containsNextElement(s, node, walkForward) {
+  const start = node;
+  while (node = walkFromNode(node, walkForward)) {
+    // walking (left) can contain our own parent, which we don't want
+    if (!node.contains(start)) {
+      break;
+    }
+  }
+  if (!node) {
+    return false;
+  }
+  // we look for Element as .containsNode says true for _every_ text node, and we only care about
+  // elements themselves
+  return node instanceof Element && s.containsNode(node, true);
+}
+
+
+/**
+ * @param {!Selection} s the window selection to use
+ * @param {!Node} leftNode the left node
+ * @param {!Node} rightNode the right node
+ * @return {boolean|undefined} whether this has natural direction
+ */
+function getSelectionDirection(s, leftNode, rightNode) {
+  if (s.type !== 'Range') {
+    return undefined;  // no direction
+  }
+  const measure = () => s.toString().length;
+
+  const initialSize = measure();
+  debug && console.info(`initial selection: "${s.toString()}"`)
+
+  let updatedSize;
+
+  // Try extending forward and seeing what happens.
+  s.modify('extend', 'forward', 'character');
+  updatedSize = measure();
+  debug && console.info(`forward selection: "${s.toString()}"`)
+
+  if (updatedSize > initialSize || containsNextElement(s, rightNode, true)) {
+    debug && console.info('got forward >, moving right')
+    s.modify('extend', 'backward', 'character');
+    return true;
+  } else if (updatedSize < initialSize || !s.containsNode(leftNode)) {
+    debug && console.info('got forward <, moving left')
+    s.modify('extend', 'backward', 'character');
+    return false;
+  }
+
+  // Maybe we were at the end of something. Extend backwards instead.
+  s.modify('extend', 'backward', 'character');
+  updatedSize = measure();
+  debug && console.info(`backward selection: "${s.toString()}"`)
+
+  if (updatedSize > initialSize || containsNextElement(s, leftNode, false)) {
+    debug && console.info('got backwards >, moving left')
+    s.modify('extend', 'forward', 'character');
+    return false;
+  } else if (updatedSize < initialSize || !s.containsNode(rightNode)) {
+    debug && console.info('got backwards <, moving right')
+    s.modify('extend', 'forward', 'character');
+    return true;
+  }
+
+  // This is likely a select-all.
+  return undefined;
+}
+
+/**
+ * Returns the next valid node (element or text). This is needed as Safari doesn't support
+ * TreeWalker inside Shadow DOM. Don't escape shadow roots.
+ *
+ * @param {!Node} node to start from
+ * @param {boolean} walkForward should this walk in natural direction
+ * @return {Node} node found, if any
+ */
+function walkFromNode(node, walkForward) {
+  if (!walkForward) {
+    return node.previousSibling || node.parentNode || null;
+  }
+  while (node) {
+    if (node.nextSibling) {
+      return node.nextSibling;
+    }
+    node = node.parentNode;
+  }
+  return null;
+}
+
+
+const cachedRange = new Map();
+function getRange(root) {
+  if (hasShady) {
+    const s = document.getSelection();
+    return s.rangeCount ? s.getRangeAt(0) : null;
+  } else if (useDocument) {
+    // Document pierces Shadow Root for selection, so actively filter it down to the right node.
+    // This is only for Firefox, which does not allow selection across Shadow Root boundaries.
+    const s = document.getSelection();
+    if (s.containsNode(root, true)) {
+      return s.getRangeAt(0);
+    }
+    return null;
+  } else if (hasSelection) {
+    const s = root.getSelection();
+    return s.rangeCount ? s.getRangeAt(0) : null;
+  }
+
+  const thisFrame = cachedRange.get(root);
+  if (thisFrame) {
+    return thisFrame;
+  }
+
+  const result = internalGetShadowSelection(root);
+
+  cachedRange.set(root, result.range);
+  window.setTimeout(() => {
+    cachedRange.delete(root);
+  }, 0);
+  debug && console.debug('getRange got', result);
+  return result.range;
+}
+
+
+function internalGetShadowSelection(root) {
+  // nb. We used to check whether the selection contained the host, but this broke in Safari 13.
+  // This is "nicely formatted" whitespace as per the browser's renderer. This is fine, and we only
+  // provide selection information at this granularity.
+  const s = window.getSelection();
+
+  if (s.type === 'None') {
+    return {range: null, type: 'none'};
+  } else if (!(s.type === 'Caret' || s.type === 'Range')) {
+    throw new TypeError('unexpected type: ' + s.type);
+  }
+
+  const leftNode = findNode(s, root, true);
+  if (leftNode === root) {
+    return {range: null, mode: 'none'};
+  }
+
+  const range = document.createRange();
+
+  let rightNode = null;
+  let isNaturalDirection = undefined;
+  if (s.type === 'Range') {
+    rightNode = findNode(s, root, false);  // get right node here _before_ getSelectionDirection
+    isNaturalDirection = getSelectionDirection(s, leftNode, rightNode);
+
+    // isNaturalDirection means "going right"
+
+    if (isNaturalDirection === undefined) {
+      // This occurs when we can't move because we can't extend left or right to measure the
+      // direction we're moving in... because it's the entire range. Hooray!
+      range.setStart(leftNode, 0);
+      range.setEnd(rightNode, rightNode.length);
+      return {range, mode: 'all'};
+    }
+  }
+
+  const initialSize = s.toString().length;
+
+  // Dumbest possible approach: remove characters from left side until no more selection,
+  // re-add.
+
+  // Try right side first, as we can trim characters until selection gets shorter.
+
+  let leftOffset = 0;
+  let rightOffset = 0;
+
+  if (rightNode === null) {
+    // This is a caret selection, do nothing.
+  } else if (rightNode.nodeType === Node.TEXT_NODE) {
+    const rightText = rightNode.textContent;
+    const existingNextSibling = rightNode.nextSibling;
+
+    for (let i = rightText.length - 1; i >= 0; --i) {
+      rightNode.splitText(i);
+      const updatedSize = s.toString().length;
+      if (updatedSize !== initialSize) {
+        rightOffset = i + 1;
+        break;
+      }
+    }
+
+    // We don't use .normalize() here, as the user might already have a weird node arrangement
+    // they need to maintain.
+    rightNode.insertData(rightNode.length, rightText.substr(rightNode.length));
+    while (rightNode.nextSibling !== existingNextSibling) {
+      rightNode.nextSibling.remove();
+    }
+  }
+
+  if (leftNode.nodeType === Node.TEXT_NODE) {
+    if (leftNode !== rightNode) {
+      // If we're at the end of a text node, it's impossible to extend the selection, so add an
+      // extra character to select (that we delete later).
+      leftNode.appendData('?');
+      s.collapseToStart();
+      s.modify('extend', 'right', 'character');
+    }
+
+    const leftText = leftNode.textContent;
+    const existingNextSibling = leftNode.nextSibling;
+
+    const start = (leftNode === rightNode ? rightOffset : leftText.length - 1);
+
+    for (let i = start; i >= 0; --i) {
+      leftNode.splitText(i);
+      if (s.toString() === '') {
+        leftOffset = i;
+        break;
+      }
+    }
+
+    // As above, we don't want to use .normalize().
+    leftNode.insertData(leftNode.length, leftText.substr(leftNode.length));
+    while (leftNode.nextSibling !== existingNextSibling) {
+      leftNode.nextSibling.remove();
+    }
+
+    if (leftNode !== rightNode) {
+      leftNode.deleteData(leftNode.length - 1, 1);
+    }
+
+    if (rightNode === null) {
+      rightNode = leftNode;
+      rightOffset = leftOffset;
+    }
+
+  } else if (rightNode === null) {
+    rightNode = leftNode;
+  }
+
+  // Work around common browser bug. Single character selction is always seen as 'forward'. Check
+  // if it's actually supposed to be backward.
+  if (initialSize === 1 && recentCaretRange && recentCaretRange.node === leftNode) {
+    if (recentCaretRange.offset > leftOffset && isNaturalDirection) {
+      isNaturalDirection = false;
+    }
+  }
+
+  if (isNaturalDirection === true) {
+    s.collapse(leftNode, leftOffset);
+    s.extend(rightNode, rightOffset);
+  } else if (isNaturalDirection === false) {
+    s.collapse(rightNode, rightOffset);
+    s.extend(leftNode, leftOffset);
+  } else {
+    s.setPosition(leftNode, leftOffset);
+  }
+
+  range.setStart(leftNode, leftOffset);
+  range.setEnd(rightNode, rightOffset);
+  return {range, mode: 'normal'};
+}
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index 624992b..e26d6d9 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -75,5 +75,94 @@
     return wrappedPromise;
   };
 
+  /**
+   * Get computed style value.
+   *
+   * If ShadyCSS is provided, use ShadyCSS api.
+   * If `getComputedStyleValue` is provided on the elment, use it.
+   * Otherwise fallback to native method (in polymer 2).
+   *
+   */
+  util.getComputedStyleValue = (name, el) => {
+    let style;
+    if (window.ShadyCSS) {
+      style = ShadyCSS.getComputedStyleValue(el, name);
+    } else if (el.getComputedStyleValue) {
+      style = el.getComputedStyleValue(name);
+    } else {
+      style = getComputedStyle(el).getPropertyValue(name);
+    }
+    return style;
+  };
+
+  /**
+   * Query selector on a dom element.
+   *
+   * This is shadow DOM compatible, but only works when selector is within
+   * one shadow host, won't work if your selector is crossing
+   * multiple shadow hosts.
+   *
+   */
+  util.querySelector = (el, selector) => {
+    let nodes = [el];
+    let element = null;
+    while (nodes.length) {
+      const node = nodes.pop();
+
+      // Skip if it's an invalid node.
+      if (!node || !node.querySelector) continue;
+
+      // Try find it with native querySelector directly
+      element = node.querySelector(selector);
+
+      if (element) {
+        break;
+      } else if (node.shadowRoot) {
+        // If shadowHost detected, add the host and its children
+        nodes = nodes.concat(Array.from(node.children));
+        nodes.push(node.shadowRoot);
+      } else {
+        nodes = nodes.concat(Array.from(node.children));
+      }
+    }
+    return element;
+  };
+
+  /**
+   * Query selector all dom elements matching with certain selector.
+   *
+   * This is shadow DOM compatible, but only works when selector is within
+   * one shadow host, won't work if your selector is crossing
+   * multiple shadow hosts.
+   *
+   * Note: this can be very expensive, only use when have to.
+   */
+  util.querySelectorAll = (el, selector) => {
+    let nodes = [el];
+    const results = new Set();
+    while (nodes.length) {
+      const node = nodes.pop();
+
+      if (!node || !node.querySelectorAll) continue;
+
+      // Try find all from regular children
+      [...node.querySelectorAll(selector)]
+          .forEach(el => results.add(el));
+
+      // Add all nodes with shadowRoot and loop through
+      const allShadowNodes = [...node.querySelectorAll('*')]
+          .filter(child => !!child.shadowRoot)
+          .map(child => child.shadowRoot);
+      nodes = nodes.concat(allShadowNodes);
+
+      // Add shadowRoot of current node if has one
+      // as its not included in node.querySelectorAll('*')
+      if (node.shadowRoot) {
+        nodes.push(node.shadowRoot);
+      }
+    }
+    return [...results];
+  };
+
   window.util = util;
 })(window);
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.html b/polygerrit-ui/app/styles/dashboard-header-styles.html
index ccc17b0..88d50c0 100644
--- a/polygerrit-ui/app/styles/dashboard-header-styles.html
+++ b/polygerrit-ui/app/styles/dashboard-header-styles.html
@@ -34,7 +34,7 @@
       }
       .info {
         display: inline-block;
-        padding: 1em;
+        padding: var(--spacing-l);
         vertical-align: top;
       }
       .info > div > span {
diff --git a/polygerrit-ui/app/styles/fonts.css b/polygerrit-ui/app/styles/fonts.css
index 41aec27..c837492 100644
--- a/polygerrit-ui/app/styles/fonts.css
+++ b/polygerrit-ui/app/styles/fonts.css
@@ -34,7 +34,7 @@
   font-family: 'Roboto';
   font-style: normal;
   font-weight: 400;
-  src: local('Roboto'), local('RobotoMono-Regular'),
+  src: local('Roboto'), local('Roboto-Regular'),
        url('../fonts/Roboto-Regular.woff2') format('woff2'),
        url('../fonts/Roboto-Regular.woff') format('woff');
   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index 8f72216..345363b 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -17,9 +17,6 @@
 <dom-module id="gr-change-list-styles">
   <template>
     <style>
-      :host {
-        font-size: var(--font-size-normal);
-      }
       gr-change-list-item,
       tr {
         border-top: 1px solid var(--border-color);
@@ -99,7 +96,7 @@
         text-decoration: underline;
       }
       .cell {
-        height: 2.25rem;
+        padding: var(--spacing-s) 0;
       }
       .star {
         padding: 0;
@@ -123,7 +120,7 @@
         vertical-align: middle;
       }
       .leftPadding {
-        width: var(--default-horizontal-margin);
+        width: var(--spacing-l);
       }
       .star {
         width: 30px;
@@ -165,12 +162,12 @@
       }
       @media only screen and (max-width: 50em) {
         :host {
-          font-size: var(--font-size-large);
+          font-size: var(--font-size-h3);
         }
         gr-change-list-item {
           flex-wrap: wrap;
           justify-content: space-between;
-          padding: .25em .5em;
+          padding: var(--spacing-xs) var(--spacing-m);
         }
         gr-change-list-item[selected],
         gr-change-list-item:focus {
@@ -199,10 +196,10 @@
         }
         .groupHeader .cell,
         .noChanges .cell {
-          padding: 0 .5em;
+          padding: 0 var(--spacing-m);
         }
         .subject {
-          margin-bottom: .25em;
+          margin-bottom: var(--spacing-xs);
           width: calc(100% - 2em);
         }
         .owner,
@@ -214,11 +211,6 @@
           height: auto;
         }
       }
-      @media only screen and (min-width: 1450px) {
-        :host {
-          font-size: 14px;
-        }
-      }
     </style>
   </template>
 </dom-module>
diff --git a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.html b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.html
new file mode 100644
index 0000000..fef3872
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.html
@@ -0,0 +1,48 @@
+<!--
+@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.
+-->
+<dom-module id="gr-change-metadata-shared-styles">
+  <template>
+    <style include="shared-styles"></style>
+    <style>
+      section {
+        display: table-row;
+      }
+
+      section:not(:first-of-type) .title,
+      section:not(:first-of-type) .value {
+        padding-top: var(--spacing-s);
+      }
+
+      .title,
+      .value {
+        display: table-cell;
+      }
+
+      .title {
+        color: var(--deemphasized-text-color);
+        max-width: 20em;
+        padding-left: var(--metadata-horizontal-padding);
+        padding-right: var(--metadata-horizontal-padding);
+        word-break: break-word;
+      }
+
+      .value {
+        padding-right: var(--metadata-horizontal-padding);
+      }
+    </style>
+  </template>
+</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.html b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.html
new file mode 100644
index 0000000..834f64a
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.html
@@ -0,0 +1,54 @@
+<!--
+@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.
+-->
+<!--
+  This is shared styles for change-view-integration endpoints.
+  All plugins that registered that endpoint should include this in
+  the component to have a consistent UX:
+
+  <style include="gr-change-view-integration-shared-styles"></style>
+
+  And use those defined class to apply these styles.
+-->
+<dom-module id="gr-change-view-integration-shared-styles">
+  <template>
+    <style include="shared-styles"></style>
+    <style>
+      .header {
+        color: var(--primary-text-color);
+        background-color: var(--table-header-background-color);
+        justify-content: space-between;
+        padding: var(--spacing-m) var(--spacing-l);
+        border-bottom: 1px solid var(--border-color);
+      }
+      .header .label {
+        font-weight: var(--font-weight-bold);
+        font-size: var(--font-size-h3);
+        margin: 0 var(--spacing-l) 0 0;
+      }
+      .header .note {
+        color: var(--deemphasized-text-color);
+      }
+      .content {
+        background-color: var(--view-background-color);
+      }
+      .header a,
+      .content a {
+        color: var(--link-color);
+      }
+    </style>
+  </template>
+</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-form-styles.html b/polygerrit-ui/app/styles/gr-form-styles.html
index 65c1ae3..3fe0a72 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.html
+++ b/polygerrit-ui/app/styles/gr-form-styles.html
@@ -27,18 +27,18 @@
       }
       .gr-form-styles h1,
       .gr-form-styles h2 {
-        margin-bottom: .3em;
+        margin-bottom: var(--spacing-s);
       }
       .gr-form-styles h4 {
         font-weight: var(--font-weight-bold);
       }
       .gr-form-styles fieldset {
         border: none;
-        margin-bottom: 2em;
+        margin-bottom: var(--spacing-xxl);
       }
       .gr-form-styles section {
         display: flex;
-        margin: .25em 0;
+        margin: var(--spacing-s) 0;
         min-height: 2em;
       }
       .gr-form-styles section * {
@@ -51,12 +51,9 @@
       .gr-form-styles .title {
         color: var(--deemphasized-text-color);
         font-weight: var(--font-weight-bold);
-        padding-right: .5em;
+        padding-right: var(--spacing-m);
         width: 15em;
       }
-      .gr-form-styles iron-autogrow-textarea {
-        font-size: var(--font-size-normal);
-      }
       .gr-form-styles th {
         color: var(--deemphasized-text-color);
         text-align: left;
@@ -65,7 +62,7 @@
       .gr-form-styles td,
       .gr-form-styles tfoot th {
         height: 2em;
-        padding: .25em 0;
+        padding: var(--spacing-s) 0;
         vertical-align: middle;
       }
       .gr-form-styles .emptyHeader {
@@ -86,10 +83,9 @@
       .gr-form-styles select,
       .gr-form-styles textarea {
         border: 1px solid var(--border-color);
-        border-radius: 2px;
-        font-size: var(--font-size-normal);
+        border-radius: var(--border-radius);
         height: 2em;
-        padding: 0 .15em;
+        padding: 0 var(--spacing-xs);
       }
       .gr-form-styles td:last-child {
         width: 5em;
@@ -104,26 +100,24 @@
         min-height: 2em;
         --iron-autogrow-textarea: {
           border: 1px solid var(--border-color);
-          border-radius: 2px;
+          border-radius: var(--border-radius);
           box-sizing: border-box;
-          font-size: var(--font-size-normal);
-          padding: .25em .15em 0 .15em;
+          padding: var(--spacing-s) var(--spacing-xs) 0 var(--spacing-xs);
         }
       }
       .gr-form-styles gr-autocomplete {
         border: none;
         --gr-autocomplete: {
           border: 1px solid var(--border-color);
-          border-radius: 2px;
-          font-size: var(--font-size-normal);
+          border-radius: var(--border-radius);
           height: 2em;
-          padding: 0 .15em;
+          padding: 0 var(--spacing-xs);
           width: 14em;
         }
       }
       @media only screen and (max-width: 40em) {
         .gr-form-styles section {
-          margin-bottom: 1em;
+          margin-bottom: var(--spacing-l);
         }
         .gr-form-styles .title,
         .gr-form-styles .value {
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.html b/polygerrit-ui/app/styles/gr-menu-page-styles.html
index 48ca396..d3b95b8 100644
--- a/polygerrit-ui/app/styles/gr-menu-page-styles.html
+++ b/polygerrit-ui/app/styles/gr-menu-page-styles.html
@@ -22,12 +22,12 @@
         display: block;
       }
       main {
-        margin: 2em auto;
+        margin: var(--spacing-xxl) auto;
         max-width: 50em;
       }
       .mainHeader {
         margin-left: 14em;
-        padding: 1em 0 1em 2em;
+        padding: var(--spacing-l) 0 var(--spacing-l) var(--spacing-xxl);
       }
       main.table,
       .mainHeader {
@@ -42,11 +42,11 @@
       }
       .loading {
         color: var(--deemphasized-text-color);
-        padding: 1em var(--default-horizontal-margin);
+        padding: var(--spacing-l);
       }
       @media only screen and (max-width: 67em) {
         main {
-          margin: 2em 0 2em 15em;
+          margin: var(--spacing-xxl) 0 var(--spacing-xxl) 15em;
         }
         main.table {
           margin-left: 14em;
@@ -54,17 +54,17 @@
       }
       @media only screen and (max-width: 53em) {
         .loading {
-          padding: 0 var(--default-horizontal-margin);
+          padding: 0 var(--spacing-l);
         }
         main {
-          margin: 2em 1em;
+          margin: var(--spacing-xxl) var(--spacing-l);
         }
         main.table {
           margin: 0;
         }
         .mainHeader {
           margin-left: 0;
-          padding: .5em 0 .5em 1em;
+          padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-l);
         }
       }
     </style>
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.html b/polygerrit-ui/app/styles/gr-page-nav-styles.html
index 18ec143..ced6ecb 100644
--- a/polygerrit-ui/app/styles/gr-page-nav-styles.html
+++ b/polygerrit-ui/app/styles/gr-page-nav-styles.html
@@ -18,13 +18,13 @@
   <template>
     <style>
       .navStyles ul {
-        padding: 1em 0;
+        padding: var(--spacing-l) 0;
       }
       .navStyles li {
         border-bottom: 1px solid transparent;
         border-top: 1px solid transparent;
         display: block;
-        padding: 0 calc(var(--default-horizontal-margin) + 0.5em);
+        padding: 0 var(--spacing-xl);
       }
       .navStyles li a {
         display: block;
@@ -33,20 +33,20 @@
         white-space: nowrap;
       }
       .navStyles .subsectionItem {
-        padding-left: calc(var(--default-horizontal-margin) + 1.5em);
+        padding-left: var(--spacing-xxl);
       }
       .navStyles .hideSubsection {
         display: none;
       }
       .navStyles li.sectionTitle {
-        padding: 0 2em 0 var(--default-horizontal-margin);
+        padding: 0 var(--spacing-xxl) 0 var(--spacing-l);
       }
       .navStyles li.sectionTitle:not(:first-child) {
-        margin-top: 1em;
+        margin-top: var(--spacing-l);
       }
       .navStyles .title {
         font-weight: var(--font-weight-bold);
-        margin: .4em 0;
+        margin: var(--spacing-s) 0;
       }
       .navStyles .selected {
         background-color: var(--view-background-color);
@@ -57,7 +57,7 @@
       .navStyles a {
         color: var(--primary-text-color);
         display: inline-block;
-        margin: .4em 0;
+        margin: var(--spacing-s) 0;
       }
     </style>
   </template>
diff --git a/polygerrit-ui/app/styles/gr-subpage-styles.html b/polygerrit-ui/app/styles/gr-subpage-styles.html
index 098a604..222c38b 100644
--- a/polygerrit-ui/app/styles/gr-subpage-styles.html
+++ b/polygerrit-ui/app/styles/gr-subpage-styles.html
@@ -18,7 +18,7 @@
   <template>
     <style>
       main {
-        margin: 1em 1em;
+        margin: var(--spacing-l);
       }
       .loading {
         display: none;
diff --git a/polygerrit-ui/app/styles/gr-table-styles.html b/polygerrit-ui/app/styles/gr-table-styles.html
index 1308952..d4e4bcf 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.html
+++ b/polygerrit-ui/app/styles/gr-table-styles.html
@@ -24,7 +24,7 @@
       }
       .genericList td {
         height: 2.25rem;
-        padding: .3rem 0;
+        padding: var(--spacing-s) 0;
         vertical-align: middle;
       }
       .genericList tr {
@@ -38,11 +38,11 @@
       }
       .genericList th,
       .genericList td {
-        padding-right: 1rem;
+        padding-right: var(--spacing-l);
       }
       .genericList tr th:first-of-type,
       .genericList tr td:first-of-type {
-        padding-left: 1rem;
+        padding-left: var(--spacing-l);
       }
       .genericList tr:first-of-type {
         border-top: 1px solid var(--border-color);
@@ -51,7 +51,7 @@
       .genericList tr td:last-of-type {
         border-left: 1px solid var(--border-color);
         text-align: center;
-        padding: 0 1em;
+        padding: 0 var(--spacing-l);
       }
       .genericList tr th.delete,
       .genericList tr td.delete,
@@ -78,7 +78,7 @@
       }
       .genericList .groupHeader {
         background-color: var(--table-subheader-background-color);
-        font-size: var(--font-size-large);
+        font-size: var(--font-size-h3);
       }
       .genericList a {
         color: var(--primary-text-color);
@@ -93,7 +93,7 @@
       .genericList .loadingMsg {
         color: var(--deemphasized-text-color);
         display: block;
-        padding: .3em var(--default-horizontal-margin);
+        padding: var(--spacing-s) var(--spacing-l);
       }
       .genericList .loadingMsg:not(.loading) {
         display: none;
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.html b/polygerrit-ui/app/styles/gr-voting-styles.html
index 3b1ee64..eec79be 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.html
+++ b/polygerrit-ui/app/styles/gr-voting-styles.html
@@ -23,6 +23,7 @@
           border: 1px solid rgba(0,0,0,.12);
           border-radius: 1em;
           box-shadow: none;
+          box-sizing: border-box;
           min-width: 3em;
         }
       }
diff --git a/polygerrit-ui/app/styles/main.css b/polygerrit-ui/app/styles/main.css
index 618a2d71..4c85176 100644
--- a/polygerrit-ui/app/styles/main.css
+++ b/polygerrit-ui/app/styles/main.css
@@ -26,10 +26,10 @@
   -webkit-text-size-adjust: none;
   /*
    * Default browser fonts are 16px. We want users with default settings to see
-   * a base font of 13px. 13/16 = .8125. This needs to be in html because
+   * a base font of 14px. 14/16 = .875. This needs to be in html because
    * can use rems based on this font-size throughout the app.
    */
-  font-size: .8125em;
+  font-size: .875em;
 }
 html,
 body {
@@ -42,6 +42,8 @@
    * Work around this using font-size and font-family.
    */
   -webkit-text-size-adjust: none;
-  font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
-  line-height: 1.4;
+  font-family: var(--font-family, ''), 'Roboto', Arial, sans-serif;
+  font-size: var(--font-size-normal, 1rem);
+  line-height: var(--line-height-normal, 1.4);
+  color: var(--primary-text-color, black);
 }
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index 78abe3a..5314741 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -17,7 +17,9 @@
 <dom-module id="shared-styles">
   <template>
     <style>
+
       /* CSS reset */
+
       html, body, button, div, span, applet, object, iframe, h1, h2, h3,
       h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite,
       code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub,
@@ -33,8 +35,11 @@
         padding: 0;
         vertical-align: baseline;
       }
-      input,
-      iron-autogrow-textarea {
+      *::after,
+      *::before {
+        box-sizing: border-box;
+      }
+      input {
         background-color: inherit;
         border: 1px solid var(--border-color);
         box-sizing: border-box;
@@ -42,6 +47,20 @@
         margin: 0;
         padding: 0;
       }
+      iron-autogrow-textarea {
+        background-color: inherit;
+        color: var(--primary-text-color);
+        border: 1px solid var(--border-color);
+        border-radius: var(--border-radius);
+        box-sizing: border-box;
+        /* iron-autogrow-textarea has a "-webkit-appearance: textarea" :host
+           css rule, which prevents overriding the border color. Clear that. */
+        -webkit-appearance: none;
+
+        --iron-autogrow-textarea: {
+          padding: 4px;
+        };
+      }
       a {
         color: var(--link-color);
       }
@@ -51,9 +70,6 @@
       button {
         font: inherit;
       }
-      body {
-        line-height: 1;
-      }
       ol, ul {
         list-style: none;
       }
@@ -69,25 +85,42 @@
         border-collapse: collapse;
         border-spacing: 0;
       }
-      /* Other Shared Styles*/
-      h1 {
-        font-size: 2rem;
-        font-weight: var(--font-weight-bold);
+
+      /* Fonts */
+
+      .font-normal {
+        font-size: var(--font-size-normal);
+        font-weight: var(--font-weight-normal);
+        line-height: var(--line-height-normal);
       }
-      h2 {
-        font-size: 1.5rem;
-        font-weight: var(--font-weight-bold);
+      .font-small {
+        font-size: var(--font-size-small);
+        font-weight: var(--font-weight-normal);
+        line-height: var(--line-height-small);
       }
-      h3 {
-        font-size: 1.17em;
+      h1, .font-h1 {
+        font-size: var(--font-size-h1);
         font-weight: var(--font-weight-bold);
+        line-height: var(--line-height-h1);
+      }
+      h2, .font-h2 {
+        font-size: var(--font-size-h2);
+        font-weight: var(--font-weight-bold);
+        line-height: var(--line-height-h2);
+      }
+      h3, .font-h3 {
+        font-size: var(--font-size-h3);
+        font-weight: var(--font-weight-bold);
+        line-height: var(--line-height-h3);
       }
       iron-icon {
         color: var(--deemphasized-text-color);
         --iron-icon-height: 20px;
         --iron-icon-width: 20px;
       }
+
       /* Stopgap solution until we remove hidden$ attributes. */
+
       [hidden] {
         display: none !important;
       }
@@ -103,12 +136,19 @@
         --paper-toggle-button-checked-bar-color: var(--link-color);
         --paper-toggle-button-checked-button-color: var(--link-color);
       }
+      paper-tabs {
+        --paper-tab-content-focused: {
+          /* paper-tabs uses 700 here, which can look awkward */
+          font-weight: var(--font-weight-normal);
+        };
+        --paper-tab-content-unselected: {
+          /* paper-tabs uses 0.8 here, but we want to control the color directly */
+          opacity: 1;
+          color: var(--deemphasized-text-color);
+        };      }
       strong {
         font-weight: var(--font-weight-bold);
       }
-      :host {
-        color: var(--primary-text-color);
-      }
     </style>
   </template>
 </dom-module>
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index ec47c53..bb477c2 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -16,139 +16,167 @@
 -->
 <custom-style><style is="custom-style">
 html {
-  /* Following vars have LTS for plugin API. */
-  --primary-text-color: #000;
-  /* For backwords compatibility we keep this as --header-background-color. */
-  --header-background-color: #eee;
-  --header-title-content: 'Gerrit';
-  --header-icon: none;
-  --header-icon-size: 0em;
-  --header-text-color: #000;
-  --header-title-font-size: 1.75rem;
-  --footer-background-color: #eee;
-  --border-color: #ddd;
-  /* This allows to add a border in custom themes */
-  --header-border-bottom: 1px solid var(--border-color);
-  --header-border-image: '';
-  --footer-border-top: 1px solid var(--border-color);
+  /**
+   * When adding a new color variable make sure to also add it to the other
+   * theme files in the same directory.
+   *
+   * For colors prefer lower case hex colors.
+   *
+   * Note that plugins might be using these variables, so removing a variable
+   * can be a breaking change that should go into the release notes.
+   */
 
-  /* Following are not part of plugin API. */
-  --selection-background-color: rgba(161, 194, 250, 0.1);
-  --hover-background-color: rgba(161, 194, 250, 0.2);
-  --expanded-background-color: #eee;
-  --view-background-color: #fff;
-  --default-horizontal-margin: 1rem;
-
-  --deemphasized-text-color: #757575;
-  /* Used on text color for change list that doesn't need user's attention. */
-  --reviewed-text-color: var(--primary-text-color);
-  --font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
-  /* Used on text for change list that needs user's attention. */
-  --font-weight-bold: 500;
-  --monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace;
-  --iron-overlay-backdrop: {
-    transition: none;
-  }
-  --table-header-background-color: #fafafa;
-  --table-subheader-background-color: #eaeaea;
-
-  --chip-background-color: #eee;
-
-  --dropdown-background-color: #fff;
-
-  --select-background-color: rgb(248, 248, 248);
-
-  --assignee-highlight-color: #fcfad6;
-
-  /* Font sizes */
-  --font-size-normal: 1rem;
-  --font-size-small: .92rem;
-  --font-size-large: 1.154rem;
-
+  /* text colors */
+  --primary-text-color: black;
   --link-color: #2a66d9;
-  --primary-button-background-color: var(--link-color);
-  --primary-button-text-color: #fff;
-  --secondary-button-background-color: #fff;
-  --secondary-button-text-color: #212121;
-  --default-button-background-color: #fff;
-  --default-button-text-color: var(--link-color);
-  --dialog-background-color: #fff;
-
-  /* Used for both the old patchset header and for indicating that a particular
-    change message was selected. */
-  --emphasis-color: #fff9c4;
-
+  --comment-text-color: black;
+  --deemphasized-text-color: #5F6368;
+  --default-button-text-color: #2a66d9;
   --error-text-color: red;
+  --primary-button-text-color: white;
+    /* Used on text color for change list that doesn't need user's attention. */
+  --reviewed-text-color: black;
+  --secondary-button-text-color: #212121;
+  --tooltip-text-color: white;
+  --vote-text-color-recommended: #388e3c;
+  --vote-text-color-disliked: #d32f2f;
 
-  --vote-color-approved: #9fcc6b;
-  --vote-color-recommended: #c9dfaf;
-  --vote-color-rejected: #f7a1ad;
-  --vote-color-disliked: #f7c4cb;
-  --vote-color-neutral: #ebf5fb;
-
-  --vote-text-color-recommended: #388E3C;
-  --vote-text-color-disliked: #D32F2F;
-
-  /* Diff colors */
-  --diff-selection-background-color: #c7dbf9;
-  --light-remove-highlight-color: #FFEBEE;
-  --light-add-highlight-color: #D8FED8;
-  --light-remove-add-highlight-color: #FFF8DC;
-  --light-rebased-add-highlight-color: #EEEEFF;
-  --dark-remove-highlight-color: #FFCDD2;
-  --dark-add-highlight-color: #AAF2AA;
-  --dark-rebased-remove-highlight-color: #F7E8B7;
-  --dark-rebased-add-highlight-color: #D7D7F9;
-  --diff-context-control-color: #fff7d4;
-  --diff-context-control-border-color: #f6e6a5;
-  --diff-tab-indicator-color: var(--deemphasized-text-color);
-  --diff-trailing-whitespace-indicator: #ff9ad2;
-  --diff-highlight-range-color: rgba(255, 213, 0, 0.5);
-  --diff-highlight-range-hover-color: rgba(255, 255, 0, 0.5);
-
+  /* background colors */
+  --assignee-highlight-color: #fcfad6;
+  --chip-background-color: #eee;
+  --comment-background-color: #fcfad6;
+  --default-button-background-color: white;
+  --dialog-background-color: white;
+  --dropdown-background-color: white;
+  --edit-mode-background-color: #ebf5fb;
+  --emphasis-color: #fff9c4;
+  --expanded-background-color: #eee;
+  --hover-background-color: rgba(161, 194, 250, 0.2);
+  --primary-button-background-color: #2a66d9;
+  --secondary-button-background-color: white;
+  --select-background-color: #f8f8f8;
+  --selection-background-color: rgba(161, 194, 250, 0.1);
   --shell-command-background-color: #f5f5f5;
   --shell-command-decoration-background-color: #ebebeb;
-
-  --comment-text-color: #000;
-  --comment-background-color: #fcfad6;
-  --unresolved-comment-background-color: #fcfaa6;
-
-  --edit-mode-background-color: #ebf5fb;
-
+  --table-header-background-color: #fafafa;
+  --table-subheader-background-color: #eaeaea;
   --tooltip-background-color: #333;
-  --tooltip-text-color: #fff;
+  --unresolved-comment-background-color: #fcfaa6;
+  --view-background-color: white;
+  --vote-color-approved: #9fcc6b;
+  --vote-color-disliked: #f7c4cb;
+  --vote-color-neutral: #ebf5fb;
+  --vote-color-recommended: #c9dfaf;
+  --vote-color-rejected: #f7a1ad;
 
-  --syntax-default-color: var(--primary-text-color);
-  --syntax-attribute-color: var(--primary-text-color);
-  --syntax-function-color: var(--primary-text-color);
-  --syntax-meta-color: #FF1717;
-  --syntax-keyword-color: #9E0069;
-  --syntax-number-color: #164;
-  --syntax-selector-class-color: #164;
-  --syntax-variable-color: black;
-  --syntax-template-variable-color: #0000C0;
-  --syntax-comment-color: #3F7F5F;
-  --syntax-string-color: #2A00FF;
-  --syntax-selector-id-color: #2A00FF;
-  --syntax-built_in-color: #30a;
-  --syntax-tag-color: #170;
-  --syntax-link-color: #219;
-  --syntax-meta-keyword-color: #219;
-  --syntax-type-color: var(--color-link);
-  --syntax-title-color: #0000C0;
+  /* misc colors */
+  --border-color: #ddd;
+
+  /* fonts */
+  --font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+  --monospace-font-family: 'Roboto Mono', 'SF Mono', 'Lucida Console', Monaco, monospace;
+  --font-size-code: 12px;     /* 12px mono */
+  --font-size-mono: .929rem;  /* 13px mono */
+  --font-size-small: .857rem; /* 12px */
+  --font-size-normal: 1rem;   /* 14px */
+  --font-size-h3: 1.143rem;   /* 16px */
+  --font-size-h2: 1.429rem;   /* 20px */
+  --font-size-h1: 1.714rem;   /* 24px */
+  --line-height-code: 1.334;      /* 16px */
+  --line-height-mono: 1.286rem;   /* 18px */
+  --line-height-small: 1.143rem;  /* 16px */
+  --line-height-normal: 1.429rem; /* 20px */
+  --line-height-h3: 1.714rem;     /* 24px */
+  --line-height-h2: 2rem;         /* 28px */
+  --line-height-h1: 2.286rem;     /* 32px */
+  --font-weight-normal: 400;
+  --font-weight-bold: 500;
+
+  /* spacing */
+  --spacing-xxs: 1px;
+  --spacing-xs: 2px;
+  --spacing-s: 4px;
+  --spacing-m: 8px;
+  --spacing-l: 12px;
+  --spacing-xl: 16px;
+  --spacing-xxl: 24px;
+
+  /* header and footer */
+  --footer-background-color: #eee;
+  --footer-border-top: 1px solid var(--border-color);
+  --header-background-color: #eee;
+  --header-border-bottom: 1px solid var(--border-color);
+  --header-border-image: '';
+  --header-box-shadow: none;
+  --header-padding: 0 var(--spacing-l);
+  --header-icon-size: 0em;
+  --header-icon: none;
+  --header-text-color: black;
+  --header-title-content: 'Gerrit';
+  --header-title-font-size: 1.75rem;
+
+  /* diff colors */
+  --dark-add-highlight-color: #aaf2aa;
+  --dark-rebased-add-highlight-color: #d7d7f9;
+  --dark-rebased-remove-highlight-color: #f7e8b7;
+  --dark-remove-highlight-color: #ffcdd2;
+  --diff-blank-background-color: white;
+  --diff-context-control-background-color: #fff7d4;
+  --diff-context-control-border-color: #f6e6a5;
+  --diff-context-control-color: var(--deemphasized-text-color);
+  --diff-highlight-range-color: rgba(255, 213, 0, 0.5);
+  --diff-highlight-range-hover-color: rgba(255, 255, 0, 0.5);
+  --diff-selection-background-color: #c7dbf9;
+  --diff-tab-indicator-color: var(--deemphasized-text-color);
+  --diff-trailing-whitespace-indicator: #ff9ad2;
+  --light-add-highlight-color: #d8fed8;
+  --light-rebased-add-highlight-color: #eef;
+  --light-remove-add-highlight-color: #fff8dc;
+  --light-remove-highlight-color: #ffebee;
+
+  /* syntax colors */
   --syntax-attr-color: #219;
-  --syntax-literal-color: #219;
-  --syntax-selector-pseudo-color: #FA8602;
-  --syntax-regexp-color: #FA8602;
-  --syntax-selector-attr-color: #FA8602;
-  --syntax-template-tag-color: #FA8602;
-  --syntax-params-color: var(--primary-text-color);
+  --syntax-attribute-color: var(--primary-text-color);
+  --syntax-built_in-color: #30a;
+  --syntax-comment-color: #3f7f5f;
+  --syntax-default-color: var(--primary-text-color);
   --syntax-doctag-weight: bold;
+  --syntax-function-color: var(--primary-text-color);
+  --syntax-keyword-color: #9e0069;
+  --syntax-link-color: #219;
+  --syntax-literal-color: #219;
+  --syntax-meta-color: #ff1717;
+  --syntax-meta-keyword-color: #219;
+  --syntax-number-color: #164;
+  --syntax-params-color: var(--primary-text-color);
+  --syntax-regexp-color: #fa8602;
+  --syntax-selector-attr-color: #fa8602;
+  --syntax-selector-class-color: #164;
+  --syntax-selector-id-color: #2a00ff;
+  --syntax-selector-pseudo-color: #fa8602;
+  --syntax-string-color: #2a00ff;
+  --syntax-tag-color: #170;
+  --syntax-template-tag-color: #fa8602;
+  --syntax-template-variable-color: #0000c0;
+  --syntax-title-color: #0000c0;
+  --syntax-type-color: #2a66d9;
+  --syntax-variable-color: var(--primary-text-color);
+  /* misc */
+  --border-radius: 4px;
   --reply-overlay-z-index: 1000;
+  --iron-overlay-backdrop: {
+    transition: none;
+  };
 }
 @media screen and (max-width: 50em) {
   html {
-    --default-horizontal-margin: .7rem;
+    --spacing-xxs: 1px;
+    --spacing-xs: 1px;
+    --spacing-s: 2px;
+    --spacing-m: 4px;
+    --spacing-l: 8px;
+    --spacing-xl: 12px;
+    --spacing-xxl: 16px;
   }
 }
 </style></custom-style>
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index 718ac25..957cc25 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -17,97 +17,124 @@
 <dom-module id="dark-theme">
   <custom-style><style is="custom-style">
     html {
-      --primary-text-color: #e2e2e2;
-      --view-background-color: #212121;
-      --border-color: #555555;
-      --header-border-bottom: 1px solid var(--border-color);
-      --header-border-image: '';
-      --footer-border-bottom: 1px solid var(--border-color);
-      --table-header-background-color: #353637;
-      --table-subheader-background-color: rgb(19, 20, 22);
-      --header-background-color: #3c4043;
-      --header-text-color: var(--primary-text-color);
-      --deemphasized-text-color: #9a9a9a;
-      /* Used on text color for change list doesn't need user's attention. */
-      --reviewed-text-color: #DADCE0;
-      /* Used on text for change list that needs user's attention. */
-      --font-weight-bold: 900;
-      --footer-background-color: var(--table-header-background-color);
-      --expanded-background-color: #26282b;
-      --link-color: #5487E5;
-      --primary-button-background-color: var(--link-color);
-      --primary-button-text-color: var(--primary-text-color);
-      --secondary-button-background-color: var(--primary-text-color);
-      --secondary-button-text-color: var(--deemphasized-text-color);
-      --default-button-text-color: var(--link-color);
-      --default-button-background-color: var(--table-subheader-background-color);
-      --dropdown-background-color: var(--table-header-background-color);
-      --dialog-background-color: var(--view-background-color);
-      --chip-background-color: var(--table-header-background-color);
-      --header-title-font-size: 1.75rem;
+      /**
+       * Sections and variables must stay consistent with app-theme.html.
+       *
+       * Only modify color variables in this theme file. dark-theme extends
+       * app-theme, so there is no need to repeat all variables, but for colors
+       * it does make sense to list them all: If you override one color, then
+       * you probably want to override all.
+       */
 
-      --select-background-color: var(--table-subheader-background-color);
-
-      --assignee-highlight-color: rgb(58, 54, 28);
-
-      --diff-selection-background-color: #3A71D8;
-      --light-remove-highlight-color: #320404;
-      --light-add-highlight-color: #0F401F;
-      --light-remove-add-highlight-color: #2f3f2f;
-      --light-rebased-remove-highlight-color: rgb(60, 37, 8);
-      --light-rebased-add-highlight-color: rgb(72, 113, 101);
-      --dark-remove-highlight-color: #62110F;
-      --dark-add-highlight-color: #133820;
-      --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
-      --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
-      --diff-context-control-color: var(--table-header-background-color);
-      --diff-context-control-border-color: var(--border-color);
-      --diff-highlight-range-color: rgba(0, 100, 200, 0.5);
-      --diff-highlight-range-hover-color: rgba(0, 150, 255, 0.5);
-      --shell-command-background-color: #5f5f5f;
-      --shell-command-decoration-background-color: #999999;
+      /* text colors */
+      --primary-text-color: #e8eaed;
+      --link-color: #8ab4f8;
       --comment-text-color: var(--primary-text-color);
-      --comment-background-color: #0B162B;
-      --unresolved-comment-background-color: rgb(56, 90, 154);
+      --deemphasized-text-color: #9e9e9e;
+      --default-button-text-color: #8ab4f8;
+      --error-text-color: red;
+      --primary-button-text-color: var(--primary-text-color);
+        /* Used on text color for change list doesn't need user's attention. */
+      --reviewed-text-color: #dadce0;
+      --secondary-button-text-color: var(--deemphasized-text-color);
+      --tooltip-text-color: white;
+      --vote-text-color-recommended: #388e3c;
+      --vote-text-color-disliked: #d32f2f;
 
-      --vote-color-approved: rgb(127, 182, 107);
-      --vote-color-recommended: rgb(63, 103, 50);
-      --vote-color-rejected: #ac2d3e;
+      /* background colors */
+      --assignee-highlight-color: #3a361c;
+      --chip-background-color: #131416;
+      --comment-background-color: #0b162b;
+      --default-button-background-color: #3c4043;
+      --dialog-background-color: #131416;
+      --dropdown-background-color: #131416;
+      --edit-mode-background-color: #5c0a36;
+      --emphasis-color: #383f4a;
+      --expanded-background-color: #26282b;
+      --hover-background-color: rgba(161, 194, 250, 0.2);
+      --primary-button-background-color: var(--link-color);
+      --secondary-button-background-color: var(--primary-text-color);
+      --select-background-color: #3c4043;
+      --selection-background-color: rgba(161, 194, 250, 0.1);
+      --shell-command-background-color: #5f5f5f;
+      --shell-command-decoration-background-color: #999;
+      --table-header-background-color: #131416;
+      --table-subheader-background-color: rgba(158, 158, 158, 0.24);
+      --tooltip-background-color: #111;
+      --unresolved-comment-background-color: #385a9a;
+      --view-background-color: #131416;
+      --vote-color-approved: #7fb66b;
       --vote-color-disliked: #bf6874;
       --vote-color-neutral: #597280;
+      --vote-color-recommended: #3f6732;
+      --vote-color-rejected: #ac2d3e;
 
-      --edit-mode-background-color: rgb(92, 10, 54);
-      --emphasis-color: #383f4a;
+      /* misc colors */
+      --border-color: #5f6368;
 
-      --tooltip-background-color: #111;
+      /* fonts */
+      --font-weight-bold: 900;
 
-      --syntax-default-color: var(--primary-text-color);
-      --syntax-meta-color: #6D7EEE;
-      --syntax-keyword-color: #CD4CF0;
-      --syntax-number-color: #00998A;
-      --syntax-selector-class-color: #FFCB68;
-      --syntax-variable-color: #F77669;
-      --syntax-template-variable-color: #F77669;
+      /* spacing */
+
+      /* header and footer */
+      --footer-background-color: #131416;
+      --footer-border-top: 1px solid var(--border-color);
+      --header-background-color: #3c4043;
+      --header-border-bottom: 1px solid var(--border-color);
+      --header-padding: 0 var(--spacing-l);
+      --header-text-color: var(--primary-text-color);
+
+      /* diff colors */
+      --dark-add-highlight-color: #133820;
+      --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
+      --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
+      --dark-remove-highlight-color: #62110f;
+      --diff-blank-background-color: #212121;
+      --diff-context-control-background-color: #131416;
+      --diff-context-control-border-color: var(--border-color);
+      --diff-context-control-color: var(--deemphasized-text-color);
+      --diff-highlight-range-color: rgba(0, 100, 200, 0.5);
+      --diff-highlight-range-hover-color: rgba(0, 150, 255, 0.5);
+      --diff-selection-background-color: #3a71d8;
+      --diff-tab-indicator-color: var(--deemphasized-text-color);
+      --diff-trailing-whitespace-indicator: #ff9ad2;
+      --light-add-highlight-color: #0f401f;
+      --light-rebased-add-highlight-color: #487165;
+      --light-remove-add-highlight-color: #2f3f2f;
+      --light-remove-highlight-color: #320404;
+
+      /* syntax colors */
+      --syntax-attr-color: #80cbbf;
+      --syntax-attribute-color: var(--primary-text-color);
+      --syntax-built_in-color: #f7c369;
       --syntax-comment-color: var(--deemphasized-text-color);
-      --syntax-string-color: #C3E88D;
-      --syntax-selector-id-color: #F77669;
-      --syntax-built_in-color: rgb(247, 195, 105);
-      --syntax-tag-color: #F77669;
-      --syntax-link-color: #C792EA;
-      --syntax-meta-keyword-color: #EEFFF7;
-      --syntax-type-color: #DD5F5F;
-      --syntax-title-color: #75A5FF;
-      --syntax-attr-color: #80CBBF;
-      --syntax-literal-color: #EEFFF7;
-      --syntax-selector-pseudo-color: #C792EA;
-      --syntax-regexp-color: #F77669;
-      --syntax-selector-attr-color: #80CBBF;
-      --syntax-template-tag-color: #C792EA;
+      --syntax-default-color: var(--primary-text-color);
       --syntax-doctag-weight: bold;
+      --syntax-function-color: var(--primary-text-color);
+      --syntax-keyword-color: #cd4cf0;
+      --syntax-link-color: #c792ea;
+      --syntax-literal-color: #eefff7;
+      --syntax-meta-color: #6d7eee;
+      --syntax-meta-keyword-color: #eefff7;
+      --syntax-number-color: #00998a;
       --syntax-params-color: var(--primary-text-color);
+      --syntax-regexp-color: #f77669;
+      --syntax-selector-attr-color: #80cbbf;
+      --syntax-selector-class-color: #ffcb68;
+      --syntax-selector-id-color: #f77669;
+      --syntax-selector-pseudo-color: #c792ea;
+      --syntax-string-color: #c3e88d;
+      --syntax-tag-color: #f77669;
+      --syntax-template-tag-color: #c792ea;
+      --syntax-template-variable-color: #f77669;
+      --syntax-title-color: #75a5ff;
+      --syntax-type-color: #dd5f5f;
+      --syntax-variable-color: #f77669;
 
-      --reply-overlay-z-index: 1000;
+      /* misc */
 
+      /* rules applied to <html> */
       background-color: var(--view-background-color);
     }
   </style></custom-style>
diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js
index 3de6227..d715d7d 100644
--- a/polygerrit-ui/app/template_test_srcs/template_test.js
+++ b/polygerrit-ui/app/template_test_srcs/template_test.js
@@ -1,45 +1,6 @@
 const fs = require('fs');
 const twinkie = require('fried-twinkie');
 
-/**
- * For the purposes of template type checking, externs should be added for
- * anything set on the window object. Note that sub-properties of these
- * declared properties are considered something separate.
- *
- * @todo (beckysiegel) Gerrit's class definitions should be recognized in
- *    closure types.
- */
-const EXTERN_NAMES = [
-  'Gerrit',
-  'GrAnnotation',
-  'GrAttributeHelper',
-  'GrChangeActionsInterface',
-  'GrChangeReplyInterface',
-  'GrDiffBuilder',
-  'GrDiffBuilderImage',
-  'GrDiffBuilderSideBySide',
-  'GrDiffBuilderUnified',
-  'GrDiffGroup',
-  'GrDiffLine',
-  'GrDomHooks',
-  'GrEditConstants',
-  'GrEtagDecorator',
-  'GrFileListConstants',
-  'GrGapiAuth',
-  'GrGerritAuth',
-  'GrLinkTextParser',
-  'GrPluginEndpoints',
-  'GrPopupInterface',
-  'GrRangeNormalizer',
-  'GrReporting',
-  'GrReviewerUpdatesParser',
-  'GrCountStringFormatter',
-  'GrThemeApi',
-  'moment',
-  'page',
-  'util',
-];
-
 fs.readdir('./polygerrit-ui/temp/behaviors/', (err, data) => {
   if (err) {
     console.log('error /polygerrit-ui/temp/behaviors/ directory');
@@ -87,30 +48,39 @@
     mappings = mappingSpecificFile;
   }
 
-  additionalSources.push({
-    path: 'custom-externs.js',
-    src: '/** @externs */' +
-        EXTERN_NAMES.map( name => { return `var ${name};`; }).join(' '),
-  });
-
-  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,
+  /**
+   * 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'),
       });
     }
-  }
 
-  twinkie.checkTemplate(toCheck, additionalSources)
-      .then(() => {}, joinedErrors => {
-        if (joinedErrors) {
+    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);
-        }
-      }).catch(e => {
-        console.error(e);
-        process.exit(1);
-      });
+        });
+  });
 });
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
index a549dd4..c1d8bbd 100644
--- a/polygerrit-ui/app/test/common-test-setup.html
+++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -17,7 +17,7 @@
 -->
 
 <link rel="import"
-    href="../bower_components/polymer-resin/standalone/polymer-resin.html" />
+    href="/bower_components/polymer-resin/standalone/polymer-resin.html" />
 <link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html">
 <script>
   security.polymer_resin.install({
@@ -53,13 +53,13 @@
   (function() {
     setup(() => {
       if (!window.Gerrit) { return; }
-      if (Gerrit._resetPlugins) {
-        Gerrit._resetPlugins();
+      if (Gerrit._testOnly_resetPlugins) {
+        Gerrit._testOnly_resetPlugins();
       }
     });
   })();
 </script>
 <link rel="import"
-    href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
+    href="/bower_components/iron-test-helpers/iron-test-helpers.html" />
 <link rel="import" href="test-router.html" />
-<script src="../bower_components/moment/moment.js"></script>
+<script src="/bower_components/moment/moment.js"></script>
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
new file mode 100644
index 0000000..7ceff7e
--- /dev/null
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -0,0 +1,26 @@
+/**
+ * @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.
+ */
+
+/**
+ * Helps looking up the proper iron-input element during the Polymer 2
+ * transition. Polymer 2 uses the <iron-input> element, while Polymer 1 uses
+ * the nested <input is="iron-input"> element.
+ */
+window.ironInput = function(element) {
+  return Polymer.dom(element).querySelector(
+      Polymer.Element ? 'iron-input' : 'input[is=iron-input]');
+};
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index cd3aaec..cca7d04 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -19,10 +19,11 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>Elements Test Runner</title>
 <meta charset="utf-8">
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <script>
   const testFiles = [];
+  const scriptsPath = '../scripts/';
   const elementsPath = '../elements/';
   const behaviorsPath = '../behaviors/';
 
@@ -61,9 +62,8 @@
     'change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html',
     'change-list/gr-create-change-help/gr-create-change-help_test.html',
     'change-list/gr-dashboard-view/gr-dashboard-view_test.html',
+    'change-list/gr-repo-header/gr-repo-header_test.html',
     'change-list/gr-user-header/gr-user-header_test.html',
-    'change/gr-account-entry/gr-account-entry_test.html',
-    'change/gr-account-list/gr-account-list_test.html',
     'change/gr-change-actions/gr-change-actions_test.html',
     'change/gr-change-metadata/gr-change-metadata-it_test.html',
     'change/gr-change-metadata/gr-change-metadata_test.html',
@@ -99,16 +99,18 @@
     'core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html',
     'core/gr-main-header/gr-main-header_test.html',
     'core/gr-navigation/gr-navigation_test.html',
-    'core/gr-reporting/gr-jank-detector_test.html',
     'core/gr-reporting/gr-reporting_test.html',
     'core/gr-router/gr-router_test.html',
     'core/gr-search-bar/gr-search-bar_test.html',
     'core/gr-smart-search/gr-smart-search_test.html',
     'diff/gr-comment-api/gr-comment-api_test.html',
+    'diff/gr-coverage-layer/gr-coverage-layer_test.html',
     'diff/gr-diff-builder/gr-diff-builder_test.html',
+    'diff/gr-diff-builder/gr-diff-builder-unified_test.html',
     'diff/gr-diff-cursor/gr-diff-cursor_test.html',
     'diff/gr-diff-highlight/gr-annotation_test.html',
     'diff/gr-diff-highlight/gr-diff-highlight_test.html',
+    'diff/gr-diff-host/gr-diff-host_test.html',
     'diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html',
     'diff/gr-diff-processor/gr-diff-processor_test.html',
     'diff/gr-diff-selection/gr-diff-selection_test.html',
@@ -125,7 +127,9 @@
     'edit/gr-edit-file-controls/gr-edit-file-controls_test.html',
     'edit/gr-editor-view/gr-editor-view_test.html',
     'plugins/gr-admin-api/gr-admin-api_test.html',
+    'plugins/gr-styles-api/gr-styles-api_test.html',
     'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
+    'plugins/gr-dom-hooks/gr-dom-hooks_test.html',
     'plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html',
     'plugins/gr-event-helper/gr-event-helper_test.html',
     'plugins/gr-external-style/gr-external-style_test.html',
@@ -134,7 +138,9 @@
     'plugins/gr-popup-interface/gr-popup-interface_test.html',
     'plugins/gr-repo-api/gr-repo-api_test.html',
     'plugins/gr-settings-api/gr-settings-api_test.html',
+    'plugins/gr-theme-api/gr-theme-api_test.html',
     'settings/gr-account-info/gr-account-info_test.html',
+    'settings/gr-agreements-list/gr-agreements-list_test.html',
     'settings/gr-change-table-editor/gr-change-table-editor_test.html',
     'settings/gr-cla-view/gr-cla-view_test.html',
     'settings/gr-edit-preferences/gr-edit-preferences_test.html',
@@ -149,7 +155,9 @@
     'settings/gr-ssh-editor/gr-ssh-editor_test.html',
     'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
     'shared/gr-event-interface/gr-event-interface_test.html',
+    'shared/gr-account-entry/gr-account-entry_test.html',
     'shared/gr-account-label/gr-account-label_test.html',
+    'shared/gr-account-list/gr-account-list_test.html',
     'shared/gr-account-link/gr-account-link_test.html',
     'shared/gr-alert/gr-alert_test.html',
     'shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html',
@@ -161,33 +169,47 @@
     'shared/gr-comment-thread/gr-comment-thread_test.html',
     'shared/gr-comment/gr-comment_test.html',
     'shared/gr-copy-clipboard/gr-copy-clipboard_test.html',
+    'shared/gr-count-string-formatter/gr-count-string-formatter_test.html',
     'shared/gr-cursor-manager/gr-cursor-manager_test.html',
     'shared/gr-date-formatter/gr-date-formatter_test.html',
     'shared/gr-dialog/gr-dialog_test.html',
     'shared/gr-diff-preferences/gr-diff-preferences_test.html',
     'shared/gr-download-commands/gr-download-commands_test.html',
+    'shared/gr-dropdown/gr-dropdown_test.html',
     'shared/gr-dropdown-list/gr-dropdown-list_test.html',
     'shared/gr-editable-content/gr-editable-content_test.html',
     'shared/gr-editable-label/gr-editable-label_test.html',
     'shared/gr-formatted-text/gr-formatted-text_test.html',
+    'shared/gr-hovercard/gr-hovercard_test.html',
+    'shared/gr-js-api-interface/gr-annotation-actions-context_test.html',
+    'shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html',
     'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
     'shared/gr-js-api-interface/gr-change-reply-js-api_test.html',
+    'shared/gr-js-api-interface/gr-api-utils_test.html',
     'shared/gr-js-api-interface/gr-js-api-interface_test.html',
+    'shared/gr-js-api-interface/gr-gerrit_test.html',
+    'shared/gr-js-api-interface/gr-plugin-action-context_test.html',
+    'shared/gr-js-api-interface/gr-plugin-loader_test.html',
     'shared/gr-js-api-interface/gr-plugin-endpoints_test.html',
     'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
     'shared/gr-fixed-panel/gr-fixed-panel_test.html',
     'shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html',
+    'shared/gr-label-info/gr-label-info_test.html',
     'shared/gr-lib-loader/gr-lib-loader_test.html',
     'shared/gr-limited-text/gr-limited-text_test.html',
     'shared/gr-linked-chip/gr-linked-chip_test.html',
     'shared/gr-linked-text/gr-linked-text_test.html',
     'shared/gr-list-view/gr-list-view_test.html',
+    'shared/gr-overlay/gr-overlay_test.html',
     'shared/gr-page-nav/gr-page-nav_test.html',
     'shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html',
     'shared/gr-rest-api-interface/gr-auth_test.html',
+    'shared/gr-rest-api-interface/gr-etag-decorator_test.html',
     'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
     'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html',
+    'shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html',
     'shared/gr-select/gr-select_test.html',
+    'shared/gr-shell-command/gr-shell-command_test.html',
     'shared/gr-storage/gr-storage_test.html',
     'shared/gr-textarea/gr-textarea_test.html',
     'shared/gr-tooltip-content/gr-tooltip-content_test.html',
@@ -198,7 +220,6 @@
   for (let file of elements) {
     file = elementsPath + file;
     testFiles.push(file);
-    testFiles.push(file + '?dom=shadow');
   }
 
   // Behaviors tests.
@@ -212,8 +233,9 @@
     'rest-client-behavior/rest-client-behavior_test.html',
     'gr-access-behavior/gr-access-behavior_test.html',
     'gr-admin-nav-behavior/gr-admin-nav-behavior_test.html',
-    'gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html',
     'gr-change-table-behavior/gr-change-table-behavior_test.html',
+    'gr-list-view-behavior/gr-list-view-behavior_test.html',
+    'gr-display-name-behavior/gr-display-name-behavior_test.html',
     'gr-patch-set-behavior/gr-patch-set-behavior_test.html',
     'gr-path-list-behavior/gr-path-list-behavior_test.html',
     'gr-tooltip-behavior/gr-tooltip-behavior_test.html',
@@ -227,5 +249,17 @@
     testFiles.push(file);
   }
 
+  const scripts = [
+    'gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html',
+    'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
+    'gr-display-name-utils/gr-display-name-utils_test.html',
+    'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
+  ];
+  /* eslint-enable max-len */
+  for (let file of scripts) {
+    file = scriptsPath + file;
+    testFiles.push(file);
+  }
+
   WCT.loadSuites(testFiles);
 </script>
diff --git a/polygerrit-ui/app/types/custom-externs.js b/polygerrit-ui/app/types/custom-externs.js
new file mode 100644
index 0000000..afa094c
--- /dev/null
+++ b/polygerrit-ui/app/types/custom-externs.js
@@ -0,0 +1,63 @@
+/**
+ * @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.
+ */
+
+/**
+ * For the purposes of template type checking, externs should be added for
+ * anything set on the window object. Note that sub-properties of these
+ * declared properties are considered something separate.
+ *
+ * This file is only for template type checking, not used in Gerrit code.
+ */
+
+/* eslint-disable no-var */
+/* eslint-disable no-unused-vars */
+/** @externs */
+// @unused
+
+var Gerrit;
+var GrAnnotation;
+var GrAttributeHelper;
+var GrChangeActionsInterface;
+var GrChangeReplyInterface;
+var GrDiffBuilder;
+var GrDiffBuilderImage;
+var GrDiffBuilderSideBySide;
+var GrDiffBuilderUnified;
+var GrDiffGroup;
+var GrDiffLine;
+var GrDomHooks;
+var GrEditConstants;
+var GrEtagDecorator;
+var GrFileListConstants;
+var GrGapiAuth;
+var GrGerritAuth;
+var GrLinkTextParser;
+var GrPluginEndpoints;
+var GrPopupInterface;
+var GrRangeNormalizer;
+var GrReporting;
+var GrReviewerUpdatesParser;
+var GrCountStringFormatter;
+var GrThemeApi;
+var SiteBasedCache;
+var FetchPromisesCache;
+var GrRestApiHelper;
+var GrDisplayNameUtils;
+var GrReviewerSuggestionsProvider;
+var moment;
+var page;
+var util;
\ No newline at end of file
diff --git a/polygerrit-ui/app/types/types.js b/polygerrit-ui/app/types/types.js
new file mode 100644
index 0000000..e17bec8
--- /dev/null
+++ b/polygerrit-ui/app/types/types.js
@@ -0,0 +1,279 @@
+/**
+ * @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.
+ */
+
+// Type definitions used across multiple files in Gerrit
+
+window.Gerrit = window.Gerrit || {};
+
+/** @enum {string} */
+Gerrit.CoverageType = {
+  /**
+   * start_character and end_character of the range will be ignored for this
+   * type.
+   */
+  COVERED: 'COVERED',
+  /**
+   * start_character and end_character of the range will be ignored for this
+   * type.
+   */
+  NOT_COVERED: 'NOT_COVERED',
+  PARTIALLY_COVERED: 'PARTIALLY_COVERED',
+  /**
+   * You don't have to use this. If there is no coverage information for a
+   * range, then it implicitly means NOT_INSTRUMENTED. start_character and
+   * end_character of the range will be ignored for this type.
+   */
+  NOT_INSTRUMENTED: 'NOT_INSTRUMENTED',
+};
+
+/**
+ * @typedef {{
+ *   start_line: number,
+ *   start_character: number,
+ *   end_line: number,
+ *   end_character: number,
+ * }}
+ */
+Gerrit.Range;
+
+/**
+ * @typedef {{side: string, range: Gerrit.Range, hovering: boolean}}
+ */
+Gerrit.HoveredRange;
+
+/**
+ * @typedef {{
+ *   side: string,
+ *   type: Gerrit.CoverageType,
+ *   code_range: Gerrit.Range,
+ * }}
+ */
+Gerrit.CoverageRange;
+
+/**
+ * @typedef {{
+ *    basePatchNum: (string|number),
+ *    patchNum: (number),
+ * }}
+ */
+Gerrit.PatchRange;
+
+/**
+ * @typedef {{
+ *   changeNum: (string|number),
+ *   endpoint: string,
+ *   patchNum: (string|number|null|undefined),
+ *   errFn: (function(?Response, string=)|null|undefined),
+ *   params: (Object|null|undefined),
+ *   fetchOptions: (Object|null|undefined),
+ *   anonymizedEndpoint: (string|undefined),
+ *   reportEndpointAsIs: (boolean|undefined),
+ * }}
+ */
+Gerrit.ChangeFetchRequest;
+
+/**
+ * Object to describe a request for passing into _send.
+ * - method is the HTTP method to use in the request.
+ * - url is the URL for the request
+ * - body is a request payload.
+ *     TODO (beckysiegel) remove need for number at least.
+ * - errFn is a function to invoke when the request fails.
+ * - cancelCondition is a function that, if provided and returns true, will
+ *   cancel the response after it resolves.
+ * - contentType is the content type of the body.
+ * - headers is a key-value hash to describe HTTP headers for the request.
+ * - parseResponse states whether the result should be parsed as a JSON
+ *     object using getResponseObject.
+ *
+ * @typedef {{
+ *   method: string,
+ *   url: string,
+ *   body: (string|number|Object|null|undefined),
+ *   errFn: (function(?Response, string=)|null|undefined),
+ *   contentType: (string|null|undefined),
+ *   headers: (Object|undefined),
+ *   parseResponse: (boolean|undefined),
+ *   anonymizedUrl: (string|undefined),
+ *   reportUrlAsIs: (boolean|undefined),
+ * }}
+ */
+Gerrit.SendRequest;
+
+/**
+ * @typedef {{
+ *   changeNum: (string|number),
+ *   method: string,
+ *   patchNum: (string|number|undefined),
+ *   endpoint: string,
+ *   body: (string|number|Object|null|undefined),
+ *   errFn: (function(?Response, string=)|null|undefined),
+ *   contentType: (string|null|undefined),
+ *   headers: (Object|undefined),
+ *   parseResponse: (boolean|undefined),
+ *   anonymizedEndpoint: (string|undefined),
+ *   reportEndpointAsIs: (boolean|undefined),
+ * }}
+ */
+Gerrit.ChangeSendRequest;
+
+/**
+ * @typedef {{
+ *    url: string,
+ *    fetchOptions: (Object|null|undefined),
+ *    anonymizedUrl: (string|undefined),
+ * }}
+ */
+Gerrit.FetchRequest;
+
+/**
+ * Object to describe a request for passing into fetchJSON or fetchRawJSON.
+ * - url is the URL for the request (excluding get params)
+ * - errFn is a function to invoke when the request fails.
+ * - cancelCondition is a function that, if provided and returns true, will
+ *     cancel the response after it resolves.
+ * - params is a key-value hash to specify get params for the request URL.
+ *
+ * @typedef {{
+ *    url: string,
+ *    errFn: (function(?Response, string=)|null|undefined),
+ *    cancelCondition: (function()|null|undefined),
+ *    params: (Object|null|undefined),
+ *    fetchOptions: (Object|null|undefined),
+ *    anonymizedUrl: (string|undefined),
+ *    reportUrlAsIs: (boolean|undefined),
+ * }}
+ */
+Gerrit.FetchJSONRequest;
+
+/**
+ * @typedef {{
+ *    message: string,
+ *    icon: string,
+ *    class: string,
+ *  }}
+ */
+Gerrit.PushCertificateValidation;
+
+/**
+ * Object containing layout values to be used in rendering size-bars.
+ * `max{Inserted,Deleted}` represent the largest values of the
+ * `lines_inserted` and `lines_deleted` fields of the files respectively. The
+ * `max{Addition,Deletion}Width` represent the width of the graphic allocated
+ * to the insertion or deletion side respectively. Finally, the
+ * `deletionOffset` value represents the x-position for the deletion bar.
+ *
+ * @typedef {{
+ *    maxInserted: number,
+ *    maxDeleted: number,
+ *    maxAdditionWidth: number,
+ *    maxDeletionWidth: number,
+ *    deletionOffset: number,
+ * }}
+ */
+Gerrit.LayoutStats;
+
+/**
+ * @typedef {{
+ *    changeNum: number,
+ *    path: string,
+ *    patchRange: !Gerrit.PatchRange,
+ *    projectConfig: (Object|undefined),
+ * }}
+ */
+Gerrit.CommentMeta;
+
+/**
+ * @typedef {{
+ *    meta: !Gerrit.CommentMeta,
+ *    left: !Array,
+ *    right: !Array,
+ * }}
+ */
+Gerrit.CommentsBySide;
+
+/**
+ * The DiffIntralineInfo entity contains information about intraline edits in a
+ * file.
+ *
+ * The information consists of a list of <skip length, mark length> pairs, where
+ * the skip length is the number of characters between the end of the previous
+ * edit and the start of this edit, and the mark length is the number of edited
+ * characters following the skip. The start of the edits is from the beginning
+ * of the related diff content lines.
+ *
+ * Note that the implied newline character at the end of each line is included
+ * in the length calculation, and thus it is possible for the edits to span
+ * newlines.
+ *
+ * @typedef {!Array<number>}
+ */
+Gerrit.IntralineInfo;
+
+/**
+ * A portion of the diff that is treated the same.
+ *
+ * Called `DiffContent` in the API, see
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#diff-content
+ *
+ * @typedef {{
+ *  ab: ?Array<!string>,
+ *  a: ?Array<!string>,
+ *  b: ?Array<!string>,
+ *  skip: ?number,
+ *  edit_a: ?Array<!Gerrit.IntralineInfo>,
+ *  edit_b: ?Array<!Gerrit.IntralineInfo>,
+ *  due_to_rebase: ?boolean,
+ *  common: ?boolean
+ * }}
+ */
+Gerrit.DiffChunk;
+
+/**
+ * Special line number which should not be collapsed into a shared region.
+ *
+ * @typedef {{
+ *  number: number,
+ *  leftSide: boolean
+ * }}
+ */
+Gerrit.LineOfInterest;
+
+/**
+ * @typedef {{
+ *    html: Node,
+ *    position: number,
+ *    length: number,
+ * }}
+ */
+Gerrit.CommentLinkItem;
+
+/**
+ * @typedef {{
+ *   name: string,
+ *   value: Object,
+ * }}
+ */
+Gerrit.GrSuggestionItem;
+
+/**
+ * @typedef {{
+ *    getSuggestions: function(string): Promise<Array<Object>>,
+ *    makeSuggestionItem: function(Object): Gerrit.GrSuggestionItem,
+ * }}
+ */
+Gerrit.GrSuggestionsProvider;
\ No newline at end of file
diff --git a/polygerrit-ui/app/wct_test.sh b/polygerrit-ui/app/wct_test.sh
index dd16ba7..f1b4666 100755
--- a/polygerrit-ui/app/wct_test.sh
+++ b/polygerrit-ui/app/wct_test.sh
@@ -14,8 +14,7 @@
 
 if [ "${WCT_HEADLESS_MODE:-0}" != "0" ]; then
     CHROME_OPTIONS=[\'start-maximized\',\'headless\',\'disable-gpu\',\'no-sandbox\']
-    # TODO(paladox): Fix Firefox support for headless mode
-    FIREFOX_OPTIONS=[\'\']
+    FIREFOX_OPTIONS=[\'-headless\']
 else
     CHROME_OPTIONS=[\'start-maximized\']
     FIREFOX_OPTIONS=[\'\']
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 2f5df90..1a2d299 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -38,11 +38,12 @@
 )
 
 var (
-	plugins    = flag.String("plugins", "", "comma seperated plugin paths to serve")
-	port       = flag.String("port", ":8081", "Port to serve HTTP requests on")
-	host       = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
-	scheme     = flag.String("scheme", "https", "URL scheme")
-	cdnPattern = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_ui/[0-9.]*")
+	plugins               = flag.String("plugins", "", "comma seperated plugin paths to serve")
+	port                  = flag.String("port", ":8081", "Port to serve HTTP requests on")
+	host                  = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
+	scheme                = flag.String("scheme", "https", "URL scheme")
+	cdnPattern            = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_ui/[0-9.]*")
+	bundledPluginsPattern = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_assets/[0-9.]*")
 )
 
 func main() {
@@ -74,6 +75,7 @@
 	http.HandleFunc("/accounts/", handleProxy)
 	http.HandleFunc("/config/", handleProxy)
 	http.HandleFunc("/projects/", handleProxy)
+	http.HandleFunc("/static/", handleProxy)
 	http.HandleFunc("/accounts/self/detail", handleAccountDetail)
 
 	if len(*plugins) > 0 {
@@ -102,7 +104,8 @@
 func handleIndex(writer http.ResponseWriter, originalRequest *http.Request) {
 	fakeRequest := &http.Request{
 		URL: &url.URL{
-			Path: "/",
+			Path:     "/",
+			RawQuery: originalRequest.URL.RawQuery,
 		},
 	}
 	handleProxy(writer, fakeRequest)
@@ -168,7 +171,7 @@
 func patchResponse(req *http.Request, res *http.Response) io.Reader {
 	switch req.URL.EscapedPath() {
 	case "/":
-		return replaceCdn(res.Body)
+		return rewriteHostPage(res.Body)
 	case "/config/server/info":
 		return injectLocalPlugins(res.Body)
 	default:
@@ -176,13 +179,42 @@
 	}
 }
 
-func replaceCdn(reader io.Reader) io.Reader {
+func rewriteHostPage(reader io.Reader) io.Reader {
 	buf := new(bytes.Buffer)
 	buf.ReadFrom(reader)
 	original := buf.String()
 
+	// Simply remove all CDN references, so files are loaded from the local file system  or the proxy
+	// server instead.
 	replaced := cdnPattern.ReplaceAllString(original, "")
 
+	// Modify window.INITIAL_DATA so that it has the same effect as injectLocalPlugins. To achieve
+	// this let's add JavaScript lines at the end of the <script>...</script> snippet that also
+	// contains window.INITIAL_DATA=...
+	// Here we rely on the fact that the <script> snippet that we want to append to is the first one.
+	if len(*plugins) > 0 {
+		// If the host page contains a reference to a plugin bundle that would be preloaded, then remove it.
+		replaced = bundledPluginsPattern.ReplaceAllString(replaced, "")
+
+		insertionPoint := strings.Index(replaced, "</script>")
+		builder := new(strings.Builder)
+		builder.WriteString(
+			"window.INITIAL_DATA['/config/server/info'].plugin.html_resource_paths = []; ")
+		builder.WriteString(
+			"window.INITIAL_DATA['/config/server/info'].plugin.js_resource_paths = []; ")
+		for _, p := range strings.Split(*plugins, ",") {
+			if filepath.Ext(p) == ".html" {
+				builder.WriteString(
+					"window.INITIAL_DATA['/config/server/info'].plugin.html_resource_paths.push('" + p + "'); ")
+			}
+			if filepath.Ext(p) == ".js" {
+				builder.WriteString(
+					"window.INITIAL_DATA['/config/server/info'].plugin.js_resource_paths.push('" + p + "'); ")
+			}
+		}
+		replaced = replaced[:insertionPoint] + builder.String() + replaced[insertionPoint:]
+	}
+
 	return strings.NewReader(replaced)
 }
 
@@ -207,11 +239,11 @@
 	jsResources := getJsonPropByPath(response, jsPluginsPath).([]interface{})
 
 	for _, p := range strings.Split(*plugins, ",") {
-		if strings.HasSuffix(p, ".html") {
+		if filepath.Ext(p) == ".html" {
 			htmlResources = append(htmlResources, p)
 		}
 
-		if strings.HasSuffix(p, ".js") {
+		if filepath.Ext(p) == ".js" {
 			jsResources = append(jsResources, p)
 		}
 	}
@@ -261,7 +293,7 @@
 
 // Any path prefixes that should resolve to index.html.
 var (
-	fePaths    = []string{"/q/", "/c/", "/p/", "/x/", "/dashboard/", "/admin/"}
+	fePaths    = []string{"/q/", "/c/", "/p/", "/x/", "/dashboard/", "/admin/", "/settings/"}
 	issueNumRE = regexp.MustCompile(`^\/\d+\/?$`)
 )
 
diff --git a/prologtests/examples/BUILD b/prologtests/examples/BUILD
new file mode 100644
index 0000000..f4ebe90
--- /dev/null
+++ b/prologtests/examples/BUILD
@@ -0,0 +1,15 @@
+package(default_visibility = ["//visibility:public"])
+
+DUMMY = ["dummy.sh"]
+
+# Enable prologtests on newer Java versions again, when this Bazel bug is fixed:
+# https://github.com/bazelbuild/bazel/issues/9391
+sh_test(
+    name = "test_examples",
+    srcs = select({
+        "//:java11": DUMMY,
+        "//:java_next": DUMMY,
+        "//conditions:default": ["run.sh"],
+    }),
+    data = glob(["*.pl"]) + ["//:gerrit.war"],
+)
diff --git a/prologtests/examples/README.md b/prologtests/examples/README.md
new file mode 100644
index 0000000..12eb256e
--- /dev/null
+++ b/prologtests/examples/README.md
@@ -0,0 +1,54 @@
+# Prolog Unit Test Examples
+
+## Run all examples
+
+Build a local gerrit.war and then run the script:
+
+    ./run.sh
+
+Note that a local Gerrit server is not needed because
+these unit test examples redefine wrappers of the `gerrit:change\*`
+rules to provide mocked change data.
+
+## Add a new unit test
+
+Please follow the pattern in `t1.pl`, `t2.pl`, or `t3.pl`.
+
+* Put code to be tested in a file, e.g. `rules.pl`.
+  For easy unit testing, split long clauses into short ones
+  and test every positive and negative path.
+
+* Create a new unit test file, e.g. `t1.pl`,
+  which should _load_ the test source file and `utils.pl`.
+
+      % First load all source files and the utils.pl.
+      :- load([aosp_rules,utils]).
+
+      :- begin_tests(t1).  % give this test any name
+
+      % Use test0/1 or test1/1 to verify failed/passed goals.
+
+      :- end_tests(_,0).   % check total pass/fail counts
+
+* Optionally replace calls to gerrit functions that depend on repository.
+  For example, define the following wrappers and in source code, use
+  `change_branch/1` instead of `gerrti:change_branch/1`.
+
+      change_branch(X) :- gerrit:change_branch(X).
+      commit_label(L,U) :- gerrit:commit_label(L,U).
+
+* In unit test file, redefine the gerrit function wrappers and test.
+  For example, in `t3.pl`, we have:
+
+      :- redefine(uploader,1,uploader(user(42))).  % mocked uploader
+      :- test1(uploader(user(42))).
+      :- test0(is_exempt_uploader).
+
+      % is_exempt_uploader/0 is expected to fail because it is
+      % is_exempt_uploader :- uploader(user(Id)), memberchk(Id, [104, 106]).
+
+      % Note that gerrit:remove_label does not depend on Gerrit repository,
+      % so its caller remove_label/1 is tested without any redefinition.
+
+      :- test1(remove_label('MyReview',[],[])).
+      :- test1(remove_label('MyReview',submit(),submit())).
diff --git a/prologtests/examples/aosp_rules.pl b/prologtests/examples/aosp_rules.pl
new file mode 100644
index 0000000..18e8a73
--- /dev/null
+++ b/prologtests/examples/aosp_rules.pl
@@ -0,0 +1,148 @@
+% A simplified and mocked AOSP rules.pl
+
+%%%%% wrapper functions for unit tests
+
+change_branch(X) :- gerrit:change_branch(X).
+change_project(X) :- gerrit:change_project(X).
+commit_author(U,N,M) :- gerrit:commit_author(U,N,M).
+commit_delta(X) :- gerrit:commit_delta(X).
+commit_label(L,U) :- gerrit:commit_label(L,U).
+uploader(X) :- gerrit:uploader(X).
+
+%%%%% true/false conditions
+
+% Special auto-merger accounts.
+is_exempt_uploader :-
+  uploader(user(Id)),
+  memberchk(Id, [104, 106]).
+
+% Build cop overrides everything.
+has_build_cop_override :-
+  commit_label(label('Build-Cop-Override', 1), _).
+
+is_exempt_from_reviews :-
+  or(is_exempt_uploader, has_build_cop_override).
+
+% Some files in selected projects need API review.
+needs_api_review :-
+  commit_delta('^(.*/)?api/|^(system-api/)'),
+  change_project(Project),
+  memberchk(Project, [
+    'platform/external/apache-http',
+    'platform/frameworks/base',
+    'platform/frameworks/support',
+    'platform/packages/services/Car',
+    'platform/prebuilts/sdk'
+  ]).
+
+% Some branches need DrNo review.
+needs_drno_review :-
+  change_branch(Branch),
+  memberchk(Branch, [
+    'refs/heads/my-alpha-dev',
+    'refs/heads/my-beta-dev'
+  ]).
+
+% Some author email addresses need Qualcomm-Review.
+needs_qualcomm_review :-
+  commit_author(_, _, M),
+  regex_matches(
+'.*@(qti.qualcomm.com|qca.qualcomm.com|quicinc.com|qualcomm.com)', M).
+
+% Special projects, branches, user accounts
+% can opt out owners review.
+opt_out_find_owners :-
+  change_branch(Branch),
+  memberchk(Branch, [
+    'refs/heads/my-beta-testing',
+    'refs/heads/my-testing'
+  ]).
+
+% Special projects, branches, user accounts
+% can opt in owners review.
+% Note that opt_out overrides opt_in.
+opt_in_find_owners :- true.
+
+
+%%%%% Simple list filters.
+
+remove_label(X, In, Out) :-
+  gerrit:remove_label(In, label(X, _), Out).
+
+% Slow but simple for short input list.
+remove_review_categories(In, Out) :-
+  remove_label('API-Review', In, L1),
+  remove_label('Code-Review', L1, L2),
+  remove_label('DrNo-Review', L2, L3),
+  remove_label('Owner-Review-Vote', L3, L4),
+  remove_label('Qualcomm-Review', L4, L5),
+  remove_label('Verified', L5, Out).
+
+
+%%%%% Missing rules in Gerrit Prolog Cafe.
+
+or(InA, InB) :- once((A;B)).
+
+not(Goal) :- Goal -> false ; true.
+
+% memberchk(+Element, +List)
+memberchk(X, [H|T]) :-
+  (X = H -> true ; memberchk(X, T)).
+
+maplist(Functor, In, Out) :-
+  (In = []
+  -> Out = []
+  ;  (In = [X1|T1],
+      Out = [X2|T2],
+      Goal =.. [Functor, X1, X2],
+      once(Goal),
+      maplist(Functor, T1, T2)
+     )
+  ).
+
+
+%%%%% Conditional rules and filters.
+
+submit_filter(In, Out) :-
+  (is_exempt_from_reviews
+  -> remove_review_categories(In, Out)
+  ;  (check_review(needs_api_review,
+          'API_Review', In, L1),
+      check_review(needs_drno_review,
+          'DrNo-Review', L1, L2),
+      check_review(needs_qualcomm_review,
+          'Qualcomm-Review', L2, L3),
+      check_find_owners(L3, Out)
+     )
+  ).
+
+check_review(NeedReview, Label, In, Out) :-
+  (NeedReview
+  -> Out = In
+  ;  remove_label(Label, In, Out)
+  ).
+
+% If opt_out_find_owners is true,
+% remove all 'Owner-Review-Vote' label;
+% else if opt_in_find_owners is true,
+%      call find_owners:submit_filter;
+% else default to no find_owners filter.
+check_find_owners(In, Out) :-
+  (opt_out_find_owners
+  -> remove_label('Owner-Review-Vote', In, Temp)
+  ; (opt_in_find_owners
+    -> find_owners:submit_filter(In, Temp)
+    ; In = Temp
+    )
+  ),
+  Temp =.. [submit | L1],
+  remove_label('Owner-Approved', L1, L2),
+  maplist(owner_may_to_need, L2, L3),
+  Out =.. [submit | L3].
+
+% change may(_) to need(_) to block submit.
+owner_may_to_need(In, Out) :-
+  (In = label('Owner-Review-Vote', may(_))
+  -> Out = label('Owner-Review-Vote', need(_))
+  ;  Out = In
+  ).
diff --git a/prologtests/examples/dummy.sh b/prologtests/examples/dummy.sh
new file mode 100755
index 0000000..2e0cca3
--- /dev/null
+++ b/prologtests/examples/dummy.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+# Skip all prolog tests for newer Java versions.
+# See https://github.com/bazelbuild/bazel/issues/9391
+# for more details why we cannot support running tests
+# on newer Java versions for now.
diff --git a/prologtests/examples/load.pl b/prologtests/examples/load.pl
new file mode 100644
index 0000000..f5b49e8
--- /dev/null
+++ b/prologtests/examples/load.pl
@@ -0,0 +1,26 @@
+% If you have 1.4.3 or older Prolog-Cafe, you need to
+% use (consult(load), load(load)) to get definition of load.
+% Then use load([f1,f2,...]) to load multiple source files.
+
+% Input is a list of file names or a single file name.
+% Use a conditional expression style without cut operator.
+load(X) :-
+  ( (X = [])
+  -> true
+  ; ( (X = [H|T])
+    -> (load_file(H), load(T))
+    ;  load_file(X)
+    )
+  ).
+
+% load_file is '$consult' without the bug of unbound 'File' variable.
+% For repeated unit tests, skip statistics and print_message.
+load_file(F) :- atom(F), !,
+  '$prolog_file_name'(F, PF),
+  open(PF, read, In),
+  % print_message(info, [loading,PF,'...']),
+  % statistics(runtime, _),
+  consult_stream(PF, In),
+  % statistics(runtime, [_,T]),
+  % print_message(info, [PF,'loaded in',T,msec]),
+  close(In).
diff --git a/prologtests/examples/rules.pl b/prologtests/examples/rules.pl
new file mode 100644
index 0000000..1a7b17c
--- /dev/null
+++ b/prologtests/examples/rules.pl
@@ -0,0 +1,29 @@
+% An example source file to be tested.
+
+% Add common rules missing in Prolog Cafe.
+memberchk(X, [H|T]) :-
+  (X = H) -> true ; memberchk(X, T).
+
+% A rule that can succeed/backtrack multiple times.
+super_users(1001).
+super_users(1002).
+
+% Deterministic rule that pass/fail only once.
+is_super_user(X) :- memberchk(X, [1001, 1002]).
+
+% Another rule that can pass 5 times.
+multi_users(101).
+multi_users(102).
+multi_users(103).
+multi_users(104).
+multi_users(105).
+
+% Okay, single deterministic fact.
+single_user(abc).
+
+% Wrap calls to gerrit repository, to be redefined in tests.
+change_owner(X) :- gerrit:change_owner(X).
+
+% To test is_owner without gerrit:change_owner,
+% we should redefine change_owner.
+is_owner(X) :- change_owner(X).
diff --git a/prologtests/examples/run.sh b/prologtests/examples/run.sh
new file mode 100755
index 0000000..947c153
--- /dev/null
+++ b/prologtests/examples/run.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+TESTS="t1 t2 t3"
+
+# Note that both t1.pl and t2.pl test code in rules.pl.
+# Unit tests are usually longer than the tested code.
+# So it is common to test one source file with multiple
+# unit test files.
+
+LF=$'\n'
+PASS=""
+FAIL=""
+
+echo "#### TEST_SRCDIR = ${TEST_SRCDIR}"
+
+if [ "${TEST_SRCDIR}" == "" ]; then
+  # Assume running alone
+  GERRIT_WAR="../../bazel-bin/gerrit.war"
+  SRCDIR="."
+else
+  # Assume running from bazel
+  GERRIT_WAR=`pwd`/gerrit.war
+  SRCDIR="prologtests/examples"
+fi
+
+# Default GERRIT_TMP is ~/.gerritcodereview/tmp,
+# which won't be writable in a bazel test sandbox.
+/bin/mkdir -p /tmp/gerrit
+export GERRIT_TMP=/tmp/gerrit
+
+for T in $TESTS
+do
+
+  pushd $SRCDIR
+
+  # Unit tests do not need to define clauses in packages.
+  # Use one prolog-shell per unit test, to avoid name collision.
+  echo "### Running test ${T}.pl"
+  echo "[$T]." | java -jar ${GERRIT_WAR} prolog-shell -q -s load.pl
+
+  if [ "x$?" != "x0" ]; then
+    echo "### Test ${T}.pl failed."
+    FAIL="${FAIL}${LF}FAIL: Test ${T}.pl"
+  else
+    PASS="${PASS}${LF}PASS: Test ${T}.pl"
+  fi
+
+  popd
+
+  # java -jar ../../bazel-bin/gerrit.war prolog-shell -s $T < /dev/null
+  # Calling prolog-shell with -s flag works for small files,
+  # but got run-time exception with t3.pl.
+  #   com.googlecode.prolog_cafe.exceptions.ReductionLimitException:
+  #   exceeded reduction limit of 1048576
+done
+
+echo "$PASS"
+
+if [ "$FAIL" != "" ]; then
+  echo "$FAIL"
+  exit 1
+fi
diff --git a/prologtests/examples/t1.pl b/prologtests/examples/t1.pl
new file mode 100644
index 0000000..caf9061
--- /dev/null
+++ b/prologtests/examples/t1.pl
@@ -0,0 +1,20 @@
+:- load([rules,utils]).
+:- begin_tests(t1).
+
+:- test1(true).     % expect true to pass
+:- test0(false).    % expect false to fail
+
+:- test1(X = 3).    % unification should pass
+:- test1(_ = 3).    % unification should pass
+:- test0(X \= 3).   % not-unified should fail
+
+% (7-4) should have expected result
+:- test1((X is (7-4), X =:= 3)).
+:- test1((X is (7-4), X =\= 4)).
+
+% memberchk should pass/fail exactly once
+:- test1(memberchk(3,[1,3,5,3])).
+:- test0(memberchk(2,[1,3,5,3])).
+:- test0(memberchk(2,[])).
+
+:- end_tests_or_halt(0).  % expect no failure
diff --git a/prologtests/examples/t2.pl b/prologtests/examples/t2.pl
new file mode 100644
index 0000000..9424b53
--- /dev/null
+++ b/prologtests/examples/t2.pl
@@ -0,0 +1,25 @@
+:- load([rules,utils]).
+:- begin_tests(t2).
+
+% expected to pass or fail once.
+:- test0(super_users(1000)).
+:- test1(super_users(1001)).
+
+:- test1(is_super_user(1001)).
+:- test1(is_super_user(1002)).
+:- test0(is_super_user(1003)).
+
+:- test1(super_users(X)).  % expected fail (pass twice)
+:- test1(multi_users(X)).  % expected fail (pass many times)
+
+:- test1(single_user(X)).  % expected pass once
+
+% Redefine change_owner, skip gerrit:change_owner,
+% then test is_owner without a gerrit repository.
+
+:- redefine(change_owner,1,(change_owner(42))).
+:- test1(is_owner(42)).
+:- test1(is_owner(X)).
+:- test0(is_owner(24)).
+
+:- end_tests_or_halt(2).  % expect 2 failures
diff --git a/prologtests/examples/t3.pl b/prologtests/examples/t3.pl
new file mode 100644
index 0000000..02badc0
--- /dev/null
+++ b/prologtests/examples/t3.pl
@@ -0,0 +1,69 @@
+:- load([aosp_rules,utils]).
+
+:- begin_tests(t3_basic_conditions).
+
+%% A negative test of is_exempt_uploader.
+:- redefine(uploader,1,uploader(user(42))).  % mocked uploader
+:- test1(uploader(user(42))).
+:- test0(is_exempt_uploader).
+
+%% Helper functions for positive test of is_exempt_uploader.
+test_is_exempt_uploader(List) :- maplist(test1_uploader, List, _).
+test1_uploader(X,_) :-
+  redefine(uploader,1,uploader(user(X))),
+  test1(uploader(user(X))),
+  test1(is_exempt_uploader).
+:- test_is_exempt_uploader([104, 106]).
+
+%% Test has_build_cop_override.
+:- redefine(commit_label,2,commit_label(label('Code-Review',1),user(102))).
+:- test0(has_build_cop_override).
+commit_label(label('Build-Cop-Override',1),user(101)).  % mocked 2nd label
+:- test1(has_build_cop_override).
+:- test1(commit_label(label(_,_),_)).           % expect fail, two matches
+:- test1(commit_label(label('Build-Cop-Override',_),_)).  % good, one pass
+
+%% TODO: more test for is_exempt_from_reviews.
+
+%% Test needs_api_review, which checks commit_delta and project.
+% Helper functions:
+test_needs_api_review(File, Project, Tester) :-
+  redefine(commit_delta,1,(commit_delta(R) :- regex_matches(R, File))),
+  redefine(change_project,1,change_project(Project)),
+  Goal =.. [Tester, needs_api_review],
+  msg('# check CL with changed file ', File, ' in ', Project),
+  once((Goal ; true)).  % do not backtrack
+
+:- test_needs_api_review('apio/test.cc', 'platform/art', test0).
+:- test_needs_api_review('api/test.cc', 'platform/art', test0).
+:- test_needs_api_review('api/test.cc', 'platform/prebuilts/sdk', test1).
+:- test_needs_api_review('d1/d2/api/test.cc', 'platform/prebuilts/sdk', test1).
+:- test_needs_api_review('system-api/d/t.c', 'platform/external/apache-http', test1).
+
+%% TODO: Test needs_drno_review, needs_qualcomm_review
+
+%% TODO: Test opt_out_find_owners.
+
+:- test1(opt_in_find_owners).  % default, unless opt_out_find_owners
+
+:- end_tests_or_halt(1).  % expect 1 failure of multiple commit_label
+
+%% Test remove_label
+:- begin_tests(t3_remove_label).
+
+:- test1(remove_label('MyReview',[],[])).
+:- test1(remove_label('MyReview',submit(),submit())).
+:- test1(remove_label(myR,[label(a,X)],[label(a,X)])).
+:- test1(remove_label(myR,[label(myR,_)],[])).
+:- test1(remove_label(myR,[label(a,X),label(myR,_)],[label(a,X)])).
+:- test1(remove_label(myR,submit(label(a,X)),submit(label(a,X)))).
+:- test1(remove_label(myR,submit(label(myR,_)),submit())).
+
+%% Test maplist
+double(X,Y) :- Y is X * X.
+:- test1(maplist(double, [2,4,6], [4,16,36])).
+:- test1(maplist(double, [], [])).
+
+:- end_tests_or_halt(0).  % expect no failure
+
+%% TODO: Add more tests.
diff --git a/prologtests/examples/utils.pl b/prologtests/examples/utils.pl
new file mode 100644
index 0000000..8d15067
--- /dev/null
+++ b/prologtests/examples/utils.pl
@@ -0,0 +1,78 @@
+%% Unit test helpers
+
+% Write one line message.
+msg(A) :- write(A), nl.
+msg(A,B) :- write(A), msg(B).
+msg(A,B,C) :- write(A), msg(B,C).
+msg(A,B,C,D) :- write(A), msg(B,C,D).
+msg(A,B,C,D,E) :- write(A), msg(B,C,D,E).
+msg(A,B,C,D,E,F) :- write(A), msg(B,C,D,E,F).
+
+% Redefine a caluse.
+redefine(Atom,Arity,Clause) :- abolish(Atom/Arity), assertz(Clause).
+
+% Increment/decrement of pass/fail counters.
+set_counters(N,X,Y) :- redefine(test_count,3,test_count(N,X,Y)).
+get_counters(N,X,Y) :- clause(test_count(N,X,Y), _) -> true ; (X=0, Y=0).
+inc_pass_count :- get_counters(N,P,F), P1 is P + 1, set_counters(N,P1,F).
+inc_fail_count :- get_counters(N,P,F), F1 is F + 1, set_counters(N,P,F1).
+
+% Report pass or fail of G.
+pass_1(G) :- msg('PASS: ', G), inc_pass_count.
+fail_1(G) :- msg('FAIL: ', G), inc_fail_count.
+
+% Report pass or fail of not(G).
+pass_0(G) :- msg('PASS: not(', G, ')'), inc_pass_count.
+fail_0(G) :- msg('FAIL: not(', G, ')'), inc_fail_count.
+
+% Report a test as failed if it passed 2 or more times
+pass_twice(G) :-
+  msg('FAIL: (pass twice): ', G),
+  inc_fail_count.
+pass_many(G) :-
+  G = [A,B|_],
+  length(G, N),
+  msg('FAIL: (pass ', N, ' times): ', [A,B,'...']),
+  inc_fail_count.
+
+% Test if G fails.
+test0(G) :- once(G) -> fail_0(G) ; pass_0(G).
+
+% Test if G passes exactly once.
+test1(G) :-
+  findall(G, G, S), length(S, N),
+  (N == 0
+   -> fail_1(G)
+   ;  (N == 1
+       -> pass_1(S)
+       ;  (N == 2 -> pass_twice(S) ; pass_many(S))
+      )
+  ).
+
+% Report the begin of test N.
+begin_tests(N) :-
+  nl,
+  msg('BEGIN test ',N),
+  set_counters(N,0,0).
+
+% Repot the end of test N and total pass/fail counts,
+% and check if the numbers are as exected OutP/OutF.
+end_tests(OutP,OutF) :-
+  get_counters(N,P,F),
+  (OutP = P
+   -> msg('Expected #PASS: ', OutP)
+   ;  (msg('ERROR: expected #PASS is ',OutP), !, fail)
+  ),
+  (OutF = F
+   -> msg('Expected #FAIL: ', OutF)
+   ;  (msg('ERROR: expected #FAIL is ',OutF), !, fail)
+  ),
+  msg('END test ', N),
+  nl.
+
+% Repot the end of test N and total pass/fail counts.
+end_tests(N) :- end_tests(N,_,_).
+
+% Call end_tests/2 and halt if the fail count is unexpected.
+end_tests_or_halt(ExpectedFails) :-
+  end_tests(_,ExpectedFails); (flush_output, halt(1)).
diff --git a/proto/cache.proto b/proto/cache.proto
index b34dbf3..10e0216 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -75,7 +75,7 @@
 // Instead, we just take the tedious yet simple approach of having a "has_foo"
 // field for each nullable field "foo", indicating whether or not foo is null.
 //
-// Next ID: 19
+// Next ID: 23
 message ChangeNotesStateProto {
   // Effectively required, even though the corresponding ChangeNotesState field
   // is optional, since the field is only absent when NoteDb is disabled, in
@@ -110,8 +110,8 @@
     string submission_id = 13;
     bool has_submission_id = 14;
 
-    int32 assignee = 15;
-    bool has_assignee = 16;
+    reserved 15;  // assignee
+    reserved 16;  // has_assignee
 
     string status = 17;
     bool has_status = 18;
@@ -130,7 +130,7 @@
   // which case attempting to use the ChangeNotesCache is programmer error.
   ChangeColumnsProto columns = 3;
 
-  repeated int32 past_assignee = 4;
+  reserved  4; // past_assignee
 
   repeated string hashtag = 5;
 
@@ -178,11 +178,25 @@
   // Raw ChangeMessage proto as produced by ChangeMessageProtoConverter.
   repeated bytes change_message = 15;
 
-  // JSON produced from com.google.gerrit.reviewdb.client.Comment.
+  // JSON produced from com.google.gerrit.entities.Comment.
   repeated string published_comment = 16;
 
   reserved 17;  // read_only_until
   reserved 18;  // has_read_only_until
+
+  // Number of updates to the change's meta ref.
+  int32 update_count = 19;
+
+  string server_id = 20;
+  bool has_server_id = 21;
+
+  message AssigneeStatusUpdateProto {
+    int64 date = 1;
+    int32 updated_by = 2;
+    int32 current_assignee = 3;
+    bool has_current_assignee = 4;
+  }
+  repeated AssigneeStatusUpdateProto assignee_update = 22;
 }
 
 
diff --git a/proto/entities.proto b/proto/entities.proto
index d2851d3..374b47c 100644
--- a/proto/entities.proto
+++ b/proto/entities.proto
@@ -18,19 +18,19 @@
 
 option java_package = "com.google.gerrit.proto";
 
-// Serialized form of com.google.gerrit.reviewdb.client.Change.Id.
+// Serialized form of com.google.gerrit.entities.Change.Id.
 // Next ID: 2
 message Change_Id {
   required int32 id = 1;
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.Change.Key.
+// Serialized form of com.google.gerrit.entities.Change.Key.
 // Next ID: 2
 message Change_Key {
   optional string id = 1;
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.Change.
+// Serialized form of com.google.gerrit.entities.Change.
 // Next ID: 24
 message Change {
   required Change_Id change_id = 1;
@@ -61,14 +61,14 @@
   reserved 101;  // note_db_state
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.ChangeMessage.
+// Serialized form of com.google.gerrit.enities.ChangeMessage.
 // Next ID: 3
 message ChangeMessage_Key {
   required Change_Id change_id = 1;
   required string uuid = 2;
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.ChangeMessage.
+// Serialized form of com.google.gerrit.entities.ChangeMessage.
 // Next ID: 8
 message ChangeMessage {
   required ChangeMessage_Key key = 1;
@@ -80,18 +80,18 @@
   optional Account_Id real_author = 7;
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.PatchSet.Id.
+// Serialized form of com.google.gerrit.entities.PatchSet.Id.
 // Next ID: 3
 message PatchSet_Id {
   required Change_Id change_id = 1;
-  required int32 patch_set_id = 2;
+  required int32 id = 2;
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.PatchSet.
+// Serialized form of com.google.gerrit.entities.PatchSet.
 // Next ID: 10
 message PatchSet {
   required PatchSet_Id id = 1;
-  optional RevId revision = 2;
+  optional ObjectId commitId = 2;
   optional Account_Id uploader_account_id = 3;
   optional fixed64 created_on = 4;
   optional string groups = 6;
@@ -103,27 +103,27 @@
   reserved 7;  // pushCertficate
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.Account.Id.
+// Serialized form of com.google.gerrit.entities.Account.Id.
 // Next ID: 2
 message Account_Id {
   required int32 id = 1;
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.LabelId.
+// Serialized form of com.google.gerrit.entities.LabelId.
 // Next ID: 2
 message LabelId {
   required string id = 1;
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.PatchSetApproval.Key.
+// Serialized form of com.google.gerrit.entities.PatchSetApproval.Key.
 // Next ID: 4
 message PatchSetApproval_Key {
   required PatchSet_Id patch_set_id = 1;
   required Account_Id account_id = 2;
-  required LabelId category_id = 3;
+  required LabelId label_id = 3;
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.PatchSetApproval.
+// Serialized form of com.google.gerrit.entities.PatchSetApproval.
 // Next ID: 9
 message PatchSetApproval {
   required PatchSetApproval_Key key = 1;
@@ -138,21 +138,22 @@
   reserved 5;  // changeSortKey
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.Project.NameKey.
+// Serialized form of com.google.gerrit.entities.Project.NameKey.
 // Next ID: 2
 message Project_NameKey {
   optional string name = 1;
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.Branch.NameKey.
+// Serialized form of com.google.gerrit.entities.Branch.NameKey.
 // Next ID: 3
 message Branch_NameKey {
-  optional Project_NameKey project_name = 1;
-  optional string branch_name = 2;
+  optional Project_NameKey project = 1;
+  optional string branch = 2;
 }
 
-// Serialized form of com.google.gerrit.reviewdb.client.RevId.
+// Serialized form of org.eclipse.jgit.lib.ObjectId.
 // Next ID: 2
-message RevId {
-  optional string id = 1;
+message ObjectId {
+  // Hex string representation of the ID.
+  optional string name = 1;
 }
diff --git a/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html b/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
index 4923143..efd760f 100644
--- a/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
+++ b/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
@@ -75,11 +75,6 @@
           <a href="?id=https://login.launchpad.net/%2Bopenid" id="id_launchpad">Sign in with a Launchpad ID</a>
         </div>
 
-        <div id="provider_yahoo">
-          <img height="16" width="16" src="data:image/gif;base64,R0lGODlhEAAQAPECAAAAAP8AAP///8zMzCH5BAEAAAMALAAAAAAQABAAAAIqnI+py30BY3AgAjCkfJDjiIAQlgUkNxqWkqrm0honKk7KhZOzw/f+fygAADs=" />
-          <a href="?id=https://me.yahoo.com" id="id_yahoo">Sign in with a Yahoo! ID</a>
-        </div>
-
         <div style="margin-top: 25px;">
           <h2>What is OpenID?</h2>
           <p>OpenID provides secure single-sign-on, without revealing your passwords to this website.</p>
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 8f151a8..3feb1b4 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -19,11 +19,14 @@
 {template .Index}
   {@param canonicalPath: ?}
   {@param staticResourcePath: ?}
+  {@param gerritInitialData: /** {string} map of REST endpoint to response for startup. */ ?}
   {@param? assetsPath: ?}  /** {string} URL to static assets root, if served from CDN. */
   {@param? assetsBundle: ?}  /** {string} Assets bundle .html file, served from $assetsPath. */
   {@param? faviconPath: ?}
   {@param? versionInfo: ?}
-  {@param? polymer2: ?}
+  {@param? polyfillCE: ?}
+  {@param? polyfillSD: ?}
+  {@param? polyfillSC: ?}
   <!DOCTYPE html>{\n}
   <html lang="en">{\n}
   <meta charset="utf-8">{\n}
@@ -36,12 +39,27 @@
   </noscript>
 
   <script>
+    // Disable extra font load from paper-styles
+    window.polymerSkipLoadingFontRoboto = true;
     window.CLOSURE_NO_DEPS = true;
     {if $canonicalPath != ''}window.CANONICAL_PATH = '{$canonicalPath}';{/if}
     {if $versionInfo}window.VERSION_INFO = '{$versionInfo}';{/if}
     {if $staticResourcePath != ''}window.STATIC_RESOURCE_PATH = '{$staticResourcePath}';{/if}
     {if $assetsPath}window.ASSETS_PATH = '{$assetsPath}';{/if}
-    {if $polymer2}window.POLYMER2 = true;{/if}
+    {if $polyfillCE}if (window.customElements) window.customElements.forcePolyfill = true;{/if}
+    {if $polyfillSD}{literal}ShadyDOM = { force: true };{/literal}{/if}
+    {if $polyfillSC}{literal}ShadyCSS = { shimcssproperties: true};{/literal}{/if}
+    {if $gerritInitialData}
+      // INITIAL_DATA is a string that represents a JSON map. It's inlined here so that we can
+      // spare calls to the API when starting up the app.
+      // The map maps from endpoint to returned value. This matches Gerrit's REST API 1:1, so the
+      // values here can be used as a drop-in replacement for calls to the API.
+      //
+      // Example:
+      // '/config/server/version' => '3.0.0-468-g0757b52a7d'
+      // '/accounts/self/detail' => { 'username' : 'gerrit-user' }
+      window.INITIAL_DATA = JSON.parse({$gerritInitialData});
+    {/if}
   </script>{\n}
 
   {if $faviconPath}
@@ -60,7 +78,9 @@
   <link rel="preload" href="{$staticResourcePath}/fonts/Roboto-Medium.woff" as="font" type="font/woff" crossorigin="anonymous">{\n}
   <link rel="stylesheet" href="{$staticResourcePath}/styles/fonts.css">{\n}
   <link rel="stylesheet" href="{$staticResourcePath}/styles/main.css">{\n}
+
   <script src="{$staticResourcePath}/bower_components/webcomponentsjs/webcomponents-lite.js"></script>{\n}
+
   // Content between webcomponents-lite and the load of the main app element
   // run before polymer-resin is installed so may have security consequences.
   // Contact your local security engineer if you have any questions, and
diff --git a/resources/com/google/gerrit/pgm/init/gerrit.sh b/resources/com/google/gerrit/pgm/init/gerrit.sh
index d92ec51..ce858d5 100755
--- a/resources/com/google/gerrit/pgm/init/gerrit.sh
+++ b/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -345,8 +345,9 @@
 
 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
-  RUN_ARGS="$RUN_ARGS --slave --enable-httpd --headless"
+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`
 if test -n "$DAEMON_OPTS" ; then
diff --git a/resources/com/google/gerrit/server/mail/Abandoned.soy b/resources/com/google/gerrit/server/mail/Abandoned.soy
index 2785ffc..d5aac0e 100644
--- a/resources/com/google/gerrit/server/mail/Abandoned.soy
+++ b/resources/com/google/gerrit/server/mail/Abandoned.soy
@@ -17,7 +17,7 @@
 {namespace com.google.gerrit.server.mail.template}
 
 /**
- * .Abandoned template will determine the contents of the email related to a
+ * The .Abandoned template will determine the contents of the email related to a
  * change being abandoned.
  */
 {template .Abandoned kind="text"}
diff --git a/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy b/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy
index e997776..e88c424 100644
--- a/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy
+++ b/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy
@@ -62,3 +62,8 @@
   This might be caused by an ongoing maintenance or a data corruption.
   {call .InboundEmailRejectionFooter /}
 {/template}
+
+{template .InboundEmailRejection_COMMENT_REJECTED kind="text"}
+  Gerrit Code Review rejected one or more comments because they did not pass validation.
+  {call .InboundEmailRejectionFooter /}
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy b/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy
index f879270..e17508d 100644
--- a/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy
+++ b/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy
@@ -78,3 +78,10 @@
   <p>
   {call .InboundEmailRejectionFooterHtml /}
 {/template}
+
+{template .InboundEmailRejectionHtml_COMMENT_REJECTED}
+  <p>
+    Gerrit Code Review rejected one or more comments because they did not pass validation.
+  </p>
+  {call .InboundEmailRejectionFooterHtml /}
+{/template}
diff --git a/tools/BUILD b/tools/BUILD
index 9ccc065..f5da450 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -21,72 +21,75 @@
 # enabled. This warnings list is originally based on:
 # https://github.com/bazelbuild/BUILD_file_generator/blob/master/tools/bazel_defs/java.bzl
 # However, feel free to add any additional errors. Thus far they have all been pretty useful.
+# TODO(davido): Enable ImmutableAnnotationChecker again when these issues are fixed:
+# https://github.com/google/error-prone/issues/1348
+# https://github.com/bazelbuild/bazel/issues/9378
 java_package_configuration(
     name = "error_prone",
     javacopts = [
         "-XepDisableWarningsInGeneratedCode",
-        "-Xep:AmbiguousMethodReference:WARN",
+        "-Xep:AmbiguousMethodReference:ERROR",
         "-Xep:AutoValueFinalMethods:ERROR",
-        "-Xep:BadAnnotationImplementation:WARN",
-        "-Xep:BadComparable:WARN",
+        "-Xep:BadAnnotationImplementation:ERROR",
+        "-Xep:BadComparable:ERROR",
         "-Xep:BoxedPrimitiveConstructor:ERROR",
-        "-Xep:CannotMockFinalClass:WARN",
+        "-Xep:CannotMockFinalClass:ERROR",
         "-Xep:ClassCanBeStatic:ERROR",
-        "-Xep:ClassNewInstance:WARN",
+        "-Xep:ClassNewInstance:ERROR",
         "-Xep:DateFormatConstant:ERROR",
         "-Xep:DefaultCharset:ERROR",
-        "-Xep:DoubleCheckedLocking:WARN",
-        "-Xep:ElementsCountedInLoop:WARN",
-        "-Xep:EqualsHashCode:WARN",
-        "-Xep:EqualsIncompatibleType:WARN",
-        "-Xep:ExpectedExceptionChecker:OFF",
-        "-Xep:Finally:WARN",
-        "-Xep:FloatingPointLiteralPrecision:WARN",
-        "-Xep:FragmentInjection:WARN",
-        "-Xep:FragmentNotInstantiable:WARN",
+        "-Xep:DoubleCheckedLocking:ERROR",
+        "-Xep:ElementsCountedInLoop:ERROR",
+        "-Xep:EqualsHashCode:ERROR",
+        "-Xep:EqualsIncompatibleType:ERROR",
+        "-Xep:ExpectedExceptionChecker:ERROR",
+        "-Xep:Finally:ERROR",
+        "-Xep:FloatingPointLiteralPrecision:ERROR",
+        "-Xep:FragmentInjection:ERROR",
+        "-Xep:FragmentNotInstantiable:ERROR",
         "-Xep:FunctionalInterfaceClash:ERROR",
-        "-Xep:FutureReturnValueIgnored:WARN",
-        "-Xep:GetClassOnEnum:WARN",
-        "-Xep:ImmutableAnnotationChecker:WARN",
-        "-Xep:ImmutableEnumChecker:WARN",
-        "-Xep:IncompatibleModifiers:WARN",
-        "-Xep:InjectOnConstructorOfAbstractClass:WARN",
-        "-Xep:InputStreamSlowMultibyteRead:WARN",
-        "-Xep:IterableAndIterator:WARN",
-        "-Xep:JUnit3FloatingPointComparisonWithoutDelta:WARN",
-        "-Xep:JUnitAmbiguousTestClass:WARN",
-        "-Xep:LiteralClassName:WARN",
+        "-Xep:FutureReturnValueIgnored:ERROR",
+        "-Xep:GetClassOnEnum:ERROR",
+        "-Xep:ImmutableAnnotationChecker:OFF",
+        "-Xep:ImmutableEnumChecker:ERROR",
+        "-Xep:IncompatibleModifiers:ERROR",
+        "-Xep:InjectOnConstructorOfAbstractClass:ERROR",
+        "-Xep:InputStreamSlowMultibyteRead:ERROR",
+        "-Xep:IterableAndIterator:ERROR",
+        "-Xep:JUnit3FloatingPointComparisonWithoutDelta:ERROR",
+        "-Xep:JUnitAmbiguousTestClass:ERROR",
+        "-Xep:LiteralClassName:ERROR",
         "-Xep:MissingCasesInEnumSwitch:ERROR",
         "-Xep:MissingFail:ERROR",
-        "-Xep:MissingOverride:WARN",
+        "-Xep:MissingOverride:ERROR",
         "-Xep:MutableConstantField:ERROR",
-        "-Xep:NarrowingCompoundAssignment:WARN",
-        "-Xep:NonAtomicVolatileUpdate:WARN",
-        "-Xep:NonOverridingEquals:WARN",
-        "-Xep:NullableConstructor:WARN",
-        "-Xep:NullablePrimitive:WARN",
-        "-Xep:NullableVoid:WARN",
+        "-Xep:NarrowingCompoundAssignment:ERROR",
+        "-Xep:NonAtomicVolatileUpdate:ERROR",
+        "-Xep:NonOverridingEquals:ERROR",
+        "-Xep:NullableConstructor:ERROR",
+        "-Xep:NullablePrimitive:ERROR",
+        "-Xep:NullableVoid:ERROR",
         "-Xep:ObjectToString:ERROR",
         "-Xep:OperatorPrecedence:ERROR",
-        "-Xep:OverridesGuiceInjectableMethod:WARN",
-        "-Xep:PreconditionsInvalidPlaceholder:WARN",
-        "-Xep:ProtoFieldPreconditionsCheckNotNull:WARN",
-        "-Xep:ProtocolBufferOrdinal:WARN",
-        "-Xep:ReferenceEquality:WARN",
-        "-Xep:RequiredModifiers:WARN",
-        "-Xep:ShortCircuitBoolean:WARN",
-        "-Xep:SimpleDateFormatConstant:WARN",
-        "-Xep:StaticGuardedByInstance:WARN",
-        "-Xep:StringEquality:WARN",
-        "-Xep:SynchronizeOnNonFinalField:WARN",
-        "-Xep:TruthConstantAsserts:WARN",
-        "-Xep:TypeParameterShadowing:WARN",
-        "-Xep:TypeParameterUnusedInFormals:WARN",
-        "-Xep:URLEqualsHashCode:WARN",
-        "-Xep:UnsynchronizedOverridesSynchronized:WARN",
+        "-Xep:OverridesGuiceInjectableMethod:ERROR",
+        "-Xep:PreconditionsInvalidPlaceholder:ERROR",
+        "-Xep:ProtoFieldPreconditionsCheckNotNull:ERROR",
+        "-Xep:ProtocolBufferOrdinal:ERROR",
+        "-Xep:ReferenceEquality:ERROR",
+        "-Xep:RequiredModifiers:ERROR",
+        "-Xep:ShortCircuitBoolean:ERROR",
+        "-Xep:SimpleDateFormatConstant:ERROR",
+        "-Xep:StaticGuardedByInstance:ERROR",
+        "-Xep:StringEquality:ERROR",
+        "-Xep:SynchronizeOnNonFinalField:ERROR",
+        "-Xep:TruthConstantAsserts:ERROR",
+        "-Xep:TypeParameterShadowing:ERROR",
+        "-Xep:TypeParameterUnusedInFormals:ERROR",
+        "-Xep:URLEqualsHashCode:ERROR",
+        "-Xep:UnsynchronizedOverridesSynchronized:ERROR",
         "-Xep:UnusedException:ERROR",
-        "-Xep:WaitNotInLoop:WARN",
-        "-Xep:WildcardImport:WARN",
+        "-Xep:WaitNotInLoop:ERROR",
+        "-Xep:WildcardImport:ERROR",
     ],
     packages = ["error_prone_packages"],
 )
@@ -98,10 +101,14 @@
         "//javatests/...",
         "//plugins/codemirror-editor/...",
         "//plugins/commit-message-length-validator/...",
+        "//plugins/delete-project/...",
         "//plugins/download-commands/...",
+        "//plugins/gitiles/...",
         "//plugins/hooks/...",
+        "//plugins/plugin-manager/...",
         "//plugins/replication/...",
         "//plugins/reviewnotes/...",
         "//plugins/singleusergroup/...",
+        "//plugins/webhooks/...",
     ],
 )
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
index 3add025..62b4010 100644
--- a/tools/bzl/javadoc.bzl
+++ b/tools/bzl/javadoc.bzl
@@ -61,14 +61,14 @@
         command = " && ".join(cmd),
     )
 
-java_doc = rule(
+_java_doc = rule(
     attrs = {
         "external_docs": attr.string_list(),
         "libs": attr.label_list(allow_files = False),
         "pkgs": attr.string_list(),
         "title": attr.string(),
         "_jdk": attr.label(
-            default = Label("@bazel_tools//tools/jdk:current_java_runtime"),
+            default = Label("@bazel_tools//tools/jdk:current_host_java_runtime"),
             allow_files = True,
             providers = [java_common.JavaRuntimeInfo],
         ),
@@ -76,3 +76,16 @@
     outputs = {"zip": "%{name}.zip"},
     implementation = _impl,
 )
+
+def java_doc(**kwargs):
+    libs = kwargs.get("libs", [])
+    libs = libs + select({
+        "//:java11": [],
+        "//:java_next": [],
+        # TODO(davido): Remove this dependency, when Java 8 support is removed.
+        # auto-value generates @javax.annotation.Generated annotation on generated
+        # classes when Java 8 source compatibility level is used, but Java 11 and
+        # later don't have this class any more.
+        "//conditions:default": ["//lib:javax-annotation"],
+    })
+    _java_doc(**dict(kwargs, libs = libs))
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 9160f1d..9baa30c 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -26,7 +26,7 @@
     else:
         fail("repository %s not in {%s,%s}" % (repository, GERRIT, NPMJS))
 
-    python = ctx.which("python")
+    python = ctx.which("python3")
     script = ctx.path(ctx.attr._download_script)
 
     args = [python, script, "-o", dest, "-u", url, "-v", sha1]
@@ -49,7 +49,7 @@
 
 # for use in repo rules.
 def _run_npm_binary_str(ctx, tarball, args):
-    python_bin = ctx.which("python")
+    python_bin = ctx.which("python3")
     return " ".join([
         str(python_bin),
         str(ctx.path(ctx.attr._run_npm)),
@@ -63,7 +63,7 @@
     version_name = "%s__version.json" % ctx.name
 
     cmd = [
-        ctx.which("python"),
+        ctx.which("python3"),
         ctx.path(ctx.attr._download_bower),
         "-b",
         "%s" % _run_npm_binary_str(ctx, ctx.attr._bower_archive, []),
@@ -310,8 +310,15 @@
     destdir = ctx.outputs.html.path + ".dir"
     zips = [z for d in ctx.attr.deps for z in d[ComponentInfo].transitive_zipfiles.to_list()]
 
+    # We are splitting off the package dir from the app.path such that
+    # we can set the package dir as the root for the bundler, which means
+    # that absolute imports are interpreted relative to that root.
+    pkg_dir = ctx.attr.pkg.lstrip("/")
+    app_path = ctx.file.app.path
+    app_path = app_path[app_path.index(pkg_dir) + len(pkg_dir):]
+
     hermetic_npm_binary = " ".join([
-        "python",
+        "python3",
         "$p/" + ctx.file._run_npm.path,
         "$p/" + ctx.file._bundler_archive.path,
         "--inline-scripts",
@@ -320,10 +327,11 @@
         "--strip-comments",
         "--out-file",
         "$p/" + bundled.path,
-        ctx.file.app.path,
+        "--root",
+        pkg_dir,
+        app_path,
     ])
 
-    pkg_dir = ctx.attr.pkg.lstrip("/")
     cmd = " && ".join([
         # unpack dependencies.
         "export PATH",
@@ -361,7 +369,7 @@
 
     if ctx.attr.split:
         hermetic_npm_command = "export PATH && " + " ".join([
-            "python",
+            "python3",
             ctx.file._run_npm.path,
             ctx.file._crisper_archive.path,
             "--script-in-head=false",
@@ -480,8 +488,8 @@
         name = name + "_bin",
         compilation_level = "WHITESPACE_ONLY",
         defs = [
-            "--polymer_version=1",
-            "--language_out=ECMASCRIPT6",
+            "--polymer_version=2",
+            "--language_out=ECMASCRIPT_2017",
             "--rewrite_polyfills=false",
         ],
         deps = [
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
index 1cf82ea..66d7230 100644
--- a/tools/bzl/junit.bzl
+++ b/tools/bzl/junit.bzl
@@ -70,7 +70,6 @@
     # Enforce JDK 8 compatibility on Java 9, see
     # https://docs.oracle.com/javase/9/intl/internationalization-enhancements-jdk-9.htm#JSINT-GUID-AF5AECA7-07C1-4E7D-BC10-BC7E73DC6C7F
     "-Djava.locale.providers=COMPAT,CLDR,SPI",
-    "--add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED",
 ]
 
 def junit_tests(name, srcs, **kwargs):
@@ -82,7 +81,7 @@
     )
     jvm_flags = kwargs.get("jvm_flags", [])
     jvm_flags = jvm_flags + select({
-        "//:java9": POST_JDK8_OPTS,
+        "//:java11": POST_JDK8_OPTS,
         "//:java_next": POST_JDK8_OPTS,
         "//conditions:default": [],
     })
diff --git a/tools/bzl/license-map.py b/tools/bzl/license-map.py
index 2779130..e233318 100644
--- a/tools/bzl/license-map.py
+++ b/tools/bzl/license-map.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # reads bazel query XML files, to join target names with their licenses.
 
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
index 5a6bf7f..ec89c41 100644
--- a/tools/bzl/license.bzl
+++ b/tools/bzl/license.bzl
@@ -25,7 +25,7 @@
     # post process the XML into our favorite format.
     native.genrule(
         name = "gen_license_txt_" + name,
-        cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
+        cmd = "python3 $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
         outs = [name + ".gen.txt"],
         tools = tools,
         **kwargs
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
index 177e80b..fa7fbf7 100644
--- a/tools/bzl/maven_jar.bzl
+++ b/tools/bzl/maven_jar.bzl
@@ -8,6 +8,10 @@
 
 ECLIPSE = "ECLIPSE:"
 
+MAVEN_SNAPSHOT = "https://oss.sonatype.org/content/repositories/snapshots"
+
+SNAPSHOT = "-SNAPSHOT-"
+
 def _maven_release(ctx, parts):
     """induce jar and url name from maven coordinates."""
     if len(parts) not in [3, 4]:
@@ -20,9 +24,25 @@
         group, artifact, version = parts
         file_version = version
 
+    repository = ctx.attr.repository
+
+    if "-SNAPSHOT-" in version:
+        start = version.index(SNAPSHOT)
+        end = start + len(SNAPSHOT) - 1
+
+        # file version without snapshot constant, but with post snapshot suffix
+        file_version = version[:start] + version[end:]
+
+        # version without post snapshot suffix
+        version = version[:end]
+
+        # overwrite the repository with Maven snapshot repository
+        repository = MAVEN_SNAPSHOT
+
     jar = artifact.lower() + "-" + file_version
+
     url = "/".join([
-        ctx.attr.repository,
+        repository,
         group.replace(".", "/"),
         artifact,
         version,
@@ -134,7 +154,7 @@
     binjar_path = ctx.path("/".join(["jar", binjar]))
     binurl = url + ".jar"
 
-    python = ctx.which("python")
+    python = ctx.which("python3")
     script = ctx.path(ctx.attr._download_script)
 
     args = [python, script, "-o", binjar_path, "-u", binurl]
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 0387510..7534501 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -22,6 +22,7 @@
         manifest_entries = [],
         dir_name = None,
         target_suffix = "",
+        deploy_env = [],
         **kwargs):
     java_library(
         name = name + "__plugin",
@@ -47,6 +48,7 @@
         runtime_deps = [
             ":%s__plugin" % name,
         ] + static_jars,
+        deploy_env = deploy_env,
         visibility = ["//visibility:public"],
         **kwargs
     )
diff --git a/tools/download_file.py b/tools/download_file.py
index f86fd3e..2af2c07 100755
--- a/tools/download_file.py
+++ b/tools/download_file.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,7 +17,7 @@
 
 import argparse
 from hashlib import sha1
-from os import link, makedirs, path, remove
+from os import environ, link, makedirs, path, remove
 import shutil
 from subprocess import check_call, CalledProcessError
 from sys import stderr
@@ -25,7 +25,10 @@
 from zipfile import ZipFile, BadZipfile, LargeZipFile
 
 GERRIT_HOME = path.expanduser('~/.gerritcodereview')
-CACHE_DIR = path.join(GERRIT_HOME, 'bazel-cache', 'downloaded-artifacts')
+CACHE_DIR = environ.get(
+    'GERRIT_CACHE_HOME',
+    path.join(GERRIT_HOME, 'bazel-cache', 'downloaded-artifacts'))
+
 LOCAL_PROPERTIES = 'local.properties'
 
 
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 649f7da..77dc226 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2016 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -172,13 +172,23 @@
         impl = xml.dom.minidom.getDOMImplementation()
         return impl.createDocument(None, 'classpath', None)
 
-    def classpathentry(kind, path, src=None, out=None, exported=None):
+    def import_jgit_sources():
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit/src')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit/resources')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.archive/src',
+            excluding='org/eclipse/jgit/archive/FormatActivator.java')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.archive/resources')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/src')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/resources')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.junit/src')
+
+    def classpathentry(kind, path, src=None, out=None, exported=None, excluding=None):
         e = doc.createElement('classpathentry')
         e.setAttribute('kind', kind)
         # Excluding the BUILD file, to avoid the Eclipse warnings:
         # "The resource is a duplicate of ..."
         if kind == 'src':
-            e.setAttribute('excluding', '**/BUILD')
+            e.setAttribute('excluding', '**/BUILD' if not excluding else excluding)
         e.setAttribute('path', path)
         if src:
             e.setAttribute('sourcepath', src)
@@ -193,7 +203,7 @@
             testAtt.setAttribute('name', 'test')
             testAtt.setAttribute('value', 'true')
             atts.appendChild(testAtt)
-        if "apt_generated" in path:
+        if "apt_generated" in path or "modules/jgit" in path:
             if not atts:
                 atts = doc.createElement('attributes')
             ignoreOptionalProblems = doc.createElement('attribute')
@@ -216,6 +226,7 @@
 
     # Classpath entries are absolute for cross-cell support
     java_library = re.compile('bazel-out/.*?-fastbuild/bin/(.*)/[^/]+[.]jar$')
+    proto_library = re.compile('bazel-out/.*?-fastbuild/bin/(.*)proto/(.*)_proto-speed[.]jar$')
     srcs = re.compile('(.*/external/[^/]+)/jar/(.*)[.]jar')
     for p in _query_classpath(MAIN):
         if p.endswith('-src.jar'):
@@ -230,11 +241,7 @@
                p.endswith('com_google_protobuf/libprotobuf_java.jar') or \
                p.endswith('lucene-core-and-backward-codecs-merged_deploy.jar'):
                 lib.add(p)
-            # JGit dependency from external repository
-            if 'gerrit-' not in p and 'jgit' in p:
-                lib.add(p)
-            # Assume any jars in /proto/ are from java_proto_library rules
-            if '/bin/proto/' in p:
+            if proto_library.match(p) :
                 proto.add(p)
         else:
             # Don't mess up with Bazel internal test runner dependencies.
@@ -249,6 +256,7 @@
     classpathentry('src', 'java')
     classpathentry('src', 'javatests', out='eclipse-out/test')
     classpathentry('src', 'resources')
+    import_jgit_sources()
     for s in sorted(src):
         out = None
 
@@ -296,8 +304,8 @@
                 classpathentry('lib', j, s)
 
     for p in sorted(proto):
-        s = p.replace('-fastbuild/bin/proto/lib', '-fastbuild/genfiles/proto/')
-        s = p.replace('-fastbuild/bin/proto/testing/lib', '-fastbuild/genfiles/proto/testing/')
+        s = p.replace('/proto/lib', '/proto/')
+        s = s.replace('/proto/testing/lib', '/proto/testing/')
         s = s.replace('.jar', '-src.jar')
         classpathentry('lib', p, s)
 
diff --git a/tools/js/download_bower.py b/tools/js/download_bower.py
index 1df4b82..2a75fc1 100755
--- a/tools/js/download_bower.py
+++ b/tools/js/download_bower.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2015 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,8 +25,12 @@
 
 import bowerutil
 
-CACHE_DIR = os.path.expanduser(os.path.join(
-    '~', '.gerritcodereview', 'bazel-cache', 'downloaded-artifacts'))
+CACHE_DIR = os.environ.get(
+    'GERRIT_CACHE_HOME',
+    os.path.expanduser(os.path.join(
+        '~', '.gerritcodereview', 'bazel-cache', 'downloaded-artifacts'
+    ))
+)
 
 
 def bower_cmd(bower, *args):
diff --git a/tools/js/npm_pack.py b/tools/js/npm_pack.py
index 57f3166..33b38a0 100755
--- a/tools/js/npm_pack.py
+++ b/tools/js/npm_pack.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2015 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/js/run_npm_binary.py b/tools/js/run_npm_binary.py
index bdee5ab..31f8a54 100644
--- a/tools/js/run_npm_binary.py
+++ b/tools/js/run_npm_binary.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2015 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 581ba07..c84dfa2 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>3.0.17-SNAPSHOT</version>
+  <version>3.1.17-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
@@ -35,12 +35,24 @@
       <name>David Pursehouse</name>
     </developer>
     <developer>
+      <name>Dmitrii Filippov</name>
+    </developer>
+    <developer>
       <name>Edwin Kempin</name>
     </developer>
     <developer>
+      <name>Gal Paikin</name>
+    </developer>
+    <developer>
       <name>Han-Wen Nienhuys</name>
     </developer>
     <developer>
+      <name>Jacek Centkowski</name>
+    </developer>
+    <developer>
+      <name>Joerg Zieren</name>
+    </developer>
+    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
@@ -53,7 +65,10 @@
       <name>Matthias Sohn</name>
     </developer>
     <developer>
-      <name>Ole Rehmsen</name>
+      <name>Milutin Kristofic</name>
+    </developer>
+    <developer>
+      <name>Nasser Grainawi</name>
     </developer>
     <developer>
       <name>Patrick Hiesel</name>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 80e27c0..3a50ec1 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>3.0.17-SNAPSHOT</version>
+  <version>3.1.17-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
@@ -35,12 +35,24 @@
       <name>David Pursehouse</name>
     </developer>
     <developer>
+      <name>Dmitrii Filippov</name>
+    </developer>
+    <developer>
       <name>Edwin Kempin</name>
     </developer>
     <developer>
+      <name>Gal Paikin</name>
+    </developer>
+    <developer>
       <name>Han-Wen Nienhuys</name>
     </developer>
     <developer>
+      <name>Jacek Centkowski</name>
+    </developer>
+    <developer>
+      <name>Joerg Zieren</name>
+    </developer>
+    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
@@ -53,7 +65,10 @@
       <name>Matthias Sohn</name>
     </developer>
     <developer>
-      <name>Ole Rehmsen</name>
+      <name>Milutin Kristofic</name>
+    </developer>
+    <developer>
+      <name>Nasser Grainawi</name>
     </developer>
     <developer>
       <name>Patrick Hiesel</name>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index c1350c4..d435ba5 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>3.0.17-SNAPSHOT</version>
+  <version>3.1.17-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
@@ -35,12 +35,24 @@
       <name>David Pursehouse</name>
     </developer>
     <developer>
+      <name>Dmitrii Filippov</name>
+    </developer>
+    <developer>
       <name>Edwin Kempin</name>
     </developer>
     <developer>
+      <name>Gal Paikin</name>
+    </developer>
+    <developer>
       <name>Han-Wen Nienhuys</name>
     </developer>
     <developer>
+      <name>Jacek Centkowski</name>
+    </developer>
+    <developer>
+      <name>Joerg Zieren</name>
+    </developer>
+    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
@@ -53,7 +65,10 @@
       <name>Matthias Sohn</name>
     </developer>
     <developer>
-      <name>Ole Rehmsen</name>
+      <name>Milutin Kristofic</name>
+    </developer>
+    <developer>
+      <name>Nasser Grainawi</name>
     </developer>
     <developer>
       <name>Patrick Hiesel</name>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 53c932a..b13cdd6 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>3.0.17-SNAPSHOT</version>
+  <version>3.1.17-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
@@ -35,12 +35,24 @@
       <name>David Pursehouse</name>
     </developer>
     <developer>
+      <name>Dmitrii Filippov</name>
+    </developer>
+    <developer>
       <name>Edwin Kempin</name>
     </developer>
     <developer>
+      <name>Gal Paikin</name>
+    </developer>
+    <developer>
       <name>Han-Wen Nienhuys</name>
     </developer>
     <developer>
+      <name>Jacek Centkowski</name>
+    </developer>
+    <developer>
+      <name>Joerg Zieren</name>
+    </developer>
+    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
@@ -53,7 +65,10 @@
       <name>Matthias Sohn</name>
     </developer>
     <developer>
-      <name>Ole Rehmsen</name>
+      <name>Milutin Kristofic</name>
+    </developer>
+    <developer>
+      <name>Nasser Grainawi</name>
     </developer>
     <developer>
       <name>Patrick Hiesel</name>
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
index 60e9f15..4ed5bf9 100755
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index e9baafc..5165e09 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -17,38 +17,46 @@
     # Transitive dependency of commons-compress
     maven_jar(
         name = "tukaani-xz",
-        artifact = "org.tukaani:xz:1.6",
-        sha1 = "05b6f921f1810bdf90e25471968f741f87168b64",
+        artifact = "org.tukaani:xz:1.8",
+        sha1 = "c4f7d054303948eb6a4066194253886c8af07128",
     )
 
     maven_jar(
         name = "dropwizard-core",
-        artifact = "io.dropwizard.metrics:metrics-core:4.0.5",
-        sha1 = "b81ef162970cdb9f4512ee2da09715a856ff4c4c",
+        artifact = "io.dropwizard.metrics:metrics-core:4.0.7",
+        sha1 = "673899f605f52ca35836673ccfee97154a496a61",
     )
 
+    SSHD_VERS = "2.3.0"
+
     maven_jar(
         name = "sshd",
-        artifact = "org.apache.sshd:sshd-core:2.0.0",
-        sha1 = "f4275079a2463cfd2bf1548a80e1683288a8e86b",
+        artifact = "org.apache.sshd:sshd-core:" + SSHD_VERS,
+        sha1 = "21aeea9deba96c9b81ea0935fa4fac61aa3cf646",
+    )
+
+    maven_jar(
+        name = "sshd-common",
+        artifact = "org.apache.sshd:sshd-common:" + SSHD_VERS,
+        sha1 = "8b6e3baaa0d35b547696965eef3e62477f5e74c9",
     )
 
     maven_jar(
         name = "eddsa",
-        artifact = "net.i2p.crypto:eddsa:0.2.0",
-        sha1 = "0856a92559c4daf744cb27c93cd8b7eb1f8c4780",
+        artifact = "net.i2p.crypto:eddsa:0.3.0",
+        sha1 = "1901c8d4d8bffb7d79027686cfb91e704217c3e1",
     )
 
     maven_jar(
         name = "mina-core",
-        artifact = "org.apache.mina:mina-core:2.0.17",
-        sha1 = "7e10ec974760436d931f3e58be507d1957bcc8db",
+        artifact = "org.apache.mina:mina-core:2.0.21",
+        sha1 = "e1a317689ecd438f54e863747e832f741ef8e092",
     )
 
     maven_jar(
         name = "sshd-mina",
-        artifact = "org.apache.sshd:sshd-mina:2.0.0",
-        sha1 = "50f2669312494f6c1996d8bd0d266c1fca7be6f6",
+        artifact = "org.apache.sshd:sshd-mina:" + SSHD_VERS,
+        sha1 = "55dc0830dfcbceba01f9460812ee454978a15fe8",
     )
 
     # elasticsearch-rest-client explicitly depends on this version
@@ -91,7 +99,8 @@
     )
 
     # When upgrading elasticsearch-rest-client, also upgrade httpcore-nio
-    # and httpasyncclient as necessary.
+    # and httpasyncclient as necessary. Consider also the other
+    # org.apache.httpcomponents dependencies in ../WORKSPACE.
     maven_jar(
         name = "elasticsearch-rest-client",
         artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.8.1",
@@ -141,65 +150,21 @@
         sha1 = "dc13ae4faca6df981fc7aeb5a522d9db446d5d50",
     )
 
-    POWERM_VERS = "1.6.1"
-
-    maven_jar(
-        name = "powermock-module-junit4",
-        artifact = "org.powermock:powermock-module-junit4:" + POWERM_VERS,
-        sha1 = "ea8530b2848542624f110a393513af397b37b9cf",
-    )
-
-    maven_jar(
-        name = "powermock-module-junit4-common",
-        artifact = "org.powermock:powermock-module-junit4-common:" + POWERM_VERS,
-        sha1 = "7222ced54dabc310895d02e45c5428ca05193cda",
-    )
-
-    maven_jar(
-        name = "powermock-reflect",
-        artifact = "org.powermock:powermock-reflect:" + POWERM_VERS,
-        sha1 = "97d25eda8275c11161bcddda6ef8beabd534c878",
-    )
-
-    maven_jar(
-        name = "powermock-api-easymock",
-        artifact = "org.powermock:powermock-api-easymock:" + POWERM_VERS,
-        sha1 = "aa740ecf89a2f64d410b3d93ef8cd6833009ef00",
-    )
-
-    maven_jar(
-        name = "powermock-api-support",
-        artifact = "org.powermock:powermock-api-support:" + POWERM_VERS,
-        sha1 = "592ee6d929c324109d3469501222e0c76ccf0869",
-    )
-
-    maven_jar(
-        name = "powermock-core",
-        artifact = "org.powermock:powermock-core:" + POWERM_VERS,
-        sha1 = "5afc1efce8d44ed76b30af939657bd598e45d962",
-    )
-
-    maven_jar(
-        name = "javassist",
-        artifact = "org.javassist:javassist:3.22.0-GA",
-        sha1 = "3e83394258ae2089be7219b971ec21a8288528ad",
-    )
-
-    DOCKER_JAVA_VERS = "3.2.5"
+    DOCKER_JAVA_VERS = "3.2.7"
 
     maven_jar(
         name = "docker-java-api",
         artifact = "com.github.docker-java:docker-java-api:" + DOCKER_JAVA_VERS,
-        sha1 = "8fe5c5e39f940ce58620e77cedc0a2a52d76f9d8",
+        sha1 = "81408fc988c229ea11354fee9902c47842343f04",
     )
 
     maven_jar(
         name = "docker-java-transport",
         artifact = "com.github.docker-java:docker-java-transport:" + DOCKER_JAVA_VERS,
-        sha1 = "27af0ee7ebc2f5672e23ea64769497b5d55ce3ac",
+        sha1 = "315903a129f530422747efc163dd255f0fa2555e",
     )
 
-    # https://github.com/docker-java/docker-java/blob/3.2.5/pom.xml#L61
+    # https://github.com/docker-java/docker-java/blob/3.2.7/pom.xml#L61
     # <=> DOCKER_JAVA_VERS
     maven_jar(
         name = "jackson-annotations",
@@ -207,18 +172,18 @@
         sha1 = "0f63b3b1da563767d04d2e4d3fc1ae0cdeffebe7",
     )
 
-    TESTCONTAINERS_VERSION = "1.15.0"
+    TESTCONTAINERS_VERSION = "1.15.1"
 
     maven_jar(
         name = "testcontainers",
         artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
-        sha1 = "b627535b444d88e7b14953bb953d80d9b7b3bd76",
+        sha1 = "91e6dfab8f141f77c6a0dd147a94bd186993a22c",
     )
 
     maven_jar(
         name = "testcontainers-elasticsearch",
         artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
-        sha1 = "2bd79fd915e5c7bcf9b5d86cd8e0b7a0fff4b8ce",
+        sha1 = "6b778a270b7529fcb9b7a6f62f3ae9d38544ce2f",
     )
 
     maven_jar(
diff --git a/tools/util_test.py b/tools/util_test.py
index 1a389f5..ab1133b2 100644
--- a/tools/util_test.py
+++ b/tools/util_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/version.py b/tools/version.py
index 2326757..d02fc26 100755
--- a/tools/version.py
+++ b/tools/version.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2014 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/workspace_status.py b/tools/workspace_status.py
index 86df519..0dc4a48 100644
--- a/tools/workspace_status.py
+++ b/tools/workspace_status.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # This script will be run by bazel when the build process starts to
 # generate key-value information that represents the status of the
diff --git a/tools/workspace_status_release.py b/tools/workspace_status_release.py
new file mode 100644
index 0000000..b3e72ff
--- /dev/null
+++ b/tools/workspace_status_release.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+
+# This is a variant of the `workspace_status.py` script that in addition to
+# plain `git describe` implements a few heuristics to arrive at more to the
+# point stamps for directories. But due to the implemented heuristics, it will
+# typically take longer to run (especially if you use lots of plugins that
+# come without tags) and might slow down your development cycle when used
+# as default.
+#
+# To use it, simply add
+#
+#   --workspace_status_command="python3 ./tools/workspace_status_release.py"
+#
+# to your bazel command. So for example instead of
+#
+#   bazel build release.war
+#
+# use
+#
+#   bazel build --workspace_status_command="python3 ./tools/workspace_status_release.py" release.war
+#
+# Alternatively, you can add
+#
+#   build --workspace_status_command="python3 ./tools/workspace_status_release.py"
+#
+# to `.bazelrc` in your home directory.
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+from __future__ import print_function
+import os
+import subprocess
+import sys
+import re
+
+ROOT = os.path.abspath(__file__)
+while not os.path.exists(os.path.join(ROOT, 'WORKSPACE')):
+    ROOT = os.path.dirname(ROOT)
+REVISION_CMD = ['git', 'describe', '--always', '--dirty']
+
+
+def run(command):
+    try:
+        return subprocess.check_output(command).strip().decode("utf-8")
+    except OSError as err:
+        print('could not invoke %s: %s' % (command[0], err), file=sys.stderr)
+        sys.exit(1)
+    except subprocess.CalledProcessError:
+        # ignore "not a git repository error" to report unknown version
+        return None
+
+
+def revision_with_match(pattern=None, prefix=False, all_refs=False,
+                        return_unmatched=False):
+    """Return a description of the current commit
+
+    Keyword arguments:
+    pattern    -- (Default: None) Use only refs that match this pattern.
+    prefix     -- (Default: False) If True, the pattern is considered a prefix
+                  and does not require an exact match.
+    all_refs   -- (Default: False) If True, consider all refs, not just tags.
+    return_unmatched -- (Default: False) If False and a pattern is given that
+                  cannot be matched, return the empty string. If True, return
+                  the unmatched description nonetheless.
+    """
+
+    command = REVISION_CMD[:]
+    if pattern:
+        command += ['--match', pattern + ('*' if prefix else '')]
+    if all_refs:
+        command += ['--all', '--long']
+
+    description = run(command)
+
+    if pattern and not return_unmatched and not description.startswith(pattern):
+        return ''
+    return description
+
+
+def branch_with_match(pattern):
+    for ref_kind in ['origin/', 'gerrit/', '']:
+        description = revision_with_match(ref_kind + pattern, all_refs=True,
+                                          return_unmatched=True)
+        for cutoff in ['heads/', 'remotes/', ref_kind]:
+            if description.startswith(cutoff):
+                description = description[len(cutoff):]
+        if description.startswith(pattern):
+            return description
+    return ''
+
+
+def revision(template=None):
+    if template:
+        # We use the version `v2.16.19-1-gec686a6352` as running example for the
+        # below comments. First, we split into ['v2', '16', '19']
+        parts = template.split('-')[0].split('.')
+
+        # Although we have releases with version tags containing 4 numbers, we
+        # treat only the first three numbers for simplicity. See discussion on
+        # Ib1681b2730cf2c443a3cb55fe6e282f6484e18de.
+
+        if len(parts) >= 3:
+            # Match for v2.16.19
+            version_part = '.'.join(parts[0:3])
+            description = revision_with_match(version_part)
+            if description:
+                return description
+
+        if len(parts) >= 2:
+            version_part = '.'.join(parts[0:2])
+
+            # Match for v2.16.*
+            description = revision_with_match(version_part + '.', prefix=True)
+            if description:
+                return description
+
+            # Match for v2.16
+            description = revision_with_match(version_part)
+            if description.startswith(version_part):
+                return description
+
+            if template.startswith('v'):
+                # Match for stable-2.16 branches
+                branch = 'stable-' + version_part[1:]
+                description = branch_with_match(branch)
+                if description:
+                    return description
+
+    # None of the template based methods worked out, so we're falling back to
+    # generic matches.
+
+    # Match for master branch
+    description = branch_with_match('master')
+    if description:
+        return description
+
+    # Match for anything that looks like a version tag
+    description = revision_with_match('v[0-9].', return_unmatched=True)
+    if description.startswith('v'):
+        return description
+
+    # Still no good tag, so we re-try without any matching
+    return revision_with_match()
+
+
+# prints the stamps for the current working directory
+def print_stamps_for_cwd(name, template):
+    workspace_status_script = os.path.join(
+        'tools', 'workspace_status_release.py')
+    if os.path.isfile(workspace_status_script):
+        # directory has own workspace_status_command, so we use stamps from that
+        for line in run(["python3", workspace_status_script]).split('\n'):
+            if re.search("^STABLE_[a-zA-Z0-9().:@/_ -]*$", line):
+                print(line)
+    else:
+        # directory lacks own workspace_status_command, so we create a default
+        # stamp
+        v = revision(template)
+        print('STABLE_BUILD_%s_LABEL %s' % (name.upper(),
+                                            v if v else 'unknown'))
+
+
+# os.chdir is different from plain `cd` in shells in that it follows symlinks
+# and does not update the PWD environment. So when using os.chdir to change into
+# a symlinked directory from gerrit's `plugins` or `modules` directory, we
+# cannot recover gerrit's directory. This prevents the plugins'/modules'
+# `workspace_status_release.py` scripts to detect the name they were symlinked
+# as (E.g.: it-* plugins sometimes get linked in more than once under different
+# names) and to detect gerrit's root directory. To work around this problem, we
+# mimic the `cd` of ordinary shells. By using this function, symlink information
+# is preserved in the `PWD` environment variable (as it is for example also done
+# in bash) and plugin/module `workspace_status_release.py` scripts can pick up
+# the needed information from there.
+def cd(absolute_path):
+    os.environ['PWD'] = absolute_path
+    os.chdir(absolute_path)
+
+
+def print_stamps():
+    cd(ROOT)
+    gerrit_version = revision()
+    print("STABLE_BUILD_GERRIT_LABEL %s" % gerrit_version)
+    for kind in ['modules', 'plugins']:
+        kind_dir = os.path.join(ROOT, kind)
+        for d in os.listdir(kind_dir) if os.path.isdir(kind_dir) else []:
+            p = os.path.join(kind_dir, d)
+            if os.path.isdir(p):
+                cd(p)
+                name = os.path.basename(p)
+                print_stamps_for_cwd(name, gerrit_version)
+
+
+if __name__ == '__main__':
+    print_stamps()
diff --git a/version.bzl b/version.bzl
index 1b79934..ec93869 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = "3.0.17-SNAPSHOT"
+GERRIT_VERSION = "3.1.17-SNAPSHOT"